Skip to content

Commit e41afc8

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 3acb674 commit e41afc8

File tree

6 files changed

+285
-3
lines changed

6 files changed

+285
-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: 245 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,84 @@ 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+
// Use the flattened destination path
549+
let final_dest_path = dest_efidir_base.join(&pkg_name).join(&version_release_str);
550+
std::fs::create_dir_all(&final_dest_path)?;
551+
552+
let efi_dest_path = final_dest_path.join("EFI");
553+
std::fs::create_dir_all(&efi_dest_path)?;
554+
555+
// Copy the payload files
556+
let src_metadata = std::fs::metadata(src_input_path)?;
557+
if src_metadata.is_dir() {
558+
Command::new("cp")
559+
.args([
560+
"-rp",
561+
&format!("{}/.", src_input),
562+
efi_dest_path.to_str().unwrap(),
563+
])
564+
.run()
565+
.with_context(|| {
566+
format!(
567+
"Failed to copy contents of {:?} to {:?}",
568+
src_input, &efi_dest_path
569+
)
570+
})?;
571+
} else {
572+
Command::new("cp")
573+
.args(["-p", src_input, efi_dest_path.to_str().unwrap()])
574+
.run()?;
575+
}
576+
577+
// Create the metadata file for the firmware
578+
let firmware_meta_path = final_dest_path.join("EFI.json");
579+
let meta_file = std::fs::File::create(firmware_meta_path)?;
580+
serde_json::to_writer(meta_file, &meta_from_src)?;
581+
log::debug!("Wrote firmware metadata for {}", pkg_name);
582+
583+
Ok(Some(true))
584+
}
585+
509586
fn query_update(&self, sysroot: &openat::Dir) -> Result<Option<ContentMetadata>> {
510587
get_component_update(sysroot, self)
511588
}
@@ -858,6 +935,171 @@ Boot0003* test";
858935
paths,
859936
["usr/lib/efi/FOO/1.1/EFI", "usr/lib/efi/BAR/1.1/EFI"]
860937
);
938+
}
939+
#[test]
940+
fn test_extend_payload() -> Result<()> {
941+
use std::fs;
942+
use tempfile::TempDir;
943+
944+
let temp_sysroot = TempDir::new()?;
945+
let temp_src = TempDir::new()?;
946+
947+
let sysroot_path = temp_sysroot.path().to_str().unwrap();
948+
let src_path = temp_src.path().to_str().unwrap();
949+
950+
// mockup data source: /usr/share/uboot/rpi/
951+
// content: u-boot.bin, overlays/i2c.dtb
952+
// mockup rpm: uboot-images-2023.04-2.fc42.noarch
953+
// mockup rpm_db: /usr/lib/sysimage/rpm/Packages
954+
let src_uboot_dir = temp_src
955+
.path()
956+
.join("usr")
957+
.join("share")
958+
.join("uboot")
959+
.join("rpi");
960+
fs::create_dir_all(&src_uboot_dir)?;
961+
962+
let src_overlays_dir = src_uboot_dir.join("overlays");
963+
fs::create_dir_all(&src_overlays_dir)?;
964+
965+
// Create content files
966+
let uboot_bin = src_uboot_dir.join("u-boot.bin");
967+
fs::write(&uboot_bin, b"uboot binary content")?;
968+
let overlay_dtb = src_overlays_dir.join("i2c.dtb");
969+
fs::write(&overlay_dtb, b"device tree overlay content")?;
970+
971+
// Create a mockup RPM database structure
972+
let rpm_db_dir = temp_sysroot
973+
.path()
974+
.join("usr")
975+
.join("lib")
976+
.join("sysimage")
977+
.join("rpm");
978+
fs::create_dir_all(&rpm_db_dir)?;
979+
fs::write(rpm_db_dir.join("Packages"), b"fake rpm database file")?;
980+
981+
// Create a mock rpm script that returns uboot-images package data
982+
let mock_rpm_dir = TempDir::new()?;
983+
let mock_rpm_script = mock_rpm_dir.path().join("rpm");
984+
985+
let mock_script_content = r#"#!/bin/bash
986+
# Mock rpm script for testing
987+
if [[ "$*" == *"-q"* ]] && [[ "$*" == *"-f"* ]]; then
988+
# Return mock uboot-images package data in the expected format: nevra,buildtime
989+
echo "uboot-images-2023.04-2.fc42.noarch,1681234567"
990+
exit 0
991+
fi
992+
# For any other rpm command, just fail
993+
exit 1
994+
"#
995+
.to_string();
996+
997+
fs::write(&mock_rpm_script, mock_script_content)?;
998+
999+
#[cfg(unix)]
1000+
{
1001+
use std::os::unix::fs::PermissionsExt;
1002+
let mut perms = fs::metadata(&mock_rpm_script)?.permissions();
1003+
perms.set_mode(0o755);
1004+
fs::set_permissions(&mock_rpm_script, perms)?;
1005+
}
1006+
1007+
let original_path = std::env::var("PATH").unwrap_or_default();
1008+
let new_path = format!("{}:{}", mock_rpm_dir.path().display(), original_path);
1009+
std::env::set_var("PATH", &new_path);
1010+
1011+
// Test extend_payload
1012+
let efi_component = Efi::default();
1013+
1014+
let result = efi_component
1015+
.extend_payload(sysroot_path, &format!("{}/usr/share/uboot/rpi", src_path));
1016+
1017+
// validation beigins
1018+
std::env::set_var("PATH", &original_path);
1019+
match result {
1020+
Ok(Some(true)) => {
1021+
// Verify the files were copied to the right location
1022+
let firmware_base = temp_sysroot
1023+
.path()
1024+
.join("usr")
1025+
.join("lib")
1026+
.join("efi")
1027+
.join("firmware");
1028+
assert!(
1029+
firmware_base.exists(),
1030+
"Firmware base directory should be created"
1031+
);
1032+
1033+
// Look for the uboot package directory (package name is extracted from first part)
1034+
// From "uboot-images-2023.04-2.fc42.noarch" -> package: "uboot", version: "2023.04-2"
1035+
let uboot_dir = firmware_base.join("uboot").join("2023.04-2");
1036+
assert!(
1037+
uboot_dir.exists(),
1038+
"Package directory uboot/2023.04-2 should be created"
1039+
);
1040+
1041+
// Files should be copied to the EFI subdirectory
1042+
let efi_dir = uboot_dir.join("EFI");
1043+
assert!(efi_dir.exists(), "EFI directory should be created");
1044+
1045+
// Verify that u-boot.bin was copied to EFI subdirectory
1046+
let copied_uboot_bin = efi_dir.join("u-boot.bin");
1047+
assert!(copied_uboot_bin.exists(), "u-boot.bin should be copied");
1048+
let uboot_content = fs::read_to_string(&copied_uboot_bin)?;
1049+
assert_eq!(
1050+
uboot_content, "uboot binary content",
1051+
"u-boot.bin content should be preserved"
1052+
);
1053+
1054+
// Verify that overlays directory and i2c.dtb were copied to EFI subdirectory
1055+
let copied_overlays_dir = efi_dir.join("overlays");
1056+
assert!(
1057+
copied_overlays_dir.exists(),
1058+
"overlays directory should be copied"
1059+
);
1060+
1061+
let copied_overlay_dtb = copied_overlays_dir.join("i2c.dtb");
1062+
assert!(copied_overlay_dtb.exists(), "i2c.dtb should be copied");
1063+
let overlay_content = fs::read_to_string(&copied_overlay_dtb)?;
1064+
assert_eq!(
1065+
overlay_content, "device tree overlay content",
1066+
"i2c.dtb content should be preserved"
1067+
);
1068+
1069+
// Verify the EFI.json metadata
1070+
let metadata_file = uboot_dir.join("EFI.json");
1071+
assert!(
1072+
metadata_file.exists(),
1073+
"EFI.json metadata file should be created"
1074+
);
1075+
let metadata_content = fs::read_to_string(&metadata_file)?;
1076+
let parsed: ContentMetadata = serde_json::from_str(&metadata_content)?;
1077+
assert!(
1078+
parsed
1079+
.version
1080+
.contains("uboot-images-2023.04-2.fc42.noarch"),
1081+
"Metadata should contain uboot package"
1082+
);
1083+
1084+
println!("extend_payload test completed successfully!");
1085+
println!("✓ Files copied to: {:?}", efi_dir);
1086+
println!("✓ u-boot.bin: {:?}", copied_uboot_bin);
1087+
println!("✓ overlays/i2c.dtb: {:?}", copied_overlay_dtb);
1088+
println!("✓ Metadata created: {:?}", metadata_file);
1089+
println!("✓ Package version: {}", parsed.version);
1090+
}
1091+
Ok(Some(false)) => {
1092+
panic!("extend_payload returned false - expected success");
1093+
}
1094+
Ok(None) => {
1095+
panic!("extend_payload returned None - expected success");
1096+
}
1097+
Err(e) => {
1098+
panic!("extend_payload failed when it should have succeeded: {}", e);
1099+
}
1100+
}
1101+
8611102
Ok(())
8621103
}
8631104
}
1105+

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)