crates/gunnar-recipe/src/emit.rs
//! **The receipt writer, from gunnar's side.** The one function a `build.rs` in
//! this repository calls, and the only place that knows how a gunnar build
//! becomes a `nornir_build_thing::contract::Receipt`.
//!
//! ```text
//! gunnar-cli/build.rs -> emit::write_receipt(&[…])
//! resolves WHICH variant is being built, from the
//! features cargo resolved
//! -> nornir_build_thing::receipt::emit
//! writes OUT_DIR/…receipt.rs: a #[used]
//! #[link_section = ".nordisk.buildthing"] static
//! gunnar-cli/main.rs -> include!(env!("GUNNAR_BUILD_RECEIPT"))
//! ```
//!
//! # The section carries the RECEIPT; the model wants the MODULE too — a named gap
//!
//! `nornir_build_thing::contract::RECEIPT_SECTION` and
//! `nornir_build_thing::plugin::SECTION` are ONE name, `.nordisk.buildthing`.
//! The build-thing model wants
//! an artifact to carry its build thing's wasm module in that section, so a
//! forge holding only the artifact can load and CALL the recipe that built it;
//! what this crate puts there today is the receipt (`receipt::emit_into`, the
//! only writer edda ships — `artifact-section` is a READER,
//! `plugin_host::read_section`, and reads the same bytes). Two payloads under
//! one section name cannot both be there, and this crate is now a cdylib that
//! HAS a module to carry (2026-08-26). The writer, and the section split or
//! framing that lets receipt and module coexist, is upstream work in
//! `nornir-build-thing`; until it exists this crate writes the receipt and
//! says so here rather than embedding a module the reader would mistake for
//! one.
//!
//! # Why the include goes through an env var and not the macro
//!
//! `nornir_build_thing` ships `embed_build_receipt!()`, which is the same
//! `include!` with the file name baked in. Using it would mean gunnar-cli
//! carrying `nornir-build-thing` as a **normal** dependency — serde_json, toml,
//! object, cargo_metadata, chrono, sha2 — in the dependency graph of a git server
//! that is meant to be able to be PID 1, all so one `static` can be spelled with
//! a macro. This crate is a **build**-dependency instead: it does not reach the
//! shipped binary at all, and the only thing that crosses into the compilation is
//! the generated file's path, in [`RECEIPT_ENV`].
//!
//! The file name is still spelled exactly once — `receipt::GENERATED_FILE`, read
//! from the writer rather than repeated here.
//!
//! # What it refuses to do
//!
//! Write a receipt naming a variant the build is not. Two shapes cause that, and
//! both end as [`Attribution::NoVariant`] / [`Attribution::TargetMismatch`] with a
//! `cargo:warning`, never as a receipt:
//!
//! ```text
//! --all-features matches no declared variant's feature set
//! --features server with no --target, where the variant says musl
//! ```
//!
//! A missing receipt says "how this was built is unanswerable", which is true. A
//! receipt naming the wrong variant answers the rebuild question **wrongly**, and
//! there is no way to find out afterwards that it did.
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use nornir_build_thing::receipt::{self, ReceiptBuilder};
use crate::recipe::GunnarBuildThing;
use crate::variants::by_resolved_features;
use crate::Variant;
/// The env var `build.rs` publishes the generated file's path in, and that the
/// crate's own source `include!`s.
pub const RECEIPT_ENV: &str = "GUNNAR_BUILD_RECEIPT";
/// Which variant the running build turned out to be — or why it could not be told.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Attribution {
/// The resolved feature set is exactly one declared variant's, and the target
/// agrees. This is the only variant that produces a receipt.
Variant(&'static Variant),
/// No declared variant has this feature set. `--all-features`, or a combination
/// nobody declared.
NoVariant {
/// What cargo resolved, in `CARGO_FEATURE_*` spelling.
resolved: BTreeSet<String>,
},
/// The features name a variant, and the build is for a different target or a
/// different profile than that variant declares.
Mismatch {
/// The variant the features named.
variant: &'static Variant,
/// Which axis disagrees — `target` or `profile`.
axis: &'static str,
/// What the variant declares. `host (<triple>)` when it declares no target.
declared: String,
/// What cargo is actually doing.
actual: String,
},
}
impl Attribution {
/// The variant, when there is one.
#[must_use]
pub fn variant(&self) -> Option<&'static Variant> {
match self {
Attribution::Variant(c) => Some(c),
_ => None,
}
}
/// Whether a `build.rs` should turn [`note`](Self::note) into a
/// `cargo:warning`, given the profile cargo is actually building.
///
/// # A debug build is not a delivery, and never warns
///
/// `cargo build`, `cargo check --workspace`, every run of the test suite:
/// none of them is any declared variant, because every variant passes
/// `--no-default-features` and a default build resolves `DEFAULT` on top of
/// everything else. MEASURED 2026-08-06 — a plain workspace check resolves
/// `{CLIENT, CONTROL, DEFAULT, SERVER}`, which is nobody's variant.
///
/// Warning about that would put a `cargo:warning` on every compile in the
/// repository, and a warning that fires on every build is a warning nobody
/// reads — including the ones that matter. The reason is still recorded in
/// the generated file either way; only the shout is suppressed.
///
/// A **release** build that is not a variant is loud, because that is somebody
/// producing something and getting it wrong.
#[must_use]
pub fn should_warn(&self, actual_profile: &str) -> bool {
!matches!(self, Attribution::Variant(_)) && actual_profile != "debug"
}
/// The line a `build.rs` prints as a `cargo:warning`, when there is one to
/// print.
///
/// Names the resolved set rather than saying "no match": a warning that does
/// not say what it saw cannot be acted on, and this one fires during a build
/// somebody is in the middle of.
#[must_use]
pub fn note(&self) -> Option<String> {
match self {
Attribution::Variant(_) => None,
Attribution::NoVariant { resolved } => Some(format!(
"no build-thing receipt: the resolved feature set {{{}}} is not any declared \
variant's, so this binary cannot say which build point it is. Declared sets: {}",
comma(resolved),
declared_sets(),
)),
Attribution::Mismatch {
variant,
axis,
declared,
actual,
} => Some(format!(
"no build-thing receipt: the features name variant {:?}, which declares \
{axis} {declared:?}, and this build's {axis} is {actual:?}. A receipt naming \
a variant the build is not answers the rebuild question wrongly",
variant.name,
)),
}
}
}
/// What [`write_receipt`] did.
#[derive(Debug, Clone)]
pub struct Emitted {
/// The generated file, always written and always in [`RECEIPT_ENV`] — even
/// when it holds no receipt, because the `include!` in the crate's source
/// has to find something either way.
pub path: PathBuf,
/// Which variant this build is, or why it could not be told.
pub attribution: Attribution,
}
/// **The `build.rs` call.** Resolve the variant, write the receipt, publish the
/// path.
///
/// `extra_inputs` are build inputs only the calling crate can know —
/// `source-commit` is the one gunnar passes. They land in the receipt's `inputs`
/// map. Absolute paths and wall-clock times are **refused** by
/// `receipt::audit_receipt`, so do not pass a build date: two identical builds
/// would differ by it and by nothing else.
///
/// Prints `cargo:rustc-env=` and `cargo:warning=` on stdout, which is what a
/// build script's stdout is for.
///
/// # Errors
///
/// When `OUT_DIR` is unset (this is not a build script), when the manifest of
/// the crate being built cannot be read, or when the receipt is refused by its
/// own audit. Refusing loudly is the point: a build script that swallowed the
/// error would leave a binary with no receipt and no reason.
pub fn write_receipt(extra_inputs: &[(&str, String)]) -> anyhow::Result<Emitted> {
let out_dir = std::env::var("OUT_DIR").map_err(|_| {
anyhow::anyhow!("OUT_DIR is unset — emit::write_receipt is a build.rs call")
})?;
let out_dir = PathBuf::from(out_dir);
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
anyhow::anyhow!("CARGO_MANIFEST_DIR is unset — emit::write_receipt is a build.rs call")
})?;
let profile = std::env::var("PROFILE").unwrap_or_default();
let attribution = attribute(
&resolved_features(),
&std::env::var("TARGET").unwrap_or_default(),
&profile,
);
let path = match attribution.variant() {
Some(variant) => {
let r = receipt_for(variant, Path::new(&manifest_dir), extra_inputs)?;
// `emit_into` and not `emit`: the same writer, with the directory
// named once here rather than read from `OUT_DIR` a second time.
receipt::emit_into(&out_dir, &r)?
}
None => write_absent(&out_dir, &attribution)?,
};
if attribution.should_warn(&profile) {
if let Some(note) = attribution.note() {
println!("cargo:warning={note}");
}
}
println!("cargo:rustc-env={RECEIPT_ENV}={}", path.display());
Ok(Emitted { path, attribution })
}
/// The receipt for `variant`, with every setting resolved.
///
/// Two independent sources, deliberately:
///
/// ```text
/// the variant atom, column, kind, linkage, suites, features, profile,
/// target, package, bin -- what was DECLARED
/// cargo feature.<name> = true/false for the whole universe, plus
/// the target triple, profile, opt-level and rustc string
/// -- what was RESOLVED
/// ```
///
/// They can disagree, and that is the value: a variant declaring
/// `no-default-features` while `feature.default` resolved to `true` is a build
/// that did not do what the variant says, and the receipt carries both halves for
/// anyone who looks.
///
/// # Errors
///
/// When the manifest cannot be read or parsed, or the receipt fails its audit.
fn receipt_for(
variant: &Variant,
manifest_dir: &Path,
extra_inputs: &[(&str, String)],
) -> anyhow::Result<nornir_build_thing::contract::Receipt> {
// `from_cargo_env` is used for its INPUTS — the target triple, profile,
// opt-level, debug flag and the rustc version string of the compilation
// actually running. Its `settings` are discarded and rebuilt below.
let cargo = ReceiptBuilder::from_cargo_env(
GunnarBuildThing::coordinate(),
variant.atom.as_str(),
variant.column.as_str(),
)?
.build()?;
let mut builder = ReceiptBuilder::new(
cargo.build_thing.clone(),
cargo.atom.clone(),
cargo.target.clone(),
);
for (k, v) in &cargo.inputs {
builder = builder.input(k, v);
}
// Why the feature universe is re-derived instead of taken from
// `from_cargo_env`: that one is built from the compilation's environment
// alone, and the universe wants the manifest's whole `[features]` table so
// an OFF feature is recorded `false` rather than absent.
//
// Until 2026-08-17 the manifest was SLICED to its `[features]` table first,
// because 0.1.5's `declared_features` counted every `optional = true`
// dependency reachable only as `dep:<name>` as a declared feature and
// recorded it `false` in a binary that links it (MEASURED 2026-08-06: 27 of
// 33 `feature.*` entries on the first real gunnar-server receipt were
// dependencies) — a receipt that lies. The old comment here said the slice
// "comes out the day a `nornir-build-thing` carrying the upstream fix is
// published"; 0.1.7 is that version, the pin moved, and this is that day.
// `the_feature_universe_keeps_features_and_drops_optional_dependencies`
// still holds the property, now against the upstream directly.
let manifest = std::fs::read_to_string(manifest_dir.join("Cargo.toml"))?;
let universe = receipt::resolved_feature_settings(&manifest, |var| std::env::var(var).is_ok())?;
for (k, v) in universe {
builder = builder.setting(k, v);
}
for (k, v) in variant.resolved_settings() {
builder = builder.setting(k, v);
}
for (k, v) in extra_inputs {
builder = builder.input(*k, v.clone());
}
builder.build()
}
/// The feature set cargo resolved, in `CARGO_FEATURE_*` spelling.
///
/// Read out of the environment rather than from a command line, because the
/// environment is the RESOLVED set: it already carries whatever feature
/// unification, implication and `default` added, which is exactly the thing a
/// declaration cannot be trusted about.
fn resolved_features() -> BTreeSet<String> {
std::env::vars()
.filter_map(|(k, _)| k.strip_prefix("CARGO_FEATURE_").map(str::to_string))
.collect()
}
/// Which variant a resolved feature set, target and profile are — or why none.
///
/// Split out from [`write_receipt`] with no I/O in it so every refusal can be
/// watched going red without a build script, a manifest or an `OUT_DIR`.
///
/// `actual_profile` is cargo's `PROFILE`, which is `debug` or `release` and
/// **not** the `--profile` name: a custom profile that inherits release reports
/// `release`. That is why the comparison below is against the variant's profile
/// mapped through the same convention rather than against its label.
#[must_use]
pub fn attribute(
resolved: &BTreeSet<String>,
actual_target: &str,
actual_profile: &str,
) -> Attribution {
// **Resolve on the build point, not on the features alone.** The target was
// already in hand here and was only ever used to report a mismatch AFTER
// picking a row; with two rows sharing a feature set and differing by
// target, using it to PICK is what lets both be declared at all.
//
// The host fallback is unchanged: `None` in the table means "whatever the
// host is", so a row with no target is looked up as the host's.
let host = std::env::var("HOST").unwrap_or_default();
let variant = crate::variants::by_build_point(resolved, Some(actual_target))
.or_else(|| {
// The host row, asked for by the host's own triple.
(actual_target == host)
.then(|| crate::variants::by_build_point(resolved, None))
.flatten()
})
// Nothing matched the point; fall back to the feature set alone so a
// WRONG-TARGET build is still attributed to the row it meant and
// reported as a `Mismatch` below, rather than as "no variant".
.or_else(|| by_resolved_features(resolved));
let Some(variant) = variant else {
return Attribution::NoVariant {
resolved: resolved.clone(),
};
};
// `None` in the table means "whatever the host is", and cargo answers that
// by setting TARGET to the host triple. So the comparison is against HOST,
// not against "anything goes" — a cross build of a host-only variant is still a
// build the variant did not describe.
let declared_target = variant
.target
.map(str::to_string)
.unwrap_or_else(|| std::env::var("HOST").unwrap_or_default());
if declared_target != actual_target {
return Attribution::Mismatch {
variant,
axis: "target",
declared: if variant.target.is_some() {
declared_target
} else {
format!("host ({declared_target})")
},
actual: actual_target.to_string(),
};
}
// Only checked when cargo answered at all: an empty `PROFILE` means this is
// not a build script, and "not measured" must not be turned into a pass by
// comparing against the empty string.
let declared_profile = cargo_profile_word(variant.profile_resolved().label());
if !actual_profile.is_empty() && declared_profile != actual_profile {
return Attribution::Mismatch {
variant,
axis: "profile",
declared: format!(
"{} ({declared_profile})",
variant.profile_resolved().label()
),
actual: actual_profile.to_string(),
};
}
Attribution::Variant(variant)
}
/// The word cargo puts in a build script's `PROFILE` for a `--profile` name.
///
/// Cargo says `debug` or `release` and nothing else, so a declared profile has
/// to be mapped the same way before it can be compared. An unrecognised custom
/// name maps to `release`, because a custom profile in a delivery declaration is
/// an optimised one — and if that guess is ever wrong the result is a refused
/// receipt with the reason printed, not a wrong one.
fn cargo_profile_word(label: &str) -> &'static str {
match label {
"dev" | "debug" | "test" => "debug",
_ => "release",
}
}
/// Write a generated file that declares nothing.
///
/// The `include!` in the crate's own source has to resolve on every build, or a
/// combination nobody declared would stop compiling rather than stop carrying a
/// receipt. Those are very different failures and only one of them is wanted.
fn write_absent(out_dir: &Path, why: &Attribution) -> anyhow::Result<PathBuf> {
let path = out_dir.join(receipt::GENERATED_FILE);
let reason = why
.note()
.unwrap_or_else(|| "unattributed build".to_string());
std::fs::write(
&path,
format!("// No build-thing receipt for this compilation.\n// {reason}\n"),
)?;
Ok(path)
}
/// `a, b, c`.
fn comma(set: &BTreeSet<String>) -> String {
set.iter().cloned().collect::<Vec<_>>().join(", ")
}
/// Every declared variant's feature set, for the warning that says none matched.
fn declared_sets() -> String {
crate::variants::VARIANTS
.iter()
.filter(|c| c.package.is_some())
.map(|c| format!("{}={{{}}}", c.name, comma(&c.normalised_features())))
.collect::<Vec<_>>()
.join(" ")
}
/// The settings a variant resolves to, without a build script — so a caller can see
/// what a receipt WOULD say.
#[must_use]
pub fn settings_for(variant: &Variant) -> BTreeMap<String, String> {
variant.resolved_settings()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::atom::Column;
use crate::variants::{by_build_point, by_name, normalise_feature};
fn set(names: &[&str]) -> BTreeSet<String> {
names.iter().map(|n| normalise_feature(n)).collect()
}
/// **The cargo variants have pairwise distinct feature sets**, which is what
/// makes the resolved environment able to name one.
///
/// The count was 5 until 2026-08-27 and is 6 with `gunnar-server-thin` — the
/// build point `--opt wireguard=off` arrives at. The literal is raised only
/// AFTER the distinctness assertion below was seen green on the new row: a
/// bumped denominator that nothing re-checked is how a guard stops guarding.
///
/// Watched failing: two variants built here with the same set collide, and the
/// same `HashSet`-of-sets count is what detects it. Without the red half a
/// counting test over a table that happens to be distinct today cannot tell a
/// working rule from `true`.
#[test]
fn the_cargo_variants_have_pairwise_distinct_build_points() {
let cargo_variants: Vec<&Variant> = crate::variants::VARIANTS
.iter()
.filter(|c| c.package.is_some())
.collect();
assert_eq!(
cargo_variants.len(),
15,
"the fifteen cargo variants are the subject (GUN-PRODUCTS, 2026-10-30: the three \
products gunnar, gunnar-ui and gunnar-cli, each its crate's defaults named out \
loud; the six appliance rows; the front's gunnar-cli-musl and its metered twin; \
the four gunnar-front rows. Retired that day: the client-only gunnar, the old \
gunnar-cli row, gunnar-musl, gunnar-solo, gunnar-service, gunnar-open and \
gunnar-open-client)"
);
// **The build point is (features, target), not the features alone.**
// Raised 2026-09-19: `gunnar-cli` and `gunnar-cli-musl` are one feature
// set and two artifacts at two paths, and the front needs the second.
// Keyed on the feature set alone this assertion refused to let it be
// declared at all, which is why that binary was being built from a
// cargo line in another repository instead.
let points: BTreeSet<(BTreeSet<String>, Option<&str>)> = cargo_variants
.iter()
.map(|c| (c.normalised_features(), c.target))
.collect();
assert_eq!(
points.len(),
cargo_variants.len(),
"two variants resolve to the same build point, so a build script cannot tell \
them apart and the receipt would name whichever came first: {:?}",
cargo_variants
.iter()
.map(|c| (c.name, c.normalised_features(), c.target))
.collect::<Vec<_>>()
);
// Red side: the same count over a table that DOES collide must drop.
let colliding: BTreeSet<(BTreeSet<String>, Option<&str>)> = [
(set(&["server"]), None),
(set(&["server"]), None),
(set(&["client"]), None),
]
.into_iter()
.collect();
assert_eq!(
colliding.len(),
2,
"the de-duplication does not de-duplicate, so the assertion above cannot fail"
);
}
/// Each cargo variant's own declared set resolves back to it.
#[test]
fn every_cargo_variant_is_found_by_the_features_it_declares() {
for c in crate::variants::VARIANTS
.iter()
.filter(|c| c.package.is_some() && c.built_here())
{
let found = crate::variants::by_build_point(&c.normalised_features(), c.target);
assert_eq!(
found.map(|f| f.name),
Some(c.name),
"{} is not found by its own build point",
c.name
);
}
}
/// The two feature sets the design cares about most, spelled the way cargo
/// spells them, and the one that must NOT resolve.
#[test]
fn the_resolved_features_name_the_variant_and_refuse_when_they_do_not() {
assert_eq!(
by_resolved_features(&set(&[
"server",
"engine-znippy",
"mimalloc",
"clock-fetch",
"resolver-probe",
"fleet-pull",
"mirror-http",
"sso-egress",
"dhcp-client",
"wg-client",
"replication",
"grow-runtime",
"live-app"
]))
.map(|c| c.name),
Some("gunnar-server"),
);
// `["server", "engine-znippy"]` was the appliance until 2026-08-11 and is
// now NOTHING, by exactly the argument the paragraph below makes about
// the engine. `mimalloc` is not a tuning knob: a gunnar built without it
// serves clones at 2.2x the CPU (`gunnar-server/src/lib.rs` has the
// grid), so a receipt calling that artifact the appliance would vouch
// for a build point nobody agreed to ship.
assert_eq!(
by_resolved_features(&set(&["server", "engine-znippy"])),
None,
"an allocator-less appliance build still attributes to the server variant, so the \
slow-heap artifact would ship with a receipt saying it is the appliance"
);
// `["server"]` alone stopped being the appliance on 2026-08-10, when the
// engine became a feature of this package. It must now resolve to
// NOTHING rather than to the server variant: a build that forgot to name an
// engine is a server with no storage, and a receipt calling it
// `gunnar-server` would vouch for an artifact nobody can serve from.
assert_eq!(
by_resolved_features(&set(&["server"])),
None,
"an engine-less `--features server` still attributes to the appliance variant, so \
the one build the collapse exists to abolish would ship with a receipt saying \
it is the appliance"
);
// The product rows attribute by their crates' defaults, named out loud.
assert_eq!(
by_resolved_features(&set(&["engine-znippy", "http", "replication"])).map(|c| c.name),
Some("gunnar"),
);
// `gunnar-cli`'s set is also `gunnar-cli-musl`'s: only the target settles it.
assert_eq!(by_resolved_features(&set(&["client", "control"])), None);
assert_eq!(
by_build_point(&set(&["client", "control"]), None).map(|c| c.name),
Some("gunnar-cli"),
);
// The retired shapes (`gunnar-service`, `gunnar-solo`) are no row any more.
assert_eq!(by_resolved_features(&set(&["server", "systemd"])), None);
assert_eq!(
by_resolved_features(&set(&["server", "client", "control"])),
None
);
// `--all-features`: a superset, and not a variant.
assert_eq!(
by_resolved_features(&set(&["server", "client", "control", "systemd", "http"])),
None,
"a superset resolved to a variant, so `--all-features` would write a receipt \
naming a build point that was not built"
);
// A subset is not a variant either.
assert_eq!(by_resolved_features(&set(&["control"])), None);
// And the empty set is not `gunnar-image`. This one HAS been red: the
// container and ISO variants declare no features, so before the candidate
// set was narrowed to the cargo variants, `--no-default-features` alone
// resolved to `gunnar-image` — a variant that compiles nothing.
assert_eq!(
by_resolved_features(&BTreeSet::new()),
None,
"the empty feature set resolved to a variant that runs no cargo"
);
}
/// Measured on all three refusals of [`attribute`], each watched red for
/// its own reason and naming its own subject.
#[test]
fn attribution_refuses_a_set_a_target_and_a_profile_the_variant_denies() {
let musl = "x86_64-unknown-linux-musl";
// The appliance's resolved set. `["server"]` alone until 2026-08-10 — the
// engine is a feature of this package now and the variant names it, so an
// engine-less `--features server` is no longer this variant and no longer
// anything (see `the_resolved_features_name_the_variant_and_refuse_when_they_do_not`).
let appliance = || {
set(&[
"server",
"engine-znippy",
"mimalloc",
"clock-fetch",
"resolver-probe",
"fleet-pull",
"mirror-http",
"sso-egress",
"dhcp-client",
"wg-client",
"replication",
"grow-runtime",
"live-app",
])
};
// Green: the server variant, built for the target and profile it declares.
let a = attribute(&appliance(), musl, "release");
assert_eq!(a.variant().map(|c| c.name), Some("gunnar-server"));
assert_eq!(a.note(), None, "a clean attribution warned about something");
// Red 1: a feature set nobody declared.
let a = attribute(
&set(&[
"server",
"engine-znippy",
"mimalloc",
"clock-fetch",
"resolver-probe",
"fleet-pull",
"mirror-http",
"sso-egress",
"dhcp-client",
"wg-client",
"http",
]),
musl,
"release",
);
assert!(matches!(a, Attribution::NoVariant { .. }), "{a:?}");
let note = a
.note()
.expect("a refusal with no message cannot be acted on");
assert!(
note.contains("HTTP"),
"the warning does not say what it saw: {note}"
);
assert!(
note.contains(
"gunnar-server={CLOCK_FETCH, DHCP_CLIENT, ENGINE_ZNIPPY, FLEET_PULL, GROW_RUNTIME, LIVE_APP, MIMALLOC, MIRROR_HTTP, REPLICATION, RESOLVER_PROBE, SERVER, SSO_EGRESS, WG_CLIENT}"
),
"the warning does not say what it expected: {note}"
);
// Red 2: the right features, the wrong target. This is the one that would
// otherwise write `target = musl` into a glibc binary.
let a = attribute(&appliance(), "x86_64-unknown-linux-gnu", "release");
assert!(
matches!(a, Attribution::Mismatch { axis: "target", .. }),
"{a:?}"
);
let note = a.note().expect("a refusal with no message");
assert!(note.contains("gunnar-server"), "{note}");
assert!(note.contains("musl"), "{note}");
assert!(note.contains("gnu"), "{note}");
assert_eq!(
a.variant(),
None,
"a mismatched target still produced a variant"
);
// Red 3: the right features and target, a debug build of a variant that
// declares `release`. `cargo build` without `--release` is exactly this.
let a = attribute(&appliance(), musl, "debug");
assert!(
matches!(
a,
Attribution::Mismatch {
axis: "profile",
..
}
),
"{a:?}"
);
let note = a.note().expect("a refusal with no message");
assert!(note.contains("release"), "{note}");
assert!(note.contains("debug"), "{note}");
// And the "not measured" hole: no `PROFILE` at all must not be compared
// against, because an empty string equals nothing and would refuse every
// build. The target check still applies.
assert_eq!(
attribute(&appliance(), musl, "").variant().map(|c| c.name),
Some("gunnar-server"),
"an unset PROFILE was treated as a profile"
);
}
/// **Which refusals are loud.** A `cargo:warning` on every `cargo build` in
/// the repository is a warning nobody reads.
///
/// Watched red in both directions: the silent case must stay silent, and the
/// loud ones must stay loud — a `should_warn` that answered `false` for
/// everything would pass the first half and switch the guard off.
#[test]
fn only_the_ordinary_developer_build_refuses_quietly() {
let musl = "x86_64-unknown-linux-musl";
// Silent: `cargo build`. MEASURED — a default build resolves DEFAULT on
// top of everything, and every variant is `--no-default-features`, so no
// ordinary developer command is ever a variant.
let dev = attribute(
&set(&["server", "client", "control", "default"]),
musl,
"debug",
);
assert!(matches!(dev, Attribution::NoVariant { .. }), "{dev:?}");
assert!(!dev.should_warn("debug"), "every debug build would warn");
assert!(
dev.note().is_some(),
"the reason is still recorded — the generated file carries it, only the \
cargo:warning is suppressed"
);
// Same set, same silence, for the other refusal shape.
assert!(!attribute(&set(&["server", "engine-znippy"]), musl, "debug").should_warn("debug"));
// Loud: a RELEASE build that is not a variant is somebody producing
// something and getting it wrong.
assert!(attribute(&set(&["server", "http"]), musl, "release").should_warn("release"));
// And this one is now loud for a NEW reason worth spelling out: a release
// build of `--features server` with no engine named is not the appliance
// any more, it is a server with no storage. It resolves to no variant at all.
assert!(attribute(&set(&["server"]), musl, "release").should_warn("release"));
assert!(attribute(
&set(&[
"server",
"engine-znippy",
"mimalloc",
"clock-fetch",
"resolver-probe",
"fleet-pull",
"mirror-http",
"sso-egress",
"dhcp-client",
"wg-client"
]),
"x86_64-unknown-linux-gnu",
"release"
)
.should_warn("release"));
// And a clean attribution says nothing, in any profile.
let good = attribute(
&set(&[
"server",
"engine-znippy",
"mimalloc",
"clock-fetch",
"resolver-probe",
"fleet-pull",
"mirror-http",
"sso-egress",
"dhcp-client",
"wg-client",
"replication",
"grow-runtime",
"live-app",
]),
musl,
"release",
);
assert!(!good.should_warn("release"));
assert!(!good.should_warn("debug"));
}
/// The declared-profile mapping, both ways.
#[test]
fn a_declared_profile_maps_to_the_word_cargo_puts_in_the_environment() {
assert_eq!(cargo_profile_word("release"), "release");
assert_eq!(cargo_profile_word("dev"), "debug");
assert_eq!(cargo_profile_word("debug"), "debug");
// A custom optimised profile. Guessed, and the guess is stated where it
// is made; a wrong guess refuses a receipt rather than writing one.
assert_eq!(cargo_profile_word("appliance"), "release");
// The variants' own declarations must map to something cargo can produce.
for c in crate::variants::VARIANTS
.iter()
.filter(|c| c.package.is_some())
{
let word = cargo_profile_word(c.profile_resolved().label());
assert!(
word == "debug" || word == "release",
"{} maps to {word:?}, which cargo never sets",
c.name
);
}
}
/// The feature universe keeps the features and drops the optional
/// dependencies — held against the upstream directly since the 0.1.7 pin
/// bump. This property used to be gunnar's own `features_table` slice, with
/// a red-side canary asserting the upstream STILL over-reported; 0.1.7
/// shipped the real fix (`declared_features` skips a dependency some
/// feature names as `dep:<name>`), the canary tripped, and the slice came
/// out exactly as `receipt_for`'s comment promised. What stays is the
/// property the slice existed for, against the same manifest shape that
/// measured the defect: `feature.gunnar-core = "false"` in a binary that
/// links gunnar-core is a receipt that lies.
#[test]
fn the_feature_universe_keeps_features_and_drops_optional_dependencies() {
let manifest = r#"
[package]
name = "gunnar-cli"
[features]
default = ["server"]
server = ["dep:gunnar-core"]
client = []
[dependencies]
gunnar-core = { version = "1", optional = true }
"#;
let universe =
receipt::resolved_feature_settings(manifest, |v| v == "CARGO_FEATURE_SERVER")
.expect("the manifest parses");
let keys: Vec<&str> = universe.keys().map(String::as_str).collect();
assert_eq!(
keys,
vec!["feature.client", "feature.default", "feature.server"],
"the upstream universe over-reports again — an optional dependency \
reachable only as `dep:<name>` is not a feature"
);
assert_eq!(
universe.get("feature.server").map(String::as_str),
Some("true")
);
assert_eq!(
universe.get("feature.client").map(String::as_str),
Some("false")
);
// A manifest with no [features] at all is an empty universe, not an error.
assert!(
receipt::resolved_feature_settings("[package]\nname = \"x\"\n", |_| false)
.expect("an empty features table parses")
.is_empty()
);
}
/// The receipt a variant would produce passes the audit that guards the writer —
/// no absolute paths, no timestamps, a populated recipe hash.
///
/// This is the check that would have caught `coordinate()` returning `None`
/// for `hash`, which `receipt::audit_receipt` refuses outright.
#[test]
fn every_cargo_variant_produces_a_receipt_its_own_audit_accepts() {
for variant in crate::variants::VARIANTS
.iter()
.filter(|c| c.package.is_some())
{
let mut b = ReceiptBuilder::new(
GunnarBuildThing::coordinate(),
variant.atom.as_str(),
variant.column.as_str(),
);
for (k, v) in settings_for(variant) {
b = b.setting(k, v);
}
let r = b.build().unwrap_or_else(|e| {
panic!(
"{} produces a receipt its audit refuses: {e:#}",
variant.name
)
});
assert!(
receipt::audit_receipt(&r).is_empty(),
"{} has flaws: {:?}",
variant.name,
receipt::audit_receipt(&r)
);
assert_eq!(r.build_thing.krate, crate::recipe::KRATE);
assert_eq!(
r.build_thing.hash.as_deref(),
Some(crate::recipe::CONTENT_HASH),
"{} does not pin the recipe that built it",
variant.name
);
assert!(
receipt::encode(&r).is_ok(),
"{} does not encode",
variant.name
);
}
// Red side: strip the hash and the same audit refuses, by name.
let mut r = ReceiptBuilder::new(
GunnarBuildThing::coordinate(),
"gunnar-server",
Column::Exe.as_str(),
)
.build()
.expect("a bare receipt is clean");
r.build_thing.hash = None;
let flaws = receipt::audit_receipt(&r);
assert!(
flaws
.iter()
.any(|f| f.to_string().contains("build_thing.hash")),
"the audit accepts a receipt with no recipe hash, so the passes above are \
not evidence: {flaws:?}"
);
}
/// The recipe's content hash is a real sha256 over real bytes.
///
/// It is `env!`-injected by `build.rs`, so the failure worth guarding is the
/// build script silently producing nothing — an empty or placeholder value
/// that every version of the recipe would share.
#[test]
fn the_content_hash_is_a_full_sha256_and_not_a_placeholder() {
let h = crate::recipe::CONTENT_HASH;
assert_eq!(h.len(), 64, "not a sha256: {h:?}");
assert!(h.chars().all(|c| c.is_ascii_hexdigit()), "not hex: {h:?}");
assert_ne!(
h, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"the recipe hashed the EMPTY input — build.rs found no sources"
);
assert_eq!(
GunnarBuildThing::coordinate().hash.as_deref(),
Some(h),
"the coordinate does not carry the hash the crate computed"
);
}
/// The generated file's name is the writer's, not a second spelling of it.
#[test]
fn the_absent_file_uses_the_writers_own_file_name() {
let dir = std::env::temp_dir().join(format!("gbt-absent-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("temp dir");
let why = Attribution::NoVariant {
resolved: set(&["server", "http"]),
};
let p = write_absent(&dir, &why).expect("write");
assert_eq!(p.file_name().unwrap(), receipt::GENERATED_FILE);
let body = std::fs::read_to_string(&p).expect("read back");
assert!(
!body.contains("link_section"),
"the absent file declares a section: {body}"
);
assert!(
body.contains("HTTP"),
"the absent file does not say why: {body}"
);
std::fs::remove_dir_all(&dir).ok();
}
/// `by_name` and the emitter agree about which variant is `gunnar-server`, and
/// the receipt's `target` field is the COLUMN, not the triple.
#[test]
fn the_receipt_target_field_is_the_delivery_column() {
let variant = by_name("gunnar-server").expect("the server variant");
assert_eq!(variant.column.as_str(), "exe");
let s = settings_for(variant);
assert_eq!(s.get("column").map(String::as_str), Some("exe"));
assert_eq!(
s.get("target").map(String::as_str),
Some("x86_64-unknown-linux-musl"),
"the triple belongs in settings.target; the receipt's own `target` field is \
the delivery column"
);
}
}