xtask/src/fast.rs
//! **`cargo xtask fast`**: Gunnar's estate on monetize's real code, its real box binaries in
//! rootless containers, in minutes, not the 45-minute KVM gate (monetize v2.0 P1, A24 §8.2;
//! A28 H14, lane N3). Holger's verb is the pattern; what is Gunnar's is prepared here and the
//! run is monetize's `monetize-fast` (through its binary, from `../monetize`):
//!
//! 1. Gunnar's two box rows, compiled in the toolchain container with their DECLARED argv
//! (`gunnar-front-metered`, `gunnar-server-metered`; the profile dropped, as Holger's verb
//! drops it), static musl binaries any image runs;
//! 2. Gunnar's CONTAINER MEDIA: what `gunnar-front-installer-metered` and
//! `gunnar-installer-metered` boot (the program, the row's `korp.args=` and its `korp.env.*`,
//! from the declaration, never typed here), as JSON;
//! 3. Gunnar's monetize GUEST (`gunnar-monetize-guest`, wasm32, unsigned: the run signs it under
//! its own CA) and the ONE box's kit, built by monetize's `monetize-fast kit`;
//! 4. `monetize-fast --product gunnar --kit …` on the host: it re-runs itself inside podman's
//! rootless network namespace and writes the receipt (`target/fast/receipt.txt`, `.json`).
//! The estate's names and media kinds are the guest's facts (op 90), read by monetize-fast.
//!
//! `cargo xtask fast [--out DIR] [--budget-s N] [--twin-late-s N] [--no-build]`. Also
//! `cargo xtask fast --product gunnar` from monetize.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use gunnar_recipe::nornir_build_thing::variant::{BuildVariant, BuildVariantSet};
/// The rows whose binaries the boxes run: (row, binary).
pub const ROWS: [(&str, &str); 2] = [
("gunnar-front-metered", "gunnar-front"),
("gunnar-server-metered", "gunnar-server"),
];
/// The target those rows declare.
pub const TARGET: &str = "x86_64-unknown-linux-musl";
/// The machines' hostname prefixes (the guest's `lifecycle.facts` shape) and the installer row
/// each boots (the twin boots the appliance's medium; the election makes it the twin).
pub const MEDIA: [(&str, &str); 3] = [
("gunnar-front-", gunnar_recipe::FRONT_METERED_INSTALLER),
("gunnar-appliance-", gunnar_recipe::METERED_INSTALLER),
("gunnar-twin-", gunnar_recipe::METERED_INSTALLER),
];
/// Gunnar's monetize guest: its manifest (a detached workspace) and its module.
pub const GUEST_MANIFEST: &str = "crates/gunnar-monetize-guest/Cargo.toml";
pub const GUEST_WASM: &str = "wasm32-unknown-unknown/release/gunnar_monetize_guest.wasm";
/// **What a container needs that the metal's PID 1 gives**: korp-start opens gunnar-server's
/// start-up disk test (`disktest::spawn`'s gate: PID 1 booted by korp-start); a container is not
/// that, so without its own switch a box answers the installer "the disk test has not finished"
/// for 300 s before it joins. Said here, the one place the media differ from the rows.
///
/// **The monetize key** (T-OWNERFLOW, 2026-10-26): the metered rows name the PRODUCTION box's
/// key (`GUNNAR_MONETIZE_PUBKEY=<hex>`), and the run's ONE box signs with its own, so its boxes
/// refused every order it queued (a paid sign-up's account never made). `fed`: the key the
/// run's door feeds, as the constant medium carries none (`gunnar_common::monetize_key::FED`).
pub const CONTAINER_ENV: [(&str, &str); 2] =
[("GUNNAR_DISKTEST", "1"), ("GUNNAR_MONETIZE_PUBKEY", "fed")];
/// gunnar-ui's paid console row (trunk, on the host), which the metered front embeds: the
/// sale check reads its `/login`.
pub const CONSOLE_ROW: &str = "gunnar-ui-web-metered";
/// The twin boots this many seconds after the others by default (A12's order, as Holger's).
pub const DEFAULT_TWIN_LATE_S: u64 = 20;
/// A container medium (mock-upcloud's `container::Medium`): what a machine runs from `/opt/bin`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContainerMedium {
pub program: String,
pub args: Vec<String>,
pub env: BTreeMap<String, String>,
}
fn json_str(s: &str) -> String {
let mut o = String::from("\"");
for c in s.chars() {
match c {
'"' => o.push_str("\\\""),
'\\' => o.push_str("\\\\"),
'\n' => o.push_str("\\n"),
c if (c as u32) < 0x20 => o.push_str(&format!("\\u{:04x}", c as u32)),
c => o.push(c),
}
}
o.push('"');
o
}
impl ContainerMedium {
/// The medium's JSON (`container_medium: 1`).
pub fn to_json(&self) -> String {
let args = self
.args
.iter()
.map(|a| json_str(a))
.collect::<Vec<_>>()
.join(", ");
let env = self
.env
.iter()
.map(|(k, v)| format!("{}: {}", json_str(k), json_str(v)))
.collect::<Vec<_>>()
.join(", ");
format!("{{\"container_medium\": 1, \"program\": {}, \"args\": [{args}], \"env\": {{{env}}}, \"files\": {{}}}}\n", json_str(&self.program))
}
}
/// Pure: **the container medium an installer row boots**: the program is its variant's binary
/// (what `korp.entry` execs on the metal), the arguments its `korp.args=` token, the environment
/// its `korp.env.*` tokens (its declared `env` included), as `effective_boot_params` renders them.
pub fn medium_of(set: &BuildVariantSet, installer: &str) -> Result<ContainerMedium, String> {
let row = set
.installers
.iter()
.find(|i| i.name == installer)
.ok_or_else(|| format!("[fast-media] the installer row {installer} is not declared"))?;
let variant = set
.variants
.iter()
.find(|v| v.name == row.variant)
.ok_or_else(|| {
format!(
"[fast-media] {installer} boots {}, which is not declared",
row.variant
)
})?;
let program = variant
.bin
.clone()
.ok_or_else(|| format!("[fast-media] {} names no binary", variant.name))?;
let mut args = Vec::new();
let mut env = BTreeMap::new();
for p in row.effective_boot_params() {
if let Some(a) = p.strip_prefix("korp.args=") {
args = a
.split(',')
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect();
} else if let Some(kv) = p.strip_prefix("korp.env.") {
if let Some((k, v)) = kv.split_once('=') {
env.insert(k.to_string(), v.to_string());
}
}
}
env.entry("RUST_LOG".into())
.or_insert_with(|| "info".into());
if program == "gunnar-server" {
for (k, v) in CONTAINER_ENV {
env.insert(k.to_string(), v.to_string());
}
}
Ok(ContainerMedium { program, args, env })
}
/// Pure: a row's declared build argv, the profile dropped (a debug build, as Holger's fast and
/// suite-check build), `--locked`.
pub fn row_args(v: &BuildVariant) -> Vec<String> {
let mut a = v.cargo_args();
if let Some(i) = a.iter().position(|x| x == "--profile") {
a.drain(i..(i + 2).min(a.len()));
}
a.insert(1, "--locked".into());
a
}
/// **The CI image** the toolchain container runs by default: pinned by DIGEST, the one dwarves'
/// `ci/runner.sh` and nornir-xtask-support's `ci::CI_IMAGE` pin (A KNOWN TWIN). ci4: the pinned
/// recipe plus OpenSSL headers, clippy, rustfmt, openssh-client, strace and git-lfs
/// (dwarves' `ci/ci-toolchain.Containerfile`); a box gets it by
/// `podman load -i ci4-53fec13a5c1c7f2c.oci.tar`, which keeps the digest.
pub const CI_IMAGE: &str = "localhost/nordisk-cargo-toolchain@sha256:875a7a325dfe7f6b838535d085a2a502e8ad250076c947f6d023b1e224f03334";
/// Pure: **the toolchain container's argv** for `cargo args` in this checkout, the same shape as
/// nornir-xtask-support's `ci::Container` (A KNOWN TWIN: gunnar's xtask links no
/// nornir-xtask-support): the CI image (`NORNIR_CI_IMAGE`), the constellation at `/src`, the
/// target at `/target`, the shared registry, sccache and mold (`NORNIR_CI_REGISTRY`,
/// `NORNIR_CI_FAST`, `NORNIR_CI_SCCACHE`), as the invoking user.
pub fn toolchain_argv(
root: &Path,
target: &Path,
env: &dyn Fn(&str) -> Option<String>,
args: &[String],
console: Option<&Path>,
) -> Vec<String> {
let parent = root.parent().unwrap_or(root);
let repo = root
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let mut v: Vec<String> = [
"podman",
"run",
"--rm",
"--init",
"--pids-limit=-1",
"--userns=keep-id",
"--passwd-entry=$USERNAME:*:$UID:$GID:$NAME:/tmp:/bin/sh",
]
.iter()
.map(|s| s.to_string())
.collect();
let mut bind = |host: &str, at: &str, ro: bool| {
v.push("-v".into());
v.push(format!("{host}:{at}{}", if ro { ":ro" } else { "" }));
};
bind(&parent.display().to_string(), "/src", false);
bind(&target.display().to_string(), "/target", false);
// NORNIR_CI_REGISTRY, else the caller's own `$CARGO_HOME/registry` (else
// `~/.cargo/registry`) when it exists: `ci::Container`'s rule (GHUNT-5). Without a mount
// cargo writes into the image's registry, which the keep-id user may not.
let registry = env("NORNIR_CI_REGISTRY")
.filter(|r| !r.is_empty())
.or_else(|| {
env("CARGO_HOME")
.filter(|h| !h.is_empty())
.or_else(|| {
env("HOME")
.filter(|h| !h.is_empty())
.map(|h| format!("{h}/.cargo"))
})
.map(|h| format!("{h}/registry"))
.filter(|p| Path::new(p).is_dir())
});
if let Some(r) = registry {
bind(&r, "/usr/local/cargo/registry", false);
}
if let Some(f) = env("NORNIR_CI_FAST") {
bind(&f, "/tools", true);
}
if let Some(s) = env("NORNIR_CI_SCCACHE") {
bind(&s, "/sccache", false);
}
if let Some(f) = env("NORNIR_CI_FAST") {
v.push("--env-file".into());
v.push(format!("{f}/env.sh"));
}
// The run's console, read-only at its own path, named to the front's build script.
if let Some(c) = console {
v.push("-v".into());
v.push(format!("{}:{}:ro", c.display(), c.display()));
v.push("-e".into());
v.push(format!("GUNNAR_FRONT_CONSOLE_DIST={}", c.display()));
}
for (k, val) in [
("CARGO_TARGET_DIR", "/target"),
("NORNIR_CI_IN_CONTAINER", "1"),
("GIT_CONFIG_COUNT", "1"),
("GIT_CONFIG_KEY_0", "safe.directory"),
("GIT_CONFIG_VALUE_0", "*"),
] {
v.push("-e".into());
v.push(format!("{k}={val}"));
}
if let Some(j) = env("CARGO_BUILD_JOBS") {
v.push("-e".into());
v.push(format!("CARGO_BUILD_JOBS={j}"));
}
v.push("-w".into());
v.push(format!("/src/{repo}"));
v.push(
env("NORNIR_CI_IMAGE")
.filter(|i| !i.is_empty())
.unwrap_or_else(|| CI_IMAGE.into()),
);
v.push("cargo".into());
v.extend(args.iter().cloned());
v
}
/// The toolchain container's target: `$CARGO_TARGET_DIR/toolchain` (as `ci::Container`), else
/// `<root>/target/toolchain`.
pub fn toolchain_target(root: &Path) -> PathBuf {
std::env::var("NORNIR_CI_TOOLCHAIN_TARGET")
.ok()
.filter(|v| !v.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| {
std::env::var("CARGO_TARGET_DIR")
.ok()
.filter(|v| !v.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| root.join("target"))
.join("toolchain")
})
}
/// **Where this run's console is**: `GUNNAR_FRONT_CONSOLE_DIST` when the caller names one, else
/// the run's OWN directory, `<out>/console/dist-metered`. Never gunnar-ui's source tree: two runs
/// of one checkout that start together would both build into it (FAST-REL, 2026-10-25).
pub fn console_dist(out: &Path, env: Option<&str>) -> PathBuf {
match env.filter(|v| !v.is_empty()) {
Some(d) => PathBuf::from(d),
None => out.join("console").join("dist-metered"),
}
}
/// **The lock beside trunk's intermediates**: trunk writes `wasm-bindgen/` and `wasm-opt/` into
/// the cargo target the console's build uses (`CARGO_TARGET_DIR`, else gunnar-ui's `target`), so
/// two console builds over one target take turns there; each still writes its own bundle.
pub fn console_lock_path(ui: &Path, cargo_target: Option<&str>) -> PathBuf {
cargo_target
.filter(|v| !v.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| ui.join("target"))
.join("gunnar-console-build.lock")
}
/// Held while this process builds a console over one target; removed on drop.
struct ConsoleLock(PathBuf);
impl Drop for ConsoleLock {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
/// Pure: is the lock's holder (`pid` as written in the lock) gone? An unreadable pid is gone.
pub fn holder_gone(pid: &str, alive: &dyn Fn(u32) -> bool) -> bool {
pid.trim().parse::<u32>().map(|p| !alive(p)).unwrap_or(true)
}
/// Take the lock at `path`, waiting while a live process holds it; a dead holder's lock is taken over.
fn take_console_lock(path: &Path) -> Result<ConsoleLock, String> {
if let Some(d) = path.parent() {
std::fs::create_dir_all(d).map_err(|e| format!("{}: {e}", d.display()))?;
}
let mut said = false;
loop {
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
{
Ok(mut f) => {
use std::io::Write as _;
f.write_all(std::process::id().to_string().as_bytes())
.map_err(|e| format!("{}: {e}", path.display()))?;
return Ok(ConsoleLock(path.to_path_buf()));
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
let pid = std::fs::read_to_string(path).unwrap_or_default();
if holder_gone(&pid, &|p| Path::new(&format!("/proc/{p}")).exists())
&& !pid.is_empty()
{
let _ = std::fs::remove_file(path);
continue;
}
if !said {
println!(
"fast: another console build holds {} (pid {}); waiting for it",
path.display(),
pid.trim()
);
said = true;
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
Err(e) => return Err(format!("{}: {e}", path.display())),
}
}
}
/// A front without its console serves `[console-not-on-medium]` at `/login`, and the ONE box's
/// sale check refuses the estate: refused here, by name, before anything is laid.
pub fn refuse_front_without_console(dist: &Path) -> Result<(), String> {
if dist.join("index.html").is_file() {
Ok(())
} else {
Err(format!("[fast-console] {} has no index.html: the metered front would serve no console and the sale check would refuse it; build it with `cargo xtask build --variant {CONSOLE_ROW}` in ../gunnar-ui", dist.display()))
}
}
/// **The run's console**: gunnar-ui's paid console, built (trunk, on the host) into the run's own
/// directory ([`console_dist`]) when it is not there. Its directory is returned (absolute).
pub fn build_console(root: &Path, out: &Path) -> Result<PathBuf, String> {
let out = std::path::absolute(out).map_err(|e| format!("{}: {e}", out.display()))?;
let dist = console_dist(
&out,
std::env::var("GUNNAR_FRONT_CONSOLE_DIST").ok().as_deref(),
);
if refuse_front_without_console(&dist).is_err() {
let ui = root.parent().unwrap_or(root).join("gunnar-ui");
let _held = take_console_lock(&console_lock_path(
&ui,
std::env::var("CARGO_TARGET_DIR").ok().as_deref(),
))?;
println!("fast: build gunnar-ui's console: cargo xtask build --variant {CONSOLE_ROW} in {}, into {}", ui.display(), dist.display());
let s = Command::new(cargo())
.current_dir(&ui)
.args(["xtask", "build", "--variant", CONSOLE_ROW])
.env("GUNNAR_UI_BUNDLE_DIST", &dist)
.status()
.map_err(|e| format!("cargo xtask build in {}: {e}", ui.display()))?;
if !s.success() {
return Err(format!(
"[fast-build] gunnar-ui's {CONSOLE_ROW}: cargo xtask build exited {s}"
));
}
}
refuse_front_without_console(&dist)?;
Ok(dist)
}
fn in_a_container() -> bool {
std::env::var("NORNIR_CI_IN_CONTAINER").is_ok_and(|v| v == "1")
|| Path::new("/run/.containerenv").exists()
|| Path::new("/.dockerenv").exists()
}
fn value(args: &[String], flag: &str) -> Option<String> {
args.iter()
.position(|a| a == flag)
.and_then(|i| args.get(i + 1))
.cloned()
}
fn cargo() -> String {
std::env::var("CARGO").unwrap_or_else(|_| "cargo".into())
}
fn in_toolchain(
root: &Path,
target: &Path,
what: &str,
args: &[String],
console: Option<&Path>,
) -> Result<(), String> {
let argv = toolchain_argv(
root,
target,
&|k| std::env::var(k).ok().filter(|v| !v.is_empty()),
args,
console,
);
println!("fast: build {what}: cargo {}", args.join(" "));
let s = Command::new(&argv[0])
.args(&argv[1..])
.status()
.map_err(|e| format!("{}: {e}", argv[0]))?;
if s.success() {
Ok(())
} else {
Err(format!("[fast-build] {what}: cargo exited {s}"))
}
}
/// The verb.
pub fn run(root: &Path, args: &[String]) -> Result<(), String> {
if in_a_container() && !std::env::var("NORNIR_CI_HOST").is_ok_and(|v| v == "1") {
return Err("[fast-on-the-host] cargo xtask fast starts containers: run it on the host, not inside the toolchain container".into());
}
let out = value(args, "--out")
.map(PathBuf::from)
.unwrap_or_else(|| root.join("target/fast"));
let budget = value(args, "--budget-s").unwrap_or_else(|| "180".into());
let twin_late = value(args, "--twin-late-s").unwrap_or_else(|| DEFAULT_TWIN_LATE_S.to_string());
let build = !args.iter().any(|a| a == "--no-build");
std::fs::create_dir_all(&out).map_err(|e| format!("{}: {e}", out.display()))?;
let parent = root.parent().ok_or("gunnar's parent")?;
let monetize = parent.join("monetize");
if !monetize.join("crates/monetize-fast/Cargo.toml").is_file() {
return Err(
"[fast-no-monetize] ../monetize/crates/monetize-fast is not checked out beside gunnar"
.into(),
);
}
let set = gunnar_recipe::emit_recipe::declared();
let target = toolchain_target(root);
std::fs::create_dir_all(&target).map_err(|e| format!("{}: {e}", target.display()))?;
let bin_dir = target.join(TARGET).join("debug");
// 1. the box binaries, from the declared rows, in the toolchain container; the front embeds
// gunnar-ui's console, built first when it is not there (trunk on the host, as Holger's).
let dist = if build {
build_console(root, &out)?
} else {
console_dist(
&std::path::absolute(&out).map_err(|e| e.to_string())?,
std::env::var("GUNNAR_FRONT_CONSOLE_DIST").ok().as_deref(),
)
};
refuse_front_without_console(&dist)?;
if build {
for (row, _) in ROWS {
let v = set
.variants
.iter()
.find(|v| v.name == row)
.ok_or_else(|| format!("[fast-row] {row} is not declared"))?;
// The front embeds THIS run's console: its directory, at the same path inside.
let console = (row == ROWS[0].0).then_some(dist.as_path());
in_toolchain(root, &target, row, &row_args(v), console)?;
}
}
for (_, bin) in ROWS {
if !bin_dir.join(bin).is_file() {
return Err(format!(
"[fast-build] {} is not built",
bin_dir.join(bin).display()
));
}
}
// 3. Gunnar's guest, then the ONE box's kit (monetize's).
let guest = target.join(GUEST_WASM);
let kit_dir = out.join("kit");
if build {
let a: Vec<String> = [
"build",
"--locked",
"--manifest-path",
GUEST_MANIFEST,
"--release",
"--target",
"wasm32-unknown-unknown",
]
.iter()
.map(|s| s.to_string())
.collect();
in_toolchain(root, &target, "gunnar-monetize-guest", &a, None)?;
if !guest.is_file() {
return Err(format!("[fast-build] {} is not built", guest.display()));
}
let s = Command::new(cargo())
.current_dir(&monetize)
.args([
"run",
"--quiet",
"--manifest-path",
"crates/monetize-fast/Cargo.toml",
"--bin",
"monetize-fast",
"--",
"kit",
"--out",
])
.arg(&kit_dir)
.arg("--parent")
.arg(parent)
.arg("--guest")
.arg(format!("gunnar={}", guest.display()))
.status()
.map_err(|e| format!("monetize-fast kit: {e}"))?;
if !s.success() {
return Err(format!("[fast-kit] monetize-fast kit exited {s}"));
}
}
// 2. Gunnar's container media, from the declared installer rows.
let media_dir = out.join("media");
std::fs::create_dir_all(&media_dir).map_err(|e| e.to_string())?;
let mut run = Command::new(cargo());
run.current_dir(&monetize)
.args([
"run",
"--quiet",
"--manifest-path",
"crates/monetize-fast/Cargo.toml",
"--bin",
"monetize-fast",
"--",
])
.args(["--product", "gunnar", "--kit"])
.arg(&kit_dir)
.arg("--bin-dir")
.arg(&bin_dir)
.arg("--out")
.arg(&out)
.args(["--budget-s", &budget, "--twin-late-s", &twin_late]);
for (prefix, installer) in MEDIA {
let m = medium_of(&set, installer)?;
let f = media_dir.join(format!("{prefix}json"));
std::fs::write(&f, m.to_json()).map_err(|e| format!("{}: {e}", f.display()))?;
run.arg("--medium").arg(format!("{prefix}={}", f.display()));
}
for tree in ["gunnar", "gunnar-ui", "monetize", "Skidbladnir", "edda"] {
run.arg("--tree")
.arg(format!("{tree}={}", parent.join(tree).display()));
}
// 4. the run, monetize's.
println!(
"fast: monetize-fast --product gunnar (the receipt goes to {})",
out.display()
);
let s = run
.status()
.map_err(|e| format!("cargo run monetize-fast: {e}"))?;
if !s.success() {
return Err(format!(
"[fast-red] the fast test is RED; see {}",
out.join("receipt.txt").display()
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gunnars_media_boot_what_its_metered_installer_rows_boot() {
let set = gunnar_recipe::emit_recipe::declared();
let server = medium_of(&set, gunnar_recipe::METERED_INSTALLER).unwrap();
assert_eq!(
server.program, "gunnar-server",
"korp.entry execs the server variant's binary"
);
assert_eq!(
server.args,
["serve", "--grpc-bind", "utility:50051"],
"the row's korp.args="
);
assert_eq!(
server.env.get("GUNNAR_APPLIANCE").map(String::as_str),
Some("1")
);
assert_eq!(
server.env.get("GUNNAR_DATA_ROOT").map(String::as_str),
Some("/data")
);
assert!(
server.env.contains_key("GUNNAR_MONETIZE_PUBKEY"),
"the row's declared env rides too: {:?}",
server.env
);
// ★ T-OWNERFLOW (2026-10-26, the owner's walk on the real boxes): the row names the
// PRODUCTION box's monetize key; the run's ONE box signs with its own, so the boxes
// refused every order it queued ("does not verify under this box's monetize key") and a
// paid sign-up stayed pending. The run's door feeds its key, as the constant medium's is.
assert_eq!(server.env.get("GUNNAR_MONETIZE_PUBKEY").map(String::as_str), Some("fed"), "the boxes take (gunnar_common::monetize_key::FED) the run's own monetize key from the door: {:?}", server.env);
assert_eq!(
server.env.get("GUNNAR_DISKTEST").map(String::as_str),
Some("1"),
"the disk test korp-start's PID 1 runs on the metal"
);
let front = medium_of(&set, gunnar_recipe::FRONT_METERED_INSTALLER).unwrap();
assert_eq!(front.program, "gunnar-front");
assert!(front.args.is_empty(), "gunnar-front takes no korp.args");
assert!(
!front.env.contains_key("GUNNAR_APPLIANCE"),
"no appliance gate on the front"
);
assert!(
!front.env.contains_key("GUNNAR_DISKTEST"),
"the front runs no disk test"
);
assert!(medium_of(&set, "no-such-installer")
.unwrap_err()
.contains("[fast-media]"));
}
#[test]
fn a_medium_is_monetize_fasts_container_medium_json() {
let m = ContainerMedium {
program: "gunnar-server".into(),
args: vec!["serve".into()],
env: BTreeMap::from([("K".to_string(), "a \"b\"".to_string())]),
};
assert_eq!(m.to_json(), "{\"container_medium\": 1, \"program\": \"gunnar-server\", \"args\": [\"serve\"], \"env\": {\"K\": \"a \\\"b\\\"\"}, \"files\": {}}\n");
let parsed = crate::json::Json::parse(&m.to_json()).unwrap();
assert_eq!(
parsed
.get("env")
.and_then(|e| e.get("K"))
.and_then(|v| v.str()),
Some("a \"b\"")
);
}
#[test]
fn the_box_rows_are_declared_musl_and_built_without_their_profile() {
let set = gunnar_recipe::emit_recipe::declared();
for (row, bin) in ROWS {
let v = set
.variants
.iter()
.find(|v| v.name == row)
.unwrap_or_else(|| panic!("{row}"));
assert_eq!(v.bin.as_deref(), Some(bin));
assert_eq!(v.target.as_deref(), Some(TARGET));
let a = row_args(v);
assert_eq!(&a[..2], ["build", "--locked"]);
assert!(!a.iter().any(|x| x == "--profile"), "{a:?}");
}
let front = set
.variants
.iter()
.find(|v| v.name == "gunnar-front-metered")
.unwrap();
assert!(
row_args(front)
.windows(2)
.any(|w| w[0] == "--manifest-path"
&& w[1].ends_with("crates/gunnar-front/Cargo.toml")),
"the front is a detached workspace"
);
}
#[test]
fn the_media_prefixes_are_the_guests_own_hostnames() {
// The names are Gunnar's facts (its guest's `lifecycle.facts` shape): a prefix here that
// the guest does not lay would boot no machine.
let shape = include_str!("../../crates/gunnar-monetize-guest/src/lifecycle.rs");
for (prefix, _) in MEDIA {
assert!(
shape.contains(&format!("\"{prefix}{{company}}\"")),
"{prefix} is not a hostname the guest lays"
);
}
}
#[test]
fn a_front_without_its_console_is_refused_by_name() {
let d = std::env::temp_dir().join(format!("gunnar-fast-console-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
let e = refuse_front_without_console(&d).unwrap_err();
assert!(
e.contains("[fast-console]") && e.contains(CONSOLE_ROW),
"{e}"
);
std::fs::write(d.join("index.html"), "<html>").unwrap();
assert!(refuse_front_without_console(&d).is_ok());
let _ = std::fs::remove_dir_all(&d);
assert_eq!(
console_dist(Path::new("/runs/a"), None),
Path::new("/runs/a/console/dist-metered"),
"the run's own, never gunnar-ui's tree"
);
assert_ne!(
console_dist(Path::new("/runs/a"), None),
console_dist(Path::new("/runs/b"), None),
"two runs, two consoles"
);
assert_eq!(
console_dist(Path::new("/runs/a"), Some("/x")),
Path::new("/x"),
"the caller's"
);
assert_eq!(
console_dist(Path::new("/runs/a"), Some("")),
Path::new("/runs/a/console/dist-metered"),
"an empty value is unset"
);
}
#[test]
fn the_toolchain_argv_is_the_ci_containers_shape() {
let env = |k: &str| match k {
"NORNIR_CI_IMAGE" => Some("img:1".to_string()),
"NORNIR_CI_FAST" => Some("/tools-host".to_string()),
_ => None,
};
let a = toolchain_argv(
Path::new("/c/gunnar"),
Path::new("/t/toolchain"),
&env,
&["build".into()],
None,
);
let s = a.join(" ");
assert!(
s.starts_with("podman run --rm --init --pids-limit=-1 --userns=keep-id"),
"{s}"
);
assert!(s.contains("-v /c:/src -v /t/toolchain:/target -v /tools-host:/tools:ro --env-file /tools-host/env.sh"), "{s}");
assert!(
!s.contains("--network host"),
"never host networking beside env.sh: {s}"
);
assert!(s.ends_with("-w /src/gunnar img:1 cargo build"), "{s}");
}
/// ★ GHUNT-5 (lane OX-GUNHUNT, 2026-10-28): without NORNIR_CI_REGISTRY the container
/// mounts the caller's own `$CARGO_HOME/registry` (else `~/.cargo/registry`), as the
/// known twin `nornir_xtask_support::ci::Container` does. It mounted none, so a plain
/// `cargo xtask fast` (and `cargo xtask idp`) on a dev box ran cargo against the image's
/// own registry, which the keep-id user cannot write: "failed to create directory
/// `/usr/local/cargo/registry/cache/...`: Permission denied", before any build.
#[test]
fn without_nornir_ci_registry_the_callers_cargo_home_registry_is_mounted() {
let home = std::env::temp_dir().join(format!("ghunt5-{}", std::process::id()));
std::fs::create_dir_all(home.join("cargo/registry")).unwrap();
std::fs::create_dir_all(home.join("user/.cargo/registry")).unwrap();
let cargo_home = home.join("cargo").display().to_string();
let user = home.join("user").display().to_string();
let with_cargo_home = |k: &str| match k {
"CARGO_HOME" => Some(cargo_home.clone()),
"HOME" => Some(user.clone()),
_ => None,
};
let s = toolchain_argv(
Path::new("/c/gunnar"),
Path::new("/t"),
&with_cargo_home,
&["build".into()],
None,
)
.join(" ");
assert!(
s.contains(&format!(
"-v {cargo_home}/registry:/usr/local/cargo/registry"
)),
"{s}"
);
let only_home = |k: &str| (k == "HOME").then(|| user.clone());
let s = toolchain_argv(
Path::new("/c/gunnar"),
Path::new("/t"),
&only_home,
&["build".into()],
None,
)
.join(" ");
assert!(
s.contains(&format!(
"-v {user}/.cargo/registry:/usr/local/cargo/registry"
)),
"{s}"
);
let named = |k: &str| match k {
"NORNIR_CI_REGISTRY" => Some("/reg".to_string()),
"CARGO_HOME" => Some(cargo_home.clone()),
_ => None,
};
let s = toolchain_argv(
Path::new("/c/gunnar"),
Path::new("/t"),
&named,
&["build".into()],
None,
)
.join(" ");
assert!(
s.contains("-v /reg:/usr/local/cargo/registry"),
"the named one wins: {s}"
);
assert_eq!(
s.matches(":/usr/local/cargo/registry").count(),
1,
"one registry: {s}"
);
let _ = std::fs::remove_dir_all(&home);
}
/// ★ Without NORNIR_CI_IMAGE the toolchain container is the CI image pinned by DIGEST, the
/// one dwarves' ci/runner.sh and nornir-xtask-support's `ci::CI_IMAGE` pin (ci4: ssh client,
/// strace, git-lfs; `:live` had none and names another image on each box).
#[test]
fn the_toolchain_image_is_the_ci_image_pinned_by_digest() {
let env = |_: &str| None;
let a = toolchain_argv(
Path::new("/c/gunnar"),
Path::new("/t"),
&env,
&["build".into()],
None,
);
let s = a.join(" ");
assert!(
s.ends_with(&format!("-w /src/gunnar {CI_IMAGE} cargo build")),
"{s}"
);
let (name, digest) = CI_IMAGE
.split_once("@sha256:")
.expect("CI_IMAGE names a digest");
assert_eq!(name, "localhost/nordisk-cargo-toolchain");
assert_eq!(digest.len(), 64, "{digest}");
assert!(
digest
.bytes()
.all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()),
"{digest}"
);
let empty = |k: &str| (k == "NORNIR_CI_IMAGE").then(String::new);
assert!(
toolchain_argv(
Path::new("/c/gunnar"),
Path::new("/t"),
&empty,
&["build".into()],
None
)
.join(" ")
.contains(CI_IMAGE),
"an empty NORNIR_CI_IMAGE is unset"
);
// The runner's pin, when the dwarves sibling is checked out beside this repo.
let runner = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../dwarves/ci/runner.sh");
if let Ok(r) = std::fs::read_to_string(&runner) {
let line = r
.lines()
.find(|l| l.starts_with("CI_IMAGE="))
.expect("runner.sh sets CI_IMAGE");
assert_eq!(
line.trim_start_matches("CI_IMAGE=")
.trim()
.trim_matches('"'),
CI_IMAGE,
"{}",
runner.display()
);
}
}
/// ★ The front row embeds THE RUN's console: mounted read-only at its own path and named to
/// its build script; the other rows see none.
#[test]
fn the_front_row_reads_the_runs_own_console() {
let env = |_: &str| None;
let s = toolchain_argv(
Path::new("/c/gunnar"),
Path::new("/t"),
&env,
&["build".into()],
Some(Path::new("/runs/a/console/dist-metered")),
)
.join(" ");
assert!(s.contains("-v /runs/a/console/dist-metered:/runs/a/console/dist-metered:ro -e GUNNAR_FRONT_CONSOLE_DIST=/runs/a/console/dist-metered"), "{s}");
let s = toolchain_argv(
Path::new("/c/gunnar"),
Path::new("/t"),
&env,
&["build".into()],
None,
)
.join(" ");
assert!(!s.contains("GUNNAR_FRONT_CONSOLE_DIST"), "{s}");
}
/// ★ Console builds over one target take turns at one lock beside trunk's intermediates; a
/// lock whose holder died is taken over, a live holder's is waited for.
#[test]
fn console_builds_over_one_target_share_one_lock() {
assert_eq!(
console_lock_path(Path::new("/c/gunnar-ui"), Some("/t/fast-gunnar")),
Path::new("/t/fast-gunnar/gunnar-console-build.lock")
);
assert_eq!(
console_lock_path(Path::new("/c/gunnar-ui"), None),
Path::new("/c/gunnar-ui/target/gunnar-console-build.lock")
);
assert!(holder_gone("4242", &|_| false), "a dead holder");
assert!(!holder_gone("4242\n", &|p| p == 4242), "a live holder");
assert!(holder_gone("not a pid", &|_| true), "an unreadable lock");
let d = std::env::temp_dir().join(format!("gunnar-console-lock-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
let p = d.join("l.lock");
std::fs::create_dir_all(&d).unwrap();
std::fs::write(&p, "999999999").unwrap();
let held = take_console_lock(&p).expect("a dead holder's lock is taken over");
assert_eq!(
std::fs::read_to_string(&p).unwrap(),
std::process::id().to_string()
);
drop(held);
assert!(!p.exists(), "released on drop");
let _ = std::fs::remove_dir_all(&d);
}
}