Skip to content

Commit 7e5c8f5

Browse files
committed
feat(extend_payload_to_esp):stage payload to /usr for installation
Bootup extend_payload_to_esp <path> will move the it 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. example usage: `bootupctl backend extend_payload_to_esp /usr/lib/mydata` will move the content of the dir to `/usr/lib/efi/firmware/<name>/<version>/EFI/` stores metadata at `/usr/lib/efi/firmware/<name>/<version>/EFI.json`. Added test_extend_payload_to_esp unit test.
1 parent b50ffc4 commit 7e5c8f5

File tree

6 files changed

+285
-1
lines changed

6 files changed

+285
-1
lines changed

src/bios.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,4 +267,7 @@ impl Component for Bios {
267267
fn get_efi_vendor(&self, _: &openat::Dir) -> Result<Option<String>> {
268268
Ok(None)
269269
}
270+
fn extend_payload(&self, _: &str, _: &str) -> Result<Option<bool>> {
271+
Ok(None)
272+
}
270273
}

src/cli/bootupctl.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ pub enum CtlBackend {
7373
Generate(super::bootupd::GenerateOpts),
7474
#[clap(name = "install", hide = true)]
7575
Install(super::bootupd::InstallOpts),
76+
#[clap(name = "extend-payload-to-esp", hide = true)]
77+
ExtendPayload(super::bootupd::ExtendPayloadOpts),
7678
}
7779

7880
#[derive(Debug, Parser)]
@@ -109,6 +111,9 @@ impl CtlCommand {
109111
CtlVerb::Backend(CtlBackend::Install(opts)) => {
110112
super::bootupd::DCommand::run_install(opts)
111113
}
114+
CtlVerb::Backend(CtlBackend::ExtendPayload(opts)) => {
115+
super::bootupd::DCommand::run_extend_payload(opts)
116+
}
112117
CtlVerb::MigrateStaticGrubConfig => Self::run_migrate_static_grub_config(),
113118
}
114119
}

src/cli/bootupd.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ pub enum DVerb {
3535
GenerateUpdateMetadata(GenerateOpts),
3636
#[clap(name = "install", about = "Install components")]
3737
Install(InstallOpts),
38+
#[clap(
39+
name = "extend-payload-to-esp",
40+
about = "Extend bootloader payload with additional files"
41+
)]
42+
ExtendPayload(ExtendPayloadOpts),
3843
}
3944

4045
#[derive(Debug, Parser)]
@@ -82,12 +87,20 @@ pub struct GenerateOpts {
8287
sysroot: Option<String>,
8388
}
8489

90+
#[derive(Debug, Parser)]
91+
pub struct ExtendPayloadOpts {
92+
/// Source directory containing files to add
93+
#[clap(value_parser)]
94+
src_root: String,
95+
}
96+
8597
impl DCommand {
8698
/// Run CLI application.
8799
pub fn run(self) -> Result<()> {
88100
match self.cmd {
89101
DVerb::Install(opts) => Self::run_install(opts),
90102
DVerb::GenerateUpdateMetadata(opts) => Self::run_generate_meta(opts),
103+
DVerb::ExtendPayload(opts) => Self::run_extend_payload(opts),
91104
}
92105
}
93106

@@ -122,4 +135,17 @@ impl DCommand {
122135
.context("boot data installation failed")?;
123136
Ok(())
124137
}
138+
139+
pub(crate) fn run_extend_payload(opts: ExtendPayloadOpts) -> Result<()> {
140+
let components = crate::bootupd::get_components();
141+
let sysroot = "/";
142+
for component in components.values() {
143+
if let Some(updated) = component.extend_payload(sysroot, &opts.src_root)? {
144+
if updated {
145+
println!("Extended payload for {} successfully", component.name());
146+
}
147+
}
148+
}
149+
Ok(())
150+
}
125151
}

src/component.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ pub(crate) trait Component {
7979

8080
/// Locating efi vendor dir
8181
fn get_efi_vendor(&self, sysroot: &openat::Dir) -> Result<Option<String>>;
82+
83+
/// Extending payload from input dir
84+
fn extend_payload(&self, sysroot: &str, src_root: &str) -> Result<Option<bool>>;
8285
}
8386

8487
/// Given a component name, create an implementation.

src/efi.rs

Lines changed: 245 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::os::unix::io::AsRawFd;
99
use std::path::{Path, PathBuf};
1010
use std::process::Command;
1111

12-
use anyhow::{bail, Context, Result};
12+
use anyhow::{anyhow, bail, Context, Result};
1313
use bootc_utils::CommandRunExt;
1414
use cap_std::fs::Dir;
1515
use cap_std_ext::cap_std;
@@ -462,6 +462,84 @@ impl Component for Efi {
462462
Ok(meta)
463463
}
464464

465+
fn extend_payload(&self, sysroot_path: &str, src_input: &str) -> Result<Option<bool>> {
466+
let dest_efidir_base = Path::new(sysroot_path).join("usr/lib/efi").join("firmware");
467+
468+
let src_input_path = Path::new(src_input);
469+
let path_to_query = if src_input_path.is_dir() {
470+
WalkDir::new(src_input_path)
471+
.into_iter()
472+
.filter_map(|e| e.ok())
473+
.find(|e| e.file_type().is_file())
474+
.map(|e| e.path().to_path_buf())
475+
.ok_or_else(|| anyhow!("No file found in directory {}", src_input))?
476+
} else {
477+
src_input_path.to_path_buf()
478+
};
479+
480+
let meta_from_src = packagesystem::query_files(sysroot_path, [path_to_query])
481+
.context(format!("Querying RPM metadata for {:?}", src_input_path))?;
482+
483+
let version_string_part =
484+
meta_from_src.version.split(',').next().ok_or_else(|| {
485+
anyhow!("RPM query returned an empty or malformed version string")
486+
})?;
487+
488+
let parts: Vec<&str> = version_string_part.split('-').collect();
489+
let (pkg_name, version_release_str) = if parts.len() >= 3 {
490+
(
491+
parts[0].to_string(),
492+
format!(
493+
"{}-{}",
494+
parts[parts.len() - 2],
495+
parts[parts.len() - 1]
496+
.split('.')
497+
.next()
498+
.unwrap_or(parts[parts.len() - 1])
499+
),
500+
)
501+
} else {
502+
anyhow::bail!("Unexpected RPM version string format");
503+
};
504+
505+
// Use the flattened destination path
506+
let final_dest_path = dest_efidir_base.join(&pkg_name).join(&version_release_str);
507+
std::fs::create_dir_all(&final_dest_path)?;
508+
509+
let efi_dest_path = final_dest_path.join("EFI");
510+
std::fs::create_dir_all(&efi_dest_path)?;
511+
512+
// Copy the payload files
513+
let src_metadata = std::fs::metadata(src_input_path)?;
514+
if src_metadata.is_dir() {
515+
Command::new("cp")
516+
.args([
517+
"-rp",
518+
&format!("{}/.", src_input),
519+
efi_dest_path.to_str().unwrap(),
520+
])
521+
.run()
522+
.with_context(|| {
523+
format!(
524+
"Failed to copy contents of {:?} to {:?}",
525+
src_input, &efi_dest_path
526+
)
527+
})?;
528+
} else {
529+
Command::new("cp")
530+
.args(["-p", src_input, efi_dest_path.to_str().unwrap()])
531+
.run()?;
532+
}
533+
534+
// Create the metadata file for the firmware
535+
let firmware_meta_path = final_dest_path.join("EFI.json");
536+
let meta_file = std::fs::File::create(firmware_meta_path)?;
537+
serde_json::to_writer(meta_file, &meta_from_src)?;
538+
log::debug!("Wrote firmware metadata for {}", pkg_name);
539+
540+
Ok(Some(true))
541+
}
542+
465543
fn query_update(&self, sysroot: &openat::Dir) -> Result<Option<ContentMetadata>> {
466544
get_component_update(sysroot, self)
467545
}
@@ -774,4 +852,170 @@ Boot0003* test";
774852
}
775853
Ok(())
776854
}
855+
#[test]
856+
fn test_extend_payload() -> Result<()> {
857+
use std::fs;
858+
use tempfile::TempDir;
859+
860+
let temp_sysroot = TempDir::new()?;
861+
let temp_src = TempDir::new()?;
862+
863+
let sysroot_path = temp_sysroot.path().to_str().unwrap();
864+
let src_path = temp_src.path().to_str().unwrap();
865+
866+
// mockup data source: /usr/share/uboot/rpi/
867+
// content: u-boot.bin, overlays/i2c.dtb
868+
// mockup rpm: uboot-images-2023.04-2.fc42.noarch
869+
// mockup rpm_db: /usr/lib/sysimage/rpm/Packages
870+
let src_uboot_dir = temp_src
871+
.path()
872+
.join("usr")
873+
.join("share")
874+
.join("uboot")
875+
.join("rpi");
876+
fs::create_dir_all(&src_uboot_dir)?;
877+
878+
let src_overlays_dir = src_uboot_dir.join("overlays");
879+
fs::create_dir_all(&src_overlays_dir)?;
880+
881+
// Create content files
882+
let uboot_bin = src_uboot_dir.join("u-boot.bin");
883+
fs::write(&uboot_bin, b"uboot binary content")?;
884+
let overlay_dtb = src_overlays_dir.join("i2c.dtb");
885+
fs::write(&overlay_dtb, b"device tree overlay content")?;
886+
887+
// Create a mockup RPM database structure
888+
let rpm_db_dir = temp_sysroot
889+
.path()
890+
.join("usr")
891+
.join("lib")
892+
.join("sysimage")
893+
.join("rpm");
894+
fs::create_dir_all(&rpm_db_dir)?;
895+
fs::write(rpm_db_dir.join("Packages"), b"fake rpm database file")?;
896+
897+
// Create a mock rpm script that returns uboot-images package data
898+
let mock_rpm_dir = TempDir::new()?;
899+
let mock_rpm_script = mock_rpm_dir.path().join("rpm");
900+
901+
let mock_script_content = format!(
902+
r#"#!/bin/bash
903+
# Mock rpm script for testing
904+
if [[ "$*" == *"-q"* ]] && [[ "$*" == *"-f"* ]]; then
905+
# Return mock uboot-images package data in the expected format: nevra,buildtime
906+
echo "uboot-images-2023.04-2.fc42.noarch,1681234567"
907+
exit 0
908+
fi
909+
# For any other rpm command, just fail
910+
exit 1
911+
"#
912+
);
913+
914+
fs::write(&mock_rpm_script, mock_script_content)?;
915+
916+
#[cfg(unix)]
917+
{
918+
use std::os::unix::fs::PermissionsExt;
919+
let mut perms = fs::metadata(&mock_rpm_script)?.permissions();
920+
perms.set_mode(0o755);
921+
fs::set_permissions(&mock_rpm_script, perms)?;
922+
}
923+
924+
let original_path = std::env::var("PATH").unwrap_or_default();
925+
let new_path = format!("{}:{}", mock_rpm_dir.path().display(), original_path);
926+
std::env::set_var("PATH", &new_path);
927+
928+
// Test extend_payload
929+
let efi_component = Efi::default();
930+
931+
let result = efi_component
932+
.extend_payload(sysroot_path, &format!("{}/usr/share/uboot/rpi", src_path));
933+
934+
// validation beigins
935+
std::env::set_var("PATH", &original_path);
936+
match result {
937+
Ok(Some(true)) => {
938+
// Verify the files were copied to the right location
939+
let firmware_base = temp_sysroot
940+
.path()
941+
.join("usr")
942+
.join("lib")
943+
.join("efi")
944+
.join("firmware");
945+
assert!(
946+
firmware_base.exists(),
947+
"Firmware base directory should be created"
948+
);
949+
950+
// Look for the uboot package directory (package name is extracted from first part)
951+
// From "uboot-images-2023.04-2.fc42.noarch" -> package: "uboot", version: "2023.04-2"
952+
let uboot_dir = firmware_base.join("uboot").join("2023.04-2");
953+
assert!(
954+
uboot_dir.exists(),
955+
"Package directory uboot/2023.04-2 should be created"
956+
);
957+
958+
// Files should be copied to the EFI subdirectory
959+
let efi_dir = uboot_dir.join("EFI");
960+
assert!(efi_dir.exists(), "EFI directory should be created");
961+
962+
// Verify that u-boot.bin was copied to EFI subdirectory
963+
let copied_uboot_bin = efi_dir.join("u-boot.bin");
964+
assert!(copied_uboot_bin.exists(), "u-boot.bin should be copied");
965+
let uboot_content = fs::read_to_string(&copied_uboot_bin)?;
966+
assert_eq!(
967+
uboot_content, "uboot binary content",
968+
"u-boot.bin content should be preserved"
969+
);
970+
971+
// Verify that overlays directory and i2c.dtb were copied to EFI subdirectory
972+
let copied_overlays_dir = efi_dir.join("overlays");
973+
assert!(
974+
copied_overlays_dir.exists(),
975+
"overlays directory should be copied"
976+
);
977+
978+
let copied_overlay_dtb = copied_overlays_dir.join("i2c.dtb");
979+
assert!(copied_overlay_dtb.exists(), "i2c.dtb should be copied");
980+
let overlay_content = fs::read_to_string(&copied_overlay_dtb)?;
981+
assert_eq!(
982+
overlay_content, "device tree overlay content",
983+
"i2c.dtb content should be preserved"
984+
);
985+
986+
// Verify the EFI.json metadata
987+
let metadata_file = uboot_dir.join("EFI.json");
988+
assert!(
989+
metadata_file.exists(),
990+
"EFI.json metadata file should be created"
991+
);
992+
let metadata_content = fs::read_to_string(&metadata_file)?;
993+
let parsed: ContentMetadata = serde_json::from_str(&metadata_content)?;
994+
assert!(
995+
parsed
996+
.version
997+
.contains("uboot-images-2023.04-2.fc42.noarch"),
998+
"Metadata should contain uboot package"
999+
);
1000+
1001+
println!("extend_payload test completed successfully!");
1002+
println!("✓ Files copied to: {:?}", efi_dir);
1003+
println!("✓ u-boot.bin: {:?}", copied_uboot_bin);
1004+
println!("✓ overlays/i2c.dtb: {:?}", copied_overlay_dtb);
1005+
println!("✓ Metadata created: {:?}", metadata_file);
1006+
println!("✓ Package version: {}", parsed.version);
1007+
}
1008+
Ok(Some(false)) => {
1009+
panic!("extend_payload returned false - expected success");
1010+
}
1011+
Ok(None) => {
1012+
panic!("extend_payload returned None - expected success");
1013+
}
1014+
Err(e) => {
1015+
panic!("extend_payload failed when it should have succeeded: {}", e);
1016+
}
1017+
}
1018+
1019+
Ok(())
1020+
}
7771021
}

src/model.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ pub(crate) struct InstalledContent {
3939
pub(crate) filetree: Option<crate::filetree::FileTree>,
4040
/// The version this was originally adopted from
4141
pub(crate) adopted_from: Option<ContentMetadata>,
42+
/// Nested map of supplemental firmware payloads.
43+
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
44+
pub(crate) firmware: BTreeMap<String, Box<InstalledContent>>,
4245
}
4346

4447
/// Will be serialized into /boot/bootupd-state.json

0 commit comments

Comments
 (0)