use committer timestamp
This commit is contained in:
parent
41208f3732
commit
2326154d81
2 changed files with 46 additions and 18 deletions
39
build.zig
39
build.zig
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue