|
| 1 | +//! Embeds build identity, following the shared build-versioning contract. |
| 2 | +//! |
| 3 | +//! The canonical stamp is a single-line JSON document under the name |
| 4 | +//! `CLI_BUILD_STAMP`, in one of two shapes: |
| 5 | +//! |
| 6 | +//! ```json |
| 7 | +//! {"type":"local","rev":"abc123","ts":1739999700,"dirty":true} |
| 8 | +//! {"type":"nix","version":"0.1.0","rev":"def456","commitTs":1739740800,"dirty":false} |
| 9 | +//! ``` |
| 10 | +//! |
| 11 | +//! Resolution order at build time: |
| 12 | +//! |
| 13 | +//! 1. `CLI_BUILD_STAMP` in the build environment — how the Nix packaging layer |
| 14 | +//! injects a `nix` stamp. Passed through verbatim. |
| 15 | +//! 2. Otherwise a `local` stamp derived from git. |
| 16 | +//! 3. Otherwise empty, which resolves to `sourceKind: package` at runtime. |
| 17 | +//! |
| 18 | +//! **This must never fail the build.** Compass is packaged with Nix, where the |
| 19 | +//! build runs in a sandbox with no git and no `.git` directory. Every step |
| 20 | +//! here degrades to the next rather than panicking. |
| 21 | +
|
| 22 | +use std::process::Command; |
| 23 | + |
| 24 | +fn main() { |
| 25 | + println!("cargo:rerun-if-env-changed=CLI_BUILD_STAMP"); |
| 26 | + // A commit or a dirty working tree changes the stamp. |
| 27 | + for p in [".git/HEAD", ".git/index"] { |
| 28 | + if std::path::Path::new(p).exists() { |
| 29 | + println!("cargo:rerun-if-changed={p}"); |
| 30 | + } |
| 31 | + } |
| 32 | + |
| 33 | + let stamp = std::env::var("CLI_BUILD_STAMP") |
| 34 | + .ok() |
| 35 | + .filter(|s| !s.trim().is_empty()) |
| 36 | + .or_else(local_stamp) |
| 37 | + .unwrap_or_default(); |
| 38 | + |
| 39 | + // Values reach the compiler as an env var, so newlines are not allowed. |
| 40 | + let stamp = stamp.replace(['\n', '\r'], " "); |
| 41 | + println!("cargo:rustc-env=COMPASS_BUILD_STAMP={stamp}"); |
| 42 | +} |
| 43 | + |
| 44 | +/// Derive a `local` stamp from git, or `None` when git is unavailable. |
| 45 | +fn local_stamp() -> Option<String> { |
| 46 | + let rev = git(&["rev-parse", "--short=12", "HEAD"])?; |
| 47 | + let ts = git(&["log", "-1", "--format=%ct"]) |
| 48 | + .and_then(|s| s.parse::<i64>().ok()) |
| 49 | + .unwrap_or(0); |
| 50 | + // An empty porcelain listing means a clean tree. A git failure here is |
| 51 | + // reported as clean rather than guessed as dirty. |
| 52 | + let dirty = git(&["status", "--porcelain"]) |
| 53 | + .map(|s| !s.trim().is_empty()) |
| 54 | + .unwrap_or(false); |
| 55 | + |
| 56 | + Some(format!( |
| 57 | + r#"{{"type":"local","rev":"{}","ts":{},"dirty":{}}}"#, |
| 58 | + escape(&rev), |
| 59 | + ts, |
| 60 | + dirty |
| 61 | + )) |
| 62 | +} |
| 63 | + |
| 64 | +fn git(args: &[&str]) -> Option<String> { |
| 65 | + let out = Command::new("git").args(args).output().ok()?; |
| 66 | + if !out.status.success() { |
| 67 | + return None; |
| 68 | + } |
| 69 | + let s = String::from_utf8(out.stdout).ok()?.trim().to_string(); |
| 70 | + (!s.is_empty()).then_some(s) |
| 71 | +} |
| 72 | + |
| 73 | +fn escape(s: &str) -> String { |
| 74 | + s.replace('\\', "\\\\").replace('"', "\\\"") |
| 75 | +} |
0 commit comments