summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHermes Agent <hermes@localhost>2026-08-12 01:16:54 +0000
committerHermes Agent <hermes@localhost>2026-08-12 01:16:54 +0000
commit9a07a0e20b88838d22bb04fae80b2353d41b2cd0 (patch)
treee01de934f6c14a3bd5efc1fa37d92b0a1ab86be7
parent915fcc5efbf45d8adee870cfbe2a25e466454a2f (diff)
Add native initramfs stage adapter
-rw-r--r--Cargo.lock72
-rw-r--r--Cargo.toml1
-rw-r--r--src/initramfs.rs195
-rw-r--r--src/lib.rs1
-rw-r--r--tests/initramfs.rs83
5 files changed, 352 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock
index aa4be7e..9202766 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -9,6 +9,7 @@ dependencies = [
"anyhow",
"clap",
"serde",
+ "sha2",
"tar",
"tempfile",
"toml",
@@ -78,6 +79,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -130,6 +140,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -162,6 +201,16 @@ dependencies = [
]
[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -310,6 +359,17 @@ dependencies = [
]
[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -390,6 +450,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -402,6 +468,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index f5568c4..f642720 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,6 +15,7 @@ path = "src/main.rs"
anyhow = "1.0"
clap = { version = "4.6", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
+sha2 = "0.10"
toml = "1.0"
tar = "0.4"
walkdir = "2.5"
diff --git a/src/initramfs.rs b/src/initramfs.rs
new file mode 100644
index 0000000..ddb1010
--- /dev/null
+++ b/src/initramfs.rs
@@ -0,0 +1,195 @@
+use std::ffi::OsString;
+use std::fs;
+use std::path::{Path, PathBuf};
+use std::process::Command;
+
+use anyhow::{bail, Context, Result};
+use sha2::{Digest, Sha256};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct KernelVersion(String);
+
+impl KernelVersion {
+ pub fn discover_rt(rootfs: &Path) -> Result<Self> {
+ let boot = rootfs.join("boot");
+ let entries = fs::read_dir(&boot)
+ .with_context(|| format!("read boot directory {}", boot.display()))?;
+ let mut kernels = entries
+ .filter_map(|entry| entry.ok())
+ .filter_map(|entry| {
+ let name = entry.file_name();
+ let name = name.to_str()?;
+ let version = name.strip_prefix("vmlinuz-")?;
+ (version.contains("rt")).then(|| version.to_owned())
+ })
+ .collect::<Vec<_>>();
+ kernels.sort();
+ match kernels.as_slice() {
+ [] => bail!("no RT kernel image found in {}", boot.display()),
+ [version] => Ok(Self(version.clone())),
+ _ => bail!("multiple RT kernel images found in {}: {}", boot.display(), kernels.join(", ")),
+ }
+ }
+
+ pub fn as_str(&self) -> &str {
+ &self.0
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct InitramfsRecipe {
+ features: Vec<String>,
+ modules: Vec<String>,
+}
+
+impl InitramfsRecipe {
+ pub fn new(
+ features: impl IntoIterator<Item = impl Into<String>>,
+ modules: impl IntoIterator<Item = impl Into<String>>,
+ ) -> Result<Self> {
+ let mut features = normalize("initrd feature", features)?;
+ let mut modules = normalize("initrd module", modules)?;
+ features.sort();
+ features.dedup();
+ modules.sort();
+ modules.dedup();
+ Ok(Self { features, modules })
+ }
+
+ pub fn render(&self) -> String {
+ format!(
+ "FEATURES += {}\nMODULES += {}\n",
+ self.features.join(" "),
+ self.modules.join(" ")
+ )
+ }
+}
+
+fn normalize(kind: &str, values: impl IntoIterator<Item = impl Into<String>>) -> Result<Vec<String>> {
+ values
+ .into_iter()
+ .map(Into::into)
+ .map(|value: String| {
+ if value.trim().is_empty() || value.chars().any(char::is_whitespace) {
+ bail!("{kind} cannot be empty or contain whitespace");
+ }
+ Ok(value)
+ })
+ .collect()
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct InitramfsRequest {
+ rootfs: PathBuf,
+ kernel: KernelVersion,
+}
+
+impl InitramfsRequest {
+ pub fn discover(rootfs: impl AsRef<Path>) -> Result<Self> {
+ let rootfs = rootfs.as_ref();
+ if !rootfs.is_dir() {
+ bail!("rootfs is not a directory: {}", rootfs.display());
+ }
+ Ok(Self {
+ rootfs: rootfs.to_path_buf(),
+ kernel: KernelVersion::discover_rt(rootfs)?,
+ })
+ }
+
+ pub fn rootfs(&self) -> &Path {
+ &self.rootfs
+ }
+
+ pub fn kernel(&self) -> &KernelVersion {
+ &self.kernel
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Invocation {
+ program: OsString,
+ arguments: Vec<OsString>,
+}
+
+impl Invocation {
+ pub fn new(program: impl Into<OsString>, arguments: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
+ Self { program: program.into(), arguments: arguments.into_iter().map(Into::into).collect() }
+ }
+}
+
+pub trait CommandRunner {
+ fn run(&mut self, invocation: Invocation) -> Result<()>;
+}
+
+#[derive(Debug, Default)]
+pub struct ProcessRunner;
+
+impl CommandRunner for ProcessRunner {
+ fn run(&mut self, invocation: Invocation) -> Result<()> {
+ let status = Command::new(&invocation.program)
+ .args(&invocation.arguments)
+ .status()
+ .with_context(|| format!("run {}", invocation.program.to_string_lossy()))?;
+ if !status.success() {
+ bail!("{} exited with {status}", invocation.program.to_string_lossy());
+ }
+ Ok(())
+ }
+}
+
+pub trait InitramfsBuilder {
+ fn build(&mut self, request: &InitramfsRequest) -> Result<InitramfsResult>;
+}
+
+#[derive(Debug)]
+pub struct MakeInitrdBuilder<R> {
+ runner: R,
+}
+
+impl<R> MakeInitrdBuilder<R> {
+ pub fn new(runner: R) -> Self {
+ Self { runner }
+ }
+
+ pub fn runner(&self) -> &R {
+ &self.runner
+ }
+}
+
+impl<R: CommandRunner> InitramfsBuilder for MakeInitrdBuilder<R> {
+ fn build(&mut self, request: &InitramfsRequest) -> Result<InitramfsResult> {
+ self.runner.run(Invocation::new(
+ "chroot",
+ [
+ request.rootfs().as_os_str().to_owned(),
+ "make-initrd".into(),
+ "-k".into(),
+ request.kernel().as_str().into(),
+ ],
+ ))?;
+ InitramfsResult::from_rootfs(request.rootfs(), request.kernel())
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct InitramfsResult {
+ initrd_path: PathBuf,
+ sha256: String,
+}
+
+impl InitramfsResult {
+ fn from_rootfs(rootfs: &Path, kernel: &KernelVersion) -> Result<Self> {
+ let initrd_path = PathBuf::from(format!("boot/initrd-{}.img", kernel.as_str()));
+ let contents = fs::read(rootfs.join(&initrd_path))
+ .with_context(|| format!("read generated initrd {}", initrd_path.display()))?;
+ Ok(Self { initrd_path, sha256: format!("{:x}", Sha256::digest(contents)) })
+ }
+
+ pub fn initrd_path(&self) -> &Path {
+ &self.initrd_path
+ }
+
+ pub fn sha256(&self) -> &str {
+ &self.sha256
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index cfcc645..92ad4bc 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,6 +1,7 @@
pub mod cli;
pub mod files;
pub mod hasher;
+pub mod initramfs;
pub mod model;
pub mod package_installer;
pub mod plan;
diff --git a/tests/initramfs.rs b/tests/initramfs.rs
new file mode 100644
index 0000000..b15c54f
--- /dev/null
+++ b/tests/initramfs.rs
@@ -0,0 +1,83 @@
+use std::fs;
+use std::path::Path;
+
+use alt_controller_image::initramfs::{
+ CommandRunner, InitramfsBuilder, InitramfsRecipe, InitramfsRequest, Invocation, KernelVersion,
+ MakeInitrdBuilder,
+};
+use tempfile::tempdir;
+
+#[test]
+fn discovers_the_single_rt_kernel_and_renders_a_sorted_oem_recipe() {
+ let rootfs = tempdir().expect("rootfs directory");
+ fs::create_dir(rootfs.path().join("boot")).expect("boot directory");
+ fs::write(rootfs.path().join("boot/vmlinuz-6.12.8-alt1.rt1"), "kernel")
+ .expect("RT kernel");
+ fs::write(rootfs.path().join("boot/vmlinuz-6.12.8-alt1.std"), "kernel")
+ .expect("non-RT kernel");
+
+ let kernel = KernelVersion::discover_rt(rootfs.path()).expect("discover RT kernel");
+ assert_eq!(kernel.as_str(), "6.12.8-alt1.rt1");
+ assert_eq!(
+ InitramfsRecipe::new(["rootfs", "compress"], ["virtio_blk.ko", "ext4"])
+ .expect("valid recipe")
+ .render(),
+ "FEATURES += compress rootfs\nMODULES += ext4 virtio_blk.ko\n"
+ );
+}
+
+#[test]
+fn diagnoses_missing_or_ambiguous_rt_kernels() {
+ let rootfs = tempdir().expect("rootfs directory");
+ fs::create_dir(rootfs.path().join("boot")).expect("boot directory");
+ let missing = KernelVersion::discover_rt(rootfs.path()).expect_err("missing RT kernel");
+ assert!(missing.to_string().contains("no RT kernel image"));
+
+ fs::write(rootfs.path().join("boot/vmlinuz-6.12-rt1"), "kernel").expect("first RT kernel");
+ fs::write(rootfs.path().join("boot/vmlinuz-6.11-rt1"), "kernel").expect("second RT kernel");
+ let ambiguous = KernelVersion::discover_rt(rootfs.path()).expect_err("ambiguous RT kernel");
+ assert!(ambiguous.to_string().contains("multiple RT kernel images"));
+}
+
+#[test]
+fn make_initrd_adapter_uses_typed_chroot_arguments_and_records_output_digest() {
+ let rootfs = tempdir().expect("rootfs directory");
+ fs::create_dir(rootfs.path().join("boot")).expect("boot directory");
+ fs::write(rootfs.path().join("boot/vmlinuz-6.12-rt1"), "kernel").expect("RT kernel");
+ fs::write(rootfs.path().join("boot/initrd-6.12-rt1.img"), b"generated initrd")
+ .expect("generated initrd");
+ let request = InitramfsRequest::discover(rootfs.path()).expect("initramfs request");
+ let mut builder = MakeInitrdBuilder::new(RecordingRunner::default());
+
+ let result = builder.build(&request).expect("build initramfs");
+
+ assert_eq!(
+ builder.runner().invocations,
+ vec![Invocation::new(
+ "chroot",
+ [
+ rootfs.path().as_os_str().to_owned(),
+ "make-initrd".into(),
+ "-k".into(),
+ "6.12-rt1".into(),
+ ],
+ )]
+ );
+ assert_eq!(result.initrd_path(), Path::new("boot/initrd-6.12-rt1.img"));
+ assert_eq!(
+ result.sha256(),
+ "aec42bc86f526931d7c7f01ecba8d643dbb748f9102539a99db5d4855ee4435f"
+ );
+}
+
+#[derive(Debug, Default)]
+struct RecordingRunner {
+ invocations: Vec<Invocation>,
+}
+
+impl CommandRunner for RecordingRunner {
+ fn run(&mut self, invocation: Invocation) -> anyhow::Result<()> {
+ self.invocations.push(invocation);
+ Ok(())
+ }
+}