crates/gunnar-build-thing/src/main.rs
//! `gunnar-build <variant>`: build one declared build point, and prove it landed.
//!
//! # Why this binary exists
//!
//! [`VARIANTS`] already declares every build point gunnar has — package, bin,
//! features, profile, target — migrated verbatim from
//! `.nornir/build-variants.toml` and held to it by `tests/toml_parity.rs`. What
//! did not exist was any way to *run* one. So every build was typed by hand,
//! and on 2026-08-08 two sweeps were lost to the two ways that goes wrong:
//!
//! 1. **The wrong binary.** `gunnar` and `gunnar-service` are different
//! artifacts and a sweep needs both. Rebuilding one and re-running reports
//! the other's arms still stale, which reads exactly like a code regression.
//! 2. **`required-features`, silently.** `gunnar-service` is
//! `required-features = ["systemd"]`, so
//! `cargo build --release -p gunnar-cli --bin gunnar-service` **builds
//! nothing, prints a hint, and exits 0** — leaving the stale binary in place.
//! That is the failure "measure, never assert" is written against: "a
//! malformed command fails silently and its empty output is shaped exactly
//! like a clean result."
//!
//! Neither is a mistake anyone stops making by being careful. Both are gone by
//! construction here: the feature set is a property of the variant, and success is
//! read off the **artifact on disk**, never off cargo's exit code.
//!
//! # It is not a second declaration
//!
//! One implementation. This binary owns no build knowledge. It looks a variant
//! up in [`VARIANTS`] and materializes the argv that variant already describes.
//! Adding a build point means editing `.nornir/build-variants.toml` and
//! [`VARIANTS`] — which `toml_parity` keeps in step — and never this file.
//!
//! # Why not `nornir build`
//!
//! The installed `nornir` CLI cannot parse gunnar's matrix: it rejects
//! `role = "solo"` (`.nornir/build-variants.toml:469`), accepting only
//! `server|client|library|tool`. gunnar's own loader parses it, so gunnar's
//! declaration is ahead of the upstream schema. Recorded rather than worked
//! around: when upstream learns `solo`, this binary becomes a thin shim over
//! `nornir build` and should be replaced by one.
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
use std::time::SystemTime;
use gunnar_build_thing::{by_name, Variant, VARIANTS};
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
if args.is_empty() || args.iter().any(|a| a == "-h" || a == "--help") {
usage();
return ExitCode::FAILURE;
}
// ── The EMISSION doors (2026-08-30) ─────────────────────────────────────────
//
// `.nornir/build-variants.toml` is this crate's OUTPUT, not its input. It was
// a hand-maintained mirror until this emitter, and the mirror had already
// lost two whole groups to a loader that dropped what it could not parse (an
// audit found them). `emit` is the only way the file is now written.
if args[0] == "toml" {
return match gunnar_build_thing::emitted_toml() {
Ok(t) => {
print!("{t}");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("gunnar-build-thing: {e:#}");
ExitCode::FAILURE
}
};
}
if args[0] == "emit" {
let root = args.get(1).map(PathBuf::from).unwrap_or_else(repo_root);
return match gunnar_build_thing::write_emitted(&root) {
Ok(p) => {
println!("emitted {}", p.display());
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("gunnar-build-thing: {e:#}");
ExitCode::FAILURE
}
};
}
if args[0] == "--list" {
for c in VARIANTS {
let what = match (c.package, c.bin) {
(Some(p), Some(b)) => format!("-p {p} --bin {b}"),
(Some(p), None) => format!("-p {p}"),
_ => "(composes other variants; runs no cargo)".to_string(),
};
println!("{:<24} {what}", c.name);
}
return ExitCode::SUCCESS;
}
let repo = repo_root();
let newest = newest_source(&repo);
let mut failures = Vec::new();
for name in &args {
let Some(variant) = by_name(name) else {
eprintln!("gunnar-build: no build point named `{name}`");
eprintln!(" try `gunnar-build --list`");
return ExitCode::FAILURE;
};
if variant.package.is_none() {
eprintln!("gunnar-build: `{name}` composes other variants and runs no cargo; skipping");
continue;
}
eprintln!("gunnar-build: {name}");
if let Err(e) = build_one(&repo, variant, newest) {
failures.push(format!("{name}: {e}"));
}
}
if failures.is_empty() {
return ExitCode::SUCCESS;
}
eprintln!("\ngunnar-build: {} FAILED", failures.len());
for f in &failures {
eprintln!(" ✗ {f}");
}
ExitCode::FAILURE
}
/// Materialize this variant's cargo invocation, run it, and assert the artifact.
fn build_one(repo: &Path, variant: &Variant, newest: Option<SystemTime>) -> Result<(), String> {
let mut cmd = Command::new(env!("CARGO"));
cmd.current_dir(repo).args(cargo_args(variant));
match cmd.status() {
Ok(s) if s.success() => {}
Ok(s) => return Err(format!("cargo exited {s}")),
Err(e) => return Err(format!("could not run cargo: {e}")),
}
// ── the assertion cargo's exit code cannot make ─────────────────────────
let Some(bin) = variant.bin else {
return Ok(());
};
let out = artifact(repo, variant, bin);
let meta = std::fs::metadata(&out).map_err(|_| {
format!(
"cargo exited 0 and {} does not exist.\n \
That is the `required-features` skip: cargo prints a hint and succeeds.",
out.display()
)
})?;
if let (Ok(built), Some(src)) = (meta.modified(), newest) {
if built < src {
let behind = src
.duration_since(built)
.map(|d| d.as_secs() / 60)
.unwrap_or(0);
return Err(format!(
"{} is {behind} minute(s) OLDER than the newest source under crates/.\n \
cargo exited 0 without relinking it, so this is a different binary from the \
one the source describes.",
out.display()
));
}
}
eprintln!(" ok {}", out.display());
Ok(())
}
/// The `cargo` arguments that build this variant: `build`, the profile unless it is the
/// default one, the package and bin, the feature switches and the target.
fn cargo_args(variant: &Variant) -> Vec<String> {
let mut args = vec!["build".to_string()];
if !variant.profile.is_empty() && variant.profile != "dev" {
args.extend(["--profile".to_string(), variant.profile.to_string()]);
}
if let Some(p) = variant.package {
args.extend(["-p".to_string(), p.to_string()]);
}
if let Some(b) = variant.bin {
args.extend(["--bin".to_string(), b.to_string()]);
}
if variant.no_default_features {
args.push("--no-default-features".to_string());
}
if !variant.features.is_empty() {
args.extend(["--features".to_string(), variant.features.join(",")]);
}
if let Some(t) = variant.target {
args.extend(["--target".to_string(), t.to_string()]);
}
args
}
/// Where cargo puts this variant's binary.
///
/// `CARGO_TARGET_DIR` is honoured because the forge bench arms look for gunnar
/// in the bench's own target directory and nowhere else — building into one
/// directory while an arm reads another is the failure this whole binary exists
/// to prevent.
fn artifact(repo: &Path, variant: &Variant, bin: &str) -> PathBuf {
let root = std::env::var("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| repo.join("target"));
let dir = match variant.profile {
"" | "dev" => "debug",
other => other,
};
match variant.target {
Some(t) => root.join(t).join(dir).join(bin),
None => root.join(dir).join(bin),
}
}
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("crates/gunnar-build-thing has two ancestors")
.to_path_buf()
}
/// The newest mtime under `crates/`, which is the rule
/// `bench/examples/nornir-bench.rs` refuses to measure against.
///
/// Deliberately the SAME rule rather than a looser one: a check here that
/// passed where the sweep's fails would be worse than none, because it would
/// send someone into a forty-minute run believing they were ready.
fn newest_source(repo: &Path) -> Option<SystemTime> {
fn walk(dir: &Path, newest: &mut Option<SystemTime>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let Ok(meta) = entry.metadata() else { continue };
if meta.is_dir() {
if path.file_name().is_some_and(|n| n == "target") {
continue;
}
walk(&path, newest);
} else if path.extension().is_some_and(|e| e == "rs" || e == "toml") {
if let Ok(m) = meta.modified() {
if newest.is_none() || newest.is_some_and(|n| m > n) {
*newest = Some(m);
}
}
}
}
}
let mut newest = None;
walk(&repo.join("crates"), &mut newest);
newest
}
fn usage() {
eprintln!(
"usage: gunnar-build <variant>... build declared build points and prove they landed
gunnar-build --list every build point gunnar declares
gunnar-build toml the emitted .nornir/build-variants.toml, on stdout
gunnar-build emit [root] REGENERATE .nornir/build-variants.toml from the code
The declaration is `GunnarBuildThing::variants` in this crate; the toml is
EMITTED from it (code → toml, one-directional) and is never read back as
authority. This binary adds no build knowledge of its own (one implementation)."
);
}
#[cfg(test)]
mod tests {
use super::*;
fn target_root(repo: &Path) -> PathBuf {
std::env::var("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| repo.join("target"))
}
/// The artifact is looked for where cargo writes it: the `dev` profile (and the
/// unnamed one) under `debug`, never under a directory called `dev`; a named
/// profile under its own name; a target triple between the root and the profile.
#[test]
fn the_artifact_is_looked_for_where_cargo_writes_it() {
let repo = Path::new("/work/gunnar");
let root = target_root(repo);
let dev = Variant {
profile: "dev",
target: None,
..VARIANTS[0]
};
assert_eq!(
artifact(repo, &dev, "gunnar"),
root.join("debug").join("gunnar")
);
let unnamed = Variant {
profile: "",
target: None,
..VARIANTS[0]
};
assert_eq!(
artifact(repo, &unnamed, "gunnar"),
root.join("debug").join("gunnar")
);
let musl = Variant {
profile: "dev-media",
target: Some("x86_64-unknown-linux-musl"),
..VARIANTS[0]
};
assert_eq!(
artifact(repo, &musl, "gunnar-server"),
root.join("x86_64-unknown-linux-musl")
.join("dev-media")
.join("gunnar-server")
);
}
/// The argv is the variant's and nothing else: the default profile is not named
/// (cargo's `dev` is spelled by omission), every feature travels in one
/// `--features` list, and `--no-default-features` and the target are passed when the
/// variant says so.
#[test]
fn the_cargo_line_is_the_variants_own() {
let full = Variant {
profile: "release",
package: Some("gunnar-server"),
bin: Some("gunnar-server"),
no_default_features: true,
features: &["engine-znippy", "http"],
target: Some("x86_64-unknown-linux-musl"),
..VARIANTS[0]
};
assert_eq!(
cargo_args(&full).join(" "),
"build --profile release -p gunnar-server --bin gunnar-server --no-default-features \
--features engine-znippy,http --target x86_64-unknown-linux-musl"
);
let plain = Variant {
profile: "dev",
package: Some("gunnar"),
bin: None,
no_default_features: false,
features: &[],
target: None,
..VARIANTS[0]
};
assert_eq!(cargo_args(&plain), ["build", "-p", "gunnar"]);
}
/// The staleness rule reads `.rs` and `.toml` files under `crates/` and skips every
/// `target` directory: a build's own output is newer than every source and would make
/// every binary look stale.
#[test]
fn the_newest_source_skips_targets_and_other_files() {
let dir = std::env::temp_dir().join(format!("gunnar-build-newest-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let at = |rel: &str, secs: u64| {
let path = dir.join(rel);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let file = std::fs::File::create(&path).unwrap();
file.set_modified(std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs))
.unwrap();
};
assert_eq!(newest_source(&dir), None, "no crates/ at all");
at("crates/a/src/lib.rs", 1_000);
at("crates/a/Cargo.toml", 2_000);
at("crates/a/target/debug/build.rs", 9_000);
at("crates/a/README.md", 8_000);
let newest = newest_source(&dir).expect("two sources");
assert_eq!(
newest,
std::time::UNIX_EPOCH + std::time::Duration::from_secs(2_000)
);
let _ = std::fs::remove_dir_all(&dir);
}
}