The build line names its release: benilla build 0.2.0 · <sha>, and 0.2.0+N for a commit N past its tag (#452)

The startup line, crash reports and the debug panel now name the release a build is, so a bug report carries it: `benilla build 0.2.0 · b17be27 · 2026-09-27 · release` on a release, `0.2.0+12 · …` twelve commits past its tag.

- `benilla-buildstamp` also stamps `git describe --tags --long --match 'v[0-9]*'` and watches `packed-refs` and `refs/tags`, so a fetched tag restamps without a new commit.
- `BuildId` carries the workspace version and the describe. `release()` reads the version, with `+N` when the describe's tag is that version's; with no tag in reach (a source archive, a shallow clone) or the previous version's tag (the release commit before its tag exists) it reads the version alone.
- The bug template's example line shows the new form.

Checked: the four new `build_id` tests fail with the distance parser broken and pass restored; the describe command in a scratch clone reads empty with no tag, `v0.1.0-443-g…` over a launch tag, `v0.2.0-0-g…` on the release commit, and skips tags that are not `v`-numbered; the build script's output carries the new watches and variable. Gates green on macOS; no platform seam touched.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-09-27 08:48:12 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent e4cc695f84
commit a5cae4eb40
6 changed files with 132 additions and 22 deletions
+1 -1
View File
@@ -22,4 +22,4 @@ body:
attributes:
label: Build and setup
description: The `benilla build` line from the start of the terminal output, your OS, and the server you play on.
placeholder: "benilla build 64cca13 · 2026-09-24 · release, macOS 15, vmangos"
placeholder: "benilla build 0.2.0 · 64cca13 · 2026-09-24 · release, macOS 15, vmangos"
+3 -1
View File
@@ -133,6 +133,8 @@ mod tests {
use super::*;
const BUILD: BuildId = BuildId {
version: "0.2.0",
describe: "v0.2.0-3-g0123456",
sha: "0123456789abcdef0123456789abcdef01234567",
short: "0123456",
date: "2026-09-16",
@@ -154,7 +156,7 @@ mod tests {
});
for needle in [
"sha 0123456789abcdef0123456789abcdef01234567",
"0123456 · 2026-09-16 · debug",
"0.2.0+3 · 0123456 · 2026-09-16 · debug",
"12.3 s after launch",
"thread: main",
"at: crates/benilla-app/src/net/apply.rs:100:5",
+18 -9
View File
@@ -6,20 +6,23 @@
use std::path::Path;
use std::process::Command;
/// Stamp the commit this binary was built from into the calling package as four `rustc-env`
/// vars, which its `main.rs` reads back with `env!` into a `BuildId`:
/// Stamp the commit this binary was built from into the calling package as five `rustc-env`
/// vars, which its `main.rs` reads back with `env!` into a `BuildId`, beside its own
/// `CARGO_PKG_VERSION`:
///
/// - `BENILLA_GIT_SHA`: the full sha.
/// - `BENILLA_GIT_SHORT`: git's own abbreviation of it.
/// - `BENILLA_GIT_DATE`: the commit date (`%cs`, `YYYY-MM-DD`), not the build date.
/// - `BENILLA_GIT_DESCRIBE`: `git describe --tags --long` against the nearest release tag
/// (`v0.2.0-12-gb17be27`), which says how far past its release the commit is.
/// - `BENILLA_PROFILE`: the profile directory's name, which tells `ship` from `release` where
/// cargo's `PROFILE` does not.
///
/// The git vars are empty when git cannot answer (no `.git`, no `git` on `PATH`), which the
/// runtime reports as an unknown build. The rerun triggers are `HEAD` and the ref it names,
/// resolved with `git rev-parse --git-path` so a linked worktree resolves too; only paths that
/// exist are emitted, since cargo reruns a build script whose watched path is missing on every
/// build.
/// The git vars are empty when git cannot answer (no `.git`, no `git` on `PATH`, no tag in
/// reach), which the runtime reports as an unknown commit or the bare version. The rerun
/// triggers are `HEAD`, the ref it names, `packed-refs` and the tags, resolved with
/// `git rev-parse --git-path` so a linked worktree resolves too; only paths that exist are
/// emitted, since cargo reruns a build script whose watched path is missing on every build.
pub fn emit() {
let dir = std::env::var("CARGO_MANIFEST_DIR").expect("cargo sets CARGO_MANIFEST_DIR");
let git = |args: &[&str]| -> Option<String> {
@@ -44,10 +47,12 @@ pub fn emit() {
};
watch(git(&["rev-parse", "--git-path", "HEAD"]));
if let Some(git_ref) = git(&["symbolic-ref", "-q", "HEAD"]) {
// The loose ref, plus `packed-refs`: a fresh clone's branch has no loose file.
watch(git(&["rev-parse", "--git-path", &git_ref]));
watch(git(&["rev-parse", "--git-path", "packed-refs"]));
}
// A fresh clone's refs live only in `packed-refs`; a fetched or new tag lands in either, and
// moves the describe without moving `HEAD`. Cargo scans a watched directory whole.
watch(git(&["rev-parse", "--git-path", "packed-refs"]));
watch(git(&["rev-parse", "--git-path", "refs/tags"]));
// An edit to the rule restamps: cargo reruns a build script when `build.rs` or one of its
// build-dependencies changes.
println!("cargo::rerun-if-changed=build.rs");
@@ -56,6 +61,10 @@ pub fn emit() {
("BENILLA_GIT_SHA", &["rev-parse", "HEAD"][..]),
("BENILLA_GIT_SHORT", &["rev-parse", "--short", "HEAD"]),
("BENILLA_GIT_DATE", &["log", "-1", "--format=%cs"]),
(
"BENILLA_GIT_DESCRIBE",
&["describe", "--tags", "--long", "--match", "v[0-9]*"],
),
] {
println!(
"cargo::rustc-env={var}={}",
+106 -11
View File
@@ -1,13 +1,19 @@
//! Which build is running: the commit the binary was built from, stamped at compile time by the
//! launcher shims' build script (`benilla-buildstamp`) and passed to `run` as the [`BuildId`]
//! resource. It shows in the startup log line ([`banner`]) and in the debug panel's footer.
//! Which build is running: the release and the commit the binary was built from, stamped at
//! compile time by the launcher shims' build script (`benilla-buildstamp`) and passed to `run` as
//! the [`BuildId`] resource. It shows in the startup log line ([`banner`]), in crash reports and in
//! the debug panel's footer.
use bevy::prelude::*;
/// The launcher shim's compile-time git stamp. The git fields are empty when the build had no
/// checkout or no `git` on `PATH`, which [`BuildId::summary`] reports as an unknown build.
/// The launcher shim's compile-time stamp. The git fields are empty when the build had no
/// checkout or no `git` on `PATH`, which [`BuildId::summary`] reports as an unknown commit.
#[derive(Resource, Clone, Copy)]
pub struct BuildId {
/// The workspace version from `Cargo.toml`, which every release bumps.
pub version: &'static str,
/// `git describe --tags --long` against the nearest `v*` tag (`v0.2.0-12-gb17be27`); empty
/// when no release tag is in reach, as in a source archive or a shallow clone.
pub describe: &'static str,
/// The full sha, which the debug panel copies.
pub sha: &'static str,
/// Git's own abbreviation of [`sha`](Self::sha).
@@ -19,17 +25,106 @@ pub struct BuildId {
}
impl BuildId {
/// The one-line build id: short sha, commit date and profile.
pub fn summary(&self) -> String {
if self.short.is_empty() {
format!("unknown (built without a git checkout) · {}", self.profile)
} else {
format!("{} · {} · {}", self.short, self.date, self.profile)
/// The release this build is: the version, and `+N` when the commit is N past that version's
/// tag. With no tag for this version in reach (none fetched, or the release commit before its
/// tag exists), the version alone.
pub fn release(&self) -> String {
match commits_past_tag(self.describe, self.version) {
Some(n) if n > 0 => format!("{}+{n}", self.version),
_ => self.version.to_owned(),
}
}
/// The one-line build id: release, short sha, commit date and profile.
pub fn summary(&self) -> String {
if self.short.is_empty() {
format!(
"{} · unknown commit (built without a git checkout) · {}",
self.release(),
self.profile
)
} else {
format!(
"{} · {} · {} · {}",
self.release(),
self.short,
self.date,
self.profile
)
}
}
}
/// How many commits past `v{version}` a `--long` describe (`<tag>-<n>-g<hash>`) is, or `None`
/// when the describe is empty, malformed or names another version's tag.
fn commits_past_tag(describe: &str, version: &str) -> Option<u32> {
let mut parts = describe.rsplitn(3, '-');
parts.next().filter(|hash| hash.starts_with('g'))?;
let n: u32 = parts.next()?.parse().ok()?;
let tag = parts.next()?;
(tag.strip_prefix('v')? == version).then_some(n)
}
/// Log the build id once at startup, not env-gated, so every pasted log carries it.
pub fn banner(build: Res<BuildId>) {
info!("benilla build {}", build.summary());
}
#[cfg(test)]
mod tests {
use super::*;
fn build(version: &'static str, describe: &'static str, short: &'static str) -> BuildId {
BuildId {
version,
describe,
sha: "",
short,
date: "2026-09-27",
profile: "release",
}
}
#[test]
fn a_tagged_release_reads_as_its_version() {
let b = build("0.2.0", "v0.2.0-0-gb17be27", "b17be27");
assert_eq!(b.summary(), "0.2.0 · b17be27 · 2026-09-27 · release");
}
#[test]
fn a_commit_past_the_release_counts_its_distance() {
assert_eq!(
build("0.2.0", "v0.2.0-12-gb17be27", "b17be27").release(),
"0.2.0+12"
);
// A pre-release tag's own hyphens stay in the tag.
assert_eq!(
build("1.0.0-rc1", "v1.0.0-rc1-3-gabc1234", "abc1234").release(),
"1.0.0-rc1+3"
);
}
#[test]
fn another_versions_tag_or_none_reads_as_the_version_alone() {
// The release commit itself, before its tag exists: the nearest tag is the previous one.
assert_eq!(
build("0.2.0", "v0.1.0-450-gb17be27", "b17be27").release(),
"0.2.0"
);
// No release tag in reach: a source archive or a shallow clone.
assert_eq!(build("0.2.0", "", "b17be27").release(), "0.2.0");
assert_eq!(
build("0.2.0", "not-a-describe", "b17be27").release(),
"0.2.0"
);
}
#[test]
fn a_build_without_git_still_names_its_release() {
let b = build("0.2.0", "", "");
assert_eq!(
b.summary(),
"0.2.0 · unknown commit (built without a git checkout) · release"
);
}
}
+2
View File
@@ -6,6 +6,8 @@ use benilla_world::build_id::BuildId;
fn main() -> benilla_world::AppExit {
benilla_world::worldview::run(BuildId {
version: env!("CARGO_PKG_VERSION"),
describe: env!("BENILLA_GIT_DESCRIBE"),
sha: env!("BENILLA_GIT_SHA"),
short: env!("BENILLA_GIT_SHORT"),
date: env!("BENILLA_GIT_DATE"),
+2
View File
@@ -5,6 +5,8 @@ use benilla_app::BuildId;
fn main() -> benilla_app::AppExit {
benilla_app::run(BuildId {
version: env!("CARGO_PKG_VERSION"),
describe: env!("BENILLA_GIT_DESCRIBE"),
sha: env!("BENILLA_GIT_SHA"),
short: env!("BENILLA_GIT_SHORT"),
date: env!("BENILLA_GIT_DATE"),