From b155d4879cef41f9eab2dd6f89f2adc00ab633ba Mon Sep 17 00:00:00 2001 From: saypaul Date: Fri, 25 Jul 2025 19:40:42 +0530 Subject: [PATCH 1/5] feat(extend_payload_to_esp):stage payload to /usr for installation extend_payload_to_esp will move the all contents of the to /usr This features gives user ability to stage payloads so that install and update can install the contents to esp.It will move all the files and sub dirs under the path provide by user to /usr/lib/efi/firmware///EFI/ Incase of an exiting path extend-payload will only keep the latest version of the payload. example usage: `bootupctl backend extend_payload_to_esp /usr/lib/mydata` will move the content of the dir to `/usr/lib/efi/firmware///EFI/` stores metadata at `/usr/lib/efi/firmware///EFI.json`. Added test_extend_payload_to_esp unit test. --- src/bios.rs | 3 + src/cli/bootupctl.rs | 5 + src/cli/bootupd.rs | 26 +++++ src/component.rs | 3 + src/efi.rs | 263 ++++++++++++++++++++++++++++++++++++++++++- src/model.rs | 3 + 6 files changed, 300 insertions(+), 3 deletions(-) diff --git a/src/bios.rs b/src/bios.rs index 4b34c207..2de51f7b 100644 --- a/src/bios.rs +++ b/src/bios.rs @@ -268,4 +268,7 @@ impl Component for Bios { fn get_efi_vendor(&self, _: &openat::Dir) -> Result> { Ok(None) } + fn extend_payload(&self, _: &str, _: &str) -> Result> { + Ok(None) + } } diff --git a/src/cli/bootupctl.rs b/src/cli/bootupctl.rs index 4e831feb..d39bb820 100644 --- a/src/cli/bootupctl.rs +++ b/src/cli/bootupctl.rs @@ -73,6 +73,8 @@ pub enum CtlBackend { Generate(super::bootupd::GenerateOpts), #[clap(name = "install", hide = true)] Install(super::bootupd::InstallOpts), + #[clap(name = "extend-payload-to-esp", hide = true)] + ExtendPayload(super::bootupd::ExtendPayloadOpts), } #[derive(Debug, Parser)] @@ -109,6 +111,9 @@ impl CtlCommand { CtlVerb::Backend(CtlBackend::Install(opts)) => { super::bootupd::DCommand::run_install(opts) } + CtlVerb::Backend(CtlBackend::ExtendPayload(opts)) => { + super::bootupd::DCommand::run_extend_payload(opts) + } CtlVerb::MigrateStaticGrubConfig => Self::run_migrate_static_grub_config(), } } diff --git a/src/cli/bootupd.rs b/src/cli/bootupd.rs index 4a6b8cf1..1520ef2d 100644 --- a/src/cli/bootupd.rs +++ b/src/cli/bootupd.rs @@ -35,6 +35,11 @@ pub enum DVerb { GenerateUpdateMetadata(GenerateOpts), #[clap(name = "install", about = "Install components")] Install(InstallOpts), + #[clap( + name = "extend-payload-to-esp", + about = "Extend bootloader payload with additional files" + )] + ExtendPayload(ExtendPayloadOpts), } #[derive(Debug, Parser)] @@ -82,12 +87,20 @@ pub struct GenerateOpts { sysroot: Option, } +#[derive(Debug, Parser)] +pub struct ExtendPayloadOpts { + /// Source directory containing files to add + #[clap(value_parser)] + src_root: String, +} + impl DCommand { /// Run CLI application. pub fn run(self) -> Result<()> { match self.cmd { DVerb::Install(opts) => Self::run_install(opts), DVerb::GenerateUpdateMetadata(opts) => Self::run_generate_meta(opts), + DVerb::ExtendPayload(opts) => Self::run_extend_payload(opts), } } @@ -122,4 +135,17 @@ impl DCommand { .context("boot data installation failed")?; Ok(()) } + + pub(crate) fn run_extend_payload(opts: ExtendPayloadOpts) -> Result<()> { + let components = crate::bootupd::get_components(); + let sysroot = "/"; + for component in components.values() { + if let Some(updated) = component.extend_payload(sysroot, &opts.src_root)? { + if updated { + println!("Extended payload for {} successfully", component.name()); + } + } + } + Ok(()) + } } diff --git a/src/component.rs b/src/component.rs index 5ca32df5..d73b1a5e 100644 --- a/src/component.rs +++ b/src/component.rs @@ -79,6 +79,9 @@ pub(crate) trait Component { /// Locating efi vendor dir fn get_efi_vendor(&self, sysroot: &openat::Dir) -> Result>; + + /// Extending payload from input dir + fn extend_payload(&self, sysroot: &str, src_root: &str) -> Result>; } /// Given a component name, create an implementation. diff --git a/src/efi.rs b/src/efi.rs index ee42e261..a3238501 100644 --- a/src/efi.rs +++ b/src/efi.rs @@ -9,9 +9,8 @@ use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf}; use std::process::Command; -use anyhow::{bail, Context, Result}; -use bootc_internal_utils::CommandRunExt; -use camino::{Utf8Path, Utf8PathBuf}; +use anyhow::{anyhow, bail, Context, Result}; +use bootc_utils::CommandRunExt; use cap_std::fs::Dir; use cap_std_ext::cap_std; use chrono::prelude::*; @@ -506,6 +505,99 @@ impl Component for Efi { Ok(meta) } + fn extend_payload(&self, sysroot_path: &str, src_input: &str) -> Result> { + let dest_efidir_base = Path::new(sysroot_path).join("usr/lib/efi").join("firmware"); + + let src_input_path = Path::new(src_input); + let path_to_query = if src_input_path.is_dir() { + WalkDir::new(src_input_path) + .into_iter() + .filter_map(|e| e.ok()) + .find(|e| e.file_type().is_file()) + .map(|e| e.path().to_path_buf()) + .ok_or_else(|| anyhow!("No file found in directory {}", src_input))? + } else { + src_input_path.to_path_buf() + }; + + let meta_from_src = packagesystem::query_files(sysroot_path, [path_to_query]) + .context(format!("Querying RPM metadata for {:?}", src_input_path))?; + + let version_string_part = + meta_from_src.version.split(',').next().ok_or_else(|| { + anyhow!("RPM query returned an empty or malformed version string") + })?; + + let parts: Vec<&str> = version_string_part.split('-').collect(); + let (pkg_name, version_release_str) = if parts.len() >= 3 { + ( + parts[0].to_string(), + format!( + "{}-{}", + parts[parts.len() - 2], + parts[parts.len() - 1] + .split('.') + .next() + .unwrap_or(parts[parts.len() - 1]) + ), + ) + } else { + anyhow::bail!("Unexpected RPM version string format"); + }; + + // Clean up any existing firmware versions for this package to ensure only one version + let pkg_firmware_dir = dest_efidir_base.join(&pkg_name); + if pkg_firmware_dir.exists() { + log::debug!( + "Removing existing firmware versions for package: {}", + pkg_name + ); + std::fs::remove_dir_all(&pkg_firmware_dir).with_context(|| { + format!( + "Failed to remove existing firmware directory {:?}", + pkg_firmware_dir + ) + })?; + } + + // Use the flattened destination path + let final_dest_path = dest_efidir_base.join(&pkg_name).join(&version_release_str); + std::fs::create_dir_all(&final_dest_path)?; + + let efi_dest_path = final_dest_path.join("EFI"); + std::fs::create_dir_all(&efi_dest_path)?; + + // Copy the payload files + let src_metadata = std::fs::metadata(src_input_path)?; + if src_metadata.is_dir() { + Command::new("cp") + .args([ + "-rp", + &format!("{}/.", src_input), + efi_dest_path.to_str().unwrap(), + ]) + .run() + .with_context(|| { + format!( + "Failed to copy contents of {:?} to {:?}", + src_input, &efi_dest_path + ) + })?; + } else { + Command::new("cp") + .args(["-p", src_input, efi_dest_path.to_str().unwrap()]) + .run()?; + } + + // Create the metadata file for the firmware + let firmware_meta_path = final_dest_path.join("EFI.json"); + let meta_file = std::fs::File::create(firmware_meta_path)?; + serde_json::to_writer(meta_file, &meta_from_src)?; + log::debug!("Wrote firmware metadata for {}", pkg_name); + + Ok(Some(true)) + } + fn query_update(&self, sysroot: &openat::Dir) -> Result> { get_component_update(sysroot, self) } @@ -858,6 +950,171 @@ Boot0003* test"; paths, ["usr/lib/efi/FOO/1.1/EFI", "usr/lib/efi/BAR/1.1/EFI"] ); + } + #[test] + fn test_extend_payload() -> Result<()> { + use std::fs; + use tempfile::TempDir; + + let temp_sysroot = TempDir::new()?; + let temp_src = TempDir::new()?; + + let sysroot_path = temp_sysroot.path().to_str().unwrap(); + let src_path = temp_src.path().to_str().unwrap(); + + // mockup data source: /usr/share/uboot/rpi/ + // content: u-boot.bin, overlays/i2c.dtb + // mockup rpm: uboot-images-2023.04-2.fc42.noarch + // mockup rpm_db: /usr/lib/sysimage/rpm/Packages + let src_uboot_dir = temp_src + .path() + .join("usr") + .join("share") + .join("uboot") + .join("rpi"); + fs::create_dir_all(&src_uboot_dir)?; + + let src_overlays_dir = src_uboot_dir.join("overlays"); + fs::create_dir_all(&src_overlays_dir)?; + + // Create content files + let uboot_bin = src_uboot_dir.join("u-boot.bin"); + fs::write(&uboot_bin, b"uboot binary content")?; + let overlay_dtb = src_overlays_dir.join("i2c.dtb"); + fs::write(&overlay_dtb, b"device tree overlay content")?; + + // Create a mockup RPM database structure + let rpm_db_dir = temp_sysroot + .path() + .join("usr") + .join("lib") + .join("sysimage") + .join("rpm"); + fs::create_dir_all(&rpm_db_dir)?; + fs::write(rpm_db_dir.join("Packages"), b"fake rpm database file")?; + + // Create a mock rpm script that returns uboot-images package data + let mock_rpm_dir = TempDir::new()?; + let mock_rpm_script = mock_rpm_dir.path().join("rpm"); + + let mock_script_content = r#"#!/bin/bash +# Mock rpm script for testing +if [[ "$*" == *"-q"* ]] && [[ "$*" == *"-f"* ]]; then + # Return mock uboot-images package data in the expected format: nevra,buildtime + echo "uboot-images-2023.04-2.fc42.noarch,1681234567" + exit 0 +fi +# For any other rpm command, just fail +exit 1 +"# + .to_string(); + + fs::write(&mock_rpm_script, mock_script_content)?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&mock_rpm_script)?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(&mock_rpm_script, perms)?; + } + + let original_path = std::env::var("PATH").unwrap_or_default(); + let new_path = format!("{}:{}", mock_rpm_dir.path().display(), original_path); + std::env::set_var("PATH", &new_path); + + // Test extend_payload + let efi_component = Efi::default(); + + let result = efi_component + .extend_payload(sysroot_path, &format!("{}/usr/share/uboot/rpi", src_path)); + + // validation beigins + std::env::set_var("PATH", &original_path); + match result { + Ok(Some(true)) => { + // Verify the files were copied to the right location + let firmware_base = temp_sysroot + .path() + .join("usr") + .join("lib") + .join("efi") + .join("firmware"); + assert!( + firmware_base.exists(), + "Firmware base directory should be created" + ); + + // Look for the uboot package directory (package name is extracted from first part) + // From "uboot-images-2023.04-2.fc42.noarch" -> package: "uboot", version: "2023.04-2" + let uboot_dir = firmware_base.join("uboot").join("2023.04-2"); + assert!( + uboot_dir.exists(), + "Package directory uboot/2023.04-2 should be created" + ); + + // Files should be copied to the EFI subdirectory + let efi_dir = uboot_dir.join("EFI"); + assert!(efi_dir.exists(), "EFI directory should be created"); + + // Verify that u-boot.bin was copied to EFI subdirectory + let copied_uboot_bin = efi_dir.join("u-boot.bin"); + assert!(copied_uboot_bin.exists(), "u-boot.bin should be copied"); + let uboot_content = fs::read_to_string(&copied_uboot_bin)?; + assert_eq!( + uboot_content, "uboot binary content", + "u-boot.bin content should be preserved" + ); + + // Verify that overlays directory and i2c.dtb were copied to EFI subdirectory + let copied_overlays_dir = efi_dir.join("overlays"); + assert!( + copied_overlays_dir.exists(), + "overlays directory should be copied" + ); + + let copied_overlay_dtb = copied_overlays_dir.join("i2c.dtb"); + assert!(copied_overlay_dtb.exists(), "i2c.dtb should be copied"); + let overlay_content = fs::read_to_string(&copied_overlay_dtb)?; + assert_eq!( + overlay_content, "device tree overlay content", + "i2c.dtb content should be preserved" + ); + + // Verify the EFI.json metadata + let metadata_file = uboot_dir.join("EFI.json"); + assert!( + metadata_file.exists(), + "EFI.json metadata file should be created" + ); + let metadata_content = fs::read_to_string(&metadata_file)?; + let parsed: ContentMetadata = serde_json::from_str(&metadata_content)?; + assert!( + parsed + .version + .contains("uboot-images-2023.04-2.fc42.noarch"), + "Metadata should contain uboot package" + ); + + println!("extend_payload test completed successfully!"); + println!("✓ Files copied to: {:?}", efi_dir); + println!("✓ u-boot.bin: {:?}", copied_uboot_bin); + println!("✓ overlays/i2c.dtb: {:?}", copied_overlay_dtb); + println!("✓ Metadata created: {:?}", metadata_file); + println!("✓ Package version: {}", parsed.version); + } + Ok(Some(false)) => { + panic!("extend_payload returned false - expected success"); + } + Ok(None) => { + panic!("extend_payload returned None - expected success"); + } + Err(e) => { + panic!("extend_payload failed when it should have succeeded: {}", e); + } + } + Ok(()) } } + diff --git a/src/model.rs b/src/model.rs index 86b866a9..de929ecf 100644 --- a/src/model.rs +++ b/src/model.rs @@ -39,6 +39,9 @@ pub(crate) struct InstalledContent { pub(crate) filetree: Option, /// The version this was originally adopted from pub(crate) adopted_from: Option, + /// Nested map of supplemental firmware payloads. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub(crate) firmware: BTreeMap>, } /// Will be serialized into /boot/bootupd-state.json From 292237b75e0e40a60a067c9a2b95c00d0c90004f Mon Sep 17 00:00:00 2001 From: saypaul Date: Fri, 25 Jul 2025 19:54:55 +0530 Subject: [PATCH 2/5] update(efi::install)install from extend-payload-path Added additional flow in efi::install to read from the path created by extend_payload_to_esp command and install to /boot/efi --- src/bios.rs | 4 +++ src/efi.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++- src/model_legacy.rs | 7 ++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/bios.rs b/src/bios.rs index 2de51f7b..b2a4918b 100644 --- a/src/bios.rs +++ b/src/bios.rs @@ -3,6 +3,7 @@ use camino::Utf8PathBuf; use openat_ext::OpenatDirExt; #[cfg(target_arch = "powerpc64")] use std::borrow::Cow; +use std::collections::BTreeMap; use std::io::prelude::*; use std::path::Path; use std::process::Command; @@ -122,6 +123,7 @@ impl Component for Bios { meta, filetree: None, adopted_from: None, + firmware: BTreeMap::new(), }) } @@ -236,6 +238,7 @@ impl Component for Bios { meta: update.clone(), filetree: None, adopted_from: Some(meta.version), + firmware: BTreeMap::new(), })) } @@ -258,6 +261,7 @@ impl Component for Bios { meta: updatemeta, filetree: None, adopted_from, + firmware: BTreeMap::new(), }) } diff --git a/src/efi.rs b/src/efi.rs index a3238501..ae641a01 100644 --- a/src/efi.rs +++ b/src/efi.rs @@ -5,6 +5,7 @@ */ use std::cell::RefCell; +use std::collections::BTreeMap; use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf}; use std::process::Command; @@ -332,6 +333,7 @@ impl Component for Efi { meta: updatemeta.clone(), filetree: Some(updatef), adopted_from: Some(meta.version), + firmware: BTreeMap::new(), })) } @@ -376,15 +378,70 @@ impl Component for Efi { .arg(destpath) .current_dir(format!("/proc/self/fd/{}", src_root.as_raw_fd())) .run()?; + + let mut found_firmware = BTreeMap::new(); + // Scan and install supplemental firmware + let firmware_base_dir_path = Path::new("usr/lib/efi/firmware"); + if src_root.exists(firmware_base_dir_path)? { + let firmware_base_dir = src_root.sub_dir(firmware_base_dir_path)?; + for pkg_entry in firmware_base_dir.list_dir(".")?.flatten() { + if firmware_base_dir.get_file_type(&pkg_entry)? != openat::SimpleType::Dir { + continue; + } + let pkg_name = pkg_entry.file_name().to_string_lossy().to_string(); + let pkg_dir = firmware_base_dir.sub_dir(pkg_entry.file_name())?; + + let mut versions: Vec<_> = pkg_dir.list_dir(".")?.filter_map(Result::ok).collect(); + versions.sort_by_key(|e| e.file_name().to_owned()); + + if let Some(ver_entry) = versions.pop() { + let ver_dir = pkg_dir.sub_dir(ver_entry.file_name())?; + let meta_path = Path::new("EFI.json"); + + if ver_dir.exists(meta_path)? { + log::debug!( + "Found supplemental firmware: {}/{}", + pkg_name, + ver_entry.file_name().to_string_lossy() + ); + let firmware_meta: ContentMetadata = + serde_json::from_reader(ver_dir.open_file(meta_path)?)?; + let payload_src_dir = ver_dir.sub_dir("EFI")?; + let firmware_filetree = + crate::filetree::FileTree::new_from_dir(&payload_src_dir)?; + // copy all by applying a diff with a empty filetree + let empty_filetree = filetree::FileTree { + children: Default::default(), + }; + let diff = empty_filetree.diff(&firmware_filetree)?; + filetree::apply_diff(&payload_src_dir, destd, &diff, None) + .context("applying supplemental firmware")?; + + found_firmware.insert( + pkg_name.clone(), + Box::new(InstalledContent { + meta: firmware_meta, + filetree: Some(firmware_filetree), + adopted_from: None, + firmware: BTreeMap::new(), + }), + ); + } + } + } + } + if update_firmware { - if let Some(vendordir) = self.get_efi_vendor(&src_root)? { + if let Some(vendordir) = self.get_efi_vendor(src_root)? { self.update_firmware(device, destd, &vendordir)? } } + Ok(InstalledContent { meta, filetree: Some(ft), adopted_from: None, + firmware: found_firmware, }) } @@ -428,6 +485,7 @@ impl Component for Efi { meta: updatemeta, filetree: Some(updatef), adopted_from, + firmware: BTreeMap::new(), }) } diff --git a/src/model_legacy.rs b/src/model_legacy.rs index 0487d2dc..1a68e3f7 100644 --- a/src/model_legacy.rs +++ b/src/model_legacy.rs @@ -30,6 +30,8 @@ pub(crate) struct InstalledContent01 { pub(crate) meta: ContentMetadata01, /// File tree pub(crate) filetree: Option, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub(crate) firmware: BTreeMap>, } /// Will be serialized into /boot/bootupd-state.json @@ -59,6 +61,11 @@ impl InstalledContent01 { meta: self.meta.upconvert(), filetree: self.filetree, adopted_from: None, + firmware: self + .firmware + .into_iter() + .map(|(k, v)| (k, Box::new(v.upconvert()))) + .collect(), } } } From b40a1f2120fb4616ef779b1a6217ced2aa6ea570 Mon Sep 17 00:00:00 2001 From: saypaul Date: Fri, 25 Jul 2025 19:56:30 +0530 Subject: [PATCH 3/5] update(efi::update)update extended payload path efi::update is updated to read from path created by extend_payload_to_esp and update the firmware. --- src/efi.rs | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 99 insertions(+), 5 deletions(-) diff --git a/src/efi.rs b/src/efi.rs index ae641a01..15ef162c 100644 --- a/src/efi.rs +++ b/src/efi.rs @@ -11,7 +11,8 @@ use std::path::{Path, PathBuf}; use std::process::Command; use anyhow::{anyhow, bail, Context, Result}; -use bootc_utils::CommandRunExt; +use bootc_internal_utils::CommandRunExt; +use camino::{Utf8Path, Utf8PathBuf}; use cap_std::fs::Dir; use cap_std_ext::cap_std; use chrono::prelude::*; @@ -465,14 +466,106 @@ impl Component for Efi { let Some(esp_devices) = blockdev::find_colocated_esps(&rootcxt.devices)? else { anyhow::bail!("Failed to find all esp devices"); }; + let mut updated_firmware = BTreeMap::new(); - for esp in esp_devices { - let destpath = &self.ensure_mounted_esp(rootcxt.path.as_ref(), Path::new(&esp))?; + for esp in esp_devices.iter() { + let destpath = &self.ensure_mounted_esp(rootcxt.path.as_ref(), Path::new(esp))?; let destdir = openat::Dir::open(&destpath.join("EFI")).context("opening EFI dir")?; validate_esp_fstype(&destdir)?; log::trace!("applying diff: {}", &diff); filetree::apply_diff(&updated, &destdir, &diff, None) .context("applying filesystem changes")?; + // update firmware + let firmware_base_dir_path = Path::new("usr/lib/efi/firmware"); + + let available_payloads = { + let mut payloads = BTreeMap::new(); + if rootcxt.sysroot.exists(firmware_base_dir_path)? { + let firmware_base_dir = rootcxt.sysroot.sub_dir(firmware_base_dir_path)?; + for pkg_entry in firmware_base_dir.list_dir(".")?.flatten() { + if firmware_base_dir.get_file_type(&pkg_entry)? == openat::SimpleType::Dir { + let pkg_name = pkg_entry.file_name().to_string_lossy().to_string(); + payloads.insert(pkg_name, pkg_entry.file_name().to_owned()); + } + } + } + payloads + }; + + let old_keys: std::collections::HashSet<_> = current.firmware.keys().collect(); + let new_keys: std::collections::HashSet<_> = available_payloads.keys().collect(); + let all_keys: std::collections::HashSet<_> = old_keys.union(&new_keys).collect(); + + // determine if it should be added, updated, or removed. + for pkg_name in all_keys { + let old_payload = current.firmware.get(*pkg_name); + let new_payload_path = available_payloads.get(*pkg_name); + + let (diff, src_dir, new_content) = match (old_payload, new_payload_path) { + // Payload exists in both old state and new source. + (Some(old), Some(new_path)) => { + let new_ver_dir = rootcxt + .sysroot + .sub_dir(firmware_base_dir_path)? + .sub_dir(new_path.as_os_str())?; + let new_payload_dir = new_ver_dir.sub_dir("EFI")?; + let new_ft = crate::filetree::FileTree::new_from_dir(&new_payload_dir)?; + let old_ft = old.filetree.as_ref().unwrap_or(&new_ft); + let diff = old_ft.diff(&new_ft)?; + + let meta: ContentMetadata = + serde_json::from_reader(new_ver_dir.open_file("EFI.json")?)?; + let content = Box::new(InstalledContent { + meta, + filetree: Some(new_ft), + adopted_from: None, + firmware: BTreeMap::new(), + }); + (diff, Some(new_payload_dir), Some(content)) + } + // add as old payload is none + (None, Some(new_path)) => { + let new_ver_dir = rootcxt + .sysroot + .sub_dir(firmware_base_dir_path)? + .sub_dir(new_path.as_os_str())?; + let new_payload_dir = new_ver_dir.sub_dir("EFI")?; + let new_ft = crate::filetree::FileTree::new_from_dir(&new_payload_dir)?; + let empty_ft = crate::filetree::FileTree { + children: BTreeMap::new(), + }; + let diff = empty_ft.diff(&new_ft)?; + + let meta: ContentMetadata = + serde_json::from_reader(new_ver_dir.open_file("EFI.json")?)?; + let content = Box::new(InstalledContent { + meta, + filetree: Some(new_ft), + adopted_from: None, + firmware: BTreeMap::new(), + }); + (diff, Some(new_payload_dir), Some(content)) + } + // continue with old firmware + (Some(_old), None) => continue, + // Should not happen. + (None, None) => continue, + }; + + //apply the above diffs + for esp in esp_devices.iter() { + let destpath = + &self.ensure_mounted_esp(rootcxt.path.as_ref(), Path::new(esp))?; + let destdir = openat::Dir::open(destpath)?; + let src_dir = src_dir.as_ref().unwrap_or(&destdir); + filetree::apply_diff(src_dir, &destdir, &diff, None) + .context(format!("applying firmware diff for {}", pkg_name))?; + } + + if let Some(content) = new_content { + updated_firmware.insert(pkg_name.to_string(), content); + } + } // Do the sync before unmount fsfreeze_thaw_cycle(destdir.open_file(".")?)?; @@ -485,7 +578,7 @@ impl Component for Efi { meta: updatemeta, filetree: Some(updatef), adopted_from, - firmware: BTreeMap::new(), + firmware: updated_firmware, }) } @@ -1008,7 +1101,9 @@ Boot0003* test"; paths, ["usr/lib/efi/FOO/1.1/EFI", "usr/lib/efi/BAR/1.1/EFI"] ); + Ok(()) } + #[test] fn test_extend_payload() -> Result<()> { use std::fs; @@ -1175,4 +1270,3 @@ exit 1 Ok(()) } } - From 9afa751c605978571cfa579ac5b82194cfb971dc Mon Sep 17 00:00:00 2001 From: saypaul Date: Thu, 31 Jul 2025 15:57:47 +0530 Subject: [PATCH 4/5] ci: added tests for install and update for extended payload --- .github/workflows/ci.yml | 192 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb37b0b5..4dcb3652 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,3 +139,195 @@ jobs: bootupctl backend generate-update-metadata -vvv cat ${updates}/EFI.json | jq ' + + - name: Test install after extend-payload + run: | + set -xeuo pipefail + sudo truncate -s 5G myimage-extend.raw + sudo podman run --rm --privileged -v .:/target --pid=host --security-opt label=disable \ + -v /var/lib/containers:/var/lib/containers \ + -v /dev:/dev \ + localhost/bootupd:latest bash -c ' + # Create test firmware directory and files + mkdir -p /usr/share/uboot/rpi/overlays + echo "test uboot binary content" > /usr/share/uboot/rpi/u-boot.bin + echo "i2c device tree overlay" > /usr/share/uboot/rpi/overlays/i2c.dtb + + # Create a fake RPM database for testing + mkdir -p /usr/lib/sysimage/rpm + echo "fake rpm database" > /usr/lib/sysimage/rpm/Packages + + # Create mock rpm script using a here-document for clarity + cat << '"'EOT'"' > /usr/local/bin/rpm + #!/bin/bash + if [[ "$*" == *"-q"* ]] && [[ "$*" == *"-f"* ]]; then + echo "uboot-images-2023.04-2.fc42.noarch,1681234567" + exit 0 + fi + exec /usr/bin/rpm.orig "$@" + EOT + + # Backup original rpm and make our mock executable + cp /usr/bin/rpm /usr/bin/rpm.orig + chmod +x /usr/local/bin/rpm + export PATH="/usr/local/bin:$PATH" + + # Run extend-payload-to-esp first + bootupctl backend extend-payload-to-esp /usr/share/uboot/rpi + + # Verify firmware was extended correctly + test -d /usr/lib/efi/firmware || { echo "firmware directory not created"; exit 1; } + firmware_ver_dir=$(find /usr/lib/efi/firmware -name "*2023.04*" -type d | head -1) + test -n "${firmware_ver_dir}" || { echo "firmware version directory not found"; exit 1; } + test -f "${firmware_ver_dir}/EFI/u-boot.bin" || { echo "u-boot.bin not copied"; exit 1; } + echo "✓ extend-payload completed successfully" + # Now test install to disk with extended firmware + bootc install to-disk --skip-fetch-check \ + --disable-selinux --generic-image --via-loopback /target/myimage-extend.raw + ' + + # Verify firmware files were installed to ESP + sudo losetup -P -f myimage-extend.raw + device=$(losetup -a myimage-extend.raw --output NAME -n) + esp_part=$(sudo sfdisk -l -J "${device}" | jq -r '.partitiontable.partitions[] | select(.type == "C12A7328-F81F-11D2-BA4B-00A0C93EC93B").node') + sudo mount "${esp_part}" /mnt/ + + # Check that firmware files were copied to ESP during install + if sudo test -f /mnt/u-boot.bin; then + sudo grep -q "test uboot binary content" /mnt/u-boot.bin || { echo "u-boot.bin content incorrect on ESP"; exit 1; } + echo "✓ Firmware files correctly installed to ESP" + else + echo "Note: u-boot.bin not found on ESP (firmware install integration may need work)" + fi + + sudo umount /mnt + sudo losetup -D "${device}" + sudo rm -f myimage-extend.raw + + - name: Test update after extend-payload + run: | + set -xeuo pipefail + sudo truncate -s 5G myimage-update.raw + sudo podman run --rm --privileged -v .:/target --pid=host --security-opt label=disable \ + -v /var/lib/containers:/var/lib/containers \ + -v /dev:/dev \ + localhost/bootupd:latest bash -c ' + # Create initial test firmware directory and files + mkdir -p /usr/share/uboot/rpi/overlays + echo "initial uboot binary content v1.0" > /usr/share/uboot/rpi/u-boot.bin + echo "initial i2c device tree overlay" > /usr/share/uboot/rpi/overlays/i2c.dtb + + # Create a fake RPM database for testing + mkdir -p /usr/lib/sysimage/rpm + echo "fake rpm database" > /usr/lib/sysimage/rpm/Packages + + # Create mock rpm script that returns initial package data + cat << '"'EOT'"' > /usr/local/bin/rpm + #!/bin/bash + if [[ "$*" == *"-q"* ]] && [[ "$*" == *"-f"* ]]; then + echo "uboot-images-2023.04-1.fc42.noarch,1681234567" + exit 0 + fi + exec /usr/bin/rpm.orig "$@" + EOT + + # Backup original rpm and make our mock executable + cp /usr/bin/rpm /usr/bin/rpm.orig + chmod +x /usr/local/bin/rpm + export PATH="/usr/local/bin:$PATH" + + # Run initial extend-payload-to-esp + bootupctl backend extend-payload-to-esp /usr/share/uboot/rpi + + # Verify initial firmware was extended correctly + test -d /usr/lib/efi/firmware || { echo "firmware directory not created"; exit 1; } + firmware_ver_dir=$(find /usr/lib/efi/firmware -name "*2023.04-1*" -type d | head -1) + test -n "${firmware_ver_dir}" || { echo "initial firmware version directory not found"; exit 1; } + test -f "${firmware_ver_dir}/EFI/u-boot.bin" || { echo "initial u-boot.bin not copied"; exit 1; } + grep -q "initial uboot binary content v1.0" "${firmware_ver_dir}/EFI/u-boot.bin" + echo "✓ initial extend-payload completed successfully" + + # Install to disk with initial firmware + bootc install to-disk --skip-fetch-check \ + --disable-selinux --generic-image --via-loopback /target/myimage-update.raw + + # Now simulate a firmware update by creating new firmware files + echo "updated uboot binary content v2.0" > /usr/share/uboot/rpi/u-boot.bin + echo "updated i2c device tree overlay" > /usr/share/uboot/rpi/overlays/i2c.dtb + echo "new overlay for v2" > /usr/share/uboot/rpi/overlays/spi.dtb + + # Update mock rpm to return new version + cat << '"'EOT'"' > /usr/local/bin/rpm + #!/bin/bash + if [[ "$*" == *"-q"* ]] && [[ "$*" == *"-f"* ]]; then + echo "uboot-images-2023.04-2.fc42.noarch,1681234999" + exit 0 + fi + exec /usr/bin/rpm.orig "$@" + EOT + + # Run updated extend-payload-to-esp + bootupctl backend extend-payload-to-esp /usr/share/uboot/rpi + + # Verify updated firmware was extended correctly (only v2.0 should exist now) + updated_firmware_ver_dir=$(find /usr/lib/efi/firmware -name "*2023.04-2*" -type d | head -1) + test -n "${updated_firmware_ver_dir}" || { echo "updated firmware version directory not found"; exit 1; } + test -f "${updated_firmware_ver_dir}/EFI/u-boot.bin" || { echo "updated u-boot.bin not copied"; exit 1; } + grep -q "updated uboot binary content v2.0" "${updated_firmware_ver_dir}/EFI/u-boot.bin" + test -f "${updated_firmware_ver_dir}/EFI/overlays/spi.dtb" || { echo "new spi.dtb not copied"; exit 1; } + + # Verify old version (2023.04-1) was removed + old_firmware_ver_dir=$(find /usr/lib/efi/firmware -name "*2023.04-1*" -type d | head -1) + test -z "${old_firmware_ver_dir}" || { echo "old firmware version should have been removed but still exists: ${old_firmware_ver_dir}"; exit 1; } + + echo "✓ updated extend-payload completed successfully (old version cleaned up)" + + # Run bootupctl update to apply the updated firmware to ESP + bootupctl update + echo "✓ bootupctl update completed successfully" + ' + + # Verify updated firmware files were applied to ESP + sudo losetup -P -f myimage-update.raw + device=$(losetup -a myimage-update.raw --output NAME -n) + esp_part=$(sudo sfdisk -l -J "${device}" | jq -r '.partitiontable.partitions[] | select(.type == "C12A7328-F81F-11D2-BA4B-00A0C93EC93B").node') + sudo mount "${esp_part}" /mnt/ + + # Check that updated firmware files were applied to ESP during update + if sudo test -f /mnt/u-boot.bin; then + sudo grep -q "updated uboot binary content v2.0" /mnt/u-boot.bin || { echo "u-boot.bin was not updated on ESP"; exit 1; } + echo "✓ Updated firmware files correctly applied to ESP" + else + echo "Warning: u-boot.bin not found on ESP after update" + exit 1 + fi + + # Check that new overlay file was also copied + if sudo test -f /mnt/overlays/spi.dtb; then + sudo grep -q "new overlay for v2" /mnt/overlays/spi.dtb || { echo "spi.dtb content incorrect on ESP"; exit 1; } + echo "✓ New overlay files correctly applied to ESP" + else + echo "Warning: new spi.dtb not found on ESP after update" + fi + + # Verify checksums and state integrity + echo "🔍 Validating firmware checksums and state integrity..." + sudo podman run --rm --privileged -v .:/target --pid=host --security-opt label=disable \ + -v /var/lib/containers:/var/lib/containers \ + -v /dev:/dev \ + localhost/bootupd:latest bash -c ' + # Run bootupctl validate to check all checksums + bootupctl validate || { echo "bootupctl validate failed - checksum mismatch detected"; exit 1; } + echo "✓ All file checksums validated successfully" + + # Check that bootupd-state.json reflects the updated firmware + if test -f /boot/bootupd-state.json; then + # Verify firmware is tracked in state + jq -e ".installed.EFI.firmware.uboot" /boot/bootupd-state.json >/dev/null || { echo "Updated firmware not found in bootupd-state.json"; exit 1; } + echo "✓ Updated firmware properly tracked in bootupd-state.json" + fi + ' + + sudo umount /mnt + sudo losetup -D "${device}" + sudo rm -f myimage-update.raw \ No newline at end of file From fdf4b312035adcb5dfd83c4fc354a7121ae2f1f4 Mon Sep 17 00:00:00 2001 From: saypaul Date: Mon, 4 Aug 2025 20:48:04 +0530 Subject: [PATCH 5/5] verbose --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4dcb3652..178666ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -183,6 +183,7 @@ jobs: echo "✓ extend-payload completed successfully" # Now test install to disk with extended firmware bootc install to-disk --skip-fetch-check \ + -vv \ --disable-selinux --generic-image --via-loopback /target/myimage-extend.raw '