Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

crates/gunnar-cli/build.rs

//! Stamps the running build's identity into the binary.
//!
//! `--version` printed `gunnar 0.1.0` and nothing else. That is the crate
//! version, which moves when someone edits a manifest, and it says nothing
//! about *which build* is running. It cost real time: on 2026-08-04/05 several
//! benchmark runs had to identify the binary they were measuring by file size
//! and mtime, and one of them silently measured a ten-hour-old build that
//! predated the change it was benchmarking. A server that cannot name its own
//! commit cannot be held to a measurement.
//!
//! So `--version` now carries the git description and the build date beside the
//! crate version. Everything is optional by construction: a build from a
//! tarball with no `.git` still compiles and prints what it can, because the
//! alternative — a build script that fails without git — would make the crate
//! unbuildable for exactly the consumers who need it most.
//!
//! # And it writes the build-thing RECEIPT
//!
//! Added 2026-08-06. `--version` says which commit; the receipt says which
//! **recipe** — `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, and `dwarves` reads it back out with an object
//! parser without ever running the artifact.
//!
//! The two stamps are not the same thing and neither replaces the other:
//!
//! ```text
//! GUNNAR_BUILD_REF   which SOURCE this is        git describe
//! the receipt        which RECIPE built it, and  gunnar-build-thing
//!                    with which settings
//! ```
//!
//! Unlike the version stamp, the receipt REFUSES rather than degrades: a
//! compilation whose feature set is not a declared variant's gets no receipt and a
//! `cargo:warning` saying so. A receipt naming a build point that was not built
//! answers the rebuild question wrongly, and nothing downstream could find out.

use std::process::Command;

// The one stamping rule, shared with gunnar-common's `build.rs` so the banner
// and the receipt name the same commit. `build_ref.rs` here is a SYMLINK to
// gunnar-common's file (one implementation): `cargo package` copies its bytes into
// the published crate, which a `#[path = "../gunnar-common/…"]` could not reach.
#[path = "build_ref.rs"]
mod build_ref;

fn main() {
    // A published package (`cargo install gunnar-cli`): the commit it was made from,
    // from `.cargo_vcs_info.json` (`build_ref::packaged_ref`, lane GUN-VERSION-STAMP).
    let packaged = std::env::var_os("CARGO_MANIFEST_DIR")
        .and_then(|d| build_ref::packaged_ref(std::path::Path::new(&d)));

    // `git describe` against the tags: `v1.0`, `v1.0-14-g433a659`, or the bare
    // short sha when no tag is reachable. `--dirty` marks a build that does not
    // correspond to any commit, which is the state most worth knowing about.
    // The four `-c` overrides: 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 where `git-lfs` is not on PATH — inside the pinned cargo
    // toolchain image, MEASURED 2026-09-05 — `describe` dies with `git-lfs: not
    // found` and every container-built payload shipped a banner naming no source
    // (`SSH-2.0-gunnar_1.3.0`, refused by the installtest robot). Describing a
    // commit needs no filter; these switch it off for this one call.
    let describe = Command::new("git")
        .args([
            "-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());
    // A builder's passed ref wins over the checkout's own answer — the same
    // rule gunnar-common stamps `BUILD_REF` by.
    let describe = build_ref::choose(
        &build_ref::env_refs(),
        packaged.as_deref().or(describe.as_deref()),
    );

    // ONE `git describe`, feeding both stamps. Two calls could disagree on a
    // repository that moved between them, and a binary whose `--version` and
    // whose receipt name different commits is worse than one that names neither.
    write_build_thing_receipt(describe.as_deref());

    match &describe {
        Some(d) => println!("cargo:rustc-env=GUNNAR_BUILD_REF={d}"),
        // Not an error. A published crate has no `.git`, and refusing to build
        // there would be a worse failure than an unstamped binary.
        None => println!("cargo:rustc-env=GUNNAR_BUILD_REF=unknown"),
    }

    // NO `GUNNAR_BUILD_DATE_PART` HERE. This script used to spawn
    // `date -u +%Y-%m-%dT%H:%M:%SZ` and export it — a subprocess on every build
    // of this crate, for a value nothing in this crate ever read. `env!(
    // "GUNNAR_BUILD_DATE_PART")` has exactly ONE reader in the repository,
    // `gunnar-common/src/cli.rs`, and that expansion happens while
    // `gunnar-common` is being compiled, so it takes the value
    // `gunnar-common/build.rs` exported and never this one. A `rustc-env` nobody
    // expands changes no emitted code; it only reran the compile.
    //
    // The date it exported was the wall clock, which is what made gunnar
    // unreproducible (the measurement, and the source-fact stamp that replaced
    // it, are in `gunnar-common/build.rs`'s header). Re-spelling that logic here
    // would have duplicated thirty lines to feed a dead end, so the dead end is
    // gone instead. Anything in THIS crate that ever wants the stamp should read
    // `gunnar_common::cli`'s, which is the one that reaches `--version`.

    // Re-run when HEAD moves, so the stamp cannot go stale in an incremental
    // build — the failure this whole file exists to prevent.
    //
    // The paths come from `git rev-parse --git-path` rather than from
    // `../../.git/…`, because in a **git worktree** `../../.git` is a FILE
    // holding `gitdir: …`, so `../../.git/HEAD` and `../../.git/refs` name
    // nothing. Cargo treats a `rerun-if-changed` path that does not exist as
    // always-dirty, so the trigger this file exists for did not fire on the
    // change it watches — it fired on every build instead. gunnar is developed
    // almost entirely out of worktrees, so that was the normal case, not the
    // edge one. `--git-path` resolves correctly in a worktree, in a plain
    // checkout, and under `GIT_DIR`.
    //
    // If git cannot answer — the no-`.git` tarball case this file already
    // handles above — no trigger is emitted at all. That is the honest state:
    // there is no ref to watch, and naming one that does not exist is what
    // caused the bug.
    // Not in a package: its commit is its `.cargo_vcs_info.json` (`packaged_ref` watches it).
    let watched: &[&str] = if packaged.is_some() {
        &[]
    } else {
        &["HEAD", "refs"]
    };
    for what in watched {
        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}");
            }
        }
    }
}

/// Write the build-thing receipt into `OUT_DIR` and publish its path.
///
/// The recipe crate owns every decision here — which variant this is, what the
/// settings resolve to, whether the combination is one gunnar declares. This
/// function's whole job is to hand it the one input only the calling crate has,
/// the source description, and to fail loudly if the receipt is refused.
///
/// # Why a refused receipt PANICS and a missing variant does not
///
/// They are different failures. "This feature set is not a declared variant" is a
/// legitimate build — `cargo test --all-features` is one — and it gets a warning
/// and no receipt. "This IS a declared variant and its receipt would be a lie" is a
/// defect in the declaration, and shipping a binary from it is exactly what the
/// audit exists to prevent.
fn write_build_thing_receipt(describe: Option<&str>) {
    // The source, as the receipt's own 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.
    let mut inputs: Vec<(&str, String)> = Vec::new();
    if let Some(d) = describe {
        inputs.push(("source-commit", d.to_string()));
    }

    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."
        ),
    }
}