diff options
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | Cargo.lock | 7 | ||||
| -rw-r--r-- | Cargo.toml | 12 | ||||
| -rw-r--r-- | README.md | 31 | ||||
| -rw-r--r-- | profiles/alt-controller.profile | 7 | ||||
| -rw-r--r-- | profiles/package-lists/controller-base | 187 | ||||
| -rw-r--r-- | profiles/package-lists/openssh | 5 | ||||
| -rw-r--r-- | profiles/package-lists/systemd | 5 | ||||
| -rw-r--r-- | src/lib.rs | 61 | ||||
| -rw-r--r-- | src/main.rs | 54 | ||||
| -rw-r--r-- | tests/profile.rs | 16 |
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())); +} |