znippy-build-thing/src/recipe.rs
//! **The recipe** — znippy's `impl BuildThing`, the ONE declaration of what this
//! repository builds. `.nornir/build-variants.toml` is emitted from
//! [`ZnippyBuildThing::variants`] (see [`crate::emit`]); nothing here reads it.
//!
//! # Why the five-shape ladder does not apply
//!
//! korp, dwarves, nornir and modgunn were reshaped on 2026-08-02/03 onto the five
//! shapes of `workspace_nordisk/.nornir/build-matrix-2026-08-02.md` —
//! `<x>-server` ⭐ · `<x>` · `<x>-cli` · `<x>-ui` · `<x>-solo`. That ladder is an
//! axis about ONE question, *where does the server live*, and **znippy has no
//! server**. It is a compressor: an archiver you run, that exits. There is
//! nothing to be thin against, nothing to embed, no port, no unit, no deploy.
//!
//! So both rows are `role = tool` and `linkage = not-applicable`:
//!
//! | variant | features | what it is |
//! |---|---|---|
//! | `znippy` ⭐ | — | the archiver on its own floor: `no_default_features`, nothing optional linked |
//! | `znippy-full` | `iceberg`, `sign` | the same binary with the Iceberg bridge and signing compiled in |
//!
//! `linkage` is stated OUT LOUD rather than omitted, and the difference is not
//! cosmetic: `ServiceShape::default()` is `FatService`, so an omitted key reads as
//! a positive claim of self-containment that nobody made and nobody checked.
//! `not-applicable` says *this axis has no answer here* in a way a reader can tell
//! apart from *somebody forgot to fill this in*, and `validate_shape` holds it to
//! `embeds` and `reaches` both being empty — so the claim is enforced, not
//! decorative.
//!
//! # Carrying a `suites` tag is what makes an atom a Build Thing
//!
//! Rickard, 2026-08-03. Before that pass znippy declared NINE atoms and not one
//! carried the tag, so the whole repo sat outside the Build Thing system:
//! `--suite ORIGINAL` matched nothing here and `dwarves buildthing znippy` had no
//! delivery to ask for. The matrix he approved is these two rows, and one of the
//! things that left was the DUPLICATE BINARY — `znippy-cli`'s bin, not the crate:
//! the crate stays and is where all the code lives, but two build points writing
//! the same file name are one artifact built twice, and whichever ran last is
//! what sits on disk.
#[cfg(test)]
use std::collections::BTreeSet;
use nornir_build_thing::cargo_container::CargoToolchain;
use nornir_build_thing::contract::Coordinate;
use nornir_build_thing::plugin::{BuildThing, Manifest, Role, ABI_VERSION};
use nornir_build_thing::variant::{
ArtifactKind, BuildVariant, BuildVariantSet, Profile, ServiceShape,
};
/// The crate a receipt names as its recipe.
pub const KRATE: &str = "znippy-build-thing";
/// The content hash over this crate's sources, computed by `build.rs`.
pub const CONTENT_HASH: &str = env!("ZNIPPY_BUILD_THING_CONTENT_HASH");
/// The one delivery.
pub const SUITE: &str = "ORIGINAL";
/// znippy's build thing.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ZnippyBuildThing;
impl ZnippyBuildThing {
/// The coordinate a receipt names this recipe by: `krate:version:hash`.
#[must_use]
pub fn coordinate() -> Coordinate {
Coordinate {
krate: KRATE.to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
hash: Some(CONTENT_HASH.to_string()),
}
}
}
fn s(x: &str) -> String {
x.to_string()
}
/// One archiver atom: the `znippy` bin, off its own floor, with `features`.
fn atom(name: &str, features: &[&str], is_default: bool) -> BuildVariant {
BuildVariant {
name: s(name),
role: Some(Role::Tool),
is_default,
suites: vec![s(SUITE)],
member: Some(s("znippy")),
package: Some(s("znippy")),
bin: Some(s("znippy")),
kind: ArtifactKind::Exe,
// See the module doc: znippy has no server, so the fat/thin axis has no
// answer — and an omitted key would read as a `fat` claim nobody made.
shape: ServiceShape::NotApplicable,
no_default_features: true,
features: features.iter().map(|f| s(f)).collect(),
profile: Profile::Release,
..BuildVariant::default()
}
}
/// The two atoms.
pub fn variants() -> Vec<BuildVariant> {
vec![
atom("znippy", &[], true),
atom("znippy-full", &["iceberg", "sign"], false),
]
}
/// The feature space the `znippy` package offers. Both are built by
/// `znippy-full`, so neither may ever appear in [`allow_unbuilt`] — an excuse for
/// an OFFERED feature is rot, and `the_feature_axis_gate_is_clean_with_nothing_excused`
/// asserts it by name. The excuses that DO exist are for features member crates
/// declare, which is the manifest direction and a different question.
fn offers() -> Vec<&'static str> {
vec!["iceberg", "sign"]
}
/// **Features the workspace declares that no variant names, each with the
/// reason that is not a gap.** Read and enforced by `tests/feature_axis.rs`
/// (`FeatureAxis::manifest_gate`, 2026-09-05) — the manifest direction of the
/// axis, which the offers-only `gate()` below cannot see. The first run listed
/// all eight of these; none is deletable (they gate 2–100 `cfg` sites each),
/// and the four marked SHIPS are in every delivered binary already, reached
/// through a dependency's feature list rather than a variant's — a cell names
/// features of its own atom's package, so the transitive spelling is invisible
/// to the `built` column and has to be written down here instead.
fn allow_unbuilt() -> std::collections::BTreeMap<String, String> {
[
(
"host-decompressors",
"SHIPS — `znippy-cli` links `znippy-common`, the maven plugin and the python plugin with `features = [\"host-decompressors\"]` (the host's own lzip/ljar decompressors); a wasm build of a plugin turns it off, which is why it is a feature at all.",
),
(
"wasm-plugins",
"SHIPS — the wasm plugin host (wasmtime), linked by `znippy-cli` as `znippy-common/wasm-plugins`. Off only for consumers that embed znippy-common without a plugin host.",
),
(
"openzl",
"SHIPS — `znippy-common`'s `default = [\"openzl\"]`, pulled with defaults by `znippy-cli`: the openzl codec, an FFI arm (`openzl-sys-rs`) and the one place this workspace crosses the LAW's pure-Rust line; a feature so the FFI can be turned off, never so it can be turned on.",
),
(
"bundle",
"SHIPS — `znippy-common`'s shared airgap-bundle helper (`plugins::bundle`, serde_json), turned on by the skidbladnir and rust-toolchain plugins' `znippy-handler`, which `znippy-cli` pulls with defaults.",
),
(
"znippy-handler",
"SHIPS — the `default` of the skidbladnir and rust-toolchain plugins, pulled with defaults by `znippy-cli`: the plugin's in-process handler over its dependency-free classifier, which is the half a wasm shim leaves out.",
),
(
"resolve",
"the maven plugin's network arm (`ureq`): resolving coordinates over the wire. The archiver is offline by design — it archives what it is given — so nothing delivered enables it; it is for a consumer that wants a resolver.",
),
(
"bench-kernels",
"the git plugin's benchmark kernels (`fst`, `znippy-compress`): measurement scaffolding behind `examples/`, never a shipped shape.",
),
(
"sign-holger-test",
"test-only: `sign` plus `cert-helper` so a test can mint the certificates holger's signing would otherwise need; shipping it would ship a test CA.",
),
(
"provenance",
"the ELF plugin's DSSE / in-toto / SLSA binding: it turns on `znippy-common/sign` (ed25519-dalek, p256, x509-cert, the CMS stack), so it is off in every znippy delivery — `znippy-cli` takes `znippy-plugin-elf` with defaults — and exists for an embedder that verifies ELF provenance (holger's AttestStore payload and predicate types).",
),
(
"testmatrix",
"the nornir-testmatrix emit channel in `znippy-cli`: on only under `cargo test`, off in every delivery, and with it off the emit call is a no-op with the crate absent from the graph.",
),
]
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect()
}
/// **The pinned build image every cargo variant compiles in** — `[build.toolchain]`.
///
/// Until 2026-09-10 this recipe declared none, and the consequence was NOT that
/// znippy built on the host. `dwarves buildthing` builds this very crate's
/// wasm32 module INSIDE this container
/// (`dwarves::build_thing::code::run_cargo`), so an absent table meant
/// `CargoToolchain::validate` refused — *"no `image_sha256` — a named tag is a
/// convenience, NOT a pin"* — the code road closed, and the forge fell back to
/// reading the emitted `.nornir/build-variants.toml` as if it were the source.
/// The code-is-source ruling was out of force here for want of one line; the
/// 2026-09-10 inventory render says `toml` for znippy and six siblings.
///
/// The digest is the **MANIFEST** digest (64 lowercase hex, no `sha256:`), read
/// off the engine rather than typed from memory:
/// `cargo run -p nornir-build-thing --features image-build --example
/// image_digest -- nordisk-cargo-toolchain:live` — the bollard door added the
/// same day, because before it the only way to learn a digest was the
/// `podman image inspect` the no-shelling law forbids. The image recipe is
/// reproducible (edda `ac3852e`), so every box that mints it lands on this
/// digest. It moves only when the image RECIPE moves, in the same commit.
/// MOVED 2026-09-20 (loki). The value below was
/// `2cf9b012c9d2ffa42b447e5b0d127184e5a3f998dd2574d771477e9c8e366659` — a
/// second mint of this same recipe which exists on exactly one box, t14s, and
/// sat there UNTAGGED (`<none>:<none>`, 2026-07-13). On loki it does not exist
/// at all, so `CargoToolchain::validate` refused every container build of this
/// repo BY DIGEST — correctly, and that is the whole point of a pin: it told
/// the truth about an environment that was not there. The digest now names the
/// mint that is tagged `nordisk-cargo-toolchain:live` and `:live-uefi` on both
/// boxes, and which composed the live gunnar appliance ISO; `gunnar` and
/// `holger` have pinned it since 2026-09-15. Read back on loki through the
/// engine door, not from podman hearsay:
/// `cargo run -p nornir-build-thing --features image-build --example
/// image_digest -- nordisk-cargo-toolchain:live` ->
/// `86d575e874677da3eb9aff3ba6e38ca600781ee9b7948a9df43ef89a6213c762`.
/// Same `rust_version` 1.97.1, and the target list below is a subset of what
/// that image carries (it adds `x86_64-unknown-uefi`), so the pin moves and
/// nothing else does.
fn toolchain() -> Option<CargoToolchain> {
Some(CargoToolchain {
image: s("nordisk-cargo-toolchain"),
image_sha256: s("86d575e874677da3eb9aff3ba6e38ca600781ee9b7948a9df43ef89a6213c762"),
rust_version: s("1.97.1"),
// What the pinned image CARRIES std for (`rustup target list
// --installed` inside it), not what znippy delivers. wasm32 is not
// optional here: it is the target this crate's own plugin module is
// built for — and znippy has a second, unrelated wasm32 story in
// `plugins/wasm/`, which this line does not speak for.
targets: vec![
s("x86_64-unknown-linux-gnu"),
s("x86_64-unknown-linux-musl"),
s("wasm32-unknown-unknown"),
],
timeout_secs: 3600,
mem_limit_mb: 8192,
cpus: 8,
network: false,
remap_paths: true,
// MEASURED 2026-09-10
// (`--example escaping_siblings /home/rickard/git/znippy`), not
// guessed: TWENTY-ONE deps into `znippy-zoomies` — every native plugin
// reaches for `linflate` / `ljar` / `lzip-parallel` / `znippy-zoomies`
// itself, which is the compression doctrine showing up in the manifest
// graph — plus four into `edda` (`nornir-build-thing` from `tests/`,
// `xtask/` and this crate) and one into `nornir` (`xtask/`). If a
// sibling path dep is added or removed, this list moves in the SAME
// commit.
//
// **The other six are SECOND ORDER, and znippy's own manifests never
// mention any of them.** A sibling is a CHECKOUT, not a copied file:
// `xtask/` is a workspace member of this repo and depends on nornir's
// ROOT package, so cargo reads `/work/nornir/Cargo.toml` and must then
// resolve everything IT path-depends on. MEASURED two ways on
// 2026-09-10, and only because the pin let the container run at all:
//
// * by running the code road and reading what cargo said
// (`dwarves buildthing info znippy`) —
// error: failed to get `draupnir` as a dependency of package
// `nornir v0.5.7 (/work/nornir)`
// Caused by: failed to read `/work/draupnir/Cargo.toml`
// then, once draupnir was added, the same sentence for `facett-app`;
//
// * and then in full, off nornir's own root manifest rather than one
// failure at a time: its uncommented `path = "../…"` deps name
// draupnir, edda, facett, holger, modgunn, skade, Skidbladnir,
// znippy and znippy-zoomies.
//
// The `escaping_siblings` example cannot see any of this — it scans THIS
// repo's manifests, which is a first-order answer to a transitive
// question. That is why the digest pin alone did not put znippy on the
// code road. If a sibling path dep is added or removed — here OR in
// nornir's root manifest — this list moves in the SAME commit.
//
// 2026-10-16 (build-thing sweep on loki): `openzl-sys-rs` was missing and ⚒
// refused both cells — znippy-common's DEFAULT `openzl` feature path-deps
// `../../openzl-sys-rs` (moved out to mikronova). `tests/siblings.rs` now walks
// the transitive closure, so the next one is red in a test, not on the forge.
siblings: vec![
s("Skidbladnir"),
s("draupnir"),
s("edda"),
s("facett"),
s("holger"),
s("modgunn"),
s("nornir"),
s("openzl-sys-rs"),
s("skade"),
s("znippy-zoomies"),
],
})
}
/// **The `[[bin]]` targets no row builds, each with its reason** —
/// `[build.allow_unbuilt_bins]`. ⚒ Build's gate (`buildgate::tally_bins`) counts every
/// bin cargo can see in this tree; MEASURED 2026-10-16 on loki these had neither a
/// row nor a reason (`tests/build_gate.rs` now asks the same question).
fn allow_unbuilt_bins() -> std::collections::BTreeMap<String, nornir_build_thing::variant::AllowEntry> {
[
("archive_open", "the cargo-fuzz target over znippy-common's archive open / IPC guard (znippy-common/fuzz, its own workspace, built by `cargo fuzz` on nightly) — test tooling, never shipped."),
("ipc_stream", "the cargo-fuzz target over znippy-common's IPC stream guard (znippy-common/fuzz, its own workspace, built by `cargo fuzz` on nightly) — test tooling, never shipped."),
("znippy-build-thing", "this recipe crate's own emit door (`cargo run -p znippy-build-thing -- emit` regenerates .nornir/build-variants.toml); the forge loads the crate as wasm, the bin is repository tooling, never shipped."),
("xtask", "the `cargo xtask` door onto this repo's build thing — repository tooling, never shipped."),
]
.into_iter()
.map(|(k, v): (&str, &str)| (k.to_string(), nornir_build_thing::variant::AllowEntry::Reason(v.to_string())))
.collect()
}
impl BuildThing for ZnippyBuildThing {
fn manifest(&self) -> Manifest {
Manifest {
abi: ABI_VERSION,
repo: s("znippy"),
source_crate: s(KRATE),
summary: "znippy — fast multicore O(1) archiving with Apache Arrow IPC as \
storage. An archiver you run, that exits: no server, no port, \
nothing to be thin against."
.to_string(),
}
}
fn variants(&self) -> BuildVariantSet {
BuildVariantSet {
base_suite: Some(s(SUITE)),
offers: offers().into_iter().map(s).collect(),
allow_unbuilt: allow_unbuilt(),
allow_unbuilt_bins: allow_unbuilt_bins(),
variants: variants(),
toolchain: toolchain(),
..BuildVariantSet::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use nornir_build_thing::feature_axis::FeatureAxis;
/// **znippy has no server, and the recipe may not grow one.** The five-shape
/// ladder is about where a server lives; a row claiming a shape on that axis
/// would be answering a question this repo does not have.
#[test]
fn no_row_claims_a_server_shape_because_znippy_has_no_server() {
for v in ZnippyBuildThing.variants().variants {
assert_eq!(v.role, Some(Role::Tool), "'{}'", v.name);
assert_eq!(
v.shape,
ServiceShape::NotApplicable,
"'{}' would read as a fat self-contained service — a claim nobody made",
v.name
);
assert!(v.embeds.is_empty() && v.reaches.is_empty(), "'{}'", v.name);
assert_ne!(v.kind, ArtifactKind::Service, "'{}'", v.name);
}
}
/// **Every atom carries the suites tag, because carrying it is what makes an
/// atom a Build Thing** (Rickard, 2026-08-03). Nine untagged atoms is how
/// this repo sat outside the system, with `--suite ORIGINAL` matching nothing.
#[test]
fn every_atom_carries_the_delivery_tag() {
let set = ZnippyBuildThing.variants();
assert!(set.declares_suites(), "an untagged repo is outside the system");
for v in &set.variants {
assert_eq!(v.suites, vec![s(SUITE)], "'{}'", v.name);
}
assert_eq!(set.base_suite.as_deref(), Some(SUITE));
}
/// **One bin name, two atoms — and that is the whole reason the duplicate
/// went.** Both rows write `znippy`, deliberately: they are the same artifact
/// at two feature sets, never built in the same command. The removed
/// `znippy-cli` bin was a SECOND package writing the SAME file name, which is
/// one artifact built twice with whichever ran last surviving.
#[test]
fn both_atoms_are_the_same_binary_of_the_same_package() {
let set = ZnippyBuildThing.variants();
let packages: BTreeSet<_> = set.variants.iter().filter_map(|v| v.package.clone()).collect();
let bins: BTreeSet<_> = set.variants.iter().filter_map(|v| v.bin.clone()).collect();
assert_eq!(packages, ["znippy"].into_iter().map(s).collect());
assert_eq!(bins, ["znippy"].into_iter().map(s).collect());
}
/// The OFFERS direction of the feature axis does not rot: both offered
/// features are built by `znippy-full`, and no excuse is needed for either
/// — asserted by name, because `allow_unbuilt()` is no longer empty. Since
/// 2026-09-05 it carries the MANIFEST direction's excuses (features member
/// crates declare that no variant names, reached transitively or bench/test
/// only); that direction is `tests/feature_axis.rs`'s, and an excuse for an
/// OFFERED feature would still be stale rot here.
#[test]
fn the_feature_axis_gate_is_clean_with_nothing_excused() {
let set = ZnippyBuildThing.variants();
for offered in offers() {
assert!(
!set.allow_unbuilt.contains_key(offered),
"znippy builds everything it offers; an excuse for `{offered}` would be stale rot"
);
}
if let Err(rot) = FeatureAxis::of("znippy", &set).gate() {
panic!("the feature axis is rotting: {rot:#?}");
}
}
}