//! Facts about *this build* that only exist while it is happening. //! //! `atgc --version` says which release it is. That is enough when a release is //! the only way to get atgc, and it stops being enough the moment two binaries //! can carry the same version number: a `cargo install --git` build, a //! contributor's `cargo build` mid-branch, and the tag itself all print //! `0.15.0`. The four values below are what tell them apart, and every one of //! them is knowable only here — the compiler is running, the git checkout is //! present, and neither is true again afterwards. //! //! Each is emitted as a `cargo::rustc-env` variable and read back with //! `env!` in `src/build_info.rs`, which is the only file that names them. //! //! # What is deliberately not here //! //! **A dirty flag.** `git status --porcelain` is easy to run and the result //! would be a lie often enough to matter. Cargo re-runs a build script only //! when one of its declared inputs changes, and the inputs that can be //! declared are files — so the honest sequence *build a clean tree, edit a //! source file, rebuild* leaves the script un-re-run and the binary claiming //! a clean tree while compiled from a dirty one. That is the wrong direction //! to be wrong in. Declaring `src/` as an input to force the re-run would fix //! it and would also re-run this script on every single edit, which is a cost //! paid by everyone to caveat a field nobody asked for. The commit is the //! identity; whether the tree had been touched on top of it is a question for //! the person at the keyboard. //! //! **A build timestamp.** It makes two builds of identical source differ, //! which is the thing reproducible builds exist to prevent, and it answers a //! question — how old is this binary — that the commit already answers better. use std::path::PathBuf; use std::process::Command; fn main() { // Not `.git/HEAD`. Every worktree under `.claude/worktrees` has a `.git` // *file* rather than a directory, so that literal path does not exist // there, and cargo re-runs a script whose declared input is missing — // turning this into a per-build cost in exactly the checkouts this // project does most of its work in. `--git-path` resolves both layouts. for path in [git_path("HEAD"), head_ref_path()].into_iter().flatten() { println!("cargo::rerun-if-changed={}", path.display()); } emit("ATGC_BUILD_COMMIT", commit().unwrap_or_default()); emit("ATGC_BUILD_TARGET", env("TARGET")); emit("ATGC_BUILD_PROFILE", env("PROFILE")); emit("ATGC_BUILD_RUSTC", rustc().unwrap_or_default()); } fn emit(key: &str, value: String) { println!("cargo::rustc-env={key}={value}"); } fn env(key: &str) -> String { std::env::var(key).unwrap_or_default() } /// The short commit, or `None` when there is no git to ask. /// /// Absent is a normal answer, not a failure: a crates.io tarball carries no /// `.git` at all, and that is the build this project is heading towards /// publishing. Whatever goes wrong here — no git on `PATH`, no repository, a /// repository with no commits — produces an empty string, and /// `build_info` renders the line only when it has something to put in it. fn commit() -> Option { let out = Command::new("git") .args(["rev-parse", "--short", "HEAD"]) .output() .ok()?; if !out.status.success() { return None; } let sha = String::from_utf8(out.stdout).ok()?.trim().to_string(); (!sha.is_empty()).then_some(sha) } /// The compiler that is compiling this, by its release number. /// /// `rust-toolchain.toml` pins a channel, but a build can still be made with /// another one — CI resolves `cargo` and `rustc` from nixpkgs rather than /// rustup and says so in its own comments — so the pin is what was asked for /// and this is what answered. fn rustc() -> Option { // `RUSTC` is the compiler cargo is actually driving, which is the one // worth reporting; a bare `rustc` would report whatever is first on PATH. let out = Command::new(std::env::var_os("RUSTC")?) .arg("-vV") .output() .ok()?; String::from_utf8(out.stdout) .ok()? .lines() .find_map(|line| line.strip_prefix("release: ")) .map(|v| v.trim().to_string()) } fn git_path(name: &str) -> Option { let out = Command::new("git") .args(["rev-parse", "--git-path", name]) .output() .ok()?; if !out.status.success() { return None; } let path = PathBuf::from(String::from_utf8(out.stdout).ok()?.trim()); path.exists().then_some(path) } /// The file holding the commit `HEAD` names, so that committing on a branch /// re-runs this script. `HEAD` itself only changes when the branch does, so /// watching it alone would pin the recorded commit to whenever the checkout /// last switched branches. /// /// `None` on a detached HEAD, where the sha is in `HEAD` itself and already /// watched, and on a branch whose ref lives in `packed-refs` and so has no /// file of its own yet. fn head_ref_path() -> Option { let out = Command::new("git") .args(["symbolic-ref", "--quiet", "HEAD"]) .output() .ok()?; if !out.status.success() { return None; } git_path(String::from_utf8(out.stdout).ok()?.trim()) }