diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib.rs | 61 | ||||
| -rw-r--r-- | src/main.rs | 54 |
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()); +} |