Skip to content

Commit b155d48

Browse files
committed
feat(extend_payload_to_esp):stage payload to /usr for installation
extend_payload_to_esp <path> will move the all contents of the <path> 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/<name>/<version>/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/<pkg-name>/<version>/EFI/` stores metadata at `/usr/lib/efi/firmware/<pkg-name>/<version>/EFI.json`. Added test_extend_payload_to_esp unit test.
1 parent 3acb674 commit b155d48

File tree

6 files changed

+300
-3
lines changed

6 files changed

+300
-3
lines changed

src/bios.rs

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

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: 260 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ use std::os::unix::io::AsRawFd;
99
use std::path::{Path, PathBuf};
1010
use std::process::Command;
1111

12-
use anyhow::{bail, Context, Result};
13-
use bootc_internal_utils::CommandRunExt;
14-
use camino::{Utf8Path, Utf8PathBuf};
12+
use anyhow::{anyhow, bail, Context, Result};
13+
use bootc_utils::CommandRunExt;
1514
use cap_std::fs::Dir;
1615
use cap_std_ext::cap_std;
1716
use chrono::prelude::*;
@@ -506,6 +505,99 @@ impl Component for Efi {
506505
Ok(meta)
507506
}
508507

508+
fn extend_payload(&self, sysroot_path: &str, src_input: &str) -> Result<Option<bool>> {
509+
let dest_efidir_base = Path::new(sysroot_path).join("usr/lib/efi").join("firmware");
510+
511+
let src_input_path = Path::new(src_input);
512+
let path_to_query = if src_input_path.is_dir() {
513+
WalkDir::new(src_input_path)
514+
.into_iter()
515+
.filter_map(|e| e.ok())
516+
.find(|e| e.file_type().is_file())
517+
.map(|e| e.path().to_path_buf())
518+
.ok_or_else(|| anyhow!("No file found in directory {}", src_input))?
519+
} else {
520+
src_input_path.to_path_buf()
521+
};
522+
523+
let meta_from_src = packagesystem::query_files(sysroot_path, [path_to_query])
524+
.context(format!("Querying RPM metadata for {:?}", src_input_path))?;
525+
526+
let version_string_part =
527+
meta_from_src.version.split(',').next().ok_or_else(|| {
528+
anyhow!("RPM query returned an empty or malformed version string")
529+
})?;
530+
531+
let parts: Vec<&str> = version_string_part.split('-').collect();
532+
let (pkg_name, version_release_str) = if parts.len() >= 3 {
533+
(
534+
parts[0].to_string(),
535+
format!(
536+
"{}-{}",
537+
parts[parts.len() - 2],
538+
parts[parts.len() - 1]
539+
.split('.')
540+
.next()
541+
.unwrap_or(parts[parts.len() - 1])
542+
),
543+
)
544+
} else {
545+
anyhow::bail!("Unexpected RPM version string format");
546+
};
547+
548+
// Clean up any existing firmware versions for this package to ensure only one version
549+
let pkg_firmware_dir = dest_efidir_base.join(&pkg_name);
550+
if pkg_firmware_dir.exists() {
551+
log::debug!(
552+
"Removing existing firmware versions for package: {}",
553+
pkg_name
554+
);
555+
std::fs::remove_dir_all(&pkg_firmware_dir).with_context(|| {
556+
format!(
557+
"Failed to remove existing firmware directory {:?}",
558+
pkg_firmware_dir
559+
)
560+
})?;
561+
}
562+
563+
// Use the flattened destination path
564+
let final_dest_path = dest_efidir_base.join(&pkg_name).join(&version_release_str);
565+
std::fs::create_dir_all(&final_dest_path)?;
566+
567+
let efi_dest_path = final_dest_path.join("EFI");
568+
std::fs::create_dir_all(&efi_dest_path)?;
569+
570+
// Copy the payload files
571+
let src_metadata = std::fs::metadata(src_input_path)?;
572+
if src_metadata.is_dir() {
573+
Command::new("cp")
574+
.args([
575+
"-rp",
576+
&format!("{}/.", src_input),
577+
efi_dest_path.to_str().unwrap(),
578+
])
579+
.run()
580+
.with_context(|| {
581+
format!(
582+
"Failed to copy contents of {:?} to {:?}",
583+
src_input, &efi_dest_path
584+
)
585+
})?;
586+
} else {
587+
Command::new("cp")
588+
.args(["-p", src_input, efi_dest_path.to_str().unwrap()])
589+
.run()?;
590+
}
591+
592+
// Create the metadata file for the firmware
593+
let firmware_meta_path = final_dest_path.join("EFI.json");
594+
let meta_file = std::fs::File::create(firmware_meta_path)?;
595+
serde_json::to_writer(meta_file, &meta_from_src)?;
596+
log::debug!("Wrote firmware metadata for {}", pkg_name);
597+
598+
Ok(Some(true))
599+
}
600+
509601
fn query_update(&self, sysroot: &openat::Dir) -> Result<Option<ContentMetadata>> {
510602
get_component_update(sysroot, self)
511603
}
@@ -858,6 +950,171 @@ Boot0003* test";
858950
paths,
859951
["usr/lib/efi/FOO/1.1/EFI", "usr/lib/efi/BAR/1.1/EFI"]
860952
);
953+
}
954+
#[test]
955+
fn test_extend_payload() -> Result<()> {
956+
use std::fs;
957+
use tempfile::TempDir;
958+
959+
let temp_sysroot = TempDir::new()?;
960+
let temp_src = TempDir::new()?;
961+
962+
let sysroot_path = temp_sysroot.path().to_str().unwrap();
963+
let src_path = temp_src.path().to_str().unwrap();
964+
965+
// mockup data source: /usr/share/uboot/rpi/
966+
// content: u-boot.bin, overlays/i2c.dtb
967+
// mockup rpm: uboot-images-2023.04-2.fc42.noarch
968+
// mockup rpm_db: /usr/lib/sysimage/rpm/Packages
969+
let src_uboot_dir = temp_src
970+
.path()
971+
.join("usr")
972+
.join("share")
973+
.join("uboot")
974+
.join("rpi");
975+
fs::create_dir_all(&src_uboot_dir)?;
976+
977+
let src_overlays_dir = src_uboot_dir.join("overlays");
978+
fs::create_dir_all(&src_overlays_dir)?;
979+
980+
// Create content files
981+
let uboot_bin = src_uboot_dir.join("u-boot.bin");
982+
fs::write(&uboot_bin, b"uboot binary content")?;
983+
let overlay_dtb = src_overlays_dir.join("i2c.dtb");
984+
fs::write(&overlay_dtb, b"device tree overlay content")?;
985+
986+
// Create a mockup RPM database structure
987+
let rpm_db_dir = temp_sysroot
988+
.path()
989+
.join("usr")
990+
.join("lib")
991+
.join("sysimage")
992+
.join("rpm");
993+
fs::create_dir_all(&rpm_db_dir)?;
994+
fs::write(rpm_db_dir.join("Packages"), b"fake rpm database file")?;
995+
996+
// Create a mock rpm script that returns uboot-images package data
997+
let mock_rpm_dir = TempDir::new()?;
998+
let mock_rpm_script = mock_rpm_dir.path().join("rpm");
999+
1000+
let mock_script_content = r#"#!/bin/bash
1001+
# Mock rpm script for testing
1002+
if [[ "$*" == *"-q"* ]] && [[ "$*" == *"-f"* ]]; then
1003+
# Return mock uboot-images package data in the expected format: nevra,buildtime
1004+
echo "uboot-images-2023.04-2.fc42.noarch,1681234567"
1005+
exit 0
1006+
fi
1007+
# For any other rpm command, just fail
1008+
exit 1
1009+
"#
1010+
.to_string();
1011+
1012+
fs::write(&mock_rpm_script, mock_script_content)?;
1013+
1014+
#[cfg(unix)]
1015+
{
1016+
use std::os::unix::fs::PermissionsExt;
1017+
let mut perms = fs::metadata(&mock_rpm_script)?.permissions();
1018+
perms.set_mode(0o755);
1019+
fs::set_permissions(&mock_rpm_script, perms)?;
1020+
}
1021+
1022+
let original_path = std::env::var("PATH").unwrap_or_default();
1023+
let new_path = format!("{}:{}", mock_rpm_dir.path().display(), original_path);
1024+
std::env::set_var("PATH", &new_path);
1025+
1026+
// Test extend_payload
1027+
let efi_component = Efi::default();
1028+
1029+
let result = efi_component
1030+
.extend_payload(sysroot_path, &format!("{}/usr/share/uboot/rpi", src_path));
1031+
1032+
// validation beigins
1033+
std::env::set_var("PATH", &original_path);
1034+
match result {
1035+
Ok(Some(true)) => {
1036+
// Verify the files were copied to the right location
1037+
let firmware_base = temp_sysroot
1038+
.path()
1039+
.join("usr")
1040+
.join("lib")
1041+
.join("efi")
1042+
.join("firmware");
1043+
assert!(
1044+
firmware_base.exists(),
1045+
"Firmware base directory should be created"
1046+
);
1047+
1048+
// Look for the uboot package directory (package name is extracted from first part)
1049+
// From "uboot-images-2023.04-2.fc42.noarch" -> package: "uboot", version: "2023.04-2"
1050+
let uboot_dir = firmware_base.join("uboot").join("2023.04-2");
1051+
assert!(
1052+
uboot_dir.exists(),
1053+
"Package directory uboot/2023.04-2 should be created"
1054+
);
1055+
1056+
// Files should be copied to the EFI subdirectory
1057+
let efi_dir = uboot_dir.join("EFI");
1058+
assert!(efi_dir.exists(), "EFI directory should be created");
1059+
1060+
// Verify that u-boot.bin was copied to EFI subdirectory
1061+
let copied_uboot_bin = efi_dir.join("u-boot.bin");
1062+
assert!(copied_uboot_bin.exists(), "u-boot.bin should be copied");
1063+
let uboot_content = fs::read_to_string(&copied_uboot_bin)?;
1064+
assert_eq!(
1065+
uboot_content, "uboot binary content",
1066+
"u-boot.bin content should be preserved"
1067+
);
1068+
1069+
// Verify that overlays directory and i2c.dtb were copied to EFI subdirectory
1070+
let copied_overlays_dir = efi_dir.join("overlays");
1071+
assert!(
1072+
copied_overlays_dir.exists(),
1073+
"overlays directory should be copied"
1074+
);
1075+
1076+
let copied_overlay_dtb = copied_overlays_dir.join("i2c.dtb");
1077+
assert!(copied_overlay_dtb.exists(), "i2c.dtb should be copied");
1078+
let overlay_content = fs::read_to_string(&copied_overlay_dtb)?;
1079+
assert_eq!(
1080+
overlay_content, "device tree overlay content",
1081+
"i2c.dtb content should be preserved"
1082+
);
1083+
1084+
// Verify the EFI.json metadata
1085+
let metadata_file = uboot_dir.join("EFI.json");
1086+
assert!(
1087+
metadata_file.exists(),
1088+
"EFI.json metadata file should be created"
1089+
);
1090+
let metadata_content = fs::read_to_string(&metadata_file)?;
1091+
let parsed: ContentMetadata = serde_json::from_str(&metadata_content)?;
1092+
assert!(
1093+
parsed
1094+
.version
1095+
.contains("uboot-images-2023.04-2.fc42.noarch"),
1096+
"Metadata should contain uboot package"
1097+
);
1098+
1099+
println!("extend_payload test completed successfully!");
1100+
println!("✓ Files copied to: {:?}", efi_dir);
1101+
println!("✓ u-boot.bin: {:?}", copied_uboot_bin);
1102+
println!("✓ overlays/i2c.dtb: {:?}", copied_overlay_dtb);
1103+
println!("✓ Metadata created: {:?}", metadata_file);
1104+
println!("✓ Package version: {}", parsed.version);
1105+
}
1106+
Ok(Some(false)) => {
1107+
panic!("extend_payload returned false - expected success");
1108+
}
1109+
Ok(None) => {
1110+
panic!("extend_payload returned None - expected success");
1111+
}
1112+
Err(e) => {
1113+
panic!("extend_payload failed when it should have succeeded: {}", e);
1114+
}
1115+
}
1116+
8611117
Ok(())
8621118
}
8631119
}
1120+

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)