@@ -19,13 +19,12 @@ use chrono::prelude::*;
1919use fn_error_context:: context;
2020use openat_ext:: OpenatDirExt ;
2121use os_release:: OsRelease ;
22- use rustix:: { fd:: AsFd , fd:: BorrowedFd , fs:: StatVfsMountFlags } ;
23- use walkdir:: WalkDir ;
24- #[ cfg( target_os = "linux" ) ]
2522use rustix:: mount:: {
2623 fsconfig_create, fsconfig_set_path, fsmount, fsopen, move_mount, FsMountFlags , FsOpenFlags ,
27- MoveMountFlags , MountAttrFlags , UnmountFlags ,
24+ MountAttrFlags , MoveMountFlags , UnmountFlags ,
2825} ;
26+ use rustix:: { fd:: AsFd , fd:: BorrowedFd , fs:: StatVfsMountFlags } ;
27+ use walkdir:: WalkDir ;
2928use widestring:: U16CString ;
3029
3130use crate :: bootupd:: RootContext ;
@@ -70,15 +69,19 @@ pub(crate) fn is_efi_booted() -> Result<bool> {
7069 . map_err ( Into :: into)
7170}
7271
73- #[ cfg( target_os = "linux" ) ]
7472fn mount_esp ( esp_device : & Path , target : & Path ) -> Result < ( ) > {
7573 use rustix:: fs:: CWD ;
7674
7775 let fs_fd = fsopen ( "vfat" , FsOpenFlags :: empty ( ) ) . context ( "fsopen vfat" ) ?;
78- fsconfig_set_path ( fs_fd. as_fd ( ) , "source" , esp_device, CWD ) . context ( "fsconfig_set_path source" ) ?;
76+ fsconfig_set_path ( fs_fd. as_fd ( ) , "source" , esp_device, CWD )
77+ . context ( "fsconfig_set_path source" ) ?;
7978 fsconfig_create ( fs_fd. as_fd ( ) ) . context ( "fsconfig_create" ) ?;
80- let mount_fd =
81- fsmount ( fs_fd. as_fd ( ) , FsMountFlags :: empty ( ) , MountAttrFlags :: empty ( ) ) . context ( "fsmount" ) ?;
79+ let mount_fd = fsmount (
80+ fs_fd. as_fd ( ) ,
81+ FsMountFlags :: empty ( ) ,
82+ MountAttrFlags :: empty ( ) ,
83+ )
84+ . context ( "fsmount" ) ?;
8285 let target_dir = std:: fs:: File :: open ( target) . context ( "open target dir for move_mount" ) ?;
8386 let target_fd = unsafe { BorrowedFd :: borrow_raw ( target_dir. as_raw_fd ( ) ) } ;
8487 move_mount (
@@ -140,13 +143,11 @@ impl Efi {
140143 if !mnt. exists ( ) {
141144 continue ;
142145 }
143- #[ cfg( target_os = "linux" ) ]
144146 if mount_esp ( esp_device, & mnt) . is_ok ( ) {
145147 log:: debug!( "Mounted at {mnt:?}" ) ;
146148 mountpoint = Some ( mnt) ;
147149 break ;
148150 }
149- #[ cfg( target_os = "linux" ) ]
150151 log:: trace!( "Mount failed, falling back to mount(8)" ) ;
151152 std:: process:: Command :: new ( "mount" )
152153 . arg ( esp_device)
@@ -188,7 +189,6 @@ impl Efi {
188189 . unwrap_or ( false ) ;
189190 if should_unmount {
190191 if let Some ( mount) = self . mountpoint . borrow_mut ( ) . take ( ) {
191- #[ cfg( target_os = "linux" ) ]
192192 if rustix:: mount:: unmount ( & mount. path , UnmountFlags :: empty ( ) ) . is_ok ( ) {
193193 log:: trace!( "Unmounted (new mount API)" ) ;
194194 } else {
@@ -198,7 +198,6 @@ impl Efi {
198198 . with_context ( || format ! ( "Failed to unmount {:?}" , mount. path) ) ?;
199199 log:: trace!( "Unmounted" ) ;
200200 }
201- #[ cfg( not( target_os = "linux" ) ) ]
202201 {
203202 Command :: new ( "umount" )
204203 . arg ( & mount. path )
@@ -248,35 +247,73 @@ impl Efi {
248247 clear_efi_target ( & product_name) ?;
249248 create_efi_boot_entry ( device, esp_part_num. trim ( ) , & loader, & product_name)
250249 }
250+ /// Copy EFI components to ESP using the same "write alongside + atomic rename" pattern
251+ /// as bootable container updates, so the system stays bootable if any step fails.
251252 fn copy_efi_components_to_esp (
252253 & self ,
253254 sysroot_dir : & openat:: Dir ,
254255 esp_dir : & openat:: Dir ,
255- esp_path : & Path ,
256+ _esp_path : & Path ,
256257 efi_components : & [ EFIComponent ] ,
257258 ) -> Result < ( ) > {
258- let dest_str = esp_path
259+ // Build a merged source tree in a temp dir (same layout as desired ESP/EFI)
260+ let temp_dir = tempfile:: tempdir ( ) . context ( "Creating temp dir for EFI merge" ) ?;
261+ let temp_efi_path = temp_dir. path ( ) . join ( "EFI" ) ;
262+ std:: fs:: create_dir_all ( & temp_efi_path)
263+ . with_context ( || format ! ( "Creating {}" , temp_efi_path. display( ) ) ) ?;
264+ let temp_efi_str = temp_efi_path
259265 . to_str ( )
260- . with_context ( || format ! ( "Invalid UTF-8: {}" , esp_path . display ( ) ) ) ?;
266+ . context ( "Temp EFI path is not valid UTF-8" ) ?;
261267
262268 for efi_comp in efi_components {
263269 log:: info!(
264- "Copying EFI component {} version {} to ESP at {} " ,
270+ "Merging EFI component {} version {} into update tree " ,
265271 efi_comp. name,
266- efi_comp. version,
267- esp_path. display( )
272+ efi_comp. version
268273 ) ;
274+ // Copy contents of component's EFI dir (e.g. fedora/) into temp_efi_path so merged
275+ // layout is EFI/fedora/..., not EFI/EFI/fedora/...
276+ let src_efi_contents = format ! ( "{}/." , efi_comp. path) ;
277+ filetree:: copy_dir_with_args (
278+ sysroot_dir,
279+ src_efi_contents. as_str ( ) ,
280+ temp_efi_str,
281+ OPTIONS ,
282+ )
283+ . with_context ( || format ! ( "Copying {} to merge dir" , efi_comp. path) ) ?;
284+ }
269285
270- filetree:: copy_dir_with_args ( sysroot_dir, efi_comp. path . as_str ( ) , dest_str, OPTIONS )
271- . with_context ( || {
272- format ! (
273- "Failed to copy {} from {} to {}" ,
274- efi_comp. name, efi_comp. path, dest_str
275- )
276- } ) ?;
286+ // Ensure ESP/EFI exists (e.g. first install)
287+ esp_dir. ensure_dir_all ( std:: path:: Path :: new ( "EFI" ) , 0o755 ) ?;
288+ let esp_efi_dir = esp_dir. sub_dir ( "EFI" ) . context ( "Opening ESP EFI dir" ) ?;
289+
290+ let source_dir =
291+ openat:: Dir :: open ( & temp_efi_path) . context ( "Opening merged EFI source dir" ) ?;
292+ let source_filetree =
293+ filetree:: FileTree :: new_from_dir ( & source_dir) . context ( "Building source filetree" ) ?;
294+ let current_filetree =
295+ filetree:: FileTree :: new_from_dir ( & esp_efi_dir) . context ( "Building current filetree" ) ?;
296+ let mut diff = current_filetree
297+ . diff ( & source_filetree)
298+ . context ( "Computing EFI diff" ) ?;
299+ diff. removals . clear ( ) ;
300+
301+ // Check available space before writing to prevent partial updates when the partition is full
302+ let required_bytes = current_filetree. total_size ( ) + source_filetree. total_size ( ) ;
303+ let available_bytes = util:: available_space_bytes ( & esp_efi_dir) ?;
304+ if available_bytes < required_bytes {
305+ anyhow:: bail!(
306+ "ESP has insufficient free space for update: need {} MiB, have {} MiB" ,
307+ required_bytes / ( 1024 * 1024 ) ,
308+ available_bytes / ( 1024 * 1024 )
309+ ) ;
277310 }
278311
279- // Sync the whole ESP filesystem
312+ // Same logic as bootable container: write to .btmp.* then atomic rename
313+ filetree:: apply_diff ( & source_dir, & esp_efi_dir, & diff, None )
314+ . context ( "Applying EFI update (write alongside + atomic rename)" ) ?;
315+
316+ // Sync the whole ESP filesystem
280317 fsfreeze_thaw_cycle ( esp_dir. open_file ( "." ) ?) ?;
281318
282319 Ok ( ( ) )
@@ -286,6 +323,7 @@ impl Efi {
286323 fn package_mode_copy_to_boot_impl ( & self , sysroot : & Path ) -> Result < ( ) > {
287324 let sysroot_path = Utf8Path :: from_path ( sysroot)
288325 . with_context ( || format ! ( "Invalid UTF-8: {}" , sysroot. display( ) ) ) ?;
326+ let sysroot_dir = openat:: Dir :: open ( sysroot) . context ( "Opening sysroot for reading" ) ?;
289327
290328 let efi_comps = match get_efi_component_from_usr ( sysroot_path, EFILIB ) ? {
291329 Some ( comps) if !comps. is_empty ( ) => comps,
@@ -309,7 +347,6 @@ impl Efi {
309347 . with_context ( || format ! ( "Opening ESP at {}" , esp_path. display( ) ) ) ?;
310348 validate_esp_fstype ( & esp_dir) ?;
311349
312- let sysroot_dir = openat:: Dir :: open ( sysroot) . context ( "Opening sysroot for reading" ) ?;
313350 self . copy_efi_components_to_esp ( & sysroot_dir, & esp_dir, & esp_path, & efi_comps) ?;
314351
315352 log:: info!(
0 commit comments