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 { "" }
))
}