summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/build.rs114
-rw-r--r--src/cli.rs25
-rw-r--r--src/initramfs.rs3
-rw-r--r--src/lib.rs1
-rw-r--r--src/package_installer.rs6
-rw-r--r--tests/build.rs91
6 files changed, 238 insertions, 2 deletions
diff --git a/src/build.rs b/src/build.rs
new file mode 100644
index 0000000..4439d2f
--- /dev/null
+++ b/src/build.rs
@@ -0,0 +1,114 @@
+use std::path::{Path, PathBuf};
+
+use anyhow::{bail, Context, Result};
+
+use crate::archive::NativeTarWriter;
+use crate::initramfs::{InitramfsBuilder, InitramfsRequest, InitramfsResult};
+use crate::manifest::{ArtifactManifest, InitrdRecord};
+use crate::package_installer::PackageInstaller;
+use crate::rootfs::{InitrdOem, RootfsFinalization};
+use crate::BuildPlan;
+
+/// Executes the native stages after a plan has been validated.
+///
+/// The only platform facilities are injected package and initramfs adapters;
+/// filesystem finalization, archive writing, and manifest collection are native.
+#[derive(Debug)]
+pub struct BuildExecutor<I, B> {
+ installer: I,
+ initramfs_builder: B,
+ tar_writer: NativeTarWriter,
+}
+
+impl<I, B> BuildExecutor<I, B> {
+ pub fn new(installer: I, initramfs_builder: B) -> Self {
+ Self { installer, initramfs_builder, tar_writer: NativeTarWriter::new() }
+ }
+}
+
+impl<I: PackageInstaller, B: InitramfsBuilder> BuildExecutor<I, B> {
+ pub fn execute(
+ &mut self,
+ plan: &BuildPlan,
+ workspace: impl AsRef<Path>,
+ apt_config: impl AsRef<Path>,
+ artifact: impl AsRef<Path>,
+ ) -> Result<BuildResult> {
+ let workspace = workspace.as_ref();
+ if workspace.exists() {
+ bail!("workspace already exists; refusing to reuse it: {}", workspace.display());
+ }
+ let artifact = artifact.as_ref();
+ if artifact.exists() {
+ bail!("output artifact already exists; refusing to overwrite it: {}", artifact.display());
+ }
+ let parent = artifact
+ .parent()
+ .filter(|parent| !parent.as_os_str().is_empty())
+ .ok_or_else(|| anyhow::anyhow!("artifact path has no parent: {}", artifact.display()))?;
+ if parent.exists() && !parent.is_dir() {
+ bail!("artifact parent is not a directory: {}", parent.display());
+ }
+
+ std::fs::create_dir_all(workspace)
+ .with_context(|| format!("create workspace {}", workspace.display()))?;
+ let request = plan.package_request(workspace, apt_config)?;
+ self.installer.install(&request)?;
+
+ let rootfs = workspace.join("chroot");
+ if !rootfs.is_dir() {
+ bail!("package installer did not create rootfs: {}", rootfs.display());
+ }
+ RootfsFinalization::new(
+ Vec::new(),
+ InitrdOem::new(
+ plan.spec().boot.initrd_features.iter().cloned(),
+ plan.spec().boot.initrd_modules.iter().cloned(),
+ ),
+ Vec::new(),
+ )
+ .apply(&rootfs)?;
+
+ let initrd = self.initramfs_builder.build(&InitramfsRequest::discover(&rootfs)?)?;
+ std::fs::create_dir_all(parent)
+ .with_context(|| format!("create artifact directory {}", parent.display()))?;
+ self.tar_writer.write(&rootfs, artifact)?;
+ let manifest = ArtifactManifest::collect(
+ &rootfs,
+ Vec::new(),
+ Some(InitrdRecord::new(
+ initrd.initrd_path().display().to_string(),
+ initrd.sha256(),
+ )),
+ artifact,
+ )?;
+ let manifest_path = manifest.write_beside(artifact)?;
+
+ Ok(BuildResult {
+ artifact: artifact.to_path_buf(),
+ manifest: manifest_path,
+ initrd: Some(initrd),
+ })
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct BuildResult {
+ artifact: PathBuf,
+ manifest: PathBuf,
+ initrd: Option<InitramfsResult>,
+}
+
+impl BuildResult {
+ pub fn artifact(&self) -> PathBuf {
+ self.artifact.clone()
+ }
+
+ pub fn manifest(&self) -> &Path {
+ &self.manifest
+ }
+
+ pub fn initrd(&self) -> Option<&InitramfsResult> {
+ self.initrd.as_ref()
+ }
+}
diff --git a/src/cli.rs b/src/cli.rs
index 4445f20..30e1a46 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -3,7 +3,10 @@ use std::path::PathBuf;
use anyhow::{bail, Result};
use clap::{Parser, Subcommand};
+use crate::build::BuildExecutor;
use crate::compare::compare;
+use crate::hasher::{HasherInstaller, ProcessRunner as HasherProcessRunner};
+use crate::initramfs::{MakeInitrdBuilder, ProcessRunner as InitramfsProcessRunner};
use crate::manifest::ArtifactManifest;
use crate::{BuildPlan, ImageSpec};
@@ -57,7 +60,27 @@ pub fn run(cli: Cli) -> Result<()> {
}
Ok(())
}
- Command::Build { .. } => bail!("build execution is not available yet"),
+ Command::Build {
+ spec,
+ workspace,
+ output,
+ } => {
+ let plan = BuildPlan::compile(ImageSpec::load(&spec)?)?;
+ let apt_config = spec
+ .parent()
+ .unwrap_or_else(|| std::path::Path::new("."))
+ .join("apt.conf");
+ let installer = HasherInstaller::new(HasherProcessRunner);
+ let initramfs = MakeInitrdBuilder::new(InitramfsProcessRunner);
+ let mut executor = BuildExecutor::new(installer, initramfs);
+ let result = executor.execute(&plan, workspace, apt_config, output)?;
+ println!("artifact: {}", result.artifact().display());
+ println!("manifest: {}", result.manifest().display());
+ if let Some(initrd) = result.initrd() {
+ println!("initrd: {} sha256:{}", initrd.initrd_path().display(), initrd.sha256());
+ }
+ Ok(())
+ }
Command::Inspect { artifact } => {
let manifest = load_artifact(&artifact)?;
println!("{}", toml::to_string_pretty(&manifest)?);
diff --git a/src/initramfs.rs b/src/initramfs.rs
index ddb1010..1c5c089 100644
--- a/src/initramfs.rs
+++ b/src/initramfs.rs
@@ -178,7 +178,8 @@ pub struct InitramfsResult {
}
impl InitramfsResult {
- fn from_rootfs(rootfs: &Path, kernel: &KernelVersion) -> Result<Self> {
+ /// Record the generated initrd as a rootfs-relative semantic fact.
+ pub 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()))?;
diff --git a/src/lib.rs b/src/lib.rs
index 595d0e9..d896ac6 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,4 +1,5 @@
pub mod archive;
+pub mod build;
pub mod cli;
pub mod compare;
pub mod files;
diff --git a/src/package_installer.rs b/src/package_installer.rs
index b08c469..c5c6451 100644
--- a/src/package_installer.rs
+++ b/src/package_installer.rs
@@ -89,3 +89,9 @@ fn normalize(
pub trait PackageInstaller {
fn install(&self, request: &PackageRequest) -> Result<()>;
}
+
+impl<T: PackageInstaller + ?Sized> PackageInstaller for &T {
+ fn install(&self, request: &PackageRequest) -> Result<()> {
+ (*self).install(request)
+ }
+}
diff --git a/tests/build.rs b/tests/build.rs
new file mode 100644
index 0000000..fd919a0
--- /dev/null
+++ b/tests/build.rs
@@ -0,0 +1,91 @@
+use std::cell::RefCell;
+use std::fs;
+
+use alt_controller_image::build::BuildExecutor;
+use alt_controller_image::initramfs::{InitramfsBuilder, InitramfsRequest, InitramfsResult};
+use alt_controller_image::manifest::ArtifactManifest;
+use alt_controller_image::package_installer::{PackageInstaller, PackageRequest};
+use alt_controller_image::{BuildPlan, ImageSpec};
+use tempfile::tempdir;
+
+#[derive(Default)]
+struct FixtureInstaller {
+ requests: RefCell<Vec<PackageRequest>>,
+}
+
+impl PackageInstaller for FixtureInstaller {
+ fn install(&self, request: &PackageRequest) -> anyhow::Result<()> {
+ self.requests.borrow_mut().push(request.clone());
+ let boot = request.workdir().join("chroot/boot");
+ fs::create_dir_all(&boot)?;
+ fs::write(boot.join("vmlinuz-6.12.0-rt1"), "kernel")?;
+ Ok(())
+ }
+}
+
+#[derive(Default)]
+struct FixtureInitramfsBuilder;
+
+impl InitramfsBuilder for FixtureInitramfsBuilder {
+ fn build(&mut self, request: &InitramfsRequest) -> anyhow::Result<InitramfsResult> {
+ fs::write(
+ request
+ .rootfs()
+ .join(format!("boot/initrd-{}.img", request.kernel().as_str())),
+ "initrd",
+ )?;
+ InitramfsResult::from_rootfs(request.rootfs(), request.kernel())
+ }
+}
+
+#[test]
+fn executes_the_native_build_stages_and_writes_a_companion_manifest() {
+ let fixture = tempdir().expect("fixture directory");
+ let spec = ImageSpec::load(std::path::Path::new("profiles/alt-controller.toml"))
+ .expect("load controller spec");
+ let plan = BuildPlan::compile(spec).expect("compile build plan");
+ let installer = FixtureInstaller::default();
+ let mut executor = BuildExecutor::new(&installer, FixtureInitramfsBuilder);
+ let workspace = fixture.path().join("work");
+ let artifact = fixture.path().join("out/controller.tar");
+
+ let result = executor
+ .execute(&plan, &workspace, "profiles/apt.conf", &artifact)
+ .expect("execute native build");
+
+ assert_eq!(installer.requests.borrow().len(), 1);
+ assert_eq!(result.artifact(), artifact);
+ assert_eq!(
+ fs::read_to_string(workspace.join("chroot/etc/initrd.mk.oem")).expect("read OEM recipe"),
+ "FEATURES += add-modules cleanup compress qemu rdshell rootfs usb\nMODULES += ahci.ko ahci_platform.ko ext4 sd_mod.ko virtio_blk.ko virtio_net.ko virtio_pci.ko\n"
+ );
+ assert!(artifact.is_file());
+ let manifest = ArtifactManifest::load(result.manifest()).expect("load companion manifest");
+ let initrd = result.initrd().expect("initrd result");
+ assert_eq!(
+ manifest.initrd,
+ Some(alt_controller_image::manifest::InitrdRecord::new(
+ initrd.initrd_path().display().to_string(),
+ initrd.sha256(),
+ ))
+ );
+}
+
+#[test]
+fn refuses_to_reuse_an_existing_workspace() {
+ let fixture = tempdir().expect("fixture directory");
+ let spec = ImageSpec::load(std::path::Path::new("profiles/alt-controller.toml"))
+ .expect("load controller spec");
+ let plan = BuildPlan::compile(spec).expect("compile build plan");
+ let installer = FixtureInstaller::default();
+ let mut executor = BuildExecutor::new(&installer, FixtureInitramfsBuilder);
+ let workspace = fixture.path().join("existing-work");
+ fs::create_dir(&workspace).expect("create existing workspace");
+
+ let error = executor
+ .execute(&plan, &workspace, "profiles/apt.conf", fixture.path().join("image.tar"))
+ .expect_err("existing workspace must be rejected");
+
+ assert!(error.to_string().contains("workspace already exists"));
+ assert!(installer.requests.borrow().is_empty());
+}