diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb37b0b5..178666ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,3 +139,196 @@ 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 \ + -vv \ + --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 diff --git a/src/bios.rs b/src/bios.rs index 4b34c207..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(), }) } @@ -268,4 +272,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..15ef162c 100644 --- a/src/efi.rs +++ b/src/efi.rs @@ -5,11 +5,12 @@ */ use std::cell::RefCell; +use std::collections::BTreeMap; use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf}; use std::process::Command; -use anyhow::{bail, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use bootc_internal_utils::CommandRunExt; use camino::{Utf8Path, Utf8PathBuf}; use cap_std::fs::Dir; @@ -333,6 +334,7 @@ impl Component for Efi { meta: updatemeta.clone(), filetree: Some(updatef), adopted_from: Some(meta.version), + firmware: BTreeMap::new(), })) } @@ -377,15 +379,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, }) } @@ -409,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(".")?)?; @@ -429,6 +578,7 @@ impl Component for Efi { meta: updatemeta, filetree: Some(updatef), adopted_from, + firmware: updated_firmware, }) } @@ -506,6 +656,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) } @@ -860,4 +1103,170 @@ Boot0003* test"; ); Ok(()) } + + #[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 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(), } } }