use git version in metno identifier

This commit is contained in:
Emil Lerch 2026-01-05 13:34:37 -08:00
parent ad93634100
commit b2cb5d537f
Signed by: lobo
GPG key ID: A7B62D657EF764F8
3 changed files with 171 additions and 2 deletions

View file

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const GitVersion = @import("build/GitVersion.zig");
pub fn build(b: *std.Build) void { pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
@ -53,6 +54,10 @@ pub fn build(b: *std.Build) void {
maxminddb.installHeadersDirectory(maxminddb_upstream.path("include"), "", .{}); maxminddb.installHeadersDirectory(maxminddb_upstream.path("include"), "", .{});
const version = GitVersion.getVersion(b, .{});
const build_options = b.addOptions();
build_options.addOption([]const u8, "version", version);
const exe = b.addExecutable(.{ const exe = b.addExecutable(.{
.name = "wttr", .name = "wttr",
.root_module = b.createModule(.{ .root_module = b.createModule(.{
@ -67,6 +72,7 @@ pub fn build(b: *std.Build) void {
exe.root_module.addAnonymousImport("airports.dat", .{ exe.root_module.addAnonymousImport("airports.dat", .{
.root_source_file = openflights.path("data/airports.dat"), .root_source_file = openflights.path("data/airports.dat"),
}); });
exe.root_module.addOptions("build_options", build_options);
exe.root_module.addIncludePath(maxminddb_upstream.path("include")); exe.root_module.addIncludePath(maxminddb_upstream.path("include"));
exe.root_module.addConfigHeader(maxminddb_config); exe.root_module.addConfigHeader(maxminddb_config);
exe.linkLibrary(maxminddb); exe.linkLibrary(maxminddb);
@ -87,6 +93,7 @@ pub fn build(b: *std.Build) void {
const test_options = b.addOptions(); const test_options = b.addOptions();
test_options.addOption(bool, "download_geoip", download_geoip); test_options.addOption(bool, "download_geoip", download_geoip);
test_options.addOption([]const u8, "version", version);
const tests = b.addTest(.{ const tests = b.addTest(.{
.root_module = b.createModule(.{ .root_module = b.createModule(.{

161
build/GitVersion.zig Normal file
View file

@ -0,0 +1,161 @@
const std = @import("std");
const Build = std.Build;
pub const Options = struct {
/// Length of the short hash (default: 7, git's default)
hash_length: u6 = 7,
/// String to append when working tree is dirty
dirty_flag: []const u8 = "*",
};
/// Get git version information by reading .git files directly
pub fn getVersion(b: *Build, options: Options) []const u8 {
const allocator = b.allocator;
// Find build root by looking for build.zig
const build_root = findBuildRoot(allocator) catch return "unknown";
defer allocator.free(build_root);
// Read .git/HEAD relative to build root
const head_path = std.fmt.allocPrint(allocator, "{s}/.git/HEAD", .{build_root}) catch return "unknown";
defer allocator.free(head_path);
const head_data = std.fs.cwd().readFileAlloc(allocator, head_path, 1024) catch {
return "not under version control";
};
defer allocator.free(head_data);
const head_trimmed = std.mem.trim(u8, head_data, &std.ascii.whitespace);
// Parse HEAD - either "ref: refs/heads/branch" or direct hash
const hash_owned = if (std.mem.startsWith(u8, head_trimmed, "ref: ")) blk: {
const ref_path_rel = std.mem.trimLeft(u8, head_trimmed[5..], &std.ascii.whitespace);
const ref_file = std.fmt.allocPrint(allocator, "{s}/.git/{s}", .{ build_root, ref_path_rel }) catch return "unknown";
defer allocator.free(ref_file);
const ref_fd = std.fs.openFileAbsolute(ref_file, .{}) catch return "unknown";
defer ref_fd.close();
var ref_buf: [1024]u8 = undefined;
const bytes_read = ref_fd.readAll(&ref_buf) catch return "unknown";
const ref_data = ref_buf[0..bytes_read];
const ref_trimmed = std.mem.trim(u8, ref_data, &std.ascii.whitespace);
break :blk allocator.dupe(u8, ref_trimmed) catch return "unknown";
} else allocator.dupe(u8, head_trimmed) catch return "unknown";
defer allocator.free(hash_owned);
// Truncate to short hash
const short_hash = if (hash_owned.len > options.hash_length)
hash_owned[0..options.hash_length]
else
hash_owned;
// Check if dirty using simple heuristic:
// If any .zig files are newer than .git/index, mark as dirty
const is_dirty = isDirty(allocator, build_root) catch return "unknown";
if (is_dirty) {
return std.fmt.allocPrint(allocator, "{s}{s}", .{ short_hash, options.dirty_flag }) catch return "unknown";
}
return allocator.dupe(u8, short_hash) catch return "unknown";
}
fn findBuildRoot(allocator: std.mem.Allocator) ![]const u8 {
var buf: [std.fs.max_path_bytes]u8 = undefined;
const start_cwd = try std.fs.cwd().realpath(".", &buf);
var cwd: []const u8 = start_cwd;
while (true) {
// Check if build.zig exists in current directory
var dir = std.fs.openDirAbsolute(cwd, .{}) catch break;
defer dir.close();
dir.access("build.zig", .{}) catch {
// build.zig not found, try parent
const parent = std.fs.path.dirname(cwd) orelse break;
if (std.mem.eql(u8, parent, cwd)) break; // Reached root
cwd = parent;
continue;
};
return allocator.dupe(u8, cwd);
}
return error.BuildRootNotFound;
}
fn isDirty(allocator: std.mem.Allocator, build_root: []const u8) !bool {
// Get .git/index mtime
const index_path = try std.fs.path.join(allocator, &[_][]const u8{ build_root, ".git", "index" });
defer allocator.free(index_path);
const index_stat = std.fs.cwd().statFile(index_path) catch return error.CannotDetermineDirty;
const index_mtime = index_stat.mtime;
// Read .gitignore
const ignore_path = try std.fs.path.join(allocator, &[_][]const u8{ build_root, ".gitignore" });
defer allocator.free(ignore_path);
const ignore_data = std.fs.cwd().readFileAlloc(allocator, ignore_path, 1024 * 1024) catch
try allocator.dupe(u8, "");
defer allocator.free(ignore_data);
// Walk source files in build root and check if any are newer
var dir = std.fs.openDirAbsolute(build_root, .{ .iterate = true }) catch return error.CannotDetermineDirty;
defer dir.close();
var walker = dir.walk(allocator) catch return error.CannotDetermineDirty;
defer walker.deinit();
while (walker.next() catch return error.CannotDetermineDirty) |entry| {
if (entry.kind != .file) continue;
// Always ignore .git/
if (std.mem.startsWith(u8, entry.path, ".git/")) continue;
// Check if path matches any ignore pattern
var lines = std.mem.splitScalar(u8, ignore_data, '\n');
var ignored = false;
while (lines.next()) |line| {
const trimmed = std.mem.trim(u8, line, &std.ascii.whitespace);
if (trimmed.len == 0) continue;
if (trimmed[0] == '#') continue;
if (isIgnored(entry.path, trimmed)) {
ignored = true;
break;
}
}
if (ignored) continue;
const stat = entry.dir.statFile(entry.basename) catch continue;
if (stat.mtime > index_mtime) {
return true;
}
}
return false;
}
/// Handles checking a gitignore style pattern to determine if a path should
/// be ignored. Mostly, globs are not handled, but an extension-like pattern
/// is common and recognized by this function, so things like '*.swp' will work
/// as expected
fn isIgnored(path: []const u8, pattern: []const u8) bool {
// TODO: Handle other glob patterns (?, [], *prefix, suffix*)
// Handle *.extension pattern
if (std.mem.startsWith(u8, pattern, "*."))
return std.mem.endsWith(u8, path, pattern[1..]);
// Pattern ending with / matches directory prefix
if (std.mem.endsWith(u8, pattern, "/"))
return std.mem.startsWith(u8, path, pattern);
// Pattern starting with / matches from root
if (std.mem.startsWith(u8, pattern, "/"))
return std.mem.eql(u8, path, pattern[1..]);
// Otherwise match as substring (file/dir name anywhere in path)
return std.mem.indexOf(u8, path, pattern) != null;
}

View file

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const build_options = @import("build_options");
const WeatherProvider = @import("Provider.zig"); const WeatherProvider = @import("Provider.zig");
const Coordinates = @import("../Coordinates.zig"); const Coordinates = @import("../Coordinates.zig");
const types = @import("types.zig"); const types = @import("types.zig");
@ -119,8 +120,8 @@ fn fetchRaw(ptr: *anyopaque, allocator: std.mem.Allocator, coords: Coordinates)
const user_agent = try std.fmt.allocPrint( const user_agent = try std.fmt.allocPrint(
self.allocator, self.allocator,
"wttr/1.0 git.lerch.org/lobo/wttr {s}", "wttr/{s} git.lerch.org/lobo/wttr {s}",
.{self.identifying_email}, .{ build_options.version, self.identifying_email },
); );
defer self.allocator.free(user_agent); defer self.allocator.free(user_agent);