summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib.rs61
-rw-r--r--src/main.rs54
2 files changed, 115 insertions, 0 deletions
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..b7bf8c2
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,61 @@
+use std::fs;
+use std::path::{Path, PathBuf};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Profile {
+ pub packages: Vec<String>,
+ pub regex_packages: Vec<String>,
+}
+
+impl Profile {
+ pub fn load(path: &Path) -> Result<Self, String> {
+ let profile = fs::read_to_string(path).map_err(|error| error.to_string())?;
+ let parent = path.parent().ok_or("profile path has no parent")?;
+ let mut packages = Vec::new();
+ let mut regex_packages = Vec::new();
+
+ for line in profile.lines() {
+ let line = line.trim();
+ if line.is_empty() || line.starts_with('#') {
+ continue;
+ }
+ if let Some(values) = line.strip_prefix("base:") {
+ extend_words(&mut packages, values);
+ } else if let Some(relative) = line.strip_prefix("include:") {
+ let list = parent.join(relative.trim());
+ let contents = fs::read_to_string(&list)
+ .map_err(|error| format!("{}: {error}", list.display()))?;
+ for entry in contents.lines().map(str::trim) {
+ if entry.is_empty() || entry.starts_with('#') {
+ continue;
+ }
+ let package = entry.split('@').next().unwrap_or(entry).trim();
+ if !package.is_empty() {
+ packages.push(package.to_owned());
+ }
+ }
+ } else if let Some(value) = line.strip_prefix("regex:") {
+ regex_packages.push(value.trim().to_owned());
+ } else {
+ return Err(format!("unsupported profile statement: {line}"));
+ }
+ }
+ packages.sort();
+ packages.dedup();
+ regex_packages.sort();
+ regex_packages.dedup();
+ Ok(Self { packages, regex_packages })
+ }
+
+ pub fn install_arguments(&self) -> Vec<String> {
+ self.regex_packages.iter().chain(self.packages.iter()).cloned().collect()
+ }
+}
+
+fn extend_words(target: &mut Vec<String>, values: &str) {
+ target.extend(values.split_whitespace().map(str::to_owned));
+}
+
+pub fn artifact_path(project: &Path) -> PathBuf {
+ project.join("out/alt-controller-rootfs.tar")
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..44cb296
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,54 @@
+use std::env;
+use std::fs;
+use std::path::PathBuf;
+use std::process::Command;
+
+use alt_controller_image::{artifact_path, Profile};
+
+fn run(command: &mut Command, description: &str) {
+ let status = command.status().unwrap_or_else(|error| panic!("{description}: {error}"));
+ assert!(status.success(), "{description} exited with {status}");
+}
+
+fn main() {
+ let project = env::current_dir().expect("current directory");
+ let profile_path = project.join("profiles/alt-controller.profile");
+ let profile = Profile::load(&profile_path).unwrap_or_else(|error| panic!("profile: {error}"));
+ let workdir: PathBuf = project.join("work/alt-controller");
+ let rootfs = workdir.join("chroot");
+ let artifact = artifact_path(&project);
+
+ if workdir.exists() {
+ fs::remove_dir_all(&workdir).expect("remove previous Hasher workdir");
+ }
+ fs::create_dir_all(&workdir).expect("create Hasher workdir");
+ fs::create_dir_all(artifact.parent().expect("artifact parent")).expect("create output directory");
+
+ run(
+ Command::new("hsh")
+ .args(["--mountpoints=/proc", "--initroot-only", "--workdir"])
+ .arg(&workdir),
+ "initialize isolated Hasher root",
+ );
+
+ let mut install = Command::new("hsh-install");
+ install.args(["--mountpoints=/proc", "--workdir"]);
+ install.arg(&workdir);
+ install.args(profile.install_arguments());
+ run(&mut install, "install target rootfs packages");
+
+ if artifact.exists() {
+ fs::remove_file(&artifact).expect("remove previous artifact");
+ }
+ run(
+ Command::new("tar")
+ .args(["--numeric-owner", "--xattrs", "--acls", "--exclude=./.host", "-C"])
+ .arg(&rootfs)
+ .args(["-cpf"])
+ .arg(&artifact)
+ .arg("."),
+ "archive isolated rootfs",
+ );
+
+ println!("{}", artifact.display());
+}