From b2cb5d537f98f9d3554cfbb33756bfb2ab851dbf Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Mon, 5 Jan 2026 13:34:37 -0800 Subject: [PATCH] use git version in metno identifier --- build.zig | 7 ++ build/GitVersion.zig | 161 ++++++++++++++++++++++++++++++++++++++++++ src/weather/MetNo.zig | 5 +- 3 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 build/GitVersion.zig diff --git a/build.zig b/build.zig index 533fa6e..3a8eca2 100644 --- a/build.zig +++ b/build.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const GitVersion = @import("build/GitVersion.zig"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); @@ -53,6 +54,10 @@ pub fn build(b: *std.Build) void { 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(.{ .name = "wttr", .root_module = b.createModule(.{ @@ -67,6 +72,7 @@ pub fn build(b: *std.Build) void { exe.root_module.addAnonymousImport("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.addConfigHeader(maxminddb_config); exe.linkLibrary(maxminddb); @@ -87,6 +93,7 @@ pub fn build(b: *std.Build) void { const test_options = b.addOptions(); test_options.addOption(bool, "download_geoip", download_geoip); + test_options.addOption([]const u8, "version", version); const tests = b.addTest(.{ .root_module = b.createModule(.{ diff --git a/build/GitVersion.zig b/build/GitVersion.zig new file mode 100644 index 0000000..df43430 --- /dev/null +++ b/build/GitVersion.zig @@ -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; +} diff --git a/src/weather/MetNo.zig b/src/weather/MetNo.zig index a455b32..a166ea0 100644 --- a/src/weather/MetNo.zig +++ b/src/weather/MetNo.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const build_options = @import("build_options"); const WeatherProvider = @import("Provider.zig"); const Coordinates = @import("../Coordinates.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( self.allocator, - "wttr/1.0 git.lerch.org/lobo/wttr {s}", - .{self.identifying_email}, + "wttr/{s} git.lerch.org/lobo/wttr {s}", + .{ build_options.version, self.identifying_email }, ); defer self.allocator.free(user_agent);