crates/nornir-build-thing/src/suite_cli.rs
//! **The ONE way to build a suite member** — `cargo xtask build --suite <s> [--variant <v>]`.
//!
//! (`--atom` is the deprecated spelling of `--variant` and is accepted forever —
//! the written laws cite it and muscle memory is not to be broken.)
//!
//! # Why this lives here and not in each repo's xtask
//!
//! Every member repo grew its own build tooling next to its declared build space,
//! and the two drifted. Measured 2026-08-01 across the constellation: `holger-build`
//! hardcodes three tier names and cannot see the other ten variants on disk;
//! `nornir-buildspec-holger` re-declares holger's variants in Rust (8 in code vs 13
//! in the TOML, and the Rust marks NO default at all while the TOML does);
//! `dwarves/src/service.rs` hand-writes `cargo build -p dwarves --bin dwarves-full
//! --features embed-nornir,server` while the declaration for that same binary says
//! `features = ["embed-all", "read-fat"]`; `korp/src/service.rs` does the same for
//! `korp-server` with a third feature set. Four hand-rolled build paths, four
//! different answers to "what is this binary".
//!
//! The fix is not a better copy. It is that there is ONE function, here, that turns
//! `(suite, atom)` into a cargo invocation by READING `.nornir/build-variants.toml`,
//! and every xtask in the house is three lines that call it.
//!
//! # Why it delegates to this crate rather than to dwarves
//!
//! dwarves is the FORGE that drives builds, and the obvious phrasing is "xtask calls
//! dwarves". It cannot: dwarves is the downstream-most member, it path-deps nornir,
//! and `nornir/tests/no_dwarves_edge.rs` exists specifically to keep nornir from
//! depending on it. An `xtask -> dwarves` edge in nornir (or in korp, which nornir's
//! graph also constrains) inverts the suite dep graph — the very rule
//! [`BuildVariantSet::validate_shape`](crate::variant::BuildVariantSet::validate_shape)
//! enforces on artifacts. So the shared code sits in `nornir-build-thing`, the
//! dep-light leaf BOTH dwarves and every member already depend on, and dwarves keeps
//! being the forge that CALLS it. One writer either way; this one is legal.
//!
//! # The suite is a PARAMETER
//!
//! `--suite` is not a flag some repos take and others reject. Every member tags its
//! atoms `suites = ["ORIGINAL"]` even though it ships one delivery, so
//! `cargo xtask build --suite ORIGINAL` works in korp, dwarves, nornir, modgunn and
//! holger alike, and holger's second delivery is `--suite MVP` with no new flag, no
//! new verb and no tooling that learns a special case. Adding a second delivery to
//! any of the others later is a DECLARATION change and nothing else.
//!
//! An unknown suite is a hard error that lists the declared ones. A selector that
//! quietly builds everything (or nothing) when it does not recognise its argument is
//! decorative, and decorative selectors are how a "we build the MVP nightly" claim
//! stays green while building the full product.
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, bail, Context, Result};
use crate::variant::{BuildVariant, BuildVariantSet, UNSUITED};
/// The parsed `build` verb.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct BuildArgs {
/// `--suite <name>`. `None` means the repo's single delivery, and is an ERROR
/// the moment a repo declares more than one — see [`resolve`].
pub suite: Option<String>,
/// `--variant <name>` (deprecated alias: `--atom`). `None` builds every
/// cargo-buildable variant of the suite.
///
/// The FIELD keeps its legacy name on purpose: downstream constructs
/// `BuildArgs { atom: … }` directly (nornir's bin, dwarves' forge), and the
/// vocabulary fix is a CLI-surface rename, not an API break.
pub atom: Option<String>,
/// `--list` — resolve and print, build nothing. The guard tests use this to
/// assert WHICH variants a suite selects without paying for a release link.
pub list: bool,
/// `--attest` — build the delivery so its receipt can be PROVEN.
///
/// See [`attest_atom`]. Off by default: a developer's `xtask build` should
/// not pay for a stripper and an attestation file, and a delivery should not
/// be able to go out without one.
pub attest: bool,
/// Repeatable `--param name=value` — install-time inputs for iso/installer
/// cells (see [`crate::params`]): kept RAW here (unparsed pairs) so the
/// refusals — unknown name, secret-shaped name, wrong shape — come from
/// the ONE validation path (`apply_cli_params`) at the point of use, next
/// to the cell whose declaration they are checked against.
pub params: Vec<String>,
/// Repeatable `--opt name=on|off` — the OPT-OUT toggles an iso/installer
/// variant DECLARES (see [`crate::options`]). Kept RAW here for the same
/// reason `params` is: the refusals — an undeclared name, a spelling that
/// is neither on nor off — come from the ONE resolution path, next to the
/// declaration they are checked against.
pub opts: Vec<String>,
/// Repeatable `--build <path|coordinate>` — build the consumed build thing
/// behind an EDGE of the variant's tree as a step of this build
/// ([`crate::flags::Flag::Build`]; law C renders and parses it). Kept RAW
/// like the others: resolving the target to an edge of the tree is the
/// door's job — [`build_line`](BuildArgs::build_line) gives the three raw
/// lists their ONE meaning ([`crate::build_dag::BuildLine`]), and
/// [`BuildDag::derive`](crate::build_dag::BuildDag::derive) the ordered steps.
/// A door that does not chain refuses the flag BY NAME with the DAG's own
/// lines (the manual two-step, in the order the DAG would run it).
pub builds: Vec<String>,
/// `--features a,b` · `--no-default-features` · `--all-features` — the
/// operator's cargo feature choice, applied on top of the chosen CARGO
/// variant(s) and passed to cargo exactly as given
/// ([`BuildVariant::with_cargo_features`]).
pub cargo: crate::variant::CargoFeatures,
}
impl BuildArgs {
/// **The `--build` line these arguments spell** — `opts`, `params` and
/// `builds` folded into one canonical [`BuildLine`](crate::build_dag::BuildLine)
/// (a `--build X { … }` typed on the prompt arrives in `builds` as one
/// string and is parsed with its brace). Syntax only; resolving against the
/// target variant's tree is [`BuildLine::canonical_in`](crate::build_dag::BuildLine::canonical_in).
pub fn build_line(&self) -> Result<crate::build_dag::BuildLine> {
crate::build_dag::BuildLine::from_args(&self.opts, &self.params, &self.builds)
}
}
/// **The suite-level installer door (gap analysis item 19)** — the seam that
/// makes "master a per-site installer ISO" a SUITE capability instead of one
/// repo's xtask verb. nornir-build-thing owns the grammar (cell resolution,
/// `--param` refusals, provenance) but cannot own the COMPOSITION (korp-boot,
/// the PID-1s, the pinned kernel live in korp-installer, which depends on
/// this crate — the arrow points the other way), so the composition arrives
/// by injection: korp-installer's `ForgeComposer` implements this, and any
/// CLI (dwarves `buildthing`, a product xtask) passes it in.
pub trait InstallerComposer {
/// **The fixtures this composition can mint** for a `--param name=@fixture`
/// against the cell's declared `[build.installer.fixtures]`. Default: none
/// — `@fixture` is then refused by name. The composition that lays a
/// `hub.conf` onto P3 (korp-installer's `ForgeComposer`) is also the one
/// that can mint the hub, so it overrides this; the seam is here rather
/// than a crate feature because minting a WireGuard hub needs
/// `wg-appliance-core`, which this dep-light crate does not pull.
fn fixtures(&self) -> &dyn crate::params::FixtureSource {
&crate::params::NoFixtures
}
/// Compose + master `cell` (params already applied) into an installer
/// ISO. `kernel` is a real EFI-stub bzImage; `app` the product's packed
/// `.znippy`. Returns the mastered ISO's path.
fn compose_installer(
&self,
cell: &crate::installer::InstallerBuild,
kernel: &Path,
app: &Path,
repo_root: &Path,
repo_label: &str,
) -> Result<std::path::PathBuf>;
}
/// Resolve an installer CELL, apply `--param` values through the ONE
/// validation path, and hand the composition to `composer`. The suite-CLI
/// twin of a product's `installer-iso` verb: same refusals, same provenance
/// (applied params ride the cell into the attested descriptor).
pub fn run_installer(
repo_root: &Path,
repo_label: &str,
cell_name: Option<&str>,
kernel: &Path,
app: &Path,
raw_params: &[String],
composer: &dyn InstallerComposer,
) -> Result<std::path::PathBuf> {
run_installer_applied(repo_root, repo_label, cell_name, kernel, app, raw_params, composer)
.map(|(iso, _)| iso)
}
/// What one installer run applied: the parameter record and the OPTION record.
/// Both ride the cell into the attested descriptor; this only surfaces them so
/// a door can print them and a pane can show them in the receipt.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AppliedSelection {
/// The parameter values, in declaration order.
pub params: Vec<crate::params::AppliedParam>,
/// The option states, in declaration order.
pub options: Vec<crate::options::AppliedOption>,
}
impl AppliedSelection {
/// The lines a door prints, options first — a toggle changes what the
/// binary IS, so it is read before the values that configure it.
pub fn describe(&self) -> Vec<String> {
self.options
.iter()
.map(|o| format!("opt {}", o.describe()))
.chain(self.params.iter().map(|p| format!("param {}", p.describe())))
.collect()
}
}
/// [`run_installer`], also handing back the APPLIED parameter record — what
/// a door prints beside the ISO path and what the pane's result head shows
/// (`AppliedParam::describe`: a fixture-filled value says so). The record is
/// the same one the attested descriptor carries; this only surfaces it.
pub fn run_installer_applied(
repo_root: &Path,
repo_label: &str,
cell_name: Option<&str>,
kernel: &Path,
app: &Path,
raw_params: &[String],
composer: &dyn InstallerComposer,
) -> Result<(std::path::PathBuf, Vec<crate::params::AppliedParam>)> {
run_installer_selected(repo_root, repo_label, cell_name, kernel, app, raw_params, &[], composer)
.map(|(iso, applied)| (iso, applied.params))
}
/// [`run_installer_applied`] with the OPTION toggles as well — the full door.
///
/// The toggles are resolved and the values applied in one step
/// (`InstallerBuild::apply_cli_selection_with`), because the toggles decide
/// which values are required and which are refused; see that method for why
/// the order cannot be the caller's choice.
pub fn run_installer_selected(
repo_root: &Path,
repo_label: &str,
cell_name: Option<&str>,
kernel: &Path,
app: &Path,
raw_params: &[String],
raw_opts: &[String],
composer: &dyn InstallerComposer,
) -> Result<(std::path::PathBuf, AppliedSelection)> {
let set = BuildVariantSet::discover(repo_root)?.ok_or_else(|| {
anyhow!(
"no {} — this repo declares no build space",
BuildVariantSet::conventional_path(repo_root).display()
)
})?;
let mut cell = match cell_name {
Some(name) => set
.installers
.iter()
.find(|i| i.name == name)
.cloned()
.ok_or_else(|| {
anyhow!(
"no [[build.installer]] cell named '{name}' (declared: {})",
set.installers.iter().map(|i| i.name.as_str()).collect::<Vec<_>>().join(", ")
)
})?,
None => match set.installers.as_slice() {
[one] => one.clone(),
[] => bail!("this repo declares no [[build.installer]] cell"),
many => bail!(
"this repo declares {} installer cells — name one: {}",
many.len(),
many.iter().map(|i| i.name.as_str()).collect::<Vec<_>>().join(", ")
),
},
};
let supplied = crate::params::parse_cli_params(raw_params)?;
// **The TREE splits the `--opt` line, not `parse_cli_options`.** That
// function's return type is `name → bool`, so it answered every `--opt`
// with "an option is on or off" — and this door is the one an operator's
// `--opt kernel.preset=nuked` actually reached on the ISO-compose path
// (`assemble_cell::compose_installer_row` hands the composer the UNSPLIT
// line, correctly: the kernel choice is judged upstream against the
// DONOR's declaration, and this door must simply not choke on it).
// gunnar's BUILD-KEYED-INSTALLER.md carried a "do NOT add --opt
// kernel.preset=" warning for that sentence instead of a fix.
//
// The tree already knows what each path IS: `dhcp` a feature, `data.fs` a
// val with two choices, `kernel.preset` a val behind an EDGE — and the
// three buckets are exactly the three things this door does with them.
let tree = crate::tree::SettingsTree::of_installer(&cell, None);
let split = tree.split_opts(raw_opts).map_err(|e| anyhow!("installer '{}': {e:#}", cell.name))?;
// A val that belongs to a DECLARED GROUP lands on the group, through the
// one writer both doors call (`InstallerBuild::apply_group_choices`).
cell.apply_group_choices(&split.choices)?;
cell.apply_cli_selection_with(&split.features, &supplied, composer.fixtures())?;
let iso = composer.compose_installer(&cell, kernel, app, repo_root, repo_label)?;
Ok((
iso,
AppliedSelection { params: cell.applied_params, options: cell.applied_options },
))
}
/// The usage banner. One string, so every repo's `--help` says the same words.
pub const USAGE: &str = "\
cargo xtask build [--suite <name>] [--variant <name>] [--list] [--attest] [--param k=v]... [--opt name=on|off]... [--build <path|repo/variant>]... [--features a,b] [--no-default-features] [--all-features]
--suite <name> the delivery (utgåva) to build. Required once a repo declares
more than one. An unknown name is an error, never a no-op.
--variant <name> one declared build row (of any kind: exe · iso · installer ·
container …) of that delivery. Omitted = all of them.
`--atom` is the deprecated old spelling and still works.
--list resolve and print the variants; build nothing.
--attest build with the symbol table kept, AUDIT the receipt against
it, strip the delivery out of the audited binary, and write
<artifact>.attestation.json beside it. Fails the build when a
receipt is disproven, and when it could not be measured.
--param k=v repeatable: an install-time input a [[build.iso]] /
[[build.installer]] cell DECLARES (a hub public key, a site
name; `@path` reads the value from a file; `@fixture` fills
it from the fixture the cell declares for that name, e.g.
`[build.installer.fixtures] hub_conf = \"wg-hub\"`). Only
non-secret values — a secret-shaped name is refused by name.
The applied values ride the attested descriptor (the
provenance), a fixture-filled one saying so.
--opt name=on|off
repeatable: an OPT-OUT an [[build.iso]] / [[build.installer]]
variant DECLARES (`--opt wireguard=off`). OFF removes the
cargo feature the option names from the APP binary this
medium packs, so the code is NOT COMPILED IN — not disabled
at runtime — and the audit reads that back out of the symbol
table. A toggle also decides which --param values apply: with
DHCP off the static address is required, with WireGuard off a
supplied hub_conf is refused by name. Omitted = the recipe's
declared default, which for an opt-out is ON.
--build <path|repo/variant>
repeatable: BUILD the build thing behind a consumption EDGE
of the variant's tree (`--build kernel`, `--build
tunnr/tunnr-kernel`) as a step of this build. A target that
is no edge of the tree is refused by name; a door that does
not chain yet refuses the flag by name and spells the
two-step.
--features a,b repeatable: cargo features to build the chosen cargo
variant(s) WITH, on top of the declared ones — passed to
cargo exactly as given, and named by the provenance
(`inputs.features`). An image/iso/installer row has none.
--no-default-features / --all-features
passed to cargo exactly as given.
Reads .nornir/build-variants.toml. There is no other build path.";
/// Parse the argv AFTER the `build` verb.
///
/// Hand-rolled rather than clap: this crate is dep-light on purpose (it compiles to
/// the wasm plugin sandbox), and the grammar is three flags.
pub fn parse<S: AsRef<str>>(args: &[S]) -> Result<BuildArgs> {
let mut out = BuildArgs::default();
let mut it = args.iter().map(|s| s.as_ref());
while let Some(a) = it.next() {
match a {
"--suite" => {
out.suite = Some(
it.next()
.ok_or_else(|| anyhow!("--suite needs a name\n\n{USAGE}"))?
.to_string(),
)
}
"--variant" => {
out.atom = Some(
it.next()
.ok_or_else(|| anyhow!("--variant needs a name\n\n{USAGE}"))?
.to_string(),
)
}
// Deprecated spelling of `--variant`. NEVER an error: the written laws
// cite `--atom` and muscle memory is not to be broken — a one-line note
// is the whole deprecation.
"--atom" => {
eprintln!("note: `--atom` is the deprecated spelling of `--variant`; both work");
out.atom = Some(
it.next()
.ok_or_else(|| anyhow!("--atom needs a name\n\n{USAGE}"))?
.to_string(),
)
}
"--list" => out.list = true,
"--attest" => out.attest = true,
"--param" => out.params.push(
it.next()
.ok_or_else(|| anyhow!("--param needs name=value\n\n{USAGE}"))?
.to_string(),
),
"--opt" => out.opts.push(
it.next()
.ok_or_else(|| anyhow!("--opt needs name=on|off\n\n{USAGE}"))?
.to_string(),
),
"--features" => out.cargo.add_features([it
.next()
.ok_or_else(|| anyhow!("--features needs a name or a comma list\n\n{USAGE}"))?]),
"--no-default-features" => out.cargo.no_default_features = true,
"--all-features" => out.cargo.all_features = true,
"--build" => out.builds.push(
it.next()
.ok_or_else(|| anyhow!("--build needs a path or a repo/variant coordinate\n\n{USAGE}"))?
.to_string(),
),
"-h" | "--help" => bail!("{USAGE}"),
other if other.starts_with("--suite=") => {
out.suite = Some(other["--suite=".len()..].to_string())
}
other if other.starts_with("--variant=") => {
out.atom = Some(other["--variant=".len()..].to_string())
}
other if other.starts_with("--atom=") => {
eprintln!("note: `--atom=` is the deprecated spelling of `--variant=`; both work");
out.atom = Some(other["--atom=".len()..].to_string())
}
other if other.starts_with("--param=") => {
out.params.push(other["--param=".len()..].to_string())
}
other if other.starts_with("--opt=") => {
out.opts.push(other["--opt=".len()..].to_string())
}
other if other.starts_with("--features=") => out.cargo.add_features([&other["--features=".len()..]]),
other if other.starts_with("--build=") => {
out.builds.push(other["--build=".len()..].to_string())
}
other => bail!("unknown argument '{other}'\n\n{USAGE}"),
}
}
if out.suite.as_deref().map(str::trim) == Some("") {
bail!("--suite was given an empty name\n\n{USAGE}");
}
Ok(out)
}
/// **Resolve `(suite, variant)` to the variants that will be built**, in declaration order.
///
/// Every failure mode is LOUD:
///
/// * an unknown `--suite` names the declared deliveries;
/// * an omitted `--suite` in a repo with two deliveries refuses rather than picking;
/// * an unknown `--variant`, or one that exists but ships in a DIFFERENT delivery,
/// names the variants the chosen delivery actually has;
/// * a suite that resolves to zero buildable variants is an error, not an empty success
/// — "built 0 things" and "built everything" must never look alike to a caller.
pub fn resolve<'a>(
set: &'a BuildVariantSet,
args: &BuildArgs,
) -> Result<(&'a str, Vec<&'a crate::variant::BuildVariant>)> {
let declared: Vec<&str> = set.suites();
let suite: &str = match args.suite.as_deref().map(str::trim) {
Some(want) => {
// A repo that declares NO suite still answers to the name of its single
// unnamed delivery only through UNSUITED, which is angle-bracketed and
// unspellable on a command line. So: if the repo declares nothing, any
// --suite is wrong, and the message must say that rather than list "".
if declared.is_empty() {
bail!(
"--suite {want}: this repo declares no deliveries at all \
(.nornir/build-variants.toml has no `suites = [...]` on any variant), \
so there is nothing to select. Drop --suite, or tag the variants."
);
}
*declared.iter().find(|d| **d == want).ok_or_else(|| {
anyhow!(
"--suite {want}: no such delivery. Declared: {}",
declared.join(", ")
)
})?
}
None => match declared.len() {
0 => UNSUITED,
1 => declared[0],
_ => bail!(
"this repo ships {} deliveries ({}) — say which one with --suite",
declared.len(),
declared.join(", ")
),
},
};
let in_suite = set.in_suite(suite);
let chosen: Vec<&crate::variant::BuildVariant> = match args.atom.as_deref().map(str::trim) {
Some(want) => {
let hit = in_suite
.iter()
.find(|v| v.name == want)
.copied()
.ok_or_else(|| {
let elsewhere = set.get(want).is_some();
let names: Vec<&str> = in_suite.iter().map(|v| v.name.as_str()).collect();
if elsewhere {
anyhow!(
"--variant {want}: declared, but not in delivery '{suite}'. \
'{suite}' has: {}",
names.join(", ")
)
} else {
anyhow!(
"--variant {want}: no such variant. '{suite}' has: {}",
names.join(", ")
)
}
})?;
vec![hit]
}
None => in_suite,
};
if chosen.is_empty() {
bail!("delivery '{suite}' resolves to NO variants — an empty build is not a green one");
}
Ok((suite, chosen))
}
/// Resolve, then actually build — the whole verb.
///
/// Non-cargo kinds (container / vm / iso) are reported and skipped: they are built
/// by the image/vm/iso builders, not by `cargo build`, and silently pretending to
/// have built one is the failure this crate exists to prevent. The returned list is
/// the variants that were BUILT, so a caller can assert on it.
/// **The `--list` rendering, as VALUES.** The lines [`run`]'s `--list` leg prints —
/// returned rather than written, so a door that is not a terminal can serve the same
/// answer.
///
/// It was `println!` inline until 2026-09-20, which made this describe verb reachable
/// only from a terminal. An MCP tool that called [`run`] to answer "what does
/// `mvp-server` resolve to" got ONE line back and wrote the other three onto its own
/// stdio transport — i.e. into the middle of the JSON-RPC stream. A renderer that can
/// only print is a renderer that can only be used by one door, and copying it into the
/// second door is how two spellings of one build start.
///
/// [`run`] still prints, line for line, so the CLI output is unchanged.
pub fn list_lines(suite: &str, chosen: &[&crate::variant::BuildVariant]) -> Vec<String> {
// The MATERIALIZED cargo command is rendered for every atom, not just its name.
//
// This is the whole value of a describe verb. A row saying `mvp-server · server ·
// service · holger-mvp` tells you an atom exists; it does not tell you what will
// be compiled, and "what features does this artifact actually get" is precisely
// the question every hand-rolled build path in the house answered differently.
//
// Non-cargo kinds render their kind in place of a command: a container/vm/iso has
// no cargo invocation, and inventing a plausible-looking one would be worse than
// saying so.
let mut out = vec![format!("delivery '{suite}' — {} variant(s):", chosen.len())];
for v in chosen {
out.push(format!(
" {:<24} {:<9} {:<8} {}",
v.name,
v.role.map(|r| r.as_str()).unwrap_or("-"),
v.kind.as_str(),
v.bin.as_deref().unwrap_or("<default bin>")
));
if v.kind.is_cargo() {
out.push(format!(" {}", v.command_line()));
} else {
out.push(format!(
" (not a cargo build — built by the {} builder)",
v.kind.as_str()
));
}
}
out
}
/// **Resolve a `(suite, variant)` selection and RENDER it, writing nothing.** The
/// `--list` leg of [`run`] with the terminal taken out: the same
/// [`discover_validated`](BuildVariantSet::discover_validated) load, the same
/// [`resolve`], the same [`list_lines`]. For doors that must return an answer instead
/// of printing one (the forge's rpc, the MCP lens).
pub fn list_resolved(repo_root: &Path, args: &BuildArgs) -> Result<Vec<String>> {
let set = BuildVariantSet::discover_validated(repo_root)?.ok_or_else(|| {
anyhow!(
"no {} — this repo declares no build space",
BuildVariantSet::conventional_path(repo_root).display()
)
})?;
let (suite, chosen) = resolve(&set, args)?;
Ok(list_lines(suite, &chosen))
}
pub fn run(repo_root: &Path, args: &BuildArgs) -> Result<Vec<String>> {
// The load path VALIDATES (audit-w2 #1): every shape rule, the on-disk
// inputs, and every consumption edge type-checked against the donor beside
// this checkout — a declaration this door acts on is refused at load, by
// name, never first at the build it would have broken.
let set = BuildVariantSet::discover_validated(repo_root)?.ok_or_else(|| {
anyhow!(
"no {} — this repo declares no build space, so there is nothing for \
`xtask build` to read",
BuildVariantSet::conventional_path(repo_root).display()
)
})?;
// `--build <X>` (audit-w2 #18): this door assembles cargo variants of ONE
// repo; chaining the build thing behind an edge — another checkout, its own
// container, its pin — is the forge's (`dwarves buildthing`, the runner over
// `build_dag::BuildDag`). Honoured or refused BY NAME, never swallowed: the
// DAG is derived here so the refusal spells the exact lines in the order the
// forge would run them. A target that is no edge, an unknown variant, a
// cycle — each is the DAG's own refusal.
if !args.builds.is_empty() {
use crate::build_dag::{BesideCheckouts, BuildDag, Coordinate};
let Some(variant) = args.atom.as_deref() else {
bail!(
"--build {}: say which variant the edge belongs to with --variant (`--build` chains the \
build thing behind an EDGE of that variant's tree)",
args.builds.join(" --build ")
);
};
let decls = BesideCheckouts::of(repo_root);
let target = Coordinate::new(decls.own.clone(), variant);
let dag = BuildDag::derive(&target, &args.build_line()?, &decls)?;
bail!(
"--build {}: `xtask build` assembles cargo variants of this repo and does not chain \
a build across checkouts — the forge does. The steps, in the order the edge DAG runs \
them:\n {}",
args.builds.join(" --build "),
dag.lines().join("\n ")
);
}
// `--param` values are install-time inputs for [[build.iso]]/[[build.installer]]
// cells, and THIS verb builds cargo atoms only (the iso/installer builders are
// the library seam: `apply_cli_params` → `image_build::build_iso`). Refuse
// rather than silently dropping a value the operator believes was baked in.
if !args.params.is_empty() {
bail!(
"--param supplies install-time inputs for iso/installer cells, and `build` \
assembles cargo variants — drive the product's iso/installer verb instead \
(it parses these with params::parse_cli_params and lands them via \
apply_cli_params; the applied values ride the attested descriptor)"
);
}
let (suite, chosen) = resolve(&set, args)?;
if args.list {
// The MATERIALIZED cargo command is printed for every atom, not just its name.
//
// This is the whole value of a describe verb. A row saying `mvp-server · server ·
// service · holger-mvp` tells you an atom exists; it does not tell you what will
// be compiled, and "what features does this artifact actually get" is precisely
// the question every hand-rolled build path in the house answered differently.
// holger's retired `build --list` printed the command line and its test asserted
// on it; moving the selector here would have silently dropped that, so it lives
// here now and all five repos gain it rather than one keeping it.
//
// Non-cargo kinds print their kind in place of a command: a container/vm/iso has
// no cargo invocation, and inventing a plausible-looking one would be worse than
// saying so.
for line in list_lines(suite, &chosen) {
println!("{line}");
}
return Ok(chosen.iter().map(|v| v.name.clone()).collect());
}
let mut built = Vec::new();
let mut skipped: BTreeSet<String> = BTreeSet::new();
for v in &chosen {
if !v.kind.is_cargo() {
skipped.insert(format!("{} ({})", v.name, v.kind.as_str()));
continue;
}
// the operator's cargo feature choice, on top of the declared row
let v = &v.with_cargo_features(&args.cargo);
let set = set.with_cargo_features(&v.name, &args.cargo)?;
eprintln!("── {} · {} · {}", suite, v.name, v.command_line());
if args.attest {
attest_atom(&set, v, repo_root)?;
} else {
v.build(&set, repo_root)?;
}
built.push(v.name.clone());
}
if !skipped.is_empty() {
eprintln!(
"not cargo builds, skipped (use the image/vm/iso builders): {}",
skipped.iter().cloned().collect::<Vec<_>>().join(", ")
);
}
if built.is_empty() {
bail!(
"delivery '{suite}' has no cargo-buildable variant — every one of {} is a \
container/vm/iso. Nothing was built; that is not a success.",
chosen.len()
);
}
Ok(built)
}
/// **The attesting delivery: compile with symbols, audit, strip, write it down.**
///
/// The caller for everything `history::deliver_stripped` and
/// `receipt_proof::audit_artifact` can do. Without this the audit was a library
/// nobody ran, which is exactly the state the receipt writer was in until
/// 2026-08-06 and the reason its first PLAN entry read "the writer exists in
/// edda, nothing calls it".
///
/// ```text
/// 1 cargo build, CARGO_PROFILE_<P>_STRIP=none the audited binary
/// 2 copy it aside target/ is about to be reused
/// 3 deliver_stripped(audited -> artifact path) audit, then strip
/// 4 write <artifact>.attestation.json the proof, as text
/// 5 refuse to return Ok on a receipt that lies
/// ```
///
/// Step 2 is not tidiness. Cargo writes one path per build point, so the
/// unstripped binary and the stripped delivery are the same file name; without
/// the copy, step 3 would strip its own input and then audit the result, and the
/// audit would report `NotMeasured` for a delivery it had just destroyed the
/// evidence of.
///
/// # What it does NOT do
///
/// Sign anything. The attestation says the artifact carries the program that was
/// audited; it says nothing about who built it. That distinction is in the
/// written-out `outsider` block of every proof.
///
/// # Errors
///
/// When the variant has no `bin` (the artifact path is unresolvable and a guessed
/// one is worse than none), when the build fails, when the artifact is not where
/// cargo said it would be, when the attestation is refused, and — loudly — when
/// the receipt does not hold. A delivery whose receipt is disproven must not
/// leave the build green.
pub fn attest_atom(set: &BuildVariantSet, v: &BuildVariant, repo_root: &Path) -> Result<PathBuf> {
let under = v.artifact_under_target().ok_or_else(|| {
anyhow!(
"variant '{}' declares no `bin`, so its artifact path cannot be resolved without \
cargo metadata — and an attestation over a guessed path would name the wrong \
bytes",
v.name
)
})?;
// Cargo's per-profile override, spelled the way cargo spells it: the profile
// NAME uppercased, non-alphanumerics to `_`. `dev` here is `DEV`, not `DEBUG`
// — that mapping is the directory's, not the profile's.
let profile = String::from(v.profile.clone());
let key = format!(
"CARGO_PROFILE_{}_STRIP",
profile
.chars()
.map(|c| if c.is_ascii_alphanumeric() {
c.to_ascii_uppercase()
} else {
'_'
})
.collect::<String>()
);
v.build_with_env(set, repo_root, &[(key.clone(), "none".to_string())])?;
// Cargo's target directory, resolved the way cargo resolves it. A relative
// `CARGO_TARGET_DIR` is relative to the directory cargo runs in, which is
// `repo_root` because that is where `build_with_env` puts it.
let produced = match std::env::var_os("CARGO_TARGET_DIR") {
Some(dir) if !dir.is_empty() => repo_root.join(dir).join(&under),
_ => repo_root.join("target").join(&under),
};
if !produced.is_file() {
bail!(
"variant '{}' built, but no artifact at {} — the attestation would be about a file \
that is not there",
v.name,
produced.display()
);
}
let audited = produced.with_extension("audited");
std::fs::copy(&produced, &audited)
.with_context(|| format!("keep the audited binary at {}", audited.display()))?;
let attestation = crate::history::deliver_stripped(&audited, &produced)
.map_err(|e| anyhow!("variant '{}': {e}", v.name))?;
let proof = attestation
.proof
.as_ref()
.ok_or_else(|| anyhow!("variant '{}': the delivery produced no proof", v.name))?;
let json_path = produced.with_extension("attestation.json");
std::fs::write(
&json_path,
serde_json::to_vec_pretty(&attestation)
.with_context(|| format!("serialise the attestation for '{}'", v.name))?,
)
.with_context(|| format!("write {}", json_path.display()))?;
for line in proof.lines() {
eprintln!(" {line}");
}
eprintln!(" attestation: {}", json_path.display());
if !attestation.proof_holds() {
bail!(
"variant '{}': its receipt does NOT survive its own symbol table. The attestation is \
written to {} so the evidence is not lost, and the build is red.\n {}",
v.name,
json_path.display(),
proof.lines().join("\n ")
);
}
Ok(json_path)
}
#[cfg(test)]
mod tests {
use super::*;
/// Item 19's seam, proven without the heavy composition: a fake composer
/// records what arrives — the resolved cell WITH params applied — and the
/// refusals (unknown cell, ambiguous cell-less call, unknown param) come
/// from the shared paths, by name.
#[test]
fn the_installer_door_resolves_applies_params_and_refuses_by_name() {
use std::sync::Mutex;
struct Probe(Mutex<Option<crate::installer::InstallerBuild>>);
impl InstallerComposer for Probe {
fn compose_installer(
&self,
cell: &crate::installer::InstallerBuild,
_kernel: &Path,
_app: &Path,
_root: &Path,
_label: &str,
) -> Result<std::path::PathBuf> {
*self.0.lock().unwrap() = Some(cell.clone());
Ok(std::path::PathBuf::from("/fake.iso"))
}
}
let dir = std::env::temp_dir().join(format!("suite-installer-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join(".nornir")).unwrap();
std::fs::write(
dir.join(".nornir/build-variants.toml"),
r#"
# `[[build.variants]]` until 2026-08-26 — PLURAL, and therefore not a key the
# schema has. It parsed to nothing, this fixture declared zero variants, and the
# test passed anyway because its subject is the installer. `deny_unknown_fields`
# on `BuildVariantSet` caught it on the first run.
[[build.variant]]
name = "probe"
kind = "exe"
[[build.installer]]
name = "probe-installer"
variant = ""
volume_id = "PROBE"
boot_params = ["korp.entry=probe"]
[[build.installer.params]]
name = "site"
label = "Site"
kind = "text"
required = false
boot_param = "korp.site"
"#,
)
.unwrap();
let probe = Probe(Mutex::new(None));
let iso = run_installer(
&dir,
"probe",
None,
Path::new("/k"),
Path::new("/a"),
&["site=oden-7".to_string()],
&probe,
)
.expect("the single cell resolves without naming it");
assert_eq!(iso, std::path::PathBuf::from("/fake.iso"));
let seen = probe.0.lock().unwrap().clone().expect("the composer saw the cell");
assert!(
seen.effective_boot_params().iter().any(|t| t == "korp.site=oden-7"),
"the param was APPLIED before composition: {:?}",
seen.effective_boot_params()
);
// RED: unknown cell name, and an unknown param, each refused by name.
let err = run_installer(&dir, "probe", Some("nope"), Path::new("/k"), Path::new("/a"), &[], &probe)
.unwrap_err()
.to_string();
assert!(err.contains("nope") && err.contains("probe-installer"), "{err}");
let err = run_installer(&dir, "probe", None, Path::new("/k"), Path::new("/a"), &["mystery=1".to_string()], &probe)
.unwrap_err()
.to_string();
assert!(err.contains("mystery"), "{err}");
let _ = std::fs::remove_dir_all(&dir);
}
const TWO: &str = r#"
[build]
base_suite = "full"
[[build.variant]]
name = "server-full"
package = "holger"
bin = "holger-server"
role = "server"
default = true
suites = ["full"]
features = ["registry", "gui"]
[[build.variant]]
name = "server-mvp"
package = "holger"
bin = "holger-server"
role = "server"
default = true
suites = ["mvp"]
features = ["registry"]
"#;
const ONE: &str = r#"
[build]
base_suite = "ORIGINAL"
[[build.variant]]
name = "korp-server"
package = "korp"
bin = "korp-server"
role = "server"
default = true
suites = ["ORIGINAL"]
[[build.variant]]
name = "korp-ui"
package = "korp"
bin = "korp-ui"
role = "client"
default = true
suites = ["ORIGINAL"]
"#;
fn set(t: &str) -> BuildVariantSet {
BuildVariantSet::from_toml_str(t).expect("parse")
}
/// ★ **RED-FIRST 2026-08-20 — the vocabulary is `--variant`.** A declared build
/// row is a VARIANT with a KIND; "atom" is retired prose (see the crate docs).
/// `--variant X` and `--atom X` must produce the SAME `BuildArgs`, `--atom` stays
/// accepted forever (the written laws cite `cargo xtask build --suite ORIGINAL
/// --atom dwarves-server` and muscle memory is not to be broken), and the help
/// text must teach the new word.
#[test]
fn variant_is_the_flag_and_atom_stays_its_alias() {
let v = parse(&["--suite", "MVP", "--variant", "ui"]).expect("--variant parses");
let a = parse(&["--suite", "MVP", "--atom", "ui"]).expect("--atom still parses");
assert_eq!(v, a, "the two spellings must resolve IDENTICALLY");
assert_eq!(v.atom.as_deref(), Some("ui"));
let v2 = parse(&["--variant=ui"]).expect("--variant= parses");
let a2 = parse(&["--atom=ui"]).expect("--atom= still parses");
assert_eq!(v2, a2);
assert_eq!(v2.atom.as_deref(), Some("ui"));
assert!(
parse(&["--variant"]).is_err(),
"a flag with no value must not pass"
);
assert!(USAGE.contains("--variant"), "USAGE must teach --variant");
assert!(
USAGE.contains("--atom"),
"USAGE must still admit the deprecated alias so a reader of an old law \
is not stranded"
);
}
/// `--build <path|coordinate>` (law C's fourth flag) parses in both shell
/// shapes, in order, raw — resolving it to an edge is the door's job.
#[test]
fn build_flags_parse_raw_in_both_shell_shapes() {
let a = parse(&["--build", "kernel", "--build=tunnr/tunnr-kernel", "--opt", "dhcp=off"]).expect("parse");
assert_eq!(a.builds, ["kernel", "tunnr/tunnr-kernel"]);
// The three raw lists have ONE meaning: the canonical --build line (wave 3).
assert_eq!(
a.build_line().unwrap().render(),
"--opt dhcp=off --build kernel --build tunnr/tunnr-kernel"
);
let braced = parse(&["--build", "kernel { --opt unix=off }", "--opt", "dhcp=off"]).expect("parse");
assert_eq!(braced.build_line().unwrap().render(), "--opt dhcp=off --build kernel --opt kernel.unix=off");
assert_eq!(a.opts, ["dhcp=off"]);
let e = format!("{:#}", parse(&["--build"]).unwrap_err());
assert!(e.contains("--build needs a path"), "{e}");
assert!(USAGE.contains("--build <path|repo/variant>"), "USAGE must teach --build");
}
#[test]
fn flags_parse_in_both_spellings() {
let a = parse(&["--suite", "MVP", "--atom", "ui", "--list"]).expect("parse");
assert_eq!(a.suite.as_deref(), Some("MVP"));
assert_eq!(a.atom.as_deref(), Some("ui"));
assert!(a.list);
let b = parse(&["--suite=MVP", "--atom=ui"]).expect("parse");
assert_eq!(b.suite.as_deref(), Some("MVP"));
assert_eq!(b.atom.as_deref(), Some("ui"));
assert!(
parse(&["--suite"]).is_err(),
"a flag with no value must not pass"
);
assert!(parse(&["--nope"]).is_err());
}
/// ★ **THE NON-VACUOUS ANCHOR: `--suite` must SELECT.** Two deliveries, and the
/// same command with a different suite must produce a DIFFERENT build. If both
/// resolved to the same atom the parameter would be decorative.
#[test]
fn suite_actually_selects_a_different_build() {
let s = set(TWO);
let (n1, full) = resolve(&s, &parse(&["--suite", "full"]).unwrap()).expect("full");
let (n2, mvp) = resolve(&s, &parse(&["--suite", "mvp"]).unwrap()).expect("mvp");
assert_eq!((n1, n2), ("full", "mvp"));
assert_eq!(full.len(), 1);
assert_eq!(mvp.len(), 1);
assert_ne!(
full[0].name, mvp[0].name,
"the two deliveries picked the same atom"
);
// …and the DIFFERENCE reaches cargo, not just the name.
assert!(full[0].features.contains(&"gui".to_string()));
assert!(!mvp[0].features.contains(&"gui".to_string()));
assert_ne!(full[0].command_line(), mvp[0].command_line());
}
/// An unknown suite FAILS and names the real ones. A selector that shrugs is the
/// silent-green shape.
#[test]
fn an_unknown_suite_fails_loudly() {
let s = set(TWO);
let e = resolve(&s, &parse(&["--suite", "NOPE"]).unwrap()).expect_err("must fail");
let m = format!("{e}");
assert!(m.contains("no such delivery"), "{m}");
assert!(
m.contains("full") && m.contains("mvp"),
"it must list the real ones: {m}"
);
// And on a ONE-delivery repo it is just as loud.
let one = set(ONE);
let e = resolve(&one, &parse(&["--suite", "NOPE"]).unwrap()).expect_err("must fail");
assert!(format!("{e}").contains("ORIGINAL"), "{e}");
}
/// Omitting `--suite` with two deliveries refuses instead of guessing.
#[test]
fn two_deliveries_and_no_suite_refuses() {
let e = resolve(&set(TWO), &BuildArgs::default()).expect_err("must fail");
assert!(format!("{e}").contains("ships 2 deliveries"), "{e}");
}
/// One delivery: `--suite ORIGINAL` and no flag resolve to the SAME set, so the
/// uniform command shape costs a one-delivery repo nothing.
#[test]
fn one_delivery_answers_to_its_name_and_to_silence() {
let s = set(ONE);
let (a, xs) = resolve(&s, &parse(&["--suite", "ORIGINAL"]).unwrap()).expect("named");
let (b, ys) = resolve(&s, &BuildArgs::default()).expect("bare");
assert_eq!((a, b), ("ORIGINAL", "ORIGINAL"));
let names = |v: &Vec<&crate::variant::BuildVariant>| -> Vec<String> {
v.iter().map(|x| x.name.clone()).collect()
};
assert_eq!(names(&xs), names(&ys));
assert_eq!(names(&xs), vec!["korp-server", "korp-ui"]);
}
/// ★ **`--list` must print the MATERIALIZED cargo command, not only the name.**
///
/// A describe verb that names atoms without saying what they compile cannot answer
/// the one question that matters — "what features does this artifact get" — which is
/// exactly what four hand-rolled build paths each answered differently. The two
/// deliveries of [`TWO`] build the SAME bin (`holger-server`) from the same package
/// and differ ONLY in features, so a row without the command line is literally
/// identical for both: that is the non-vacuous anchor here.
///
/// Asserted on `command_line()` rather than on captured stdout because `println!`
/// is not capturable from a unit test; the printed line is that exact string, and
/// the test therefore asserts the thing that IS printed rather than a restatement.
#[test]
fn the_command_line_distinguishes_two_atoms_a_name_row_cannot() {
let s = set(TWO);
let (_, full) = resolve(&s, &parse(&["--suite", "full"]).unwrap()).expect("full");
let (_, mvp) = resolve(&s, &parse(&["--suite", "mvp"]).unwrap()).expect("mvp");
// The NAME-ONLY row is identical for both: same role, same kind, same bin. If
// this assert ever fails the fixture drifted and the point below is moot.
let name_row = |v: &crate::variant::BuildVariant| {
format!(
"{} {} {}",
v.role.map(|r| r.as_str()).unwrap_or("-"),
v.kind.as_str(),
v.bin.as_deref().unwrap_or("<default bin>")
)
};
assert_eq!(
name_row(full[0]),
name_row(mvp[0]),
"fixture must have two atoms whose name-row is indistinguishable"
);
// The command line is what tells them apart, and it names the real features.
let cf = full[0].command_line();
let cm = mvp[0].command_line();
assert_ne!(cf, cm, "the command line must distinguish them: {cf}");
assert!(cf.contains("--features") && cf.contains("gui"), "{cf}");
assert!(!cm.contains("gui"), "the mvp delivery has no gui: {cm}");
assert!(
cf.contains("holger-server") && cm.contains("holger-server"),
"{cf} / {cm}"
);
}
/// A non-cargo atom has NO cargo command, and `--list` must say so rather than
/// print a plausible-looking invocation nobody will run.
#[test]
fn a_non_cargo_atom_has_no_cargo_command() {
let s = BuildVariantSet::from_toml_str(
"[build]\nbase_suite = \"ORIGINAL\"\n\
[[build.variant]]\nname = \"img\"\nkind = \"container\"\n\
suites = [\"ORIGINAL\"]\ndefault = true\n",
)
.expect("parse");
let (_, chosen) = resolve(&s, &parse(&["--suite", "ORIGINAL"]).unwrap()).expect("resolve");
assert_eq!(chosen.len(), 1);
assert!(
!chosen[0].kind.is_cargo(),
"the fixture must be a NON-cargo kind or this test proves nothing"
);
}
/// A `kind = "kernel"` variant rides the delivery selector like the other
/// non-cargo kinds: `--list` shows it with its kind and never invents a
/// cargo argv for it (it is built by the kernel builder seam).
#[test]
fn a_kernel_variant_resolves_and_lists_as_a_non_cargo_kind() {
let s = BuildVariantSet::from_toml_str(
"[build]\nbase_suite = \"ORIGINAL\"\n\
[[build.variant]]\nname = \"lil-kernel\"\nkind = \"kernel\"\n\
suites = [\"ORIGINAL\"]\ndefault = true\n",
)
.expect("parse");
let (_, chosen) =
resolve(&s, &parse(&["--suite", "ORIGINAL"]).unwrap()).expect("resolve");
assert_eq!(chosen.len(), 1);
assert_eq!(chosen[0].kind.as_str(), "kernel");
assert!(
!chosen[0].kind.is_cargo(),
"a kernel is built by the kernel builder, never by cargo"
);
}
/// A variant that exists but belongs to the OTHER delivery is rejected with a
/// message that says which — not "no such variant", which would send the caller
/// looking for a typo that is not there.
#[test]
fn a_variant_from_another_delivery_says_so() {
let s = set(TWO);
let e = resolve(
&s,
&parse(&["--suite", "mvp", "--atom", "server-full"]).unwrap(),
)
.expect_err("must fail");
let m = format!("{e}");
assert!(m.contains("not in delivery 'mvp'"), "{m}");
assert!(m.contains("server-mvp"), "{m}");
let e = resolve(&s, &parse(&["--suite", "mvp", "--atom", "typo"]).unwrap())
.expect_err("must fail");
assert!(format!("{e}").contains("no such variant"), "{e}");
}
/// **The compose door takes a CHOICE and a DONOR's pair without choking** —
/// the `--opt kernel.preset=` fix, and `--opt data.fs=` reaching the row.
///
/// Seen red on this exact door before the tree split it: `parse_cli_options`
/// refused BOTH pairs with `--opt kernel.preset=nuked: an option is on or
/// off (on|off, true|false, yes|no, 1|0)`, which is why gunnar's
/// `BUILD-KEYED-INSTALLER.md` told operators not to type the flag. The
/// donor's pair belongs to the DONOR's door (`resolve_kernel_cli`, judged
/// upstream against its declaration), so this door's job is to route it, not
/// to read it; the group's choice is this door's and lands on the row.
#[test]
fn a_choice_and_a_donors_pair_pass_this_door_and_the_group_lands_on_the_row() {
use std::sync::Mutex;
struct Probe(Mutex<Option<crate::installer::InstallerBuild>>);
impl InstallerComposer for Probe {
fn compose_installer(
&self,
cell: &crate::installer::InstallerBuild,
_kernel: &Path,
_app: &Path,
_root: &Path,
_label: &str,
) -> Result<std::path::PathBuf> {
*self.0.lock().unwrap() = Some(cell.clone());
Ok(std::path::PathBuf::from("/fake.iso"))
}
}
let dir = std::env::temp_dir().join(format!("suite-groupopt-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join(".nornir")).unwrap();
std::fs::write(
dir.join(".nornir/build-variants.toml"),
r#"
[[build.variant]]
name = "probe"
kind = "exe"
no_default_features = true
features = ["wg-client"]
[[build.installer]]
name = "probe-installer"
variant = "probe"
volume_id = "PROBE"
[build.installer.kernel_donor]
repo = "tunnr"
kernel = "tunnr-kernel"
[build.installer.data]
fs = "xfs"
[[build.installer.options]]
name = "wireguard"
default = true
feature = "wg-client"
"#,
)
.unwrap();
let probe = Probe(Mutex::new(None));
run_installer_selected(
&dir,
"probe",
None,
Path::new("/k"),
Path::new("/a"),
&[],
&["kernel.preset=nuked".to_string(), "data.fs=fat".to_string(), "wireguard=off".to_string()],
&probe,
)
.expect("a donor pair, a group choice and a toggle on one line");
let seen = probe.0.lock().unwrap().clone().expect("the composer saw the cell");
assert_eq!(seen.data.fs, crate::data::DataFs::Fat, "--opt data.fs=fat moved the DECLARED xfs");
assert!(seen.applied_options.iter().any(|o| o.name == "wireguard" && !o.on), "{:?}", seen.applied_options);
// RED, still: a value that is not on the val's roster, and a toggle
// given a choice — each named, each with the roster or the shape.
let e = format!(
"{:#}",
run_installer_selected(&dir, "probe", None, Path::new("/k"), Path::new("/a"), &[], &["data.fs=ext4".to_string()], &probe)
.unwrap_err()
);
assert!(e.contains("data.fs") && e.contains("fat") && e.contains("xfs"), "{e}");
let e = format!(
"{:#}",
run_installer_selected(&dir, "probe", None, Path::new("/k"), Path::new("/a"), &[], &["wireguard=nuked".to_string()], &probe)
.unwrap_err()
);
assert!(e.contains("on or off"), "{e}");
let _ = std::fs::remove_dir_all(&dir);
}
}