@@ -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 (
@@ -248,35 +253,67 @@ impl Efi {
248253 clear_efi_target ( & product_name) ?;
249254 create_efi_boot_entry ( device, esp_part_num. trim ( ) , & loader, & product_name)
250255 }
256+ /// Copy EFI components to ESP using the same "write alongside + atomic rename" pattern
257+ /// as bootable container updates, so the system stays bootable if any step fails.
251258 fn copy_efi_components_to_esp (
252259 & self ,
253260 sysroot_dir : & openat:: Dir ,
254261 esp_dir : & openat:: Dir ,
255- esp_path : & Path ,
262+ _esp_path : & Path ,
256263 efi_components : & [ EFIComponent ] ,
257264 ) -> Result < ( ) > {
258- let dest_str = esp_path
265+ // Build a merged source tree in a temp dir (same layout as desired ESP/EFI)
266+ let temp_dir = tempfile:: tempdir ( ) . context ( "Creating temp dir for EFI merge" ) ?;
267+ let temp_efi_path = temp_dir. path ( ) . join ( "EFI" ) ;
268+ std:: fs:: create_dir_all ( & temp_efi_path)
269+ . with_context ( || format ! ( "Creating {}" , temp_efi_path. display( ) ) ) ?;
270+ let temp_efi_str = temp_efi_path
259271 . to_str ( )
260- . with_context ( || format ! ( "Invalid UTF-8: {}" , esp_path . display ( ) ) ) ?;
272+ . context ( "Temp EFI path is not valid UTF-8" ) ?;
261273
262274 for efi_comp in efi_components {
263275 log:: info!(
264- "Copying EFI component {} version {} to ESP at {} " ,
276+ "Merging EFI component {} version {} into update tree " ,
265277 efi_comp. name,
266- efi_comp. version,
267- esp_path. display( )
278+ efi_comp. version
268279 ) ;
280+ // Copy contents of component's EFI dir (e.g. fedora/) into temp_efi_path so merged
281+ // layout is EFI/fedora/..., not EFI/EFI/fedora/...
282+ let src_efi_contents = format ! ( "{}/." , efi_comp. path) ;
283+ filetree:: copy_dir_with_args ( sysroot_dir, src_efi_contents. as_str ( ) , temp_efi_str, OPTIONS )
284+ . with_context ( || format ! ( "Copying {} to merge dir" , efi_comp. path) ) ?;
285+ }
269286
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- } ) ?;
287+ // Ensure ESP/EFI exists (e.g. first install)
288+ esp_dir. ensure_dir_all ( std:: path:: Path :: new ( "EFI" ) , 0o755 ) ?;
289+ let esp_efi_dir = esp_dir. sub_dir ( "EFI" ) . context ( "Opening ESP EFI dir" ) ?;
290+
291+ let source_dir =
292+ openat:: Dir :: open ( & temp_efi_path) . context ( "Opening merged EFI source dir" ) ?;
293+ let source_filetree =
294+ filetree:: FileTree :: new_from_dir ( & source_dir) . context ( "Building source filetree" ) ?;
295+ let current_filetree =
296+ filetree:: FileTree :: new_from_dir ( & esp_efi_dir) . context ( "Building current filetree" ) ?;
297+ let diff = current_filetree
298+ . diff ( & source_filetree)
299+ . context ( "Computing EFI diff" ) ?;
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 ( ( ) )
0 commit comments