feat: embed git SHA in version string via build.rs

Adds a build.rs that runs `git describe --tags --always --dirty` and
sets NUMA_BUILD_VERSION at compile time. A new `numa::version()` helper
returns the build version, falling back to CARGO_PKG_VERSION when git
is unavailable (source tarballs, Docker builds without .git).

Version strings:
  tagged release:      0.13.1
  commits ahead:       0.13.1+a87f907
  uncommitted changes: 0.13.1+a87f907-dirty
  no git:              0.13.1

Replaces all 6 inline env!("CARGO_PKG_VERSION") call sites with the
single version() function.
This commit is contained in:
Razvan Dimescu
2026-04-16 13:02:25 +03:00
parent a87f907d20
commit 0118ab0f44
5 changed files with 62 additions and 9 deletions

47
build.rs Normal file
View File

@@ -0,0 +1,47 @@
fn main() {
let git_version = std::process::Command::new("git")
.args(["describe", "--tags", "--always", "--dirty"])
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| {
let s = s.trim();
let s = s.strip_prefix('v').unwrap_or(s);
// "0.13.1" → clean tag → "0.13.1"
// "0.13.1-9-ga87f907" → ahead → "0.13.1+a87f907"
// "0.13.1-9-ga87f907-dirty" → dirty → "0.13.1+a87f907-dirty"
// "a87f907" → no tags → "0.0.0+a87f907"
// "a87f907-dirty" → no tags → "0.0.0+a87f907-dirty"
if let Some((base, rest)) = s.split_once("-") {
// Could be "0.13.1-9-ga87f907[-dirty]" or "a87f907-dirty"
if base.contains('.') {
// Tagged: extract sha from "-N-gSHA[-dirty]"
let parts: Vec<&str> = rest.splitn(3, '-').collect();
match parts.as_slice() {
[_n, sha] => format!("{}+{}", base, sha.strip_prefix('g').unwrap_or(sha)),
[_n, sha, "dirty"] => {
format!("{}+{}-dirty", base, sha.strip_prefix('g').unwrap_or(sha))
}
_ => s.to_string(),
}
} else {
// Untagged: "sha-dirty"
format!("0.0.0+{}", s)
}
} else if s.contains('.') {
// Exact tag match: "0.13.1"
s.to_string()
} else {
// Bare sha, no tags at all
format!("0.0.0+{}", s)
}
});
if let Some(v) = git_version {
println!("cargo:rustc-env=NUMA_BUILD_VERSION={}", v);
}
println!("cargo:rerun-if-changed=.git/HEAD");
println!("cargo:rerun-if-changed=.git/refs/tags/");
}

View File

@@ -540,7 +540,7 @@ async fn stats(State(ctx): State<Arc<ServerCtx>>) -> Json<StatsResponse> {
}; };
Json(StatsResponse { Json(StatsResponse {
version: env!("CARGO_PKG_VERSION"), version: crate::version(),
uptime_secs: snap.uptime_secs, uptime_secs: snap.uptime_secs,
upstream, upstream,
mode: ctx.upstream_mode.as_str(), mode: ctx.upstream_mode.as_str(),

View File

@@ -43,7 +43,7 @@ impl HealthMeta {
#[cfg(test)] #[cfg(test)]
pub fn test_fixture() -> Self { pub fn test_fixture() -> Self {
HealthMeta { HealthMeta {
version: env!("CARGO_PKG_VERSION"), version: crate::version(),
hostname: "test-host".to_string(), hostname: "test-host".to_string(),
sni: "numa.numa".to_string(), sni: "numa.numa".to_string(),
dot_enabled: false, dot_enabled: false,
@@ -99,7 +99,7 @@ impl HealthMeta {
} }
HealthMeta { HealthMeta {
version: env!("CARGO_PKG_VERSION"), version: crate::version(),
hostname: crate::hostname(), hostname: crate::hostname(),
sni: "numa.numa".to_string(), sni: "numa.numa".to_string(),
dot_enabled, dot_enabled,

View File

@@ -34,6 +34,14 @@ pub(crate) mod testutil;
pub type Error = Box<dyn std::error::Error + Send + Sync>; pub type Error = Box<dyn std::error::Error + Send + Sync>;
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
/// Build version string. On tagged releases: `0.13.1`. On commits ahead
/// of a tag: `0.13.1+a87f907`. With uncommitted changes: `0.13.1+a87f907-dirty`.
/// Falls back to `CARGO_PKG_VERSION` when built outside a git repo (e.g.
/// from a source tarball).
pub fn version() -> &'static str {
option_env!("NUMA_BUILD_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"))
}
/// Detect the machine hostname via the `hostname` command. Returns the /// Detect the machine hostname via the `hostname` command. Returns the
/// full hostname (e.g., `macbook-pro.local`), or `"numa"` if the command /// full hostname (e.g., `macbook-pro.local`), or `"numa"` if the command
/// fails. Call sites that need the short form (e.g., mDNS instance /// fails. Call sites that need the short form (e.g., mDNS instance

View File

@@ -72,7 +72,7 @@ async fn main() -> numa::Result<()> {
}; };
} }
"version" | "--version" | "-V" => { "version" | "--version" | "-V" => {
eprintln!("numa {}", env!("CARGO_PKG_VERSION")); eprintln!("numa {}", numa::version());
return Ok(()); return Ok(());
} }
"help" | "--help" | "-h" => { "help" | "--help" | "-h" => {
@@ -383,12 +383,10 @@ async fn main() -> numa::Result<()> {
}; };
// Title row: center within the box // Title row: center within the box
let title = format!( let ver = numa::version();
"{b}NUMA{r} {it}DNS that governs itself{r} {d}v{}{r}", let title = format!("{b}NUMA{r} {it}DNS that governs itself{r} {d}v{ver}{r}",);
env!("CARGO_PKG_VERSION")
);
// The title contains ANSI codes; visible length is ~38 chars. Pad to fill the box. // The title contains ANSI codes; visible length is ~38 chars. Pad to fill the box.
let title_visible_len = 4 + 2 + 24 + 2 + 1 + env!("CARGO_PKG_VERSION").len() + 1; let title_visible_len = 4 + 2 + 24 + 2 + 1 + ver.len() + 1;
let title_pad = w.saturating_sub(title_visible_len); let title_pad = w.saturating_sub(title_visible_len);
eprintln!("\n{o}{bar_top}{r}"); eprintln!("\n{o}{bar_top}{r}");
eprint!("{o}{r} {title}"); eprint!("{o}{r} {title}");