From 468f98f75e55f54db377d8273e9ae16fb75b8077 Mon Sep 17 00:00:00 2001 From: Javier Martinez Canillas Date: Sun, 22 Mar 2026 21:56:34 +0100 Subject: [PATCH 1/5] filetree: Decouple source and destination paths in FileTreeDiff bootupd stores update payloads under /usr/lib/efi/ in a // directory structure. When building the file tree used to compute an update diff, it strips the // prefix to obtain the destination path on the ESP. For example, /usr/lib/efi/shim/15.8/EFI/BOOT/shimaa64.efi is translated to the EFI/BOOT/shimaa64.efi destination path. However, the diff records the source paths directly and re-derives the destination at apply time, coupling the diff representation to the source directory layout. When multiple versions of a component are present (e.g. both shim/15.8/ and shim/15.9/), several source files map to the same destination path. Since the iteration order is non-deterministic, the file tree may end up with stale metadata from the wrong version. This causes updates to be silently skipped when the old hash wins, and prevents detection of files removed in the newer version. To avoid this issue, let's record a source_map in the diff that maps each destination path back to its original source path. The diff now always works in terms of destination paths, and only consults the map when it needs to locate the source file for copying. This prepares for building per-version file trees in a following commit, making the update payloads iteration to be deterministic. Assisted-by: Cursor (Claude Opus 4) --- src/filetree.rs | 92 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 81 insertions(+), 11 deletions(-) diff --git a/src/filetree.rs b/src/filetree.rs index a9eee816..923119c7 100644 --- a/src/filetree.rs +++ b/src/filetree.rs @@ -103,6 +103,8 @@ pub(crate) struct FileTreeDiff { pub(crate) additions: HashSet, pub(crate) removals: HashSet, pub(crate) changes: HashSet, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub(crate) source_map: HashMap, } impl Display for FileTreeDiff { @@ -241,12 +243,18 @@ impl FileTree { let mut additions = HashSet::new(); let mut removals = HashSet::new(); let mut changes = HashSet::new(); + let mut source_map = HashMap::new(); for (k, v1) in self.children.iter() { if let Some(v2) = updated.children.get(k) { if v1 != v2 { - // Save the source path for changes - changes.insert(v2.source.as_ref().unwrap_or(k).clone()); + // Save the destination key and record the source path for changes + changes.insert(k.clone()); + if let Some(src) = v2.source.as_ref() { + if src != k { + source_map.insert(k.clone(), src.clone()); + } + } } } else { removals.insert(k.clone()); @@ -257,14 +265,20 @@ impl FileTree { if self.children.contains_key(k) { continue; } - // Save the source path for additions - additions.insert(v.source.as_ref().unwrap_or(k).clone()); + // Save the destination key and record the source path for additions + additions.insert(k.clone()); + if let Some(src) = v.source.as_ref() { + if src != k { + source_map.insert(k.clone(), src.clone()); + } + } } } Ok(FileTreeDiff { additions, removals, changes, + source_map, }) } @@ -278,6 +292,7 @@ impl FileTree { pub(crate) fn relative_diff_to(&self, dir: &openat::Dir) -> Result { let mut removals = HashSet::new(); let mut changes = HashSet::new(); + let mut source_map = HashMap::new(); for (path, info) in self.children.iter() { assert!(!path.starts_with('/')); @@ -288,12 +303,22 @@ impl FileTree { let target_info = FileMetadata::new_from_path(dir, path)?; if info != &target_info { // Save the source path for changes - changes.insert(info.source.as_ref().unwrap_or(path).clone()); + changes.insert(path.clone()); + if let Some(src) = info.source.as_ref() { + if src != path { + source_map.insert(path.clone(), src.clone()); + } + } } } _ => { // If a file became a directory - changes.insert(info.source.as_ref().unwrap_or(path).clone()); + changes.insert(path.clone()); + if let Some(src) = info.source.as_ref() { + if src != path { + source_map.insert(path.clone(), src.clone()); + } + } } } } else { @@ -304,6 +329,7 @@ impl FileTree { additions: HashSet::new(), removals, changes, + source_map, }) } } @@ -474,9 +500,13 @@ pub(crate) fn apply_diff( } // Write changed or new files to temp dir or temp file for pathstr in diff.changes.iter().chain(diff.additions.iter()) { - let src_path = Utf8Path::new(pathstr); - let path = get_dest_efi_path(src_path); - let (first_dir, first_dir_tmp) = get_first_dir(&path)?; + let path = Utf8Path::new(pathstr); + let copy_src = if let Some(src) = diff.source_map.get(pathstr) { + Utf8Path::new(src) + } else { + path + }; + let (first_dir, first_dir_tmp) = get_first_dir(path)?; let mut path_tmp = Utf8PathBuf::from(&first_dir_tmp); if first_dir != path { if !destdir.exists(&first_dir_tmp)? && destdir.exists(first_dir.as_std_path())? { @@ -497,8 +527,8 @@ pub(crate) fn apply_diff( } updates.insert(first_dir, first_dir_tmp); srcdir - .copy_file_at(src_path.as_std_path(), destdir, path_tmp.as_std_path()) - .with_context(|| format!("copying {:?} to {:?}", src_path, path_tmp))?; + .copy_file_at(copy_src.as_std_path(), destdir, path_tmp.as_std_path()) + .with_context(|| format!("copying {:?} to {:?}", copy_src, path_tmp))?; } // do local exchange or rename @@ -889,4 +919,44 @@ mod tests { } Ok(()) } + + /// Test that apply_diff() uses source_map to copy from the original + /// source path when it differs from the destination key. + #[test] + fn test_apply_diff_source_map() -> Result<()> { + let tmpd = tempfile::tempdir()?; + let p = tmpd.path(); + let src = p.join("src"); + let dst = p.join("dst"); + std::fs::create_dir(&src)?; + std::fs::create_dir(&dst)?; + + let src_dir = openat::Dir::open(&src)?; + let dst_dir = openat::Dir::open(&dst)?; + + // "original/data.bin" -> "remapped/data.bin" via source_map + src_dir.ensure_dir_all("original", 0o755)?; + src_dir.write_file("original/data.bin", 0o644)?; + + let mut additions = HashSet::new(); + additions.insert("remapped/data.bin".to_string()); + + let mut source_map = HashMap::new(); + source_map.insert( + "remapped/data.bin".to_string(), + "original/data.bin".to_string(), + ); + + let diff = FileTreeDiff { + additions, + removals: HashSet::new(), + changes: HashSet::new(), + source_map, + }; + + apply_diff(&src_dir, &dst_dir, &diff, None)?; + + assert!(dst_dir.exists("remapped/data.bin")?); + Ok(()) + } } From c8c881109cfd4772a2511d32558e6cf2bb010161 Mon Sep 17 00:00:00 2001 From: Javier Martinez Canillas Date: Sun, 22 Mar 2026 21:56:40 +0100 Subject: [PATCH 2/5] filetree: Enforce deterministic update ordering in apply_diff() The atomic swap operations on the ESP are performed in arbitrary iteration order. If a system loses power mid-update, the ESP may be left with a mix of old and new content in an unpredictable combination, making failures harder to diagnose and reproduce. Sort the swap operations in a stable order: EFI vendor directories first, then other directories at the ESP root, then individual root-level files. This ensures EFI boot entries are updated before auxiliary content such as firmwares, minimizing the window where the ESP is in an inconsistent state. Also create parent directories before the swap so that new directory trees (that did not previously exist on the ESP) can be added. Assisted-by: Cursor (Claude Opus 4) --- src/filetree.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/src/filetree.rs b/src/filetree.rs index 923119c7..16469f1f 100644 --- a/src/filetree.rs +++ b/src/filetree.rs @@ -41,6 +41,12 @@ use openssl::hash::{Hasher, MessageDigest}; ))] use rustix::fd::BorrowedFd; use serde::{Deserialize, Serialize}; +#[cfg(any( + target_arch = "x86_64", + target_arch = "aarch64", + target_arch = "riscv64" +))] +use std::cmp::Ordering; #[allow(unused_imports)] use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt::Display; @@ -531,9 +537,49 @@ pub(crate) fn apply_diff( .with_context(|| format!("copying {:?} to {:?}", copy_src, path_tmp))?; } + // Sort updates to enforce stable order and atomic sequence + let mut update_list: Vec<_> = updates.iter().collect(); + update_list.sort_by(|(dst_a, tmp_a), (dst_b, tmp_b)| { + let a_is_efi = dst_a.starts_with("EFI"); + let b_is_efi = dst_b.starts_with("EFI"); + + // 1. EFI subfolders first + if a_is_efi && !b_is_efi { + return Ordering::Less; + } + if !a_is_efi && b_is_efi { + return Ordering::Greater; + } + + // 2. Directories next, then files + let a_is_dir = destdir + .metadata(tmp_a.as_str()) + .map(|m| m.is_dir()) + .unwrap_or(false); + let b_is_dir = destdir + .metadata(tmp_b.as_str()) + .map(|m| m.is_dir()) + .unwrap_or(false); + + if a_is_dir && !b_is_dir { + return Ordering::Less; + } + if !a_is_dir && b_is_dir { + return Ordering::Greater; + } + + // 3. Stable string order + dst_a.cmp(dst_b) + }); + // do local exchange or rename - for (dst, tmp) in updates.iter() { + for (dst, tmp) in update_list { let dst = dst.as_std_path(); + if let Some(parent) = dst.parent() { + if !parent.as_os_str().is_empty() { + destdir.ensure_dir_all(parent, DEFAULT_FILE_MODE)?; + } + } log::trace!("doing local exchange for {} and {:?}", tmp, dst); if destdir.exists(dst)? { destdir @@ -937,10 +983,19 @@ mod tests { // "original/data.bin" -> "remapped/data.bin" via source_map src_dir.ensure_dir_all("original", 0o755)?; src_dir.write_file("original/data.bin", 0o644)?; + // Additional files in subdirectories and at the root, + // added without remapping. + src_dir.ensure_dir_all("sub", 0o755)?; + src_dir.write_file("sub/foo.dat", 0o644)?; + src_dir.write_file("top.dat", 0o644)?; let mut additions = HashSet::new(); additions.insert("remapped/data.bin".to_string()); + additions.insert("sub/foo.dat".to_string()); + additions.insert("top.dat".to_string()); + // source_map: dest path -> source path, only needed when the + // two differ (here a prefix was added to the dest key). let mut source_map = HashMap::new(); source_map.insert( "remapped/data.bin".to_string(), @@ -957,6 +1012,8 @@ mod tests { apply_diff(&src_dir, &dst_dir, &diff, None)?; assert!(dst_dir.exists("remapped/data.bin")?); + assert!(dst_dir.exists("sub/foo.dat")?); + assert!(dst_dir.exists("top.dat")?); Ok(()) } } From 0351c0306fc0433b2d03a2a1629354ef877b5d11 Mon Sep 17 00:00:00 2001 From: Javier Martinez Canillas Date: Sun, 22 Mar 2026 21:56:43 +0100 Subject: [PATCH 3/5] filetree: Add new_from_dir_strip_prefix_for() for EFILIB layouts The /usr/lib/efi/ layout stores component files under a two-level prefix // (e.g. shim/15.8/EFI/fedora/shimaa64.efi). To compare this tree against the flat ESP layout, the prefix must be stripped so that keys match the destination paths (EFI/fedora/shimaa64.efi). Add a file tree constructor that walks a set of // subdirs, strips that prefix from each path, and records the original source path. This can be used by the source_map mechanism when applying diffs, to only include the desired version when multiple versions of a component coexist, avoiding the non-deterministic version mixing that currently exists. Assisted-by: Cursor (Claude Opus 4) --- src/filetree.rs | 70 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/filetree.rs b/src/filetree.rs index 16469f1f..227c30f1 100644 --- a/src/filetree.rs +++ b/src/filetree.rs @@ -215,6 +215,39 @@ impl FileTree { Ok(Self { children }) } + /// Like [`new_from_dir`] but only walks the specified + /// `//` subdirectories and strips that prefix from each + /// entry. Each entry in `prefixes` is a relative path like + /// `"shim/15.9-1"` that identifies the component version directory to + /// include. + /// + /// "FOO/1.0/EFI/vendor/foo.efi" -> "EFI/vendor/foo.efi" + /// "BAR/2.0/bar.dtb" -> "bar.dtb" + #[cfg(any( + target_arch = "x86_64", + target_arch = "aarch64", + target_arch = "riscv64" + ))] + #[allow(dead_code)] + pub(crate) fn new_from_dir_strip_prefix_for>( + dir: &openat::Dir, + prefixes: &[S], + ) -> Result { + let mut children = BTreeMap::new(); + for prefix_str in prefixes { + let prefix = prefix_str.as_ref(); + let sub = dir + .sub_dir(prefix) + .with_context(|| format!("opening component dir {}", prefix))?; + for (k, mut v) in Self::unsorted_from_dir(&sub)?.drain() { + let source = format!("{}/{}", prefix, k); + v.source = Some(source); + children.insert(k, v); + } + } + Ok(Self { children }) + } + /// Determine the changes *from* self to the updated tree #[cfg(any( target_arch = "x86_64", @@ -1016,4 +1049,41 @@ mod tests { assert!(dst_dir.exists("top.dat")?); Ok(()) } + + #[test] + fn test_new_from_dir_strip_prefix_for() -> Result<()> { + let tmpdir = tempfile::tempdir()?; + let root = tmpdir.path().join("root"); + + // Two-level // layout with an EFI subtree + // and a flat (non-EFI) component. + std::fs::create_dir_all(root.join("FOO/1.0/EFI/vendor"))?; + std::fs::write(root.join("FOO/1.0/EFI/vendor/foo.efi"), "foo data")?; + std::fs::create_dir_all(root.join("BAR/2.0"))?; + std::fs::write(root.join("BAR/2.0/bar.dtb"), "bar data")?; + std::fs::write(root.join("BAR/2.0/baz.bin"), "baz data")?; + + let dir = openat::Dir::open(&root)?; + + // With all prefixes, every component is included. + let ft = FileTree::new_from_dir_strip_prefix_for(&dir, &["FOO/1.0", "BAR/2.0"])?; + assert!(ft.children.contains_key("EFI/vendor/foo.efi")); + assert!(ft.children.contains_key("bar.dtb")); + assert!(ft.children.contains_key("baz.bin")); + assert_eq!( + ft.children["EFI/vendor/foo.efi"].source.as_deref(), + Some("FOO/1.0/EFI/vendor/foo.efi") + ); + assert_eq!( + ft.children["bar.dtb"].source.as_deref(), + Some("BAR/2.0/bar.dtb") + ); + + // With a subset of prefixes, only matching components are included. + let ft2 = FileTree::new_from_dir_strip_prefix_for(&dir, &["FOO/1.0"])?; + assert!(ft2.children.contains_key("EFI/vendor/foo.efi")); + assert!(!ft2.children.contains_key("bar.dtb")); + + Ok(()) + } } From d750b4e35965f460433cdb8cfb2f73e19a893370 Mon Sep 17 00:00:00 2001 From: Javier Martinez Canillas Date: Sun, 22 Mar 2026 21:56:45 +0100 Subject: [PATCH 4/5] efi: Operate on the ESP root to support non-EFI components bootupd currently assumes that all managed content lives under the EFI/ subdirectory of the ESP. Platforms (such as the Raspberry Pi) place its firmware blobs (device-tree files, GPU firmware, config.txt) directly in the ESP root, outside EFI/. These files cannot be managed by bootupd today. Widen the scope of all ESP operations (install, adopt, update, validate) to work on the ESP root rather than just the EFI/ subdirectory. Now, the file paths themselves determine their destination in the ESP. The keys starting with EFI/ are written under the EFI/ subdirectory as before, while all other files are placed directly at the ESP root. Assisted-by: Cursor (Claude Opus 4) --- src/efi.rs | 401 +++++++++++++++++++++++++++++++++++++++--------- src/filetree.rs | 17 +- 2 files changed, 347 insertions(+), 71 deletions(-) diff --git a/src/efi.rs b/src/efi.rs index 1022f4af..4c8e101f 100644 --- a/src/efi.rs +++ b/src/efi.rs @@ -30,6 +30,8 @@ use crate::freezethaw::fsfreeze_thaw_cycle; use crate::model::*; use crate::ostreeutil; use crate::util; +use uapi_version::Version; + use crate::{component::*, packagesystem::*}; use crate::{filetree, grubconfigs}; @@ -80,6 +82,11 @@ fn is_mount_point(path: &Path) -> Result { } } +/// Copy options that merge source contents into an existing destination +/// directory instead of creating a subdirectory (used for non-EFI +/// components like firmware that live at the ESP root). +const OPTIONS_MERGE: &[&str] = &["-rp", "--reflink=auto", "-T"]; + /// Return `true` if the system is booted via EFI pub(crate) fn is_efi_booted() -> Result { Path::new("/sys/firmware/efi") @@ -236,6 +243,65 @@ impl Efi { let device_path = device.path(); create_efi_boot_entry(&device_path, esp_part_num.trim(), &loader, &product_name) } + + fn ensure_efi_prefix(&self, mut ft: filetree::FileTree) -> filetree::FileTree { + let needs_prefix = + !ft.children.is_empty() && ft.children.keys().all(|k| !k.starts_with("EFI/")); + if needs_prefix { + ft.prepend_prefix("EFI"); + } + ft + } + + /// Build a `FileTree` from either the new `usr/lib/efi/` layout (when + /// pre-resolved components are provided) or the legacy + /// `usr/lib/bootupd/updates/EFI` directory. + /// + /// For the new layout, only the latest version of each component is + /// included so that duplicate destination keys cannot arise. + fn build_filetree( + &self, + root_dir: &openat::Dir, + components: Option<&[EFIComponent]>, + ) -> Result<(PathBuf, filetree::FileTree)> { + if let Some(components) = components { + let p = PathBuf::from(EFILIB); + let dir = root_dir + .sub_dir(&p) + .with_context(|| format!("opening {}", p.display()))?; + let latest = latest_versions(components); + let prefixes: Vec = latest + .iter() + .map(|c| format!("{}/{}", c.name, c.version)) + .collect(); + let ft = filetree::FileTree::new_from_dir_strip_prefix_for(&dir, &prefixes)?; + Ok((p, ft)) + } else { + let p = component_updatedirname(self); + let dir = root_dir + .sub_dir(&p) + .with_context(|| format!("opening {}", p.display()))?; + let mut ft = filetree::FileTree::new_from_dir(&dir).context("reading update dir")?; + ft = self.ensure_efi_prefix(ft); + Ok((p, ft)) + } + } + + /// Build the update `FileTree`, resolving EFI components from the + /// sysroot and delegating to `build_filetree`. + fn build_update_filetree( + &self, + sysroot: &openat::Dir, + sysroot_path: &Utf8Path, + ) -> Result<(PathBuf, filetree::FileTree)> { + let efilib_path = sysroot_path.join(EFILIB); + let components = if efilib_path.exists() { + get_efi_component_from_usr(sysroot_path, EFILIB)? + } else { + None + }; + self.build_filetree(sysroot, components.as_deref()) + } } #[context("Get product name")] @@ -344,9 +410,9 @@ impl Component for Efi { anyhow::bail!("Failed to find efi vendor"); }; - // destdir is /boot/efi/EFI + // destdir is /boot/efi let efidir = destdir - .sub_dir(&vendor) + .sub_dir(&format!("EFI/{}", vendor)) .with_context(|| format!("Opening EFI/{}", vendor))?; if !efidir.exists(grubconfigs::GRUBCONFIG_BACKUP)? { @@ -375,33 +441,25 @@ impl Component for Efi { return Ok(None); }; - let updated_path = { - let efilib_path = rootcxt.path.join(EFILIB); - if efilib_path.exists() && get_efi_component_from_usr(&rootcxt.path, EFILIB)?.is_some() - { - PathBuf::from(EFILIB) - } else { - component_updatedirname(self) - } - }; + let (updated_path, updatef) = + self.build_update_filetree(&rootcxt.sysroot, &rootcxt.path)?; let updated = rootcxt .sysroot .sub_dir(&updated_path) .with_context(|| format!("opening update dir {}", updated_path.display()))?; - let updatef = filetree::FileTree::new_from_dir(&updated).context("reading update dir")?; let esp_devices = esp_devices.unwrap_or_default(); for esp in esp_devices { let destpath = &self.ensure_mounted_esp(rootcxt.path.as_ref(), Path::new(&esp.path()))?; - let efidir = openat::Dir::open(&destpath.join("EFI")).context("opening EFI dir")?; - validate_esp_fstype(&efidir)?; + let destdir = openat::Dir::open(destpath).context("opening ESP dir")?; + validate_esp_fstype(&destdir)?; // For adoption, we should only touch files that we know about. - let diff = updatef.relative_diff_to(&efidir)?; + let diff = updatef.relative_diff_to(&destdir)?; log::trace!("applying adoption diff: {}", &diff); - filetree::apply_diff(&updated, &efidir, &diff, None) + filetree::apply_diff(&updated, &destdir, &diff, None) .context("applying filesystem changes")?; // Backup current config and install static config @@ -413,13 +471,13 @@ impl Component for Efi { ); } else { println!("ostree repo 'sysroot.bootloader' config option is not set yet"); - self.migrate_static_grub_config(rootcxt.path.as_str(), &efidir)?; + self.migrate_static_grub_config(rootcxt.path.as_str(), &destdir)?; }; } // Do the sync before unmount - fsfreeze_thaw_cycle(efidir.open_file(".")?)?; - drop(efidir); + fsfreeze_thaw_cycle(destdir.open_file(".")?)?; + drop(destdir); self.unmount().context("unmount after adopt")?; } Ok(Some(InstalledContent { @@ -480,27 +538,30 @@ impl Component for Efi { ) })?; - let efi_path = if let Some(efi_components) = efi_comps { + // Copy files to the ESP + if let Some(ref efi_components) = efi_comps { for efi in efi_components { - filetree::copy_dir_with_args(&src_dir, efi.path.as_str(), dest, OPTIONS)?; + if efi.has_efi_subdir { + filetree::copy_dir_with_args(&src_dir, efi.path.as_str(), dest, OPTIONS)?; + } else { + filetree::copy_dir_with_args(&src_dir, efi.path.as_str(), dest, OPTIONS_MERGE)?; + } } - EFILIB } else { let updates = component_updatedirname(self); let src = updates .to_str() .context("Include invalid UTF-8 characters in path")?; filetree::copy_dir_with_args(&src_dir, src, dest, OPTIONS)?; - &src.to_owned() }; - // Get filetree from efi path - let ft = crate::filetree::FileTree::new_from_dir(&src_dir.sub_dir(efi_path)?)?; + // Build the filetree from the update source + let (update_path, ft) = self.build_filetree(&src_dir, efi_comps.as_deref())?; + let efi_vendor_search = src_path.as_std_path().join(update_path); + if update_firmware { if let Some(dev) = device { - if let Some(vendordir) = - self.get_efi_vendor(src_path.join(efi_path).as_std_path())? - { + if let Some(vendordir) = self.get_efi_vendor(efi_vendor_search.as_path())? { self.update_firmware(dev, destd, &vendordir)? } } @@ -517,28 +578,23 @@ impl Component for Efi { rootcxt: &RootContext, current: &InstalledContent, ) -> Result { - let currentf = current + let mut currentf = current .filetree - .as_ref() + .clone() .ok_or_else(|| anyhow::anyhow!("No filetree for installed EFI found!"))?; + currentf = self.ensure_efi_prefix(currentf); let sysroot_dir = &rootcxt.sysroot; let updatemeta = self.query_update(sysroot_dir)?.expect("update available"); - let updated_path = { - let efilib_path = rootcxt.path.join(EFILIB); - if efilib_path.exists() && get_efi_component_from_usr(&rootcxt.path, EFILIB)?.is_some() - { - PathBuf::from(EFILIB) - } else { - component_updatedirname(self) - } - }; + + let (updated_path, updatef) = + self.build_update_filetree(&rootcxt.sysroot, &rootcxt.path)?; + + let diff = currentf.diff(&updatef)?; let updated = rootcxt .sysroot .sub_dir(&updated_path) .with_context(|| format!("opening update dir {}", updated_path.display()))?; - let updatef = filetree::FileTree::new_from_dir(&updated).context("reading update dir")?; - let diff = currentf.diff(&updatef)?; let Some(esp_devices) = rootcxt.device.find_colocated_esps()? else { anyhow::bail!("Failed to find all esp devices"); @@ -547,7 +603,7 @@ impl Component for Efi { for esp in esp_devices { let destpath = &self.ensure_mounted_esp(rootcxt.path.as_ref(), Path::new(&esp.path()))?; - let destdir = openat::Dir::open(&destpath.join("EFI")).context("opening EFI dir")?; + let destdir = openat::Dir::open(destpath).context("opening ESP dir")?; validate_esp_fstype(&destdir)?; log::trace!("applying diff: {}", &diff); filetree::apply_diff(&updated, &destdir, &diff, None) @@ -628,19 +684,20 @@ impl Component for Efi { if !is_efi_booted()? && esp_devices.is_none() { return Ok(ValidationResult::Skip); } - let currentf = current + let mut currentf = current .filetree - .as_ref() + .clone() .ok_or_else(|| anyhow::anyhow!("No filetree for installed EFI found!"))?; + currentf = self.ensure_efi_prefix(currentf); let mut errs = Vec::new(); let esp_devices = esp_devices.unwrap_or_default(); for esp in esp_devices.iter() { let destpath = &self.ensure_mounted_esp(Path::new("/"), Path::new(&esp.path()))?; - let efidir = openat::Dir::open(&destpath.join("EFI")) - .with_context(|| format!("opening EFI dir {}", destpath.display()))?; - let diff = currentf.relative_diff_to(&efidir)?; + let destdir = openat::Dir::open(destpath) + .with_context(|| format!("opening ESP dir {}", destpath.display()))?; + let diff = currentf.relative_diff_to(&destdir)?; for f in diff.changes.iter() { errs.push(format!("Changed: {}", f)); @@ -649,7 +706,7 @@ impl Component for Efi { errs.push(format!("Removed: {}", f)); } assert_eq!(diff.additions.len(), 0); - drop(efidir); + drop(destdir); self.unmount().context("unmount after validate")?; } @@ -806,9 +863,15 @@ pub struct EFIComponent { pub name: String, pub version: String, path: Utf8PathBuf, + has_efi_subdir: bool, } -/// Get EFIComponents from e.g. usr/lib/efi, like "usr/lib/efi///EFI" +/// Get EFIComponents from e.g. usr/lib/efi. +/// +/// Each component lives at `///`. When the version +/// directory contains an `EFI/` subdirectory the content is EFI-specific +/// (shim, grub, etc.) and gets the `EFI/` prefix on the ESP. Otherwise the +/// files are copied directly to the root of the ESP (e.g. RPi firmware). fn get_efi_component_from_usr<'a>( sysroot: &'a Utf8Path, usr_path: &'a str, @@ -816,34 +879,62 @@ fn get_efi_component_from_usr<'a>( let efilib_path = sysroot.join(usr_path); let skip_count = Utf8Path::new(usr_path).components().count(); - let mut components: Vec = WalkDir::new(&efilib_path) - .min_depth(3) // //EFI: so 3 levels down - .max_depth(3) - .into_iter() - .filter_map(|entry| { - let entry = entry.ok()?; - if !entry.file_type().is_dir() || entry.file_name() != "EFI" { - return None; - } + let mut components: Vec = Vec::new(); - let abs_path = entry.path(); - let rel_path = abs_path.strip_prefix(sysroot).ok()?; - let utf8_rel_path = Utf8PathBuf::from_path_buf(rel_path.to_path_buf()).ok()?; + for entry in WalkDir::new(&efilib_path) + .min_depth(2) + .max_depth(2) + .into_iter() + .filter_map(|e| e.ok()) + { + if !entry.file_type().is_dir() { + continue; + } - let mut components = utf8_rel_path.components(); + let abs_path = entry.path(); + let rel_path = match abs_path.strip_prefix(sysroot) { + Ok(p) => p, + Err(_) => continue, + }; + let utf8_rel_path = match Utf8PathBuf::from_path_buf(rel_path.to_path_buf()) { + Ok(p) => p, + Err(_) => continue, + }; - let name = components.nth(skip_count)?.to_string(); - let version = components.next()?.to_string(); + let mut comps = utf8_rel_path.components(); + let Some(name) = comps.nth(skip_count).map(|c| c.to_string()) else { + continue; + }; + let Some(version) = comps.next().map(|c| c.to_string()) else { + continue; + }; - Some(EFIComponent { + let efi_subdir = abs_path.join("EFI"); + if efi_subdir.exists() && efi_subdir.is_dir() { + components.push(EFIComponent { name, version, - path: utf8_rel_path, - }) - }) - .collect(); + path: utf8_rel_path.join("EFI"), + has_efi_subdir: true, + }); + } else { + let has_content = WalkDir::new(abs_path) + .min_depth(1) + .into_iter() + .filter_map(|e| e.ok()) + .any(|e| e.file_type().is_file()); + if has_content { + components.push(EFIComponent { + name, + version, + path: utf8_rel_path, + has_efi_subdir: false, + }); + } + } + } - if components.len() == 0 { + if components.is_empty() { return Ok(None); } components.sort_by(|a, b| a.name.cmp(&b.name)); @@ -851,6 +942,25 @@ fn get_efi_component_from_usr<'a>( Ok(Some(components)) } +/// Given a list of EFI components (potentially with multiple versions per +/// component name), return only the latest version for each name. +/// +/// Versions are compared lexicographically; this is sufficient for RPM EVR +/// strings where the epoch:version-release ordering is consistent. +fn latest_versions(components: &[EFIComponent]) -> Vec<&EFIComponent> { + let mut by_name: std::collections::HashMap<&str, &EFIComponent> = + std::collections::HashMap::new(); + for c in components { + let entry = by_name.entry(c.name.as_str()).or_insert(c); + if Version::from(c.version.as_str()) > Version::from(entry.version.as_str()) { + *entry = c; + } + } + let mut result: Vec<_> = by_name.into_values().collect(); + result.sort_by(|a, b| a.name.cmp(&b.name)); + result +} + /// Copy files from usr/lib/ostree-boot/efi/EFI to /usr/lib/efi/// fn transfer_ostree_boot_to_usr(sysroot: &Path) -> Result<()> { let ostreeboot_efi = Path::new(ostreeutil::BOOT_PREFIX).join("efi"); @@ -1043,11 +1153,13 @@ Boot0003* test"; name: "BAR".to_string(), version: "1.1".to_string(), path: Utf8PathBuf::from("usr/lib/efi/BAR/1.1/EFI"), + has_efi_subdir: true, }, EFIComponent { name: "FOO".to_string(), version: "1.1".to_string(), path: Utf8PathBuf::from("usr/lib/efi/FOO/1.1/EFI"), + has_efi_subdir: true, }, ]) ); @@ -1057,4 +1169,153 @@ Boot0003* test"; assert_eq!(efi_comps, None); Ok(()) } + + #[test] + fn test_get_efi_component_mixed() -> Result<()> { + let tmpdir = tempfile::tempdir()?; + let tpath = tmpdir.path(); + let efi_path = tpath.join("usr/lib/efi"); + + // EFI component (has EFI/ subdirectory) + let efi_comp_dir = efi_path.join("FOO/1.0/EFI/vendor"); + std::fs::create_dir_all(&efi_comp_dir)?; + std::fs::write(efi_comp_dir.join("foo.efi"), "foo data")?; + + // Non-EFI component (files directly in version dir) + let non_efi_dir = efi_path.join("BAR/2.0"); + std::fs::create_dir_all(&non_efi_dir)?; + std::fs::write(non_efi_dir.join("bar.dtb"), "bar data")?; + std::fs::write(non_efi_dir.join("baz.bin"), "baz data")?; + + // Empty version dir (should be ignored) + std::fs::create_dir_all(efi_path.join("EMPTY/1.0"))?; + + let utf8_tpath = + Utf8Path::from_path(tpath).ok_or_else(|| anyhow::anyhow!("Path is not valid UTF-8"))?; + let comps = get_efi_component_from_usr(utf8_tpath, EFILIB)?; + let comps = comps.expect("components should be found"); + + assert_eq!(comps.len(), 2); + assert_eq!(comps[0].name, "BAR"); + assert!(!comps[0].has_efi_subdir); + assert_eq!(comps[0].path, Utf8PathBuf::from("usr/lib/efi/BAR/2.0")); + + assert_eq!(comps[1].name, "FOO"); + assert!(comps[1].has_efi_subdir); + assert_eq!(comps[1].path, Utf8PathBuf::from("usr/lib/efi/FOO/1.0/EFI")); + Ok(()) + } + + #[test] + fn test_new_from_dir_strip_prefix() -> Result<()> { + let tmpdir = tempfile::tempdir()?; + let efilib = tmpdir.path().join("efilib"); + + // EFI component: FOO/1.0/EFI/vendor/foo.efi + std::fs::create_dir_all(efilib.join("FOO/1.0/EFI/vendor"))?; + std::fs::write(efilib.join("FOO/1.0/EFI/vendor/foo.efi"), "foo data")?; + + // Non-EFI component: BAR/2.0/{bar.dtb,baz.bin} + std::fs::create_dir_all(efilib.join("BAR/2.0"))?; + std::fs::write(efilib.join("BAR/2.0/bar.dtb"), "bar data")?; + std::fs::write(efilib.join("BAR/2.0/baz.bin"), "baz data")?; + + let dir = openat::Dir::open(&efilib)?; + let ft = filetree::FileTree::new_from_dir_strip_prefix_for(&dir, &["FOO/1.0", "BAR/2.0"])?; + + assert!(ft.children.contains_key("EFI/vendor/foo.efi")); + assert!(ft.children.contains_key("bar.dtb")); + assert!(ft.children.contains_key("baz.bin")); + + // Source paths should point back to the original relative paths + assert_eq!( + ft.children["EFI/vendor/foo.efi"].source.as_deref(), + Some("FOO/1.0/EFI/vendor/foo.efi") + ); + assert_eq!( + ft.children["bar.dtb"].source.as_deref(), + Some("BAR/2.0/bar.dtb") + ); + + // ensure_efi_prefix should NOT re-prefix (some keys already have EFI/) + let efi = Efi::default(); + let ft2 = efi.ensure_efi_prefix(ft.clone()); + assert_eq!( + ft.children.keys().collect::>(), + ft2.children.keys().collect::>() + ); + + Ok(()) + } + + #[test] + fn test_rpi4_esp_update_flow() -> Result<()> { + let tmpdir = tempfile::tempdir()?; + let p = tmpdir.path(); + + // Source directory simulating EFILIB with mixed EFI and non-EFI + // components (e.g. bootloader + root-level firmware) + let src = p.join("src"); + std::fs::create_dir_all(src.join("FOO/1.0/EFI/vendor"))?; + std::fs::write(src.join("FOO/1.0/EFI/vendor/foo.efi"), "foo data")?; + let fw_dir = src.join("BAR/2.0"); + std::fs::create_dir_all(&fw_dir)?; + std::fs::write(fw_dir.join("bar.dtb"), "bar data")?; + std::fs::write(fw_dir.join("baz.bin"), "baz data")?; + std::fs::write(fw_dir.join("quux.dat"), "quux data")?; + std::fs::write(fw_dir.join("conf.txt"), "conf data")?; + + let dst = p.join("dst"); + std::fs::create_dir_all(&dst)?; + + let src_dir = openat::Dir::open(&src)?; + let dst_dir = openat::Dir::open(&dst)?; + + let ft = + filetree::FileTree::new_from_dir_strip_prefix_for(&src_dir, &["FOO/1.0", "BAR/2.0"])?; + + assert!(ft.children.contains_key("EFI/vendor/foo.efi")); + assert!(ft.children.contains_key("bar.dtb")); + assert!(ft.children.contains_key("baz.bin")); + assert!(ft.children.contains_key("quux.dat")); + assert!(ft.children.contains_key("conf.txt")); + + // Install to empty ESP + let empty_ft = filetree::FileTree { + children: std::collections::BTreeMap::new(), + }; + let diff = empty_ft.diff(&ft)?; + assert_eq!(diff.additions.len(), 5); + + filetree::apply_diff(&src_dir, &dst_dir, &diff, None)?; + + assert!(dst_dir.exists("EFI/vendor/foo.efi")?); + assert!(dst_dir.exists("bar.dtb")?); + assert!(dst_dir.exists("baz.bin")?); + assert!(dst_dir.exists("quux.dat")?); + assert!(dst_dir.exists("conf.txt")?); + assert_eq!( + std::fs::read_to_string(dst.join("EFI/vendor/foo.efi"))?, + "foo data" + ); + assert_eq!(std::fs::read_to_string(dst.join("bar.dtb"))?, "bar data"); + + // Simulate update (change one non-EFI file) + std::fs::write(fw_dir.join("bar.dtb"), "bar data v2")?; + let ft_v2 = + filetree::FileTree::new_from_dir_strip_prefix_for(&src_dir, &["FOO/1.0", "BAR/2.0"])?; + + let diff2 = ft.diff(&ft_v2)?; + assert_eq!(diff2.changes.len(), 1); + assert!(diff2.changes.contains("bar.dtb")); + + filetree::apply_diff(&src_dir, &dst_dir, &diff2, None)?; + assert_eq!(std::fs::read_to_string(dst.join("bar.dtb"))?, "bar data v2"); + assert_eq!( + std::fs::read_to_string(dst.join("EFI/vendor/foo.efi"))?, + "foo data" + ); + + Ok(()) + } } diff --git a/src/filetree.rs b/src/filetree.rs index 227c30f1..05959b62 100644 --- a/src/filetree.rs +++ b/src/filetree.rs @@ -228,7 +228,6 @@ impl FileTree { target_arch = "aarch64", target_arch = "riscv64" ))] - #[allow(dead_code)] pub(crate) fn new_from_dir_strip_prefix_for>( dir: &openat::Dir, prefixes: &[S], @@ -248,6 +247,22 @@ impl FileTree { Ok(Self { children }) } + #[cfg(any( + target_arch = "x86_64", + target_arch = "aarch64", + target_arch = "riscv64" + ))] + pub(crate) fn prepend_prefix(&mut self, prefix: &str) { + let old = std::mem::take(&mut self.children); + let mut new_children = BTreeMap::new(); + for (k, v) in old { + let mut p = Utf8PathBuf::from(prefix); + p.push(k); + new_children.insert(p.into_string(), v); + } + self.children = new_children; + } + /// Determine the changes *from* self to the updated tree #[cfg(any( target_arch = "x86_64", From b1de2cb6d8ea61cfba4255a903e479fdf6e1c6a4 Mon Sep 17 00:00:00 2001 From: Javier Martinez Canillas Date: Sun, 22 Mar 2026 21:56:48 +0100 Subject: [PATCH 5/5] efi: Walk entire ostree-boot/efi/ tree in transfer_ostree_boot_to_usr() On OSTree-based deployments, the /boot/efi/ files are stored under the usr/lib/ostree-boot/efi/ directory, and must transferred to usr/lib/efi/ before bootupd can manage them. The transfer currently only walks the EFI/ sub-directory, so root-level firmware files are never picked up. It also splits the RPM package name on the first hyphen to derive the component directory name, which truncates names that contain hyphens (e.g. "bcm2711-firmware" becomes "bcm2711"). Walk the entire usr/lib/ostree-boot/efi/ tree so that root-level files are discovered alongside EFI boot entries, and use the full RPM package name as the component directory name. Closes: https://github.com/coreos/bootupd/issues/959 Assisted-by: Cursor (Claude Opus 4) --- src/efi.rs | 158 ++++++++++++++++++++------- tests/e2e-update/e2e-update-in-vm.sh | 2 +- tests/e2e-update/e2e-update.sh | 1 + tests/kola/test-bootupd | 19 +++- 4 files changed, 134 insertions(+), 46 deletions(-) diff --git a/src/efi.rs b/src/efi.rs index 4c8e101f..98e3be3a 100644 --- a/src/efi.rs +++ b/src/efi.rs @@ -635,11 +635,11 @@ impl Component for Efi { for p in cruft.iter() { ostreeboot.remove_all_optional(p)?; } - // Transfer ostree-boot EFI files to usr/lib/efi + // Transfer ostree-boot efi/ files to usr/lib/efi transfer_ostree_boot_to_usr(sysroot_path)?; - // Remove usr/lib/ostree-boot/efi/EFI dir (after transfer) or if it is empty - ostreeboot.remove_all_optional("efi/EFI")?; + // Remove the entire efi/ tree after transfer, or if it is empty + ostreeboot.remove_all_optional("efi")?; } if let Some(efi_components) = @@ -961,55 +961,73 @@ fn latest_versions(components: &[EFIComponent]) -> Vec<&EFIComponent> { result } -/// Copy files from usr/lib/ostree-boot/efi/EFI to /usr/lib/efi/// +/// Copy files from usr/lib/ostree-boot/efi/ to usr/lib/efi/// +/// +/// Walks the entire `efi/` directory (both `EFI/` subdirectories and +/// root-level firmware files) and uses `rpm -qf` to determine which +/// package owns each file so it can be placed in the right component +/// directory under `usr/lib/efi/`. fn transfer_ostree_boot_to_usr(sysroot: &Path) -> Result<()> { + transfer_ostree_boot_to_usr_impl(sysroot, |sysroot_path, filepath| { + let boot_filepath = Path::new("/boot/efi").join(filepath); + crate::packagesystem::query_file( + sysroot_path.to_str().unwrap(), + boot_filepath.to_str().unwrap(), + ) + }) +} + +/// Inner implementation that accepts a package-resolver callback so it +/// can be unit-tested without a real RPM database. +/// +/// `resolve_pkg(sysroot, filepath)` must return `" "`. +fn transfer_ostree_boot_to_usr_impl(sysroot: &Path, resolve_pkg: F) -> Result<()> +where + F: Fn(&Path, &Path) -> Result, +{ let ostreeboot_efi = Path::new(ostreeutil::BOOT_PREFIX).join("efi"); let ostreeboot_efi_path = sysroot.join(&ostreeboot_efi); - let efi = ostreeboot_efi_path.join("EFI"); - if !efi.exists() { + if !ostreeboot_efi_path.exists() { return Ok(()); } - for entry in WalkDir::new(&efi) { - let entry = entry?; - if entry.file_type().is_file() { - let entry_path = entry.path(); + let sysroot_dir = openat::Dir::open(sysroot)?; + // Source dir is usr/lib/ostree-boot/efi + let src = sysroot_dir + .sub_dir(&ostreeboot_efi) + .context("Opening ostree-boot/efi dir")?; - // get path EFI/{BOOT,}/ - let filepath = entry_path.strip_prefix(&ostreeboot_efi_path)?; - // get path /boot/efi/EFI/{BOOT,}/ - let boot_filepath = Path::new("/boot/efi").join(filepath); + for entry in WalkDir::new(&ostreeboot_efi_path) { + let entry = entry?; + if !entry.file_type().is_file() { + continue; + } - // Run `rpm -qf ` - let pkg = crate::packagesystem::query_file( - sysroot.to_str().unwrap(), - boot_filepath.to_str().unwrap(), - )?; + // get path relative to the efi/ root (e.g. EFI/BOOT/shim.efi or start4.elf) + let filepath = entry.path().strip_prefix(&ostreeboot_efi_path)?; - let (name, evr) = pkg.split_once(' ').unwrap(); - let component = name.split('-').next().unwrap_or(""); - // get path usr/lib/efi// - let efilib_path = Path::new(EFILIB).join(component).join(evr); + // Run `rpm -qf /boot/efi/` to find the owning package + let pkg = resolve_pkg(sysroot, filepath)?; - let sysroot_dir = openat::Dir::open(sysroot)?; - // Ensure dest parent directory exists - if let Some(parent) = efilib_path.join(filepath).parent() { - sysroot_dir.ensure_dir_all(parent, 0o755)?; - } + let (name, evr) = pkg + .split_once(' ') + .with_context(|| format!("parsing rpm output: {}", pkg))?; + // get path usr/lib/efi// + let efilib_path = Path::new(EFILIB).join(name).join(evr); - // Source dir is usr/lib/ostree-boot/efi - let src = sysroot_dir - .sub_dir(&ostreeboot_efi) - .context("Opening ostree-boot dir")?; - // Dest dir is usr/lib/efi// - let dest = sysroot_dir - .sub_dir(&efilib_path) - .context("Opening usr/lib/efi dir")?; - // Copy file from ostree-boot to usr/lib/efi - src.copy_file_at(filepath, &dest, filepath) - .context("Copying file to usr/lib/efi")?; + // Ensure dest parent directory exists + if let Some(parent) = efilib_path.join(filepath).parent() { + sysroot_dir.ensure_dir_all(parent, 0o755)?; } + + // Dest dir is usr/lib/efi// + let dest = sysroot_dir + .sub_dir(&efilib_path) + .context("Opening usr/lib/efi dir")?; + // Copy file from ostree-boot to usr/lib/efi + src.copy_file_at(filepath, &dest, filepath) + .context("Copying file to usr/lib/efi")?; } Ok(()) } @@ -1318,4 +1336,66 @@ Boot0003* test"; Ok(()) } + + #[test] + fn test_transfer_ostree_boot_to_usr() -> Result<()> { + let tmpdir = tempfile::tempdir()?; + let sysroot = tmpdir.path(); + + // Simulate usr/lib/ostree-boot/efi/ with both EFI/ and root-level files + let efi_dir = sysroot.join("usr/lib/ostree-boot/efi"); + std::fs::create_dir_all(efi_dir.join("EFI/vendor"))?; + std::fs::write(efi_dir.join("EFI/vendor/foo.efi"), "foo data")?; + std::fs::create_dir_all(efi_dir.join("EFI/BOOT"))?; + std::fs::write(efi_dir.join("EFI/BOOT/BOOTAA64.EFI"), "boot data")?; + // Root-level files + std::fs::write(efi_dir.join("bar.dtb"), "bar data")?; + std::fs::write(efi_dir.join("baz.bin"), "baz data")?; + std::fs::create_dir_all(efi_dir.join("sub"))?; + std::fs::write(efi_dir.join("sub/quux.dat"), "quux data")?; + + // Ensure the destination base directory exists + std::fs::create_dir_all(sysroot.join(EFILIB))?; + + // Fake resolver: EFI files belong to "FOO 1.0", root-level + // files to "BAR 2.0" + let resolve = |_sysroot: &Path, filepath: &Path| -> Result { + let s = filepath.to_str().unwrap(); + if s.starts_with("EFI") { + Ok("FOO 1.0".to_string()) + } else { + Ok("BAR 2.0".to_string()) + } + }; + + transfer_ostree_boot_to_usr_impl(sysroot, resolve)?; + + // EFI files should be under EFILIB/FOO/1.0/EFI/... + let foo_base = sysroot.join("usr/lib/efi/FOO/1.0"); + assert_eq!( + std::fs::read_to_string(foo_base.join("EFI/vendor/foo.efi"))?, + "foo data" + ); + assert_eq!( + std::fs::read_to_string(foo_base.join("EFI/BOOT/BOOTAA64.EFI"))?, + "boot data" + ); + + // Root-level files should be under EFILIB/BAR/2.0/ + let bar_base = sysroot.join("usr/lib/efi/BAR/2.0"); + assert_eq!( + std::fs::read_to_string(bar_base.join("bar.dtb"))?, + "bar data" + ); + assert_eq!( + std::fs::read_to_string(bar_base.join("baz.bin"))?, + "baz data" + ); + assert_eq!( + std::fs::read_to_string(bar_base.join("sub/quux.dat"))?, + "quux data" + ); + + Ok(()) + } } diff --git a/tests/e2e-update/e2e-update-in-vm.sh b/tests/e2e-update/e2e-update-in-vm.sh index 389f50cd..475f6af3 100755 --- a/tests/e2e-update/e2e-update-in-vm.sh +++ b/tests/e2e-update/e2e-update-in-vm.sh @@ -53,7 +53,7 @@ ok validate bootupctl status | tee out.txt assert_file_has_content_literal out.txt 'Component EFI' -assert_file_has_content_literal out.txt ' Installed: grub2-1:' +assert_file_has_content out.txt " Installed: ${TARGET_GRUB_NAME}-" assert_not_file_has_content out.txt ' Installed:.*test_bootupd_payload' assert_not_file_has_content out.txt ' Installed:.*'"${TARGET_GRUB_EVR}" assert_file_has_content out.txt 'Update: Available:.*'"${TARGET_GRUB_EVR}" diff --git a/tests/e2e-update/e2e-update.sh b/tests/e2e-update/e2e-update.sh index f2a25f4f..f4bd062b 100755 --- a/tests/e2e-update/e2e-update.sh +++ b/tests/e2e-update/e2e-update.sh @@ -74,6 +74,7 @@ grubarch= case $(arch) in x86_64) grubarch=x64;; aarch64) grubarch=aa64;; + riscv64) grubarch=riscv64;; *) fatal "Unhandled arch $(arch)";; esac target_grub_name=grub2-efi-${grubarch} diff --git a/tests/kola/test-bootupd b/tests/kola/test-bootupd index 3d557267..38d8d96a 100755 --- a/tests/kola/test-bootupd +++ b/tests/kola/test-bootupd @@ -58,8 +58,15 @@ test -n "${shimx64_path}" && test -n "${grubx64_path}" bootupctl status > out.txt evr=$(rpm -q grub2-common --qf '%{EVR}') +case $(arch) in + x86_64) grubarch=x64;; + aarch64) grubarch=aa64;; + riscv64) grubarch=riscv64;; + *) fatal "Unhandled arch $(arch)";; +esac +grub_efi_name=grub2-efi-${grubarch} assert_file_has_content_literal out.txt 'Component EFI' -assert_file_has_content_literal out.txt ' Installed: grub2-'"${evr}" +assert_file_has_content_literal out.txt ' Installed: '"${grub_efi_name}"'-'"${evr}" assert_file_has_content_literal out.txt 'Update: At latest version' assert_file_has_content out.txt '^CoreOS aleph version:' ok status @@ -92,21 +99,21 @@ mv new.json ${bootupdir}/EFI.json bootupctl status | tee out.txt assert_file_has_content_literal out.txt 'Component EFI' -assert_file_has_content_literal out.txt ' Installed: grub2-'"${evr}" -assert_not_file_has_content out.txt ' Installed: grub2-.*,test' +assert_file_has_content_literal out.txt ' Installed: '"${grub_efi_name}"'-'"${evr}" +assert_not_file_has_content out.txt ' Installed: '"${grub_efi_name}"'-.*,test' assert_file_has_content_literal out.txt 'Update: Available:' ok update avail bootupctl status --json > status.json jq -r '.components.EFI.installed.version' < status.json > installed.txt -assert_file_has_content installed.txt '^grub2-'"${evr}" +assert_file_has_content installed.txt '^'"${grub_efi_name}"'-'"${evr}" bootupctl update | tee out.txt -assert_file_has_content out.txt 'Updated EFI: grub2-.*,test' +assert_file_has_content out.txt 'Updated EFI: '"${grub_efi_name}"'-.*,test' bootupctl status > out.txt assert_file_has_content_literal out.txt 'Component EFI' -assert_file_has_content out.txt ' Installed: grub2-.*,test' +assert_file_has_content out.txt ' Installed: '"${grub_efi_name}"'-.*,test' assert_file_has_content_literal out.txt 'Update: At latest version' ok status after update