From d1247bde474ce8e572d4dc8b13c0322c92f7d789 Mon Sep 17 00:00:00 2001 From: ckyrouac Date: Thu, 26 Mar 2026 18:47:09 +0000 Subject: [PATCH] grubconfigs: Sync grub.cfg and bootuuid.cfg to all ESPs on multi-device setups On multi-device setups (e.g. RAID 1 with an ESP per disk), grubconfigs::install() only wrote grub.cfg and bootuuid.cfg to the single ESP mounted at /boot/efi. If the system booted from a different disk, GRUB would drop to a shell because no grub.cfg existed on that ESP. Extend grubconfigs::install() with an `additional_esp_devices` parameter. When non-empty, each listed ESP is temporarily mounted and receives copies of grub.cfg and bootuuid.cfg via a new write_esp_configs() helper (extracted from the existing inline logic to avoid duplication). In bootupd::install(), discover colocated ESPs via find_colocated_esps() on the first device and pass them through, consistent with the approach already used in the update and validate paths in efi.rs. Callers that do not need multi-ESP support (bios.rs, efi.rs update path) simply pass an empty slice. Fixes #1076 Assisted-by: Claude Code (Opus 4.6) Signed-off-by: ckyrouac --- src/bios.rs | 2 +- src/bootupd.rs | 19 ++++++++++ src/efi.rs | 2 +- src/grubconfigs.rs | 88 ++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 94 insertions(+), 17 deletions(-) diff --git a/src/bios.rs b/src/bios.rs index f341f994..d7775603 100644 --- a/src/bios.rs +++ b/src/bios.rs @@ -191,7 +191,7 @@ impl Component for Bios { .context("Failed to backup GRUB config")?; } - crate::grubconfigs::install(&destdir, None, None, true)?; + crate::grubconfigs::install(&destdir, None, None, true, &[])?; // Remove the real config if it is symlink and will not // if /boot/grub2/grub.cfg is file diff --git a/src/bootupd.rs b/src/bootupd.rs index e01ce923..c7d5b816 100644 --- a/src/bootupd.rs +++ b/src/bootupd.rs @@ -187,11 +187,30 @@ pub(crate) fn install( Some(uuid) => { let meta = get_static_config_meta()?; state.static_configs = Some(meta); + + // On multi-device setups (e.g. RAID 1 with an ESP per disk) we + // need every ESP to carry grub.cfg and bootuuid.cfg so the system + // can boot from any disk. Gather the additional ESP device paths + // now and pass them into grubconfigs::install so it writes to all + // of them. + let additional_esps: Vec = if installed_efi_vendor.is_some() { + devices + .first() + .and_then(|dev| dev.find_colocated_esps().ok().flatten()) + .unwrap_or_default() + .iter() + .map(|dev| dev.path()) + .collect() + } else { + Vec::new() + }; + crate::grubconfigs::install( sysroot, Some(&source_root_dir), installed_efi_vendor.as_deref(), uuid, + &additional_esps, )?; // On other architectures, assume that there's nothing to do. } diff --git a/src/efi.rs b/src/efi.rs index 1022f4af..169b1659 100644 --- a/src/efi.rs +++ b/src/efi.rs @@ -356,7 +356,7 @@ impl Component for Efi { .context("Failed to backup GRUB config")?; } - grubconfigs::install(&sysroot, None, Some(&vendor), true)?; + grubconfigs::install(&sysroot, None, Some(&vendor), true, &[])?; // Synchronize the filesystem containing /boot/efi/EFI/{vendor} to disk. fsfreeze_thaw_cycle(efidir.open_file(".")?)?; diff --git a/src/grubconfigs.rs b/src/grubconfigs.rs index 65196c3d..fa112a19 100644 --- a/src/grubconfigs.rs +++ b/src/grubconfigs.rs @@ -27,12 +27,17 @@ const GRUB_FILES: [&str; 3] = ["bootuuid.cfg", GRUBCONFIG, GRUBENV]; pub(crate) const GRUBCONFIG_FILE_MODE: u32 = 0o600; /// Install the static GRUB config files. +/// +/// When `additional_esp_devices` is non-empty, grub.cfg and bootuuid.cfg are +/// also written to every listed ESP partition (each is mounted temporarily, +/// written to, then unmounted). #[context("Installing static GRUB configs")] pub(crate) fn install( target_root: &openat::Dir, src_root: Option<&openat::Dir>, installed_efi_vendor: Option<&str>, write_uuid: bool, + additional_esp_devices: &[String], ) -> Result<()> { let bootdir = &target_root.sub_dir("boot").context("Opening /boot")?; let boot_is_mount = { @@ -117,32 +122,85 @@ pub(crate) fn install( if let Some(vendordir) = installed_efi_vendor { log::debug!("vendordir={:?}", &vendordir); - let vendor = PathBuf::from(vendordir); - let target = &vendor.join("grub.cfg"); let dest_efidir = target_root .sub_dir_optional("boot/efi/EFI") .context("Opening /boot/efi/EFI")?; if let Some(efidir) = dest_efidir { - configdir - .copy_file_at("grub-static-efi.cfg", &efidir, target) - .context("Copying static EFI")?; - println!("Installed: {target:?}"); - if let Some(uuid_path) = uuid_path { - let target = &vendor.join(uuid_path); - grub2dir - .copy_file_at(uuid_path, &efidir, target) - .context("Writing bootuuid.cfg to efi dir")?; - println!("Installed: {target:?}"); - } - fsfreeze_thaw_cycle(efidir.open_file(".")?)?; + write_esp_configs(&configdir, &grub2dir, &efidir, vendordir, uuid_path)?; } else { + let target = PathBuf::from(vendordir).join("grub.cfg"); println!("Could not find /boot/efi/EFI when installing {target:?}"); } + + // Write grub configs to every additional ESP so that any disk can + // boot independently in multi-device setups. + if !additional_esp_devices.is_empty() { + let tmpdir = tempfile::tempdir().context("Creating temporary mount point")?; + let tmpmnt = tmpdir.path(); + + for esp_path in additional_esp_devices { + log::info!("Installing GRUB configs to ESP {esp_path}"); + + std::process::Command::new("mount") + .arg(esp_path) + .arg(tmpmnt) + .run_inherited() + .with_context(|| format!("Mounting ESP {esp_path}"))?; + + let result = (|| -> Result<()> { + let efi_vendor = tmpmnt.join("EFI").join(vendordir); + std::fs::create_dir_all(&efi_vendor) + .with_context(|| format!("Creating {:?}", efi_vendor))?; + + let efidir = openat::Dir::open(&tmpmnt.join("EFI")) + .context("Opening EFI directory on ESP")?; + + write_esp_configs(&configdir, &grub2dir, &efidir, vendordir, uuid_path) + })(); + + // Always unmount, even on error + std::process::Command::new("umount") + .arg(tmpmnt) + .run_inherited() + .with_context(|| format!("Unmounting ESP {esp_path}"))?; + + result?; + } + } } Ok(()) } +/// Write grub.cfg and optionally bootuuid.cfg to an ESP's EFI directory. +/// +/// `efidir` should be the `EFI/` directory on the mounted ESP. +/// `configdir` is the static grub config source (e.g. /usr/lib/bootupd/grub2-static/). +/// `grub2dir` is the boot partition's grub2 directory (contains bootuuid.cfg). +fn write_esp_configs( + configdir: &openat::Dir, + grub2dir: &openat::Dir, + efidir: &openat::Dir, + vendordir: &str, + uuid_path: Option<&str>, +) -> Result<()> { + let vendor = PathBuf::from(vendordir); + let target = &vendor.join("grub.cfg"); + configdir + .copy_file_at("grub-static-efi.cfg", efidir, target) + .context("Copying static EFI")?; + println!("Installed: {target:?}"); + if let Some(uuid_path) = uuid_path { + let target = &vendor.join(uuid_path); + grub2dir + .copy_file_at(uuid_path, efidir, target) + .context("Writing bootuuid.cfg to efi dir")?; + println!("Installed: {target:?}"); + } + fsfreeze_thaw_cycle(efidir.open_file(".")?)?; + Ok(()) +} + #[context("Create file boot/grub2/grubenv")] fn write_grubenv(grubdir: &openat::Dir) -> Result<()> { if grubdir.exists(GRUBENV)? { @@ -203,7 +261,7 @@ mod tests { std::fs::create_dir_all(tdp.join("boot/grub2"))?; std::fs::create_dir_all(tdp.join("boot/efi/EFI/BOOT"))?; std::fs::create_dir_all(tdp.join("boot/efi/EFI/fedora"))?; - install(&td, None, Some("fedora"), false).unwrap(); + install(&td, None, Some("fedora"), false, &[]).unwrap(); assert!(td.exists("boot/grub2/grub.cfg")?); assert!(td.exists("boot/efi/EFI/fedora/grub.cfg")?);