@@ -248,35 +248,67 @@ impl Efi {
248248 clear_efi_target ( & product_name) ?;
249249 create_efi_boot_entry ( device, esp_part_num. trim ( ) , & loader, & product_name)
250250 }
251+ /// Copy EFI components to ESP using the same "write alongside + atomic rename" pattern
252+ /// as bootable container updates, so the system stays bootable if any step fails.
251253 fn copy_efi_components_to_esp (
252254 & self ,
253255 sysroot_dir : & openat:: Dir ,
254256 esp_dir : & openat:: Dir ,
255- esp_path : & Path ,
257+ _esp_path : & Path ,
256258 efi_components : & [ EFIComponent ] ,
257259 ) -> Result < ( ) > {
258- let dest_str = esp_path
260+ // Build a merged source tree in a temp dir (same layout as desired ESP/EFI)
261+ let temp_dir = tempfile:: tempdir ( ) . context ( "Creating temp dir for EFI merge" ) ?;
262+ let temp_efi_path = temp_dir. path ( ) . join ( "EFI" ) ;
263+ std:: fs:: create_dir_all ( & temp_efi_path)
264+ . with_context ( || format ! ( "Creating {}" , temp_efi_path. display( ) ) ) ?;
265+ let temp_efi_str = temp_efi_path
259266 . to_str ( )
260- . with_context ( || format ! ( "Invalid UTF-8: {}" , esp_path . display ( ) ) ) ?;
267+ . context ( "Temp EFI path is not valid UTF-8" ) ?;
261268
262269 for efi_comp in efi_components {
263270 log:: info!(
264- "Copying EFI component {} version {} to ESP at {} " ,
271+ "Merging EFI component {} version {} into update tree " ,
265272 efi_comp. name,
266- efi_comp. version,
267- esp_path. display( )
273+ efi_comp. version
268274 ) ;
275+ // Copy contents of component's EFI dir (e.g. fedora/) into temp_efi_path so merged
276+ // layout is EFI/fedora/..., not EFI/EFI/fedora/...
277+ let src_efi_contents = format ! ( "{}/." , efi_comp. path) ;
278+ filetree:: copy_dir_with_args ( sysroot_dir, src_efi_contents. as_str ( ) , temp_efi_str, OPTIONS )
279+ . with_context ( || format ! ( "Copying {} to merge dir" , efi_comp. path) ) ?;
280+ }
269281
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- } ) ?;
282+ // Ensure ESP/EFI exists (e.g. first install)
283+ esp_dir. ensure_dir_all ( std:: path:: Path :: new ( "EFI" ) , 0o755 ) ?;
284+ let esp_efi_dir = esp_dir. sub_dir ( "EFI" ) . context ( "Opening ESP EFI dir" ) ?;
285+
286+ let source_dir =
287+ openat:: Dir :: open ( & temp_efi_path) . context ( "Opening merged EFI source dir" ) ?;
288+ let source_filetree =
289+ filetree:: FileTree :: new_from_dir ( & source_dir) . context ( "Building source filetree" ) ?;
290+ let current_filetree =
291+ filetree:: FileTree :: new_from_dir ( & esp_efi_dir) . context ( "Building current filetree" ) ?;
292+ let diff = current_filetree
293+ . diff ( & source_filetree)
294+ . context ( "Computing EFI diff" ) ?;
295+
296+ // Check available space before writing to prevent partial updates when the partition is full
297+ let required_bytes = current_filetree. total_size ( ) + source_filetree. total_size ( ) ;
298+ let available_bytes = util:: available_space_bytes ( & esp_efi_dir) ?;
299+ if available_bytes < required_bytes {
300+ anyhow:: bail!(
301+ "ESP has insufficient free space for update: need {} MiB, have {} MiB" ,
302+ required_bytes / ( 1024 * 1024 ) ,
303+ available_bytes / ( 1024 * 1024 )
304+ ) ;
277305 }
278306
279- // Sync the whole ESP filesystem
307+ // Same logic as bootable container: write to .btmp.* then atomic rename
308+ filetree:: apply_diff ( & source_dir, & esp_efi_dir, & diff, None )
309+ . context ( "Applying EFI update (write alongside + atomic rename)" ) ?;
310+
311+ // Sync the whole ESP filesystem
280312 fsfreeze_thaw_cycle ( esp_dir. open_file ( "." ) ?) ?;
281313
282314 Ok ( ( ) )
0 commit comments