crates/gunnar-server/build.rs
//! Writes the build-thing RECEIPT for the appliance binary.
//!
//! The receipt says which **recipe** built this artifact — `gunnar-build-thing`,
//! its version, its content hash, and every resolved setting of the variant this
//! compilation is. It goes into the binary's `.nordisk.buildthing` section via
//! the `include!` in `src/main.rs`, and `dwarves` reads it back out with an
//! object parser without ever running the artifact.
//!
//! Until 2026-08-10 this crate had no `build.rs` and the `gunnar-server` variant
//! still named package `gunnar-cli`, where the bin lived before the extraction
//! The variant follows the code now, and so does the writer.
//!
//! This is the RECEIPT half only. The `GUNNAR_BUILD_REF`/`GUNNAR_BUILD_DATE_PART`
//! version stamps come from `gunnar-common`'s own `build.rs` — two copies of
//! the receipt static in one artifact would concatenate and be reported as
//! "more than one receipt", which is why each binary crate writes exactly its
//! own. The long story of what a receipt is and why a refused one PANICS lives
//! in `crates/gunnar-cli/build.rs`.
use std::process::Command;
fn main() {
// ONE `git describe`, as the receipt's `source-commit` input. NOT the build
// date: two identical builds would differ by it and by nothing else, and
// `nornir_build_thing::receipt::audit_receipt` refuses a timestamp outright.
// The four `-c` overrides are the SAME ones `gunnar-cli/build.rs` carries and
// for the same measured reason: a git configured for git-lfs
// (`filter.lfs.required = true`, as this box's /etc/gitconfig is) runs the
// lfs filter for `--dirty`'s status walk, and inside the pinned cargo
// toolchain image — where `git-lfs` is not on PATH — `describe` then dies
// with `git-lfs: not found`. `gunnar-cli` and `gunnar-common` were fixed on
// 2026-09-05; THIS script was left behind, and it runs the same `--dirty`
// walk. The cost here is quieter than a bare SSH banner and no less real:
// `source-commit` simply drops out of the build receipt, so a
// container-built gunnar-server ships a receipt that does not name the
// source it was built from. Describing a commit needs no filter.
// MEASURED 2026-09-15 and recorded in `gunnar-common/build.rs`, which carries
// both of these and which this script did not: inside the pinned toolchain
// container the checkout is bind-mounted and the building uid does not own
// it, so git answers `fatal: detected dubious ownership` and the stamp falls
// to nothing — there, `source-commit` simply leaves the receipt.
//
// `core.checkStat=minimal` is the one that bit THIS script, MEASURED
// 2026-09-16 on the medium composed from a clean clone at b39da6c: the
// binary carried BOTH `v1.4.0-279-gb39da6c` (the banner, from
// gunnar-common, which has this override) and
// `v1.4.0-279-gb39da6c-modified` (this receipt, which did not). The tree was
// clean on both sides of the mount. Rootless podman remaps uid/gid and git's
// default stat check compares uid, gid, inode and ctime against the index,
// so every tracked file reads as changed — and `--dirty=-modified`, which
// only this script passes, turns that into a receipt claiming a modified
// source that never was. `minimal` compares size and mtime, which a bind
// mount preserves.
//
// Reading a version is not a trust decision: nothing here runs the
// repository's hooks or config.
let describe = Command::new("git")
.args([
"-c",
"safe.directory=*",
"-c",
"core.checkStat=minimal",
"-c",
"filter.lfs.required=false",
"-c",
"filter.lfs.process=",
"-c",
"filter.lfs.clean=",
"-c",
"filter.lfs.smudge=",
"describe",
"--tags",
"--always",
"--dirty=-modified",
"--abbrev=7",
])
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_owned())
.filter(|s| !s.is_empty());
let mut inputs: Vec<(&str, String)> = Vec::new();
if let Some(d) = &describe {
inputs.push(("source-commit", d.clone()));
}
match gunnar_recipe::emit::write_receipt(&inputs) {
Ok(emitted) => {
if let Some(variant) = emitted.attribution.variant() {
println!("cargo:rustc-env=GUNNAR_BUILD_VARIANT={}", variant.name);
}
}
Err(e) => panic!(
"the build-thing receipt was refused: {e:#}\n\
This is a defect in gunnar-build-thing's declaration, not in this build. \
A binary that ships with a receipt its own audit would reject is worse than \
one with none, so the build stops here."
),
}
// Re-run when HEAD moves, so the receipt's source-commit cannot go stale in
// an incremental build. `--git-path` rather than `../../.git/…` because in a
// git WORKTREE `.git` is a file and naming paths under it triggers cargo's
// always-dirty behaviour — see gunnar-cli/build.rs for the full account.
for what in ["HEAD", "refs"] {
let path = Command::new("git")
.args(["rev-parse", "--git-path", what])
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_owned())
.filter(|s| !s.is_empty());
if let Some(path) = path {
if std::path::Path::new(&path).exists() {
println!("cargo:rerun-if-changed={path}");
}
}
}
}