@@ -19,13 +19,13 @@ 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 ;
2422#[ cfg( target_os = "linux" ) ]
2523use rustix:: mount:: {
2624 fsconfig_create, fsconfig_set_path, fsmount, fsopen, move_mount, FsMountFlags , FsOpenFlags ,
27- MoveMountFlags , MountAttrFlags , UnmountFlags ,
25+ MountAttrFlags , MoveMountFlags , UnmountFlags ,
2826} ;
27+ use rustix:: { fd:: AsFd , fd:: BorrowedFd , fs:: StatVfsMountFlags } ;
28+ use walkdir:: WalkDir ;
2929use widestring:: U16CString ;
3030
3131use crate :: bootupd:: RootContext ;
@@ -75,10 +75,15 @@ fn mount_esp(esp_device: &Path, target: &Path) -> Result<()> {
7575 use rustix:: fs:: CWD ;
7676
7777 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" ) ?;
78+ fsconfig_set_path ( fs_fd. as_fd ( ) , "source" , esp_device, CWD )
79+ . context ( "fsconfig_set_path source" ) ?;
7980 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" ) ?;
81+ let mount_fd = fsmount (
82+ fs_fd. as_fd ( ) ,
83+ FsMountFlags :: empty ( ) ,
84+ MountAttrFlags :: empty ( ) ,
85+ )
86+ . context ( "fsmount" ) ?;
8287 let target_dir = std:: fs:: File :: open ( target) . context ( "open target dir for move_mount" ) ?;
8388 let target_fd = unsafe { BorrowedFd :: borrow_raw ( target_dir. as_raw_fd ( ) ) } ;
8489 move_mount (
@@ -140,7 +145,6 @@ impl Efi {
140145 if !mnt. exists ( ) {
141146 continue ;
142147 }
143- #[ cfg( target_os = "linux" ) ]
144148 if mount_esp ( esp_device, & mnt) . is_ok ( ) {
145149 log:: debug!( "Mounted at {mnt:?}" ) ;
146150 mountpoint = Some ( mnt) ;
@@ -248,35 +252,73 @@ impl Efi {
248252 clear_efi_target ( & product_name) ?;
249253 create_efi_boot_entry ( device, esp_part_num. trim ( ) , & loader, & product_name)
250254 }
255+ /// Copy EFI components to ESP using the same "write alongside + atomic rename" pattern
256+ /// as bootable container updates, so the system stays bootable if any step fails.
251257 fn copy_efi_components_to_esp (
252258 & self ,
253259 sysroot_dir : & openat:: Dir ,
254260 esp_dir : & openat:: Dir ,
255- esp_path : & Path ,
261+ _esp_path : & Path ,
256262 efi_components : & [ EFIComponent ] ,
257263 ) -> Result < ( ) > {
258- let dest_str = esp_path
264+ // Build a merged source tree in a temp dir (same layout as desired ESP/EFI)
265+ let temp_dir = tempfile:: tempdir ( ) . context ( "Creating temp dir for EFI merge" ) ?;
266+ let temp_efi_path = temp_dir. path ( ) . join ( "EFI" ) ;
267+ std:: fs:: create_dir_all ( & temp_efi_path)
268+ . with_context ( || format ! ( "Creating {}" , temp_efi_path. display( ) ) ) ?;
269+ let temp_efi_str = temp_efi_path
259270 . to_str ( )
260- . with_context ( || format ! ( "Invalid UTF-8: {}" , esp_path . display ( ) ) ) ?;
271+ . context ( "Temp EFI path is not valid UTF-8" ) ?;
261272
262273 for efi_comp in efi_components {
263274 log:: info!(
264- "Copying EFI component {} version {} to ESP at {} " ,
275+ "Merging EFI component {} version {} into update tree " ,
265276 efi_comp. name,
266- efi_comp. version,
267- esp_path. display( )
277+ efi_comp. version
268278 ) ;
279+ // Copy contents of component's EFI dir (e.g. fedora/) into temp_efi_path so merged
280+ // layout is EFI/fedora/..., not EFI/EFI/fedora/...
281+ let src_efi_contents = format ! ( "{}/." , efi_comp. path) ;
282+ filetree:: copy_dir_with_args (
283+ sysroot_dir,
284+ src_efi_contents. as_str ( ) ,
285+ temp_efi_str,
286+ OPTIONS ,
287+ )
288+ . with_context ( || format ! ( "Copying {} to merge dir" , efi_comp. path) ) ?;
289+ }
269290
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- } ) ?;
291+ // Ensure ESP/EFI exists (e.g. first install)
292+ esp_dir. ensure_dir_all ( std:: path:: Path :: new ( "EFI" ) , 0o755 ) ?;
293+ let esp_efi_dir = esp_dir. sub_dir ( "EFI" ) . context ( "Opening ESP EFI dir" ) ?;
294+
295+ let source_dir =
296+ openat:: Dir :: open ( & temp_efi_path) . context ( "Opening merged EFI source dir" ) ?;
297+ let source_filetree =
298+ filetree:: FileTree :: new_from_dir ( & source_dir) . context ( "Building source filetree" ) ?;
299+ let current_filetree =
300+ filetree:: FileTree :: new_from_dir ( & esp_efi_dir) . context ( "Building current filetree" ) ?;
301+ let mut diff = current_filetree
302+ . diff ( & source_filetree)
303+ . context ( "Computing EFI diff" ) ?;
304+ diff. removals . clear ( ) ;
305+
306+ // Check available space before writing to prevent partial updates when the partition is full
307+ let required_bytes = current_filetree. total_size ( ) + source_filetree. total_size ( ) ;
308+ let available_bytes = util:: available_space_bytes ( & esp_efi_dir) ?;
309+ if available_bytes < required_bytes {
310+ anyhow:: bail!(
311+ "ESP has insufficient free space for update: need {} MiB, have {} MiB" ,
312+ required_bytes / ( 1024 * 1024 ) ,
313+ available_bytes / ( 1024 * 1024 )
314+ ) ;
277315 }
278316
279- // Sync the whole ESP filesystem
317+ // Same logic as bootable container: write to .btmp.* then atomic rename
318+ filetree:: apply_diff ( & source_dir, & esp_efi_dir, & diff, None )
319+ . context ( "Applying EFI update (write alongside + atomic rename)" ) ?;
320+
321+ // Sync the whole ESP filesystem
280322 fsfreeze_thaw_cycle ( esp_dir. open_file ( "." ) ?) ?;
281323
282324 Ok ( ( ) )
@@ -286,6 +328,7 @@ impl Efi {
286328 fn package_mode_copy_to_boot_impl ( & self , sysroot : & Path ) -> Result < ( ) > {
287329 let sysroot_path = Utf8Path :: from_path ( sysroot)
288330 . with_context ( || format ! ( "Invalid UTF-8: {}" , sysroot. display( ) ) ) ?;
331+ let sysroot_dir = openat:: Dir :: open ( sysroot) . context ( "Opening sysroot for reading" ) ?;
289332
290333 let efi_comps = match get_efi_component_from_usr ( sysroot_path, EFILIB ) ? {
291334 Some ( comps) if !comps. is_empty ( ) => comps,
@@ -309,7 +352,6 @@ impl Efi {
309352 . with_context ( || format ! ( "Opening ESP at {}" , esp_path. display( ) ) ) ?;
310353 validate_esp_fstype ( & esp_dir) ?;
311354
312- let sysroot_dir = openat:: Dir :: open ( sysroot) . context ( "Opening sysroot for reading" ) ?;
313355 self . copy_efi_components_to_esp ( & sysroot_dir, & esp_dir, & esp_path, & efi_comps) ?;
314356
315357 log:: info!(
0 commit comments