use committer timestamp
This commit is contained in:
parent
9af99ef8c8
commit
5e6d2dd438
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
|
/// 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`.
|
/// 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 {
|
fn buildInfoOptions(b: *std.Build) *std.Build.Module {
|
||||||
const opts = b.addOptions();
|
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();
|
const version = gitDescribe(b) orelse fallbackVersion();
|
||||||
opts.addOption([]const u8, "version", version);
|
opts.addOption([]const u8, "version", version);
|
||||||
|
|
||||||
// Seconds since the Unix epoch at build time. Rendered by `zfin version
|
// Use HEAD's committer timestamp (reproducible) instead of
|
||||||
// --verbose` as a human-readable date.
|
// `std.time.timestamp()` (changes every build). See comment above.
|
||||||
opts.addOption(i64, "build_timestamp", std.time.timestamp());
|
const timestamp = gitHeadTimestamp(b) orelse 0;
|
||||||
|
opts.addOption(i64, "build_timestamp", timestamp);
|
||||||
|
|
||||||
return opts.createModule();
|
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,
|
/// the trimmed output. Returns null on any error (git missing, not a repo,
|
||||||
/// non-zero exit).
|
/// non-zero exit).
|
||||||
fn gitDescribe(b: *std.Build) ?[]const u8 {
|
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.cwd = b.build_root.path;
|
||||||
child.stdout_behavior = .Pipe;
|
child.stdout_behavior = .Pipe;
|
||||||
child.stderr_behavior = .Ignore;
|
child.stderr_behavior = .Ignore;
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,13 @@
|
||||||
//!
|
//!
|
||||||
//! `version_string` is `git describe --tags --always --dirty` output (or the
|
//! `version_string` is `git describe --tags --always --dirty` output (or the
|
||||||
//! `.version` field from `build.zig.zon` if git is unavailable at build time).
|
//! `.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
|
//! `build_timestamp` is the committer timestamp of HEAD (Unix epoch
|
||||||
//! executed. Together they identify the binary precisely.
|
//! 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
|
//! Consumers (the `zfin version` command, snapshot metadata writers, bug
|
||||||
//! reports) should route through this module rather than importing
|
//! 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.
|
/// "1a2b3c4" (no tags yet), or a fallback from build.zig.zon.
|
||||||
pub const version_string: []const u8 = build_info.version;
|
pub const version_string: []const u8 = build_info.version;
|
||||||
|
|
||||||
/// Unix epoch seconds at build time. Rendered as ISO date by the
|
/// Unix epoch seconds — committer timestamp of HEAD at build time, or 0
|
||||||
/// `zfin version --verbose` command.
|
/// 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;
|
pub const build_timestamp: i64 = build_info.build_timestamp;
|
||||||
|
|
||||||
/// True when `version_string` ends in `-dirty`, indicating the build was
|
/// 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);
|
try std.testing.expect(version_string.len > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "build_timestamp is positive" {
|
test "build_timestamp is either zero (no-git fallback) or a plausible commit time" {
|
||||||
// A legitimate build always runs after 2020 (roughly 1577836800).
|
// Two acceptable shapes:
|
||||||
// If this fires, either the host clock is badly wrong, or the build
|
// 0 → git unavailable or `git log -1 %ct` failed
|
||||||
// options didn't wire through.
|
// > 1_577_836_800 → real committer timestamp (Jan 1 2020 onward)
|
||||||
try std.testing.expect(build_timestamp > 1_577_836_800);
|
// 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