crates/gunnar-recipe/src/variants.rs
//! **The variants — every build point gunnar declares, as Rust.**
//!
//! This is `.nornir/build-variants.toml`'s `[[build.variant]]` list, moved in and
//! typed. Nothing was dropped in the move: `tests/toml_parity.rs` reads the toml
//! that is still on disk and asserts it says exactly what this table says, field
//! for field, in both directions.
//!
//! # What being code buys, concretely
//!
//! ```text
//! toml an atom/column pair nobody allows fails when the build runs
//! rust an atom/column pair nobody allows fails when the crate compiles
//! ```
//!
//! The second is [`EVERY_VARIANT_IS_LEGAL`] below — a `const` assertion over
//! [`VARIANTS`]. Adding
//!
//! ```ignore
//! Variant { atom: Atom::GunnarCli, column: Column::IsoLive, .. }
//! ```
//!
//! does not build. That is the whole argument for dissolving the toml, and it is
//! why the argument is made with a `const fn` rather than a unit test: a test can
//! be skipped, and an `#[ignore]` on it is one line.
use std::collections::{BTreeMap, BTreeSet};
use nornir_build_thing::contract::PaneKind;
use nornir_build_thing::variant::{ArtifactKind, BuildVariant, Profile, ServiceShape};
use crate::atom::{Atom, Column, Suite};
/// One declared build point: an atom, delivered in a column, with every setting
/// the build resolves to.
///
/// A `(atom, column)` pair may appear more than once — `gunnar-server` has two
/// `exe` variants, the plain binary and the systemd service — because the pair does
/// not determine the build on its own. What distinguishes them is
/// [`Variant::resolved_settings`], which is also what the receipt records, so the
/// receipt cannot disagree with the build point it came from.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Variant {
/// The build point's stable handle. Identical to the `name` the toml used,
/// because `xtask`, `installtest` and the bench harness all name these
/// strings, and a rename would have been a migration hiding inside a
/// migration.
pub name: &'static str,
/// The row.
pub atom: Atom,
/// The column.
pub column: Column,
/// The deliveries this variant ships in.
pub suites: &'static [Suite],
/// The artifact kind. May narrow the column's own kind — see
/// [`Column::admits`].
pub kind: ArtifactKind,
/// Fat, thin, mixed, or inapplicable.
pub linkage: ServiceShape,
/// The normal case for this variant's `(suite, role)` group. Exactly one per
/// group; `BuildVariantSet::check_defaults` holds the built set to it.
pub is_default: bool,
/// The suite member this binary IS. `None` for a variant that composes other
/// variants' artifacts rather than compiling one of its own.
pub member: Option<&'static str>,
/// `cargo -p`.
pub package: Option<&'static str>,
/// `cargo --bin`.
pub bin: Option<&'static str>,
/// `cargo --features`.
pub features: &'static [&'static str],
/// `cargo --no-default-features`.
pub no_default_features: bool,
/// `cargo --profile`. `""` means the cargo default (`dev`), which is what a
/// variant that runs no cargo carries.
pub profile: &'static str,
/// `cargo --target`.
pub target: Option<&'static str>,
/// The `[[build.container]]` this variant delegates to, for `kind = container`.
pub container_ref: Option<&'static str>,
/// **The row's own switchable axis** (`BuildVariant::open`): features that
/// are OFF in this row and that an installer OPTION defaulting off may ADD
/// (`nornir_build_thing::options::OptionSpec::validate_against_variant`).
/// Only `dev-narrate` today, on the four payloads of the estate's media —
/// and the ON build is its own named row (`…-narrate`), so its receipt can
/// say it is a DEV build.
pub open: &'static [&'static str],
/// **Built from another workspace** (`BuildVariant::manifest_dir`, `--manifest-path
/// <dir>/Cargo.toml`). The four `gunnar-front` rows are, since 2026-10-22: the front
/// is a DETACHED workspace at `crates/gunnar-front`, because it needs crates only the
/// sibling checkouts have and a lone clone must load the root workspace. (The
/// metered front built from monetize-impl's commercial crate, 2026-10-08, is LEGACY
/// under ONE monetize, owner 13:35Z 2026-10-10.) `None`: the root workspace.
pub manifest_dir: Option<&'static str>,
/// The checkouts such a row's `cargo metadata` loads (`BuildVariant::siblings`); empty
/// for a row of this workspace (the toolchain's own siblings).
pub siblings: &'static [&'static str],
/// **The forge signs what this row builds** (`BuildVariant::sign`,
/// `sign = { purpose = "plugin" }`): `<artifact>.p7s` + `<artifact>.blake3`
/// beside it, under the plugin-signing certificate issued by the owner's root
/// key. Only the CLI a customer DOWNLOADS from an estate's site (`gunnar-musl`,
/// E4 2026-10-11) today.
pub sign: bool,
}
/// Where the console's crate is, from this repository's root: the gunnar-ui checkout beside
/// it (the row `gunnar-ui` builds `--manifest-path ../gunnar-ui/crates/gunnar-ui/Cargo.toml`).
pub const UI_MANIFEST_DIR: &str = "../gunnar-ui/crates/gunnar-ui";
/// **Every build point gunnar declares.**
///
/// Migrated verbatim from `.nornir/build-variants.toml`. The order is the toml's
/// order, so a diff between the two is readable.
pub const VARIANTS: &[Variant] = &[
// ══ the products (lane GUN-PRODUCTS, owner 2026-10-30) ═════════════════════
//
// Gunnar's three products, one crate each, and each row IS its `cargo install` line
// (`twins::TWINS`): the crate's defaults, the host's target, the crate's one program.
// The rows below them are the estate's builds: the appliance, the metered appliance,
// the front and the media. The MVP suite is exactly these (`cargo xtask build --suite
// MVP --list`); the retired rows (`gunnar` the client-only git command, the old
// `gunnar-cli` row, `gunnar-musl`, `gunnar-solo`, `gunnar-service`, `gunnar-open`,
// `gunnar-open-client`) are in git, and README.md names what each became.
//
// gunnar — `cargo install gunnar`: the server (`crates/gunnar`), its AIRGAP default
// (znippy, SSH and smart HTTP, replication for a twin; no monetize, no outbound client).
// The (MVP, server) normal case.
Variant {
name: "gunnar",
atom: Atom::GunnarServer,
column: Column::Exe,
suites: &[Suite::Mvp],
kind: ArtifactKind::Exe,
linkage: ServiceShape::FatService,
is_default: true,
member: Some("gunnar"),
package: Some("gunnar"),
bin: Some("gunnar"),
// The crate's defaults, named out loud (`crates/gunnar/Cargo.toml` `default`), so an
// edit of the manifest's default shows up as a twin difference instead of moving
// the row silently; `cargo install gunnar` builds exactly this (`twins::TWINS`).
features: &["engine-znippy", "http", "replication"],
no_default_features: true,
profile: "release",
target: None,
container_ref: None,
manifest_dir: None,
siblings: &[],
open: &[],
sign: false,
},
// gunnar-ui — `cargo install gunnar-ui`: the web console, built from the gunnar-ui
// checkout beside this one (`crates/gunnar-ui`, a thin bin over gunnar-ui-server with
// the console bundle built in). A client of gunnar: THIN, it reaches the server's
// control plane and nothing else.
Variant {
name: "gunnar-ui",
atom: Atom::GunnarUi,
column: Column::Exe,
suites: &[Suite::Mvp],
kind: ArtifactKind::Exe,
linkage: ServiceShape::ThinStandalone,
is_default: false,
member: None,
package: Some("gunnar-ui"),
bin: Some("gunnar-ui"),
features: &[],
no_default_features: false,
profile: "release",
target: None,
container_ref: None,
manifest_dir: Some(UI_MANIFEST_DIR),
siblings: &["gunnar-ui"],
open: &[],
sign: false,
},
// gunnar-cli — `cargo install gunnar-cli`: the command-line tool (`crates/gunnar-cli`),
// git with the encrypted repositories and the control plane, in one program. THIN: it
// reaches the server at `--at` and the remote a person names. The (MVP, client) normal
// case.
Variant {
name: "gunnar-cli",
atom: Atom::GunnarCli,
column: Column::Exe,
suites: &[Suite::Mvp],
kind: ArtifactKind::Exe,
linkage: ServiceShape::ThinStandalone,
is_default: true,
member: Some("gunnar"),
package: Some("gunnar-cli"),
bin: Some("gunnar-cli"),
// The crate's defaults, named out loud, as on the row `gunnar`.
features: &["client", "control"],
no_default_features: true,
profile: "release",
target: None,
container_ref: None,
manifest_dir: None,
siblings: &[],
open: &[],
sign: false,
},
// ══ the estate's builds ═══════════════════════════════════════════════════
//
// ── the service, and the first thing anybody should be able to build ─────
//
// musl, because the server is meant to be able to be PID 1 with no operating
// system under it, and a glibc binary needs one.
Variant {
name: "gunnar-server",
atom: Atom::GunnarServer,
column: Column::Exe,
suites: &[Suite::Mvp],
kind: ArtifactKind::Exe,
linkage: ServiceShape::FatService,
// The (MVP, server) normal case is the product `gunnar` since GUN-PRODUCTS.
is_default: false,
member: Some("gunnar"),
// `gunnar-cli` until 2026-08-10: the appliance bin moved to its own
// crate and the variant follows the code. The `server`
// feature on that crate is a marker with no dependencies — it exists so
// this variant keeps a non-empty, pairwise-distinct feature set, which is
// what `by_resolved_features` attributes on.
package: Some("gunnar-server"),
bin: Some("gunnar-server"),
// ── the appliance's diet, NAMED ──────────────────────────────────────
//
// `["server"]` until 2026-08-10. `gunnar-znippy` collapsed into
// `gunnar-server` as `engine-znippy` / `engine-gix`
// (`locked_on_target.md`, the `gunnar-znippy` collapse), and `no_default_features` below means
// that crate's `default = ["engine-znippy"]` does not reach this variant.
// Left unnamed, this atom would build a server with no storage engine
// linked — the engine-free binary the collapse exists to abolish.
//
// `engine-gix` is deliberately absent: this is the musl/scratch image
// meant to be PID 1 with no operating system under it. Naming the
// engine here rather than inheriting it means a later edit to the
// manifest's `default` cannot put gix in the appliance by accident.
//
// `mimalloc` is named here for exactly the reason the paragraph above
// gives about `engine-znippy`: `no_default_features` means the
// manifest's `default = [..., "mimalloc"]` does NOT reach this variant, and
// this variant is the one that matters. It is the musl/scratch image that
// actually serves clones, and musl's own `malloc` is materially worse
// under contention than the glibc one that already cost 2.2x on clone
// CPU (`gunnar-server/src/lib.rs` has the grid). Inheriting would have
// left the appliance — and only the appliance — on the slow allocator
// while every bench column measured the fast one.
//
// It builds here: `libmimalloc-sys` is `cc` plus vendored C with no
// cmake and no download, `musl-gcc` compiles it, and the result is
// still `static-pie` with nothing under it. VERIFIED 2026-08-11, not
// assumed — `MIMALLOC_VERBOSE=1` on the musl artifact prints the
// allocator's own banner, and a build without the feature prints
// nothing.
//
// The resolved set this produces is `{SERVER, ENGINE_ZNIPPY, MIMALLOC,
// DHCP_CLIENT, WG_CLIENT}` — every one of those features is spelled
// `dep:`, which creates no implicit feature, so nothing else joins the
// closure and `by_resolved_features` still matches by equality.
//
// `dhcp-client` and `wg-client` added 2026-08-22. They sit in the
// crate's `default` list AND here, because `no_default_features` below
// means the default cannot reach this row: unspelled, the packed
// appliance would ship with no DHCP client and no tunnel. They are also
// the two features `[[build.installer.options]]` decides, and an option
// that defaults ON is REFUSED unless its feature is in this list.
//
// ⚠ THIS PARAGRAPH USED TO END "an OFF build resolves a set no variant
// declares, so it gets `Attribution::NoVariant` and no receipt … teaching
// the receipt to record applied options is upstream work in
// `nornir-build-thing`, not something a declaration here can fix." The
// first half was MEASURED true on 2026-08-27 (the live ISO with
// `--opt wireguard=off` printed exactly that warning, naming the resolved
// set `{DHCP_CLIENT, ENGINE_ZNIPPY, MIMALLOC, SERVER}`); the last clause
// was wrong. A declaration here fixes it, because the thing the receipt
// could not name was a build point nobody had NAMED — see
// `gunnar-server-thin` below. The upstream work (recording the applied
// option on the receipt of THIS row) is a different, still-open thing.
//
// `replication` added 2026-08-30 — decided, after
// a drill measured replication working end to
// end (60 ms lag; after `SIGKILL` of the primary the standby served 5/5
// repositories `git fsck` clean) and NOT compiled into this row. It is
// named here and nowhere else for the same reason `mimalloc` is: it is
// not in the crate's `default` list, so `no_default_features` below
// means an unspelled `replication` ships an appliance that cannot keep a
// standby. It costs +4 packages (365 -> 369, MEASURED 2026-08-30), not
// the +128 the old all-or-nothing `client` feature would have.
features: &[
"server",
"engine-znippy",
"mimalloc",
"clock-fetch",
"resolver-probe",
"fleet-pull",
"mirror-http",
"sso-egress",
"dhcp-client",
"wg-client",
"replication",
"grow-runtime",
"live-app",
],
no_default_features: true,
profile: "release",
target: Some("x86_64-unknown-linux-musl"),
container_ref: None,
manifest_dir: None,
siblings: &[],
open: &["dev-narrate"],
sign: false,
},
// ── `gunnar-server` + DEV-NARRATE (2026-09-30) ──────────────────────────────
//
// The build point the installer's `dev-narrate` option ARRIVES at when it is
// ON (`--opt dev-narrate=on`, the kit's dev medium only): the same program
// plus Skidbladnir nordisk-narrate's sender — the box's boot log sealed to
// the installer's key over udp/51822 before the installer's link exists. Its
// own row so the ON binary's receipt NAMES it a dev build; `gunnar-server` keeps
// `dev-narrate` in `open` (off), and the option forbids `nordisk_narrate`
// in every OFF (release) build.
Variant {
name: "gunnar-server-narrate",
atom: Atom::GunnarServer,
column: Column::Exe,
suites: &[Suite::Mvp],
kind: ArtifactKind::Exe,
linkage: ServiceShape::FatService,
is_default: false,
member: Some("gunnar"),
package: Some("gunnar-server"),
bin: Some("gunnar-server"),
features: &[
"server",
"engine-znippy",
"mimalloc",
"clock-fetch",
"resolver-probe",
"fleet-pull",
"mirror-http",
"sso-egress",
"dhcp-client",
"wg-client",
"replication",
"grow-runtime",
"live-app",
"dev-narrate",
],
no_default_features: true,
profile: "dev-media",
target: Some("x86_64-unknown-linux-musl"),
container_ref: None,
manifest_dir: None,
siblings: &[],
open: &[],
sign: false,
},
// ── the same appliance without the tunnel ────────────────────────────────
//
// The row above minus `wg-client`. It is the build point
// `[[build.iso.options]] wireguard = off` ARRIVES at, and it exists because
// gunnar's own receipt gate asked for it out loud: without this row that build
// resolved `{DHCP_CLIENT, ENGINE_ZNIPPY, MIMALLOC, SERVER}`,
// `by_resolved_features` found nothing, and the shipped binary could not say
// which build point it was. The option axis was real either way — the symbol
// read-back found `wg_appliance_core` and `boringtun` gone, 23 247 992 →
// 22 837 368 B — but a feature set nobody declared is not a product.
//
// Same atom as the row above ON PURPOSE: this is not a second program, it is
// the same appliance with one capability subtracted, and `Atom` is what the
// program IS. `by_resolved_features` matches on the feature set, which is
// pairwise distinct, so the two rows stay tellable apart where it matters.
//
// `is_default: false`: the default appliance still carries the tunnel (an
// opt-out ships unless somebody opts out).
Variant {
name: "gunnar-server-thin",
atom: Atom::GunnarServer,
column: Column::Exe,
suites: &[Suite::Mvp],
kind: ArtifactKind::Exe,
linkage: ServiceShape::FatService,
is_default: false,
member: Some("gunnar"),
package: Some("gunnar-server"),
bin: Some("gunnar-server"),
features: &[
"server",
"engine-znippy",
"mimalloc",
"clock-fetch",
"resolver-probe",
"fleet-pull",
"mirror-http",
"sso-egress",
"dhcp-client",
"replication",
"grow-runtime",
],
no_default_features: true,
profile: "release",
target: Some("x86_64-unknown-linux-musl"),
container_ref: None,
manifest_dir: None,
siblings: &[],
open: &[],
sign: false,
},
// ── the METERED appliance: the same server, plus what a provider pays for ─
//
// `gunnar-server` + `metered`, a licensed cargo feature. Not a second
// program — same atom, same engine, same diet — but the one row that
// CONTAINS plan enforcement: the `Entitlement` service, the plan arm in
// `authorise_service`, the entitlement document, and `monetize-embed`.
// The community row above has none of that in its symbol table, and
// `gunnar-build-thing/tests/metered_symbols.rs` reads both binaries to
// say so; `gunnar-cli/tests/feature_split.rs` says the same of the graph.
//
// `is_default: false`, and deliberately: the default appliance is the
// community one. A service provider builds this row by name.
Variant {
name: "gunnar-server-metered",
atom: Atom::GunnarServer,
column: Column::Exe,
suites: &[Suite::Mvp],
kind: ArtifactKind::Exe,
linkage: ServiceShape::FatService,
is_default: false,
member: Some("gunnar"),
package: Some("gunnar-server"),
bin: Some("gunnar-server"),
features: &[
"server",
"engine-znippy",
"mimalloc",
"clock-fetch",
"resolver-probe",
"fleet-pull",
"mirror-http",
"sso-egress",
"dhcp-client",
"wg-client",
"replication",
"metered",
"grow-runtime",
"live-app",
// The usage share on the admission gate (2026-09-28, owner: yes).
// Every metered server row carries it; the budget is a runtime
// setting (`--admit-limit`, `none` = PassThrough), so an estate
// that sets none runs today's gate on this binary.
"admit-share",
],
no_default_features: true,
profile: "release",
target: Some("x86_64-unknown-linux-musl"),
container_ref: None,
manifest_dir: None,
siblings: &[],
open: &["dev-narrate"],
sign: false,
},
// ── `gunnar-server-metered` + DEV-NARRATE (2026-09-30) ──────────────────────────────
//
// The build point the installer's `dev-narrate` option ARRIVES at when it is
// ON (`--opt dev-narrate=on`, the kit's dev medium only): the same program
// plus Skidbladnir nordisk-narrate's sender — the box's boot log sealed to
// the installer's key over udp/51822 before the installer's link exists. Its
// own row so the ON binary's receipt NAMES it a dev build; `gunnar-server-metered` keeps
// `dev-narrate` in `open` (off), and the option forbids `nordisk_narrate`
// in every OFF (release) build.
Variant {
name: "gunnar-server-metered-narrate",
atom: Atom::GunnarServer,
column: Column::Exe,
suites: &[Suite::Mvp],
kind: ArtifactKind::Exe,
linkage: ServiceShape::FatService,
is_default: false,
member: Some("gunnar"),
package: Some("gunnar-server"),
bin: Some("gunnar-server"),
features: &[
"server",
"engine-znippy",
"mimalloc",
"clock-fetch",
"resolver-probe",
"fleet-pull",
"mirror-http",
"sso-egress",
"dhcp-client",
"wg-client",
"replication",
"metered",
"grow-runtime",
"live-app",
"admit-share",
"dev-narrate",
],
no_default_features: true,
profile: "dev-media",
target: Some("x86_64-unknown-linux-musl"),
container_ref: None,
manifest_dir: None,
siblings: &[],
open: &[],
sign: false,
},
// `gunnar-server-thin` + `metered`: the metered appliance without the
// tunnel, for the same `wireguard = off` option the thin row exists for.
Variant {
name: "gunnar-server-metered-thin",
atom: Atom::GunnarServer,
column: Column::Exe,
suites: &[Suite::Mvp],
kind: ArtifactKind::Exe,
linkage: ServiceShape::FatService,
is_default: false,
member: Some("gunnar"),
package: Some("gunnar-server"),
bin: Some("gunnar-server"),
features: &[
"server",
"engine-znippy",
"mimalloc",
"clock-fetch",
"resolver-probe",
"fleet-pull",
"mirror-http",
"sso-egress",
"dhcp-client",
"replication",
"metered",
"grow-runtime",
// The usage share on the admission gate (2026-09-28, owner: yes).
// Every metered server row carries it; the budget is a runtime
// setting (`--admit-limit`, `none` = PassThrough), so an estate
// that sets none runs today's gate on this binary.
"admit-share",
],
no_default_features: true,
profile: "release",
target: Some("x86_64-unknown-linux-musl"),
container_ref: None,
manifest_dir: None,
siblings: &[],
open: &[],
sign: false,
},
// ── the command-line tool, built FOR THE FRONT ───────────────────────────
//
// `gunnar-cli` as the front carries it and as every Gunnar site serves it under
// `/download/` (E4, owner 2026-10-11): the SAME program as the product row
// `gunnar-cli` (`client,control`), static musl because it lands on hosts nobody here
// pins, and SIGNED: the forge's sign step writes `gunnar-cli.p7s` (CMS over the BLAKE3
// digest, plugin-signing certificate under the owner's root) and `gunnar-cli.blake3`,
// and the download page shows how to check both against the published root
// (`xtask/src/cli_stage.rs` stages them for gunnar-front's build.rs). Until GUN-PRODUCTS
// (2026-10-30) the download was the client-only `gunnar` bin (row `gunnar-musl`), whose
// name was also the server's.
//
// Its own row rather than an option on the product row: the target is a setting that
// changes the artifact and its path (`super::by_build_point`).
Variant {
name: "gunnar-cli-musl",
atom: Atom::GunnarCli,
column: Column::Exe,
suites: &[Suite::Mvp],
kind: ArtifactKind::Exe,
linkage: ServiceShape::ThinStandalone,
is_default: false,
member: Some("gunnar"),
package: Some("gunnar-cli"),
bin: Some("gunnar-cli"),
features: &["client", "control"],
no_default_features: true,
profile: "release",
target: Some("x86_64-unknown-linux-musl"),
container_ref: None,
manifest_dir: None,
siblings: &[],
open: &[],
sign: true,
},
// ── the front's control client on a METERED estate ───────────────────────
//
// **The row that ends "the estate could see the drift and had no door".**
//
// `gunnar control appliance-identity` prints the key this appliance signs
// actor tickets with — the string a monetize needs in `[products.<id>]
// appliance_key` before a tenant can renew. It is `#[cfg(feature =
// "metered")]`, like the `Billing` service it calls, because the community
// build never mentions money. The row above is `client,control`, so on a
// METERED estate the verb was compiled out of the FRONT, which is the only
// box that can ask: the appliance and the twin have no shell.
//
// MEASURED 2026-09-20 on the live gunnar.rs front (v1.4.0-353-g482c062):
// `error: unrecognized subcommand 'appliance-identity'`, while
// monetize-server on that same box narrated, every poll, that the appliance
// signs with one key and its own config names another and every genuine
// tenant renewal would be REFUSED.
//
// Its own row rather than a feature added to `gunnar-cli-musl`, because
// that one is the open delivery and the open build must not gain a
// money verb; and its own BIN NAME rather than a second resolution of
// `gunnar-cli`, because a bin name is a file path and two rows naming one
// path are one artifact written twice (`gunnar-cli/Cargo.toml`, top).
//
// It costs nothing to carry: `cargo tree -p gunnar-cli --no-default-features
// --features client,control` and the same line plus `,metered` both resolve
// 1656 rows, and neither contains gunnar-server. The feature buys one verb,
// the proto it calls, and no dependency.
Variant {
name: "gunnar-cli-musl-metered",
atom: Atom::GunnarCli,
column: Column::Exe,
suites: &[Suite::Mvp],
kind: ArtifactKind::Exe,
linkage: ServiceShape::ThinStandalone,
is_default: false,
member: Some("gunnar"),
package: Some("gunnar-cli"),
bin: Some("gunnar-cli-metered"),
features: &["client", "control", "metered"],
no_default_features: true,
profile: "release",
target: Some("x86_64-unknown-linux-musl"),
container_ref: None,
manifest_dir: None,
siblings: &[],
open: &[],
sign: false,
},
// ── `gunnar-clock-musl` WAS HERE, and it is GONE (lane L10, 2026-10-21) ──
//
// The FRONT host's clock daemon (`gunnar-clock --features bin`, musl). The
// front is vm1's own medium since 2026-09-25 and keeps the clock as a thread
// of `gunnar-front`; the kit no longer builds the daemon. Gunnar's
// `gunnar-clock` crate moved to edda `estate-clock` (its binary is
// `estate-clock`, built in edda), and the crate is deleted here.
//
// ── vm1's one program, the FRONT medium's payload ──────────────────────
//
// `gunnar-front` (owner 2026-09-25: main and twin share the gunnar medium,
// vm1 boots its OWN one). The payload variant of `gunnar-front-installer-open`,
// as `gunnar-server` is the payload of `gunnar-installer-open`. musl for the
// reason every PID-1 payload is: korp-start execs it on a disk with no libc.
// `fat`: it holds its keys and answers; it asks no sibling for anything.
Variant {
name: "gunnar-front",
atom: Atom::GunnarFront,
column: Column::Exe,
suites: &[Suite::Mvp],
kind: ArtifactKind::Exe,
linkage: ServiceShape::FatService,
// The `(MVP, server)` group's normal case is the product `gunnar`.
is_default: false,
member: Some("gunnar"),
package: Some("gunnar-front"),
bin: Some("gunnar-front"),
// `dhcp-client` — the one real choice: a static site builds the front
// with no DHCP client, as it does gunnar-server (`--no-default-features`
// then names the set, so the receipt resolver can tell it apart).
// `console`: the web face (gunnar-ui-server + the built console) on the
// front's own :443 (2026-09-25) — the community half.
features: &[
"dhcp-client",
"console",
"forges",
"clock-fetch",
"resolver-probe",
"acme",
"egress-door",
"mail",
"sso",
],
no_default_features: true,
profile: "release",
target: Some("x86_64-unknown-linux-musl"),
container_ref: None,
// gunnar-front is a DETACHED workspace (2026-10-22): a lone clone must
// load the root one, and the front needs crates only siblings have.
manifest_dir: Some("crates/gunnar-front"),
siblings: &[],
open: &["dev-narrate"],
sign: false,
},
// ── `gunnar-front` + DEV-NARRATE (2026-09-30) ──────────────────────────────
//
// The build point the installer's `dev-narrate` option ARRIVES at when it is
// ON (`--opt dev-narrate=on`, the kit's dev medium only): the same program
// plus Skidbladnir nordisk-narrate's sender — the box's boot log sealed to
// the installer's key over udp/51822 before the installer's link exists. Its
// own row so the ON binary's receipt NAMES it a dev build; `gunnar-front` keeps
// `dev-narrate` in `open` (off), and the option forbids `nordisk_narrate`
// in every OFF (release) build.
Variant {
name: "gunnar-front-narrate",
atom: Atom::GunnarFront,
column: Column::Exe,
suites: &[Suite::Mvp],
kind: ArtifactKind::Exe,
linkage: ServiceShape::FatService,
is_default: false,
member: Some("gunnar"),
package: Some("gunnar-front"),
bin: Some("gunnar-front"),
features: &[
"dhcp-client",
"console",
"forges",
"clock-fetch",
"resolver-probe",
"acme",
"egress-door",
"mail",
"sso",
"dev-narrate",
],
no_default_features: true,
profile: "dev-media",
target: Some("x86_64-unknown-linux-musl"),
container_ref: None,
// gunnar-front is a DETACHED workspace (2026-10-22): a lone clone must
// load the root one, and the front needs crates only siblings have.
manifest_dir: Some("crates/gunnar-front"),
siblings: &[],
open: &[],
sign: false,
},
// ── the PAID front's payload: gunnar's own front, `metered` ─────────────
//
// vm1 of `gunnar-twin-one` (was `gunnar-twin-monetize`): the console's paid half (gunnar-ui-server `metered`) and
// nothing more. It runs no monetize (owner 13:35Z 2026-10-10, ONE monetize: the estate's
// boxes call monetize.ignalina.dk's Fleet door); the 2026-10-08 commercial front built
// from monetize-impl is LEGACY.
Variant {
name: "gunnar-front-metered",
atom: Atom::GunnarFront,
column: Column::Exe,
suites: &[Suite::Mvp],
kind: ArtifactKind::Exe,
linkage: ServiceShape::FatService,
is_default: false,
member: Some("gunnar"),
package: Some("gunnar-front"),
bin: Some("gunnar-front"),
// `public-site`: gunnar.rs's public face (TASKS-12, owner 2026-10-25), this
// row and its narrate twin only (`tests/public_site_row.rs`).
features: &[
"dhcp-client",
"metered",
"public-site",
"forges",
"clock-fetch",
"resolver-probe",
"acme",
"egress-door",
"mail",
"sso",
],
no_default_features: true,
profile: "release",
target: Some("x86_64-unknown-linux-musl"),
container_ref: None,
// gunnar-front is a DETACHED workspace (2026-10-22): a lone clone must
// load the root one, and the front needs crates only siblings have.
manifest_dir: Some("crates/gunnar-front"),
siblings: &[],
open: &["dev-narrate"],
sign: false,
},
// ── `gunnar-front-metered` + DEV-NARRATE (2026-09-30) ──────────────────────────────
//
// The build point the installer's `dev-narrate` option ARRIVES at when it is
// ON (`--opt dev-narrate=on`, the kit's dev medium only): the same program
// plus Skidbladnir nordisk-narrate's sender — the box's boot log sealed to
// the installer's key over udp/51822 before the installer's link exists. Its
// own row so the ON binary's receipt NAMES it a dev build; `gunnar-front-metered` keeps
// `dev-narrate` in `open` (off), and the option forbids `nordisk_narrate`
// in every OFF (release) build.
Variant {
name: "gunnar-front-metered-narrate",
atom: Atom::GunnarFront,
column: Column::Exe,
suites: &[Suite::Mvp],
kind: ArtifactKind::Exe,
linkage: ServiceShape::FatService,
is_default: false,
member: Some("gunnar"),
package: Some("gunnar-front"),
bin: Some("gunnar-front"),
features: &[
"dhcp-client",
"metered",
"public-site",
"forges",
"clock-fetch",
"resolver-probe",
"acme",
"egress-door",
"mail",
"sso",
"dev-narrate",
],
no_default_features: true,
profile: "dev-media",
target: Some("x86_64-unknown-linux-musl"),
container_ref: None,
// gunnar-front is a DETACHED workspace (2026-10-22): a lone clone must
// load the root one, and the front needs crates only siblings have.
manifest_dir: Some("crates/gunnar-front"),
siblings: &[],
open: &[],
sign: false,
},
// ── the appliance image ──────────────────────────────────────────────────
Variant {
name: "gunnar-image",
atom: Atom::GunnarServer,
column: Column::Container,
suites: &[Suite::Mvp],
kind: ArtifactKind::Container,
linkage: ServiceShape::FatService,
is_default: false,
member: None,
package: None,
bin: None,
features: &[],
no_default_features: false,
profile: "",
target: None,
container_ref: Some("gunnar-scratch"),
manifest_dir: None,
siblings: &[],
open: &[],
sign: false,
},
// ── the appliance ISO ────────────────────────────────────────────────────
//
// `iso-live`, NOT `iso-installer`: its boot parameters end in
// `serve --no-grpc`, so the medium comes up serving git and writes nothing to
// a disk. Calling it an installer would put a column label on the receipt
// that the artifact contradicts the moment somebody boots it.
Variant {
name: "gunnar-appliance-iso",
atom: Atom::GunnarServer,
column: Column::IsoLive,
suites: &[Suite::Mvp],
kind: ArtifactKind::Iso,
linkage: ServiceShape::FatService,
is_default: false,
member: None,
package: None,
bin: None,
features: &[],
no_default_features: false,
profile: "",
target: None,
container_ref: None,
manifest_dir: None,
siblings: &[],
open: &[],
sign: false,
},
// ── the per-site installer ISO ───────────────────────────────────────────
//
// The pointer variant for the `[[build.installer]]` variant, closing the one
// gap in the tagged-pointer pattern this table already uses twice
// (`gunnar-image` → container payload, `gunnar-appliance-iso` → iso
// payload). Like an iso, an installer variant carries the payload handle in
// its OWN name — `assemble()` resolves `require_installer(&variant.name)` —
// so there is no `installer_ref` field to fill.
Variant {
name: "gunnar-installer",
atom: Atom::GunnarServer,
column: Column::IsoInstaller,
suites: &[Suite::Mvp],
kind: ArtifactKind::Installer,
linkage: ServiceShape::FatService,
is_default: false,
member: None,
package: None,
bin: None,
features: &[],
no_default_features: false,
profile: "",
target: None,
container_ref: None,
manifest_dir: None,
siblings: &[],
open: &[],
sign: false,
},
// ── the KEYLESS installer ISO — door B ───────────────────────────────────
//
// The pointer variant for the second `[[build.installer]]` row,
// `gunnar-installer-open` (2026-08-28). Same atom, same column, same
// payload shape; the ONE product decision that differs is the bootstrap
// door — no key on the medium, the control plane bound off the loopback,
// first caller enrols — and that decision lives in the installer row's
// argv (`recipe::OPEN_KORP_ARGS`), not here. A second matrix cell rather
// than an `--opt`, because an option IS a cargo feature by construction and
// the door is argv (reachability.rs has the long form).
Variant {
name: "gunnar-installer-open",
atom: Atom::GunnarServer,
column: Column::IsoInstaller,
suites: &[Suite::Mvp],
kind: ArtifactKind::Installer,
linkage: ServiceShape::FatService,
is_default: false,
member: None,
package: None,
bin: None,
features: &[],
no_default_features: false,
profile: "",
target: None,
container_ref: None,
manifest_dir: None,
siblings: &[],
// The medium's DEV-NARRATE option (default OFF) is this row's own
// switchable axis: `recipe::dev_narrate_option`.
open: &["dev-narrate"],
sign: false,
},
// ── the FRONT medium — vm1's own installer ISO ──────────────────────────
//
// The pointer variant for `[[build.installer]] gunnar-front-installer-open`
// (2026-09-25). Its payload is `gunnar-front`, not `gunnar-server`: the two
// media of an estate are the gunnar medium (main + twin, byte for byte) and
// this one (vm1). `recipe::front_installer` has the row.
Variant {
name: "gunnar-front-installer-open",
atom: Atom::GunnarFront,
column: Column::IsoInstaller,
suites: &[Suite::Mvp],
kind: ArtifactKind::Installer,
linkage: ServiceShape::FatService,
is_default: false,
member: None,
package: None,
bin: None,
features: &[],
no_default_features: false,
profile: "",
target: None,
container_ref: None,
manifest_dir: None,
siblings: &[],
// The medium's DEV-NARRATE option (default OFF) is this row's own
// switchable axis: `recipe::dev_narrate_option`.
open: &["dev-narrate"],
sign: false,
},
// The pointer variant for `[[build.installer]] gunnar-front-installer-metered`
// (2026-09-25): the paid front medium, payload `gunnar-front-metered`.
Variant {
name: "gunnar-front-installer-metered",
atom: Atom::GunnarFront,
column: Column::IsoInstaller,
suites: &[Suite::Mvp],
kind: ArtifactKind::Installer,
linkage: ServiceShape::FatService,
is_default: false,
member: None,
package: None,
bin: None,
features: &[],
no_default_features: false,
profile: "",
target: None,
container_ref: None,
manifest_dir: None,
siblings: &[],
// The medium's DEV-NARRATE option (default OFF) is this row's own
// switchable axis: `recipe::dev_narrate_option`.
open: &["dev-narrate"],
sign: false,
},
// ── the virtio-scsi installer ISO — the SAME door, a different MACHINE ───
//
// The pointer variant for the third `[[build.installer]]` row,
// `gunnar-installer-gce` (2026-08-30). Same atom, same column,
// same payload, same keyless bootstrap door as the row above: what differs
// is the machine the medium is built FOR — GCE's Persistent Disk rides
// virtio-scsi, so the row declares `target.buses ∋ virtio-scsi` and takes
// a kernel donor whose preset does not veto `CONFIG_SCSI`.
//
// A separate cell rather than an `--opt`, and for a sharper reason than the
// door above: the bus decides the KERNEL, and a kernel is an artifact, not
// a feature flag. Widening the shared row would put `SCSI BLK_DEV_SD
// SCSI_VIRTIO` on the UpCloud appliance's 2.4 MB bzImage, which no customer
// of that machine asked for.
Variant {
name: "gunnar-installer-gce",
atom: Atom::GunnarServer,
column: Column::IsoInstaller,
suites: &[Suite::Mvp],
kind: ArtifactKind::Installer,
linkage: ServiceShape::FatService,
is_default: false,
member: None,
package: None,
bin: None,
features: &[],
no_default_features: false,
profile: "",
target: None,
container_ref: None,
manifest_dir: None,
siblings: &[],
open: &[],
sign: false,
},
// ── the NVMe/ENA installer ISO — the SAME door, AWS Nitro's MACHINE ──────
//
// The pointer variant for the fourth `[[build.installer]]` row,
// `gunnar-installer-aws` (2026-09-02). Same atom, same
// column, same payload, same keyless bootstrap door as the GCE row above:
// what differs is again the machine the medium is built FOR. AWS Nitro
// presents its boot disk as NVMe (`/dev/nvme0n1`, never virtio or IDE) and
// its only NIC as the Elastic Network Adapter, so the row declares
// `target.buses ∋ nvme` + `target.nic = ena` and takes the `tunnr-kernel-nvme`
// donor whose `slim` preset vetoes neither `CONFIG_BLK_DEV_NVME` nor
// `CONFIG_ENA_ETHERNET`.
//
// A separate cell and not an `--opt`, for the GCE row's exact reason: the
// bus and the NIC decide the KERNEL, and a kernel is an artifact, not a
// feature flag. Widening the shared row would put the NVMe stack and an ENA
// driver on the UpCloud appliance's 2.4 MB bzImage, for devices it does not
// have.
Variant {
name: "gunnar-installer-aws",
atom: Atom::GunnarServer,
column: Column::IsoInstaller,
suites: &[Suite::Mvp],
kind: ArtifactKind::Installer,
linkage: ServiceShape::FatService,
is_default: false,
member: None,
package: None,
bin: None,
features: &[],
no_default_features: false,
profile: "",
target: None,
container_ref: None,
manifest_dir: None,
siblings: &[],
open: &[],
sign: false,
},
// ── the Hyper-V installer ISO — the SAME door, Azure's MACHINE ────────────
//
// The pointer variant for the `gunnar-installer-azure` row (2026-09-05).
// Same atom, same column, same payload, same keyless bootstrap door as the
// GCE and Nitro rows: what differs is the machine. An Azure Gen2 VM
// presents every disk as Hyper-V synthetic SCSI (`hv_storvsc`) and its NIC
// as `hv_netvsc`, both over VMBus, so the row declares `target.buses ∋
// hv-storvsc` + `target.nic = hv-netvsc` and takes the `tunnr-kernel-hyperv`
// donor. A separate cell for the same reason as the two before it: the bus
// and the NIC decide the KERNEL, and a kernel is an artifact.
Variant {
name: "gunnar-installer-azure",
atom: Atom::GunnarServer,
column: Column::IsoInstaller,
suites: &[Suite::Mvp],
kind: ArtifactKind::Installer,
linkage: ServiceShape::FatService,
is_default: false,
member: None,
package: None,
bin: None,
features: &[],
no_default_features: false,
profile: "",
target: None,
container_ref: None,
manifest_dir: None,
siblings: &[],
open: &[],
sign: false,
},
// `gunnar-installer-metered` (2026-09-05). Same atom, same column, same
// machine, same keyless bootstrap door as the open row: what differs is the
// BINARY the medium is composed from — `gunnar-server-metered`, the build
// point that links `monetize-embed` and the entitlement gate.
//
// **This cell is what makes the row buildable.** `[[build.installer]]`
// declares how a medium is composed; the CLI door resolves `--variant` here,
// so an installer row with no cell beside it is a declaration the door
// cannot reach — `cli_matrix` iterates `set.variants`, not `set.installers`,
// and would have gone on being green about a medium nobody could build. The
// four rows above each have one for exactly this reason.
//
// A separate cell and not an `--opt`, for the GCE and Nitro rows' reason
// one step over: `metered` is a cargo feature, so it selects a different
// BINARY and therefore a different artifact. An option toggles features
// within one variant's build; it cannot swap the variant a medium composes.
Variant {
name: "gunnar-installer-metered",
atom: Atom::GunnarServer,
column: Column::IsoInstaller,
suites: &[Suite::Mvp],
kind: ArtifactKind::Installer,
linkage: ServiceShape::FatService,
is_default: false,
member: None,
package: None,
bin: None,
features: &[],
no_default_features: false,
profile: "",
target: None,
container_ref: None,
manifest_dir: None,
siblings: &[],
// The medium's DEV-NARRATE option (default OFF) is this row's own
// switchable axis: `recipe::dev_narrate_option`.
open: &["dev-narrate"],
sign: false,
},
// `gunnar-installer-metered-aws` / `-azure` (2026-09-05): the metered
// BINARY on the AWS Nitro and Azure Gen2 machines — `metered_twin` of the
// `-aws` and `-azure` rows in `recipe.rs`. Same reason for a cell each as
// the one above: an installer row with no cell beside it is a medium the
// CLI door cannot resolve. The kernel is the cloud row's (nvme / hyperv);
// the cell says nothing about it, the `[[build.installer]]` row does.
Variant {
name: "gunnar-installer-metered-aws",
atom: Atom::GunnarServer,
column: Column::IsoInstaller,
suites: &[Suite::Mvp],
kind: ArtifactKind::Installer,
linkage: ServiceShape::FatService,
is_default: false,
member: None,
package: None,
bin: None,
features: &[],
no_default_features: false,
profile: "",
target: None,
container_ref: None,
manifest_dir: None,
siblings: &[],
open: &[],
sign: false,
},
Variant {
name: "gunnar-installer-metered-azure",
atom: Atom::GunnarServer,
column: Column::IsoInstaller,
suites: &[Suite::Mvp],
kind: ArtifactKind::Installer,
linkage: ServiceShape::FatService,
is_default: false,
member: None,
package: None,
bin: None,
features: &[],
no_default_features: false,
profile: "",
target: None,
container_ref: None,
manifest_dir: None,
siblings: &[],
open: &[],
sign: false,
},
// `gunnar-twin` STOOD HERE from 2026-09-15 to 2026-09-17, a ninth pointer
// variant beside a ninth installer row. Both are gone, and the deletion is
// the fix: the row was `open_twin`'s clone, so its `variant` was
// `gunnar-server` — the binary `tests/metered_symbols.rs` proves contains no
// entitlement code — while gunnar.rs's PRIMARY is
// `gunnar-installer-metered`/`gunnar-server-metered`. A twin exists to
// become the primary, so a takeover would have swapped the metered binary
// for one with no plan enforcement in it, silently. The twin installs the
// primary's own medium with NO role on it (the role is conferred over the
// wire, `Bootstrap.BecomeTwin`), which makes "the twin's variant equals the
// primary's" true by construction rather than by care;
// `tests/twin_is_the_same_medium.rs` holds it that way.
];
/// Every variant in [`VARIANTS`] names an assembly [`Atom::assembles`] allows, and an
/// artifact kind its column admits.
///
/// `const fn`, so the answer is computed while the crate compiles. See
/// [`EVERY_VARIANT_IS_LEGAL`].
#[must_use]
pub const fn every_variant_is_legal() -> bool {
let mut i = 0;
while i < VARIANTS.len() {
let c = VARIANTS[i];
if !c.atom.supports(c.column) {
return false;
}
if !c.column.admits(c.kind) {
return false;
}
i += 1;
}
true
}
/// **The compile error.**
///
/// A variant naming a pair [`Atom::assembles`] refuses — or an artifact kind its
/// column does not admit — stops `cargo build` here, before anything has been
/// built and before dwarves has offered the combination to anybody.
///
/// The message cannot name the offending variant: a `const` panic takes a literal,
/// with no formatting. The test of the same rule in `lib.rs` is the half that
/// names it, and the two call the same `const fn` so they cannot disagree.
pub const EVERY_VARIANT_IS_LEGAL: () = assert!(
every_variant_is_legal(),
"a variant in VARIANTS names an assembly Atom::assembles refuses, or an artifact \
kind its Column does not admit. Run `cargo test -p gunnar-build-thing` — \
the test of the same rule names which one."
);
/// The variants that fill `(atom, column)`, in declaration order.
///
/// Empty is a real answer, not a missing one: `(gunnar-server, rpm)` is a legal
/// assembly nobody has declared, and `(gunnar-cli, iso-live)` is not an assembly
/// at all. [`Atom::assembles`] is what tells those two apart.
#[must_use]
pub fn variants_at(atom: Atom, column: Column) -> Vec<&'static Variant> {
VARIANTS
.iter()
.filter(|c| c.atom == atom && c.column == column)
.collect()
}
/// The variant with this name, if any.
#[must_use]
pub fn by_name(name: &str) -> Option<&'static Variant> {
VARIANTS.iter().find(|c| c.name == name)
}
/// **Which variant a resolved cargo feature set IS** — the lookup the receipt
/// writer runs from inside `build.rs`.
///
/// # Why the features are the key, and not a name somebody passes in
///
/// A build script cannot be told which variant it is building without somebody
/// declaring it a second time, and a second declaration is what the receipt
/// exists to stop trusting. What a build script CAN see is the feature set cargo
/// actually resolved, and the five cargo variants have five distinct feature sets —
/// which is not a coincidence, it is what makes them five different programs out
/// of one `main.rs`. So the resolved features identify the variant by construction.
///
/// [`the_five_cargo_variants_have_pairwise_distinct_feature_sets`] holds that
/// property, and it is the guard that fails if a sixth variant ever collides with
/// an existing one.
///
/// # Exact equality, and a `None` rather than a guess
///
/// `resolved` must equal the variant's declared feature list, normalised. A subset
/// rule would make `--all-features` resolve to whichever variant happened to be
/// biggest, and a receipt naming a variant the build was not is worse than no
/// receipt at all: the first answers the rebuild question wrongly and the second
/// answers it not at all.
///
/// [`the_five_cargo_variants_have_pairwise_distinct_feature_sets`]:
/// #the_five_cargo_variants_have_pairwise_distinct_feature_sets
/// # Only the cargo variants are candidates
///
/// `gunnar-image` and `gunnar-appliance-iso` compile nothing — they compose
/// another variant's artifact — so they declare no features, and an empty resolved
/// set would otherwise resolve to whichever of them came first in the table.
/// That is not a hypothetical; it is what the first version of this function
/// did, and `--no-default-features` with nothing else is a real build. A build
/// script only ever runs inside a cargo compilation, so `package.is_some()` is
/// the honest candidate set.
#[must_use]
pub fn by_resolved_features(resolved: &BTreeSet<String>) -> Option<&'static Variant> {
let mut hit = VARIANTS
.iter()
.filter(|c| c.package.is_some() && c.built_here())
.filter(|c| &c.normalised_features() == resolved);
let first = hit.next()?;
// **Only answer when the features alone settle it.** Where two rows share a
// feature set and differ by target, this cannot tell them apart and must
// not pick one: see [`by_build_point`], which is what a caller holding a
// target should ask.
if hit.next().is_some() {
return None;
}
Some(first)
}
/// **A build point is (features, target), not features alone.**
///
/// The identity used to be the feature set by itself, and the table could
/// therefore hold only ONE row per set. That is the rule that made the front's
/// operator binaries unbuildable here: `gunnar-cli` for this host and the same
/// `gunnar-cli` built for `x86_64-unknown-linux-musl` are the same features and
/// a different artifact at a different path, so the second one could not be
/// declared and was written out as a cargo line in another repository's xtask
/// instead — the second description of a build that this table exists to be the
/// only one of.
///
/// The model already said so; only the lookup disagreed. `variant → features →
/// SETTINGS → typ`, and the target is a setting: it changes the artifact and
/// its path. [`crate::emit::attribute`] has always taken the actual target and
/// compared it AFTER resolving, which is the same two facts in the wrong order —
/// it could only ever report a mismatch where it can now make a match.
///
/// `None` for the target means "whatever the host is", as everywhere else in
/// this file, so a caller that knows the host passes it and a caller that does
/// not passes `None` and gets the host row.
#[must_use]
pub fn by_build_point(
resolved: &BTreeSet<String>,
target: Option<&str>,
) -> Option<&'static Variant> {
VARIANTS
.iter()
.filter(|c| c.package.is_some() && c.built_here())
.find(|c| &c.normalised_features() == resolved && c.target == target)
}
/// Cargo's own spelling of a feature name in `CARGO_FEATURE_*`: uppercased, with
/// every non-alphanumeric character mapped to `_`.
///
/// The receipt writer reads the resolved set out of the environment, where the
/// names arrive in this form and cannot be mapped back (`a-b` and `a_b` are one
/// variable). So both sides are normalised forward, which is exact.
#[must_use]
pub fn normalise_feature(name: &str) -> String {
name.chars()
.map(|c| {
if c.is_ascii_alphanumeric() {
c.to_ascii_uppercase()
} else {
'_'
}
})
.collect()
}
impl Variant {
/// The panel the scene opens for this variant's artifact.
///
/// Comes from the variant's KIND rather than its column, which is the only
/// reason the narrowing in [`Column::admits`] is worth having:
/// `gunnar-service` and `gunnar-server` are both `exe` variants and want
/// different panels.
#[must_use]
pub const fn pane(&self) -> PaneKind {
PaneKind::for_artifact(self.kind)
}
/// **Built from THIS repository** — every row but the console's (`gunnar-ui`, a
/// sibling checkout's crate, `manifest_dir` `../gunnar-ui/…`). Only a row built here
/// can be attributed from a build script's resolved features: the console's builds
/// write their receipts from their own recipe.
#[must_use]
pub fn built_here(&self) -> bool {
!self.manifest_dir.is_some_and(|d| d.starts_with("../"))
}
/// This variant's `--features` list in cargo's `CARGO_FEATURE_*` spelling.
///
/// The declared list must already be closed under gunnar's own feature
/// implications — `gunnar-service` names `["server", "systemd"]` rather than
/// `["systemd"]` alone, even though `systemd = [… "server"]` implies it —
/// because the environment shows the CLOSURE and this is compared to it by
/// equality. `the_declared_feature_lists_are_closed_under_implication` in
/// `gunnar-cli` is what holds that, against the real manifest.
#[must_use]
pub fn normalised_features(&self) -> BTreeSet<String> {
self.features.iter().map(|f| normalise_feature(f)).collect()
}
/// The cargo profile, resolved. `""` in the table means the cargo default.
#[must_use]
pub fn profile_resolved(&self) -> Profile {
Profile::from(self.profile.to_string())
}
/// **Every setting this build resolves to** — the map a receipt records.
///
/// Derived from the variant's own fields rather than listed a second time
/// beside them. A hand-written settings list is a copy of the build point
/// that drifts from it silently, and a receipt that records the drifted copy
/// is worse than one that records nothing: it answers the rebuild question
/// wrongly.
///
/// Absolute paths and timestamps are deliberately absent, per the receipt's
/// own rule — they differ between two builds that are otherwise identical.
#[must_use]
pub fn resolved_settings(&self) -> BTreeMap<String, String> {
let mut s = BTreeMap::new();
s.insert("atom".to_string(), self.atom.as_str().to_string());
s.insert("column".to_string(), self.column.as_str().to_string());
// The receipt KEY keeps the retired word: it is a wire literal shipped in
// every gunnar binary built so far, and a reader comparing receipts across
// builds must not see two spellings of one setting. Renaming it is a
// receipt-version decision, not a vocabulary edit.
s.insert("cell".to_string(), self.name.to_string());
s.insert("kind".to_string(), String::from(self.kind));
s.insert("linkage".to_string(), self.linkage.as_str().to_string());
s.insert(
"suites".to_string(),
self.suites
.iter()
.map(|x| x.as_str())
.collect::<Vec<_>>()
.join(","),
);
s.insert("features".to_string(), self.features.join(","));
s.insert(
"no-default-features".to_string(),
self.no_default_features.to_string(),
);
s.insert(
"profile".to_string(),
self.profile_resolved().label().to_string(),
);
// "host" rather than an absent key: a receipt with no `target` cannot be
// told apart from one written before the key existed.
s.insert(
"target".to_string(),
self.target.unwrap_or("host").to_string(),
);
if let Some(p) = self.package {
s.insert("package".to_string(), p.to_string());
}
if let Some(b) = self.bin {
s.insert("bin".to_string(), b.to_string());
}
if let Some(c) = self.container_ref {
s.insert("container".to_string(), c.to_string());
}
s
}
/// This variant as the declaration type `nornir-build-thing` validates and
/// materializes cargo argv from.
///
/// This is the seam: everything downstream — the validator, the cargo argv,
/// the buildgate, the linkage audit — keeps working on the type it already
/// works on, and none of it learns that gunnar's declaration stopped being a
/// file.
#[must_use]
pub fn to_variant(&self) -> BuildVariant {
BuildVariant {
name: self.name.to_string(),
package: self.package.map(str::to_string),
bin: self.bin.map(str::to_string),
features: self.features.iter().map(|f| (*f).to_string()).collect(),
no_default_features: self.no_default_features,
profile: self.profile_resolved(),
target: self.target.map(str::to_string),
cfg: None,
member: self.member.map(str::to_string),
shape: self.linkage,
role: Some(self.atom.role()),
suites: self.suites.iter().map(|s| s.as_str().to_string()).collect(),
is_default: self.is_default,
kind: self.kind,
container_ref: self.container_ref.map(str::to_string),
vm_ref: None,
embeds: BTreeSet::new(),
reaches: BTreeSet::new(),
open: self.open.iter().map(|f| (*f).to_string()).collect(),
manifest_dir: self.manifest_dir.map(str::to_string),
siblings: self.siblings.iter().map(|s| (*s).to_string()).collect(),
sign: self.sign.then(nornir_build_thing::sign::SignSpec::default),
..BuildVariant::default()
}
}
}