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_ref.rs

//! **Which build reference a binary is stamped with** — the one rule both
//! build scripts that stamp `GUNNAR_BUILD_REF` (gunnar-common's and
//! gunnar-cli's) follow, kept in one file so the banner and the build-thing
//! receipt can never name different commits. Pure but for [`packaged_ref`]: the
//! build scripts read the environment and run `git describe`; this chooses, and
//! reads the commit a published package was made from.

/// The variables a builder sets, in the order they win.
pub const ENV_REFS: [&str; 2] = ["GUNNAR_BUILD_REF", "NORDISK_BUILD_REF"];

/// Choose the stamp: the builder's variables first (`env`, in [`ENV_REFS`]
/// order), then the checkout's own `git describe`, else `None`.
///
/// The builder's value is PREFERRED over git, not only a fallback for it. A
/// Build Thing container build of a tree copied without its `.git` has nothing
/// for `describe` to read — and a copied tree sitting inside some other
/// checkout would be described as THAT checkout. The one who handed the tree
/// over knows which commit it is (the Build Thing exports `NORDISK_BUILD_REF`
/// from the host's `git describe`, or the caller's own value).
pub fn choose(env: &[Option<String>], describe: Option<&str>) -> Option<String> {
    let clean = |s: &str| Some(s.trim().to_owned()).filter(|s| !s.is_empty());
    env.iter()
        .flatten()
        .find_map(|e| clean(e))
        .or_else(|| describe.and_then(clean))
}

/// The builder's variables, read in [`ENV_REFS`] order, each announced to cargo
/// so a changed value re-stamps. For a `build.rs` only.
#[allow(dead_code)] // the test target includes this file and reads no environment
pub fn env_refs() -> Vec<Option<String>> {
    ENV_REFS
        .iter()
        .map(|k| {
            println!("cargo:rerun-if-env-changed={k}");
            std::env::var(k).ok()
        })
        .collect()
}

/// **The commit a published package was made from**, from the
/// `.cargo_vcs_info.json` that `cargo package` writes into every package it makes
/// from a git checkout (lane GUN-VERSION-STAMP, 2026-10-31). `cargo install
/// gunnar` builds from such a package with no `.git` anywhere, so `git describe`
/// has nothing to read and the banner said `unknown`. The file names the full
/// `git.sha1`; the stamp is its first seven hex digits, `-modified` when cargo
/// packaged a dirty tree (`"dirty": true`), the same spelling `describe` uses.
///
/// `None` when the file is absent (a git checkout: cargo writes it only into a
/// package) or names no 40-hex commit.
#[allow(dead_code)] // the test target reads `vcs_info_ref` only
pub fn packaged_ref(manifest_dir: &std::path::Path) -> Option<String> {
    let path = manifest_dir.join(".cargo_vcs_info.json");
    let json = std::fs::read_to_string(&path).ok()?;
    println!("cargo:rerun-if-changed={}", path.display());
    vcs_info_ref(&json)
}

/// The stamp a `.cargo_vcs_info.json` text names (see [`packaged_ref`]). Read by
/// hand: a build script that adds identity must not add a JSON crate to the graph.
pub fn vcs_info_ref(json: &str) -> Option<String> {
    let value_after = |key: &str| -> Option<&str> {
        let at = json.find(key)?;
        let rest = json[at + key.len()..].trim_start();
        Some(rest.strip_prefix(':')?.trim_start())
    };
    let sha = value_after("\"sha1\"")?.strip_prefix('"')?;
    let sha = &sha[..sha.find('"')?];
    if sha.len() != 40 || !sha.bytes().all(|b| b.is_ascii_hexdigit()) {
        return None;
    }
    let dirty = value_after("\"dirty\"").is_some_and(|v| v.starts_with("true"));
    Some(format!(
        "{}{}",
        &sha[..7],
        if dirty { "-modified" } else { "" }
    ))
}