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

nordisk/gunnar

crates/gunnar-common/build.rs

//! Stamps the running build's identity into the binaries that read it.
//!
//! `--version` printed the crate version and nothing else, which moves when
//! someone edits a manifest and says nothing about *which build* is running. So
//! `--version` (rendered in `cli.rs`) now carries the git description and the
//! build date beside it. Everything is optional by construction: a build from a
//! tarball with no `.git` still compiles and prints what it can.
//!
//! This is the REF/DATE half only. The build-thing **receipt** — which recipe
//! built the binary — is written by each binary crate's own `build.rs`, next to
//! the `include!` that reads it back, because two copies of that static in one
//! artifact concatenate and get reported as "more than one receipt".
//!
//! ## The date is a fact about the SOURCE, never about the moment
//!
//! Until 2026-08-26 line 43 of this file ran `date -u +%Y-%m-%dT%H:%M:%SZ` and
//! baked the answer. Two things were wrong with that and the second is the one
//! that mattered:
//!
//! 1. it shelled out for a value with no need of a subprocess, against the
//!    constellation's pure-Rust law; and
//! 2. **it made gunnar the one repo in the constellation whose builds are not
//!    auditable.** MEASURED — the same variant built twice inside the SAME
//!    digest-pinned container came out `0fb79f0d…` vs `2b03bb75…`: identical
//!    size, 672 277 bytes differing, and the difference chased down to exactly
//!    16 symbols of 24 759, all in `gunnar_common`, all differing only in LLVM's
//!    disambiguator suffix (`.llvm.4530103879743261262` vs
//!    `.llvm.4929951002046257557`). A one-second stamp made 2.9 % of the binary
//!    volatile. Re-measured here at crate level on 2026-08-26: two
//!    `cargo build -p gunnar-common --release` two seconds apart gave
//!    `51fa847d6ca68428…` and `1f7755a02ae56944…`.
//!
//! So the wall clock is gone, and NOT replaced by a Rust wall clock —
//! `SystemTime::now()` would have fixed (1) and left (2) exactly as it was. The
//! stamp now comes from a fact about the source being compiled, in this order:
//!
//! ```text
//! SOURCE_DATE_EPOCH   the reproducible-builds standard knob, when a harness sets it
//! HEAD committer date `git log -1 --format=%ct` — the same commit `git describe` names
//! no date at all      no epoch, no `.git` (the published-package case)
//! ```
//!
//! Every one of those is identical for every build of the same source, which is
//! the whole property. It is also strictly MORE informative than the clock was:
//! "when did someone press build" answers nothing a reader needs, while "which
//! commit is this, and when was it written" answers the question `--version`
//! exists for — and the ref beside it was already stable (measured identical on
//! both sides of the container comparison).
//!
//! The fallback is NO date and deliberately not a clock: degrading to the
//! unreproducible value in the one case nobody watches is how this defect would
//! come back.
//!
//! ## A published package names the commit it was made from
//!
//! `cargo install gunnar` 1.6.0 printed `gunnar 1.6.0 (unknown, built …)`: crates.io's
//! copy has no `.git`. But `cargo package` writes `.cargo_vcs_info.json` into every
//! package it makes from a checkout, with the commit's full `git.sha1`, so the ref
//! comes from there (`build_ref::packaged_ref`, lane GUN-VERSION-STAMP 2026-10-31):
//! `gunnar 1.6.0 (ad3106b)`. The package knows no commit DATE, so without
//! `SOURCE_DATE_EPOCH` the `, built …` part is left out rather than made up
//! (`GUNNAR_BUILD_DATE_PART` is empty). The package wins over `git describe`, whose
//! answer inside a package that sits in some other checkout would be that checkout's.

use std::process::Command;

mod build_ref;

fn main() {
    // A published package (`cargo install`): the commit it was made from.
    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.
    //
    // `core.checkStat=minimal`, MEASURED 2026-09-12 on laptop: composing the
    // installer in the pinned image over a rootless-podman bind mount, the same
    // `describe` on a CLEAN tree answered `v1.4.0-78-gfe96b6f-modified` inside
    // the container and `v1.4.0-78-gfe96b6f` on the host. Rootless podman remaps
    // uid/gid, and git's default stat check compares uid, gid, inode and ctime
    // with the index, so every tracked file reads as changed until the index is
    // refreshed — which a build must not do to the checkout it was handed. Two
    // ISOs shipped banners claiming a modified source that was not. `minimal`
    // compares size and mtime, which a bind mount preserves, and the same call
    // then agrees with the host: measured clean on both sides.
    let describe = Command::new("git")
        .args([
            // MEASURED 2026-09-15: the appliance is built inside the pinned
            // toolchain container with the checkout bind-mounted, and a uid that
            // does not own the mount gets `fatal: detected dubious ownership` —
            // the stamp fell to `unknown`, the banner lost its build reference,
            // and GATE-IDENTITY refused two installed appliances. `safe.directory`
            // is honoured from the command scope. Reading a version is not a
            // trust decision: nothing here runs the repository's hooks or config.
            "-c",
            "safe.directory=*",
            "-c",
            "filter.lfs.required=false",
            "-c",
            "filter.lfs.process=",
            "-c",
            "filter.lfs.clean=",
            "-c",
            "filter.lfs.smudge=",
            "-c",
            "core.checkStat=minimal",
            "describe",
            "--tags",
            "--always",
            "--dirty=-modified",
            "--abbrev=7",
        ])
        .output();
    // Why git could not answer, kept so an unstamped build SAYS why. MEASURED
    // 2026-09-15: three appliances installed with a bare banner while this
    // script silently wrote `unknown`, and GATE-IDENTITY refused each one only
    // after a 20-minute compose and a full re-image.
    let describe_failure = match &describe {
        Ok(o) if o.status.success() => None,
        Ok(o) => Some(format!(
            "git describe exited {}: {}",
            o.status,
            String::from_utf8_lossy(&o.stderr)
                .lines()
                .next()
                .unwrap_or("")
                .trim()
        )),
        Err(e) => Some(format!("git could not be run: {e}")),
    };
    let describe = describe
        .ok()
        .filter(|o| o.status.success())
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_owned())
        .filter(|s| !s.is_empty());

    // A builder that knows the version but cannot hand this script a `.git`
    // (a copied context, a container without git) passes it in, and the passed
    // value WINS over the checkout's own answer (`build_ref.rs` says why).
    let from_env = build_ref::env_refs();

    match build_ref::choose(&from_env, packaged.as_deref().or(describe.as_deref())) {
        Some(r) => println!("cargo:rustc-env=GUNNAR_BUILD_REF={r}"),
        // Not an error. A published crate has no `.git`, and refusing to build
        // there would be a worse failure than an unstamped binary — but it is
        // said out loud, with git's own reason.
        None => {
            println!("cargo:rustc-env=GUNNAR_BUILD_REF=unknown");
            println!(
                "cargo:warning=GUNNAR_BUILD_REF=unknown — the banner will carry no build reference and GATE-IDENTITY will refuse the install ({})",
                describe_failure.as_deref().unwrap_or("git describe printed nothing")
            );
        }
    }

    // UTC, second resolution — and a fact about the SOURCE, not about the moment.
    // See the header for the measurement that retired the wall clock.
    println!("cargo:rerun-if-env-changed=SOURCE_DATE_EPOCH");
    // `, built <date>`, or nothing when the source names no date (a package with no epoch).
    println!(
        "cargo:rustc-env=GUNNAR_BUILD_DATE_PART={}",
        source_date(packaged.is_some())
            .map(|d| format!(", built {d}"))
            .unwrap_or_default()
    );

    // Re-run when HEAD moves, so the stamp cannot go stale in an incremental
    // build. The paths come from `git rev-parse --git-path` rather than from
    // `../../.git/…`, because in a git worktree `../../.git` is a FILE and those
    // paths name nothing; `--git-path` resolves correctly in a worktree, a plain
    // checkout, and under `GIT_DIR`. If git cannot answer — the no-`.git`
    // tarball case handled above — no trigger is emitted at all; nor in a package, whose
    // commit is its `.cargo_vcs_info.json` (`packaged_ref` watches that file).
    let watched: &[&str] = if packaged.is_some() {
        &[]
    } else {
        &["HEAD", "refs"]
    };
    for what in watched {
        let path = Command::new("git")
            .args(["-c", "safe.directory=*", "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}");
            }
        }
    }
}

/// **The build stamp, from the source rather than from the clock.**
///
/// `SOURCE_DATE_EPOCH` first, because that is the knob a reproducible-build
/// harness sets and honouring it is the whole contract; then HEAD's COMMITTER
/// date, which is a property of the commit `git describe` already names beside
/// this; then `None`: no date is named. A published package (`packaged`) has no
/// `.git` of its own, and a `git log` there would read whatever checkout the
/// package happens to sit in, so it is not asked.
///
/// A dirty tree is not special-cased here on purpose: `GUNNAR_BUILD_REF` carries
/// `-modified` for exactly that, so the date says which commit the tree is based
/// on and the ref says the tree is not that commit. Inventing a second signal
/// would give two answers to one question.
fn source_date(packaged: bool) -> Option<String> {
    if let Ok(raw) = std::env::var("SOURCE_DATE_EPOCH") {
        if let Ok(secs) = raw.trim().parse::<i64>() {
            return Some(iso8601_utc(secs));
        }
    }
    if packaged {
        return None;
    }
    let head_epoch = Command::new("git")
        .args(["-c", "safe.directory=*", "log", "-1", "--format=%ct"])
        .output()
        .ok()
        .filter(|o| o.status.success())
        .and_then(|o| {
            String::from_utf8_lossy(&o.stdout)
                .trim()
                .parse::<i64>()
                .ok()
        });
    // No epoch and no `.git`: no date, and NOT a clock — degrading to the
    // unreproducible value in the case nobody watches is how this defect comes back.
    head_epoch.map(iso8601_utc)
}

/// Unix seconds → `YYYY-MM-DDTHH:MM:SSZ`, in std alone.
///
/// No `chrono`/`time` build-dependency for eleven lines of arithmetic: this
/// script exists to add identity, not a crate to anybody's graph. The civil
/// date comes from Howard Hinnant's `civil_from_days`, which is exact for every
/// representable day and needs no table. Negative epochs (pre-1970) fall out
/// correctly because the era arithmetic uses floor division.
fn iso8601_utc(secs: i64) -> String {
    let days = secs.div_euclid(86_400);
    let rem = secs.rem_euclid(86_400);
    let (hh, mm, ss) = (rem / 3600, (rem % 3600) / 60, rem % 60);

    // Shift the epoch to 0000-03-01 so leap days land at the end of the cycle.
    let z = days + 719_468;
    let era = z.div_euclid(146_097);
    let doe = z.rem_euclid(146_097); // [0, 146096]
    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; // [0, 399]
    let y = yoe + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
    let mp = (5 * doy + 2) / 153; // [0, 11], March-based
    let d = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
    let m = if mp < 10 { mp + 3 } else { mp - 9 }; // [1, 12]
    let y = if m <= 2 { y + 1 } else { y };

    format!("{y:04}-{m:02}-{d:02}T{hh:02}:{mm:02}:{ss:02}Z")
}