summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHermes Agent <hermes@localhost>2026-08-11 20:53:53 +0000
committerHermes Agent <hermes@localhost>2026-08-11 20:53:53 +0000
commitde352f683f104c68106d488863f338afdb836e94 (patch)
tree47812ded4b6a042c4a86e55f16bc661c4c657d57
Add independent Rust Hasher rootfs builder
-rw-r--r--.gitignore3
-rw-r--r--Cargo.lock7
-rw-r--r--Cargo.toml12
-rw-r--r--README.md31
-rw-r--r--profiles/alt-controller.profile7
-rw-r--r--profiles/package-lists/controller-base187
-rw-r--r--profiles/package-lists/openssh5
-rw-r--r--profiles/package-lists/systemd5
-rw-r--r--src/lib.rs61
-rw-r--r--src/main.rs54
-rw-r--r--tests/profile.rs16
11 files changed, 388 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..393816f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/target/
+/work/
+/out/
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..261153e
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "alt-controller-image"
+version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..5284bb6
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "alt-controller-image"
+version = "0.1.0"
+edition = "2024"
+description = "Independent Rust/Hasher rootfs image builder for ALT Controller"
+
+[lib]
+path = "src/lib.rs"
+
+[[bin]]
+name = "alt-controller-image"
+path = "src/main.rs"
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4cd3b96
--- /dev/null
+++ b/README.md
@@ -0,0 +1,31 @@
+# alt-controller-image
+
+An independent Rust proof of concept that produces an ALT Controller rootfs tarball without invoking `mkimage-profiles`, `mkimage`, or GNU Make.
+
+## Pipeline
+
+1. Rust parses `profiles/alt-controller.profile`.
+2. The profile uses vendored package lists under `profiles/package-lists/`.
+3. Rust initializes an isolated Hasher work root with `hsh --initroot-only`.
+4. Rust installs the resolved package set with `hsh-install`.
+5. Rust archives `work/alt-controller/chroot` to `out/alt-controller-rootfs.tar` using `tar`, excluding Hasher's `.host` helper directory.
+
+Hasher is intentionally the only external build dependency in this slice. It supplies ALT package resolution and isolation; the image/profile orchestration belongs to this project.
+
+## Build
+
+```bash
+cargo test
+cargo build --release
+sudo -u hermes ./target/release/alt-controller-image
+```
+
+The output is:
+
+```text
+out/alt-controller-rootfs.tar
+```
+
+## Scope
+
+The goal is a working, independent `alt-controller` rootfs tarball. This first slice packages the target’s vendored controller, systemd, and OpenSSH lists plus the explicit target packages and real-time kernel selection. It does not yet reproduce every post-install script or VM-image conversion behavior from the legacy meta-profile; those become separately testable Rust pipeline stages rather than Make fragments.
diff --git a/profiles/alt-controller.profile b/profiles/alt-controller.profile
new file mode 100644
index 0000000..a4ef680
--- /dev/null
+++ b/profiles/alt-controller.profile
@@ -0,0 +1,7 @@
+# Independent package manifest for vm/alt-controller.tar.
+# Source package lists are vendored below profiles/package-lists/.
+base: basesystem fakeroot interactivesystem shadow-utils e2fsprogs apt apt-https bootloader-utils make-initrd chrony systemd-networkd iputils control
+include: package-lists/controller-base
+include: package-lists/systemd
+include: package-lists/openssh
+regex: ^kernel-(image|modules-())-(rt)$
diff --git a/profiles/package-lists/controller-base b/profiles/package-lists/controller-base
new file mode 100644
index 0000000..8318811
--- /dev/null
+++ b/profiles/package-lists/controller-base
@@ -0,0 +1,187 @@
+# from base list
+installer-feature-online-repo@!E2K
+#make-initrd-multipath
+lsof
+terminfo-extra
+
+### use/firmware/cpu
+make-initrd-ucode@X86
+iucode_tool@X86
+firmware-intel-ucode@X86
+
+# interactivesystem
+anacron
+apt
+apt-repo
+apt-rsync
+apt-https
+apt-scripts
+#glibc-gconv-modules
+#glibc-nss
+#glibc-utils
+interactivesystem
+kbd
+setarch
+shadow-suite
+#bc
+udev
+#udev-extras
+pciutils
+su
+#sudo
+
+make-initrd
+#make-initrd-lvm
+#make-initrd-luks
+#make-initrd-mdadm
+make-initrd-multipath
+
+# network tools
+curl
+etcnet
+dhcpcd
+openresolv
+hostinfo
+iptables
+iputils
+sysstat
+#iotop
+#lftp
+#mailx
+#netcat
+netlist
+#nfs-utils
+openssh-clients
+rsync
+traceroute
+ntpdate
+tcpdump
+#ifplugd
+
+# disk tools
+#eject
+hdparm
+#lvm2
+#mdadm
+#mtools
+#ntfs-3g
+sfdisk
+fdisk
+usbutils
+#xfsprogs
+smartmontools
+#reiserfsprogs
+#jfsutils
+#dosfstools
+multipath-tools
+#btrfs-progs
+
+# scsi
+#sdparm
+#sg3_utils
+
+# servers
+openssh-server
+openssh-server-control
+openssh-common
+openssh-blacklist
+irqbalance
+cgroup
+
+# security
+openssl
+#osec-mailreport
+audit
+
+# debug
+#strace
+
+# ?
+#rp-pppoe-base
+
+# ldap
+#nss-ldapd
+
+# haldbus :(
+#dbus
+
+# mc :(
+#mc
+#bash-completion
+
+#screen
+
+lm_sensors3
+update-kernel
+pciids
+
+# compression
+cpio
+#p7zip
+tar
+unzip
+zip
+xz
+
+#expect
+
+#system-report
+
+glibc-timezones
+
+#alt-issue-server
+#color-prompt-and-man
+local-policy
+
+#alterator-manager
+#alteratorctl
+#alt-components-base
+#alterator-backend-edition
+#polkit
+gnutls-utils
+
+#nano
+#exfatprogs
+gzip-utils
+attr
+ca-certificates-digital.gov.ru
+#libnss-role
+#alterator-roles-common
+keyutils
+gostsum
+#tmux
+
+#wget
+#nftables
+fwupd@x86_64
+fwupd-efi
+ipmitool
+inxi
+#fio
+#cfdisk
+bash-completion-util-linux
+#pinfo
+iftop
+lft
+mtr
+whois
+openssh-askpass-common
+
+cpupower
+systemd-networkd
+
+#avahi-mdns.component
+#avahi-daemon
+#avahi-tools
+#libnss-mdns
+
+# for controller
+libiec61850
+# lib60870
+linuxptp
+libgcrypt-gost20
+gcrypt-gost-utils
+openssl-gost-engine
+# some 2fa pam modules?
+rsyslog
+
diff --git a/profiles/package-lists/openssh b/profiles/package-lists/openssh
new file mode 100644
index 0000000..6593203
--- /dev/null
+++ b/profiles/package-lists/openssh
@@ -0,0 +1,5 @@
+openssh
+openssh-server
+openssh-server-control
+openssh-blacklist
+openssh-clients
diff --git a/profiles/package-lists/systemd b/profiles/package-lists/systemd
new file mode 100644
index 0000000..1ba2010
--- /dev/null
+++ b/profiles/package-lists/systemd
@@ -0,0 +1,5 @@
+systemd
+systemd-sysvinit
+systemd-analyze
+chkconfig
+hwclock
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());
+}
diff --git a/tests/profile.rs b/tests/profile.rs
new file mode 100644
index 0000000..19e6481
--- /dev/null
+++ b/tests/profile.rs
@@ -0,0 +1,16 @@
+use std::path::Path;
+
+use alt_controller_image::Profile;
+
+#[test]
+fn profile_expands_vendored_package_lists_and_discards_comments() {
+ let profile = Profile::load(Path::new("profiles/alt-controller.profile"))
+ .expect("target profile must parse");
+
+ assert!(profile.packages.contains(&"basesystem".to_owned()));
+ assert!(profile.packages.contains(&"openssh-server".to_owned()));
+ assert!(profile.packages.contains(&"systemd-sysvinit".to_owned()));
+ assert!(profile.packages.contains(&"libiec61850".to_owned()));
+ assert!(!profile.packages.iter().any(|entry| entry.starts_with('#')));
+ assert!(profile.regex_packages.contains(&"^kernel-(image|modules-())-(rt)$".to_owned()));
+}