znippy-cli/build.rs
// build.rs — stamp the commit identity into the `znippy` binary.
//
// Reads the short git SHA from the git metadata on disk with **pure
// `std::fs`** — NO `Command::new("git")`, per the pure-Rust / zero-shell law
// (build code is production code; it must not shell out). Handles a symbolic or
// detached HEAD, `packed-refs`, and linked worktrees (`.git` as a pointer
// file). znippy-cli lives in a workspace subdir, so we walk up from the crate
// manifest to find the repo's `.git`. Falls back to `unknown` off a checkout.
//
// Emits `ZNIPPY_GIT_HASH` (via `cargo:rustc-env`) which `lib.rs` folds into the
// `--version` string with `env!`.
use std::path::{Path, PathBuf};
fn main() {
let hash = git_short_hash().unwrap_or_else(|| "unknown".to_string());
println!("cargo:rustc-env=ZNIPPY_GIT_HASH={hash}");
println!("cargo:rerun-if-changed=build.rs");
}
/// Read the short (7-char) commit hash from git metadata on disk without
/// shelling out. Returns `None` off a git checkout.
fn git_short_hash() -> Option<String> {
let (gitdir, commondir) = resolve_git_dirs()?;
println!("cargo:rerun-if-changed={}", gitdir.join("HEAD").display());
let head = std::fs::read_to_string(gitdir.join("HEAD")).ok()?;
let head = head.trim();
let full = if let Some(ref_name) = head.strip_prefix("ref: ") {
// Symbolic HEAD → resolve the branch ref (loose file or packed-refs).
resolve_ref(ref_name.trim(), &gitdir, &commondir)?
} else {
// Detached HEAD: the file IS the object id.
head.to_string()
};
let full = full.trim();
if full.len() < 7 || !full.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
Some(full[..7].to_string())
}
/// Walk up from the crate manifest dir to find `.git`, then resolve the real
/// gitdir and the common dir. For a normal checkout both are `<root>/.git`. For
/// a linked worktree `.git` is a FILE (`gitdir: <path>`) and the shared refs
/// live in the common dir named by `<gitdir>/commondir`.
fn resolve_git_dirs() -> Option<(PathBuf, PathBuf)> {
let manifest = PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR")?);
let mut dir: Option<&Path> = Some(manifest.as_path());
let mut dot_git = None;
while let Some(d) = dir {
let candidate = d.join(".git");
if candidate.exists() {
dot_git = Some(candidate);
break;
}
dir = d.parent();
}
let dot_git = dot_git?;
let meta = std::fs::metadata(&dot_git).ok()?;
let gitdir = if meta.is_dir() {
dot_git
} else {
println!("cargo:rerun-if-changed={}", dot_git.display());
let contents = std::fs::read_to_string(&dot_git).ok()?;
let rel = contents.trim().strip_prefix("gitdir:")?.trim();
let p = PathBuf::from(rel);
// `gitdir:` may be relative to the `.git` pointer file's parent.
if p.is_absolute() {
p
} else {
dot_git.parent()?.join(p)
}
};
let commondir = match std::fs::read_to_string(gitdir.join("commondir")) {
Ok(rel) => {
let rel = rel.trim();
let p = PathBuf::from(rel);
if p.is_absolute() { p } else { gitdir.join(p) }
}
Err(_) => gitdir.clone(),
};
Some((gitdir, commondir))
}
/// Resolve a symbolic ref (e.g. `refs/heads/master`) to an object id: try the
/// loose ref file under the gitdir, then the common dir, then `packed-refs`.
fn resolve_ref(ref_name: &str, gitdir: &Path, commondir: &Path) -> Option<String> {
for base in [gitdir, commondir] {
let loose = base.join(ref_name);
println!("cargo:rerun-if-changed={}", loose.display());
if let Ok(s) = std::fs::read_to_string(&loose) {
let s = s.trim();
if !s.is_empty() && !s.starts_with("ref: ") {
return Some(s.to_string());
}
}
}
let packed = commondir.join("packed-refs");
println!("cargo:rerun-if-changed={}", packed.display());
let packed = std::fs::read_to_string(&packed).ok()?;
for line in packed.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
continue;
}
// Each entry is "<sha> <refname>".
if let Some((sha, name)) = line.split_once(' ') {
if name.trim() == ref_name {
return Some(sha.trim().to_string());
}
}
}
None
}