use committer timestamp
Some checks failed
Generic zig build / build (push) Failing after 22s
Generic zig build / deploy (push) Has been skipped

This commit is contained in:
Emil Lerch 2026-04-23 16:41:39 -07:00
parent 41208f3732
commit 2326154d81
Signed by: lobo
GPG key ID: A7B62D657EF764F8
2 changed files with 46 additions and 18 deletions

View file

@ -118,21 +118,27 @@ pub fn build(b: *std.Build) void {
}
/// Produce the `build_info` module exposing `version` (derived from `git
/// describe`) and `build_timestamp` (Unix epoch seconds at build time).
/// describe`) and `build_timestamp` (committer timestamp of HEAD).
/// Consumed as `@import("build_info")` from `src/version.zig`.
///
/// Both values are reproducible per-commit: `git describe` is stable
/// until a new commit/tag/dirty-flag flip, and the committer timestamp
/// comes from `git log -1 --format=%ct`. Neither varies with wall-clock
/// time, so the Options module doesn't rebuild every invocation, which
/// used to cascade into a full exe relink on every `zig build`.
///
/// Falls back to `fallbackVersion()` / `0` when git is unavailable
/// (source tarball, pre-commit environment).
fn buildInfoOptions(b: *std.Build) *std.Build.Module {
const opts = b.addOptions();
// `git describe --tags --always --dirty` gives the closest tag, with a
// distance-+-hash suffix when HEAD isn't exactly on a tag, plus a
// `-dirty` marker if the working tree has uncommitted changes. Falls
// back to the build.zig.zon `.version` when git is unavailable.
const version = gitDescribe(b) orelse fallbackVersion();
opts.addOption([]const u8, "version", version);
// Seconds since the Unix epoch at build time. Rendered by `zfin version
// --verbose` as a human-readable date.
opts.addOption(i64, "build_timestamp", std.time.timestamp());
// Use HEAD's committer timestamp (reproducible) instead of
// `std.time.timestamp()` (changes every build). See comment above.
const timestamp = gitHeadTimestamp(b) orelse 0;
opts.addOption(i64, "build_timestamp", timestamp);
return opts.createModule();
}
@ -141,7 +147,22 @@ fn buildInfoOptions(b: *std.Build) *std.Build.Module {
/// the trimmed output. Returns null on any error (git missing, not a repo,
/// non-zero exit).
fn gitDescribe(b: *std.Build) ?[]const u8 {
var child = std.process.Child.init(&.{ "git", "describe", "--tags", "--always", "--dirty" }, b.allocator);
return gitCapture(b, &.{ "git", "describe", "--tags", "--always", "--dirty" });
}
/// Run `git log -1 --format=%ct HEAD` and parse the stdout as an i64
/// (Unix seconds). Returns null on any error. Stable per-commit, which
/// keeps the Options module cache-friendly.
fn gitHeadTimestamp(b: *std.Build) ?i64 {
const text = gitCapture(b, &.{ "git", "log", "-1", "--format=%ct", "HEAD" }) orelse return null;
return std.fmt.parseInt(i64, text, 10) catch null;
}
/// Spawn a git subcommand in the repo root, capture stdout, trim and
/// dupe it through `b.allocator`. Returns null on any error (git
/// missing, non-zero exit, empty output).
fn gitCapture(b: *std.Build, argv: []const []const u8) ?[]const u8 {
var child = std.process.Child.init(argv, b.allocator);
child.cwd = b.build_root.path;
child.stdout_behavior = .Pipe;
child.stderr_behavior = .Ignore;

View file

@ -2,8 +2,13 @@
//!
//! `version_string` is `git describe --tags --always --dirty` output (or the
//! `.version` field from `build.zig.zon` if git is unavailable at build time).
//! `build_timestamp` is the Unix epoch seconds at the moment the build
//! executed. Together they identify the binary precisely.
//! `build_timestamp` is the committer timestamp of HEAD (Unix epoch
//! seconds), or 0 when git is unavailable. Together they identify the
//! binary precisely.
//!
//! The timestamp is deliberately per-commit rather than wall-clock: it
//! keeps the build-options module cache-stable, so repeated `zig build`
//! invocations don't relink the exe just because the second ticked over.
//!
//! Consumers (the `zfin version` command, snapshot metadata writers, bug
//! reports) should route through this module rather than importing
@ -17,8 +22,9 @@ const build_info = @import("build_info");
/// "1a2b3c4" (no tags yet), or a fallback from build.zig.zon.
pub const version_string: []const u8 = build_info.version;
/// Unix epoch seconds at build time. Rendered as ISO date by the
/// `zfin version --verbose` command.
/// Unix epoch seconds committer timestamp of HEAD at build time, or 0
/// when git is unavailable. Rendered as ISO date by the `zfin version
/// --verbose` command; 0 renders as 1970-01-01 (clearly-fake sentinel).
pub const build_timestamp: i64 = build_info.build_timestamp;
/// True when `version_string` ends in `-dirty`, indicating the build was
@ -31,9 +37,10 @@ test "version_string is non-empty" {
try std.testing.expect(version_string.len > 0);
}
test "build_timestamp is positive" {
// A legitimate build always runs after 2020 (roughly 1577836800).
// If this fires, either the host clock is badly wrong, or the build
// options didn't wire through.
try std.testing.expect(build_timestamp > 1_577_836_800);
test "build_timestamp is either zero (no-git fallback) or a plausible commit time" {
// Two acceptable shapes:
// 0 git unavailable or `git log -1 %ct` failed
// > 1_577_836_800 real committer timestamp (Jan 1 2020 onward)
// Anything in between would indicate a broken build-info wiring.
try std.testing.expect(build_timestamp == 0 or build_timestamp > 1_577_836_800);
}