summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--profiles/apt.conf3
-rw-r--r--src/hasher.rs95
-rw-r--r--src/lib.rs2
-rw-r--r--src/package_installer.rs91
-rw-r--r--tests/package_installer.rs72
5 files changed, 263 insertions, 0 deletions
diff --git a/profiles/apt.conf b/profiles/apt.conf
new file mode 100644
index 0000000..739e12f
--- /dev/null
+++ b/profiles/apt.conf
@@ -0,0 +1,3 @@
+// Project-owned APT configuration passed to Hasher during initroot creation.
+// Repository selection remains controlled by the host Hasher configuration.
+APT::Get::Assume-Yes "true";
diff --git a/src/hasher.rs b/src/hasher.rs
new file mode 100644
index 0000000..0be1808
--- /dev/null
+++ b/src/hasher.rs
@@ -0,0 +1,95 @@
+use std::ffi::OsString;
+use std::process::Command;
+
+use anyhow::{bail, Context, Result};
+
+use crate::package_installer::{PackageInstaller, PackageRequest};
+
+#[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 fn program(&self) -> &OsString {
+ &self.program
+ }
+
+ pub fn arguments(&self) -> &[OsString] {
+ &self.arguments
+ }
+}
+
+pub trait CommandRunner {
+ fn run(&self, invocation: Invocation) -> Result<()>;
+}
+
+#[derive(Debug, Default, Clone, Copy)]
+pub struct ProcessRunner;
+
+impl CommandRunner for ProcessRunner {
+ fn run(&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(())
+ }
+}
+
+#[derive(Debug)]
+pub struct HasherInstaller<R> {
+ runner: R,
+}
+
+impl<R> HasherInstaller<R> {
+ pub fn new(runner: R) -> Self {
+ Self { runner }
+ }
+
+ pub fn runner(&self) -> &R {
+ &self.runner
+ }
+}
+
+impl<R: CommandRunner> PackageInstaller for HasherInstaller<R> {
+ fn install(&self, request: &PackageRequest) -> Result<()> {
+ self.runner.run(Invocation::new(
+ "hsh",
+ [
+ "--mountpoints=/proc".into(),
+ "--initroot-only".into(),
+ "--apt-config".into(),
+ request.apt_config().as_path().as_os_str().to_owned(),
+ "--workdir".into(),
+ request.workdir().as_os_str().to_owned(),
+ ],
+ ))?;
+
+ let mut arguments = vec![
+ OsString::from("--mountpoints=/proc"),
+ OsString::from("--workdir"),
+ request.workdir().as_os_str().to_owned(),
+ ];
+ arguments.extend(request.selectors().iter().map(OsString::from));
+ arguments.extend(request.packages().iter().map(OsString::from));
+ self.runner.run(Invocation::new("hsh-install", arguments))
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 8016607..cfcc645 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,6 +1,8 @@
pub mod cli;
pub mod files;
+pub mod hasher;
pub mod model;
+pub mod package_installer;
pub mod plan;
pub mod rootfs;
pub mod stage;
diff --git a/src/package_installer.rs b/src/package_installer.rs
new file mode 100644
index 0000000..b08c469
--- /dev/null
+++ b/src/package_installer.rs
@@ -0,0 +1,91 @@
+use std::path::{Path, PathBuf};
+
+use anyhow::{bail, Result};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct AptConfig(PathBuf);
+
+impl AptConfig {
+ pub fn new(path: impl AsRef<Path>) -> Result<Self> {
+ let path = path.as_ref();
+ if path.as_os_str().is_empty() || path.is_dir() {
+ bail!("APT configuration must be a file path: {}", path.display());
+ }
+ Ok(Self(path.to_path_buf()))
+ }
+
+ pub fn as_path(&self) -> &Path {
+ &self.0
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct PackageRequest {
+ workdir: PathBuf,
+ apt_config: AptConfig,
+ packages: Vec<String>,
+ selectors: Vec<String>,
+}
+
+impl PackageRequest {
+ pub fn new(
+ workdir: impl AsRef<Path>,
+ apt_config: AptConfig,
+ packages: impl IntoIterator<Item = impl Into<String>>,
+ selectors: impl IntoIterator<Item = impl Into<String>>,
+ ) -> Result<Self> {
+ let workdir = workdir.as_ref();
+ if workdir.as_os_str().is_empty() {
+ bail!("Hasher workdir cannot be empty");
+ }
+
+ let mut packages = normalize("package", packages)?;
+ let mut selectors = normalize("package selector", selectors)?;
+ packages.sort();
+ packages.dedup();
+ selectors.sort();
+ selectors.dedup();
+ Ok(Self {
+ workdir: workdir.to_path_buf(),
+ apt_config,
+ packages,
+ selectors,
+ })
+ }
+
+ pub fn workdir(&self) -> &Path {
+ &self.workdir
+ }
+
+ pub fn apt_config(&self) -> &AptConfig {
+ &self.apt_config
+ }
+
+ pub fn packages(&self) -> &[String] {
+ &self.packages
+ }
+
+ pub fn selectors(&self) -> &[String] {
+ &self.selectors
+ }
+}
+
+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() {
+ bail!("{kind} cannot be empty");
+ }
+ Ok(value)
+ })
+ .collect()
+}
+
+pub trait PackageInstaller {
+ fn install(&self, request: &PackageRequest) -> Result<()>;
+}
diff --git a/tests/package_installer.rs b/tests/package_installer.rs
new file mode 100644
index 0000000..622d378
--- /dev/null
+++ b/tests/package_installer.rs
@@ -0,0 +1,72 @@
+use std::path::PathBuf;
+
+use alt_controller_image::hasher::{CommandRunner, HasherInstaller, Invocation};
+use alt_controller_image::package_installer::{AptConfig, PackageInstaller, PackageRequest};
+
+#[derive(Default)]
+struct RecordingRunner {
+ invocations: std::cell::RefCell<Vec<Invocation>>,
+}
+
+impl CommandRunner for RecordingRunner {
+ fn run(&self, invocation: Invocation) -> anyhow::Result<()> {
+ self.invocations.borrow_mut().push(invocation);
+ Ok(())
+ }
+}
+
+#[test]
+fn hasher_installer_passes_typed_request_to_initroot_and_install_commands() {
+ let runner = RecordingRunner::default();
+ let installer = HasherInstaller::new(runner);
+ let request = PackageRequest::new(
+ "/build/work",
+ AptConfig::new("profiles/apt.conf").expect("valid apt config path"),
+ ["basesystem", "make-initrd"],
+ ["^kernel-image-rt$"],
+ )
+ .expect("valid package request");
+
+ installer.install(&request).expect("installation succeeds");
+ let invocations = &installer.runner().invocations.borrow();
+
+ assert_eq!(
+ invocations.as_slice(),
+ [
+ Invocation::new(
+ "hsh",
+ [
+ "--mountpoints=/proc",
+ "--initroot-only",
+ "--apt-config",
+ "profiles/apt.conf",
+ "--workdir",
+ "/build/work",
+ ],
+ ),
+ Invocation::new(
+ "hsh-install",
+ [
+ "--mountpoints=/proc",
+ "--workdir",
+ "/build/work",
+ "^kernel-image-rt$",
+ "basesystem",
+ "make-initrd",
+ ],
+ ),
+ ]
+ );
+}
+
+#[test]
+fn package_request_rejects_empty_packages_and_unsafe_apt_config_paths() {
+ assert!(AptConfig::new("").is_err());
+ assert!(PackageRequest::new(
+ PathBuf::from("work"),
+ AptConfig::new("profiles/apt.conf").expect("valid apt config"),
+ ["basesystem", ""],
+ std::iter::empty::<&str>(),
+ )
+ .is_err());
+}