Something went wrong. Try again.
A history browser for git, a replacement for gitk, drawn with Vulkan on Wayland.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
const std = @import("std");
/// build.zig.zon is the one place the version is written.const version: []const u8 = @import("build.zig.zon").version;
pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{});
const build_options = b.addOptions(); build_options.addOption([]const u8, "version", version);
const aerie = b.dependency("aerie", .{ .target = target, .optimize = optimize }); // Windows has no zone files to read, so the database is built in. const datetime = b.dependency("datetime", .{ .target = target, .optimize = optimize, .@"embed-tzdata" = target.result.os.tag == .windows, }); const zregex = b.dependency("zregex", .{ .target = target, .optimize = optimize }); const fluent = b.dependency("fluent", .{ .target = target, .optimize = optimize }); // fluent builds datetime without the zone data built in, which makes it // a second module of the same files; it is given grebe's instead. fluent.module("fluent").addImport("datetime", datetime.module("datetime")); // Built as zig-aerie builds its font libraries, since zig-font-renderer // and struthio's `@include` both use uri, and the two must be one // module. const font_optimize: @TypeOf(optimize) = if (optimize == .debug) .safe else optimize; const struthio = b.dependency("struthio", .{ .target = target, .optimize = font_optimize }); // As zig-font-config, which uses it too, builds it. const known_folders = b.dependency("known_folders", .{ .target = target, .optimize = font_optimize });
const mod = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, .imports = &.{ .{ .name = "aerie", .module = aerie.module("aerie") }, .{ .name = "datetime", .module = datetime.module("datetime") }, .{ .name = "zregex", .module = zregex.module("zregex") }, .{ .name = "fluent", .module = fluent.module("fluent") }, .{ .name = "locales", .module = localesModule(b) }, .{ .name = "struthio", .module = struthio.module("struthio") }, .{ .name = "known_folders", .module = known_folders.module("known-folders") }, .{ .name = "build_options", .module = build_options.createModule() }, }, });
if (target.result.os.tag == .windows) if (b.lazyDependency("zigwin32", .{})) |zigwin32| mod.addImport("win32", zigwin32.module("win32"));
const exe = b.addExecutable(.{ .name = "grebe", .root_module = mod }); // A GUI program on Windows, so that one started from Explorer opens no // console window beside its own. if (target.result.os.tag == .windows) { exe.subsystem = .windows; addWindowsResources(b, exe); } // No PDB for a Windows release build, so that a commit makes one // grebe.exe and so one MSI: lld names the PDB by a hash of its contents // and stamps that into the exe. A Debug build keeps one. if (target.result.os.tag == .windows and optimize != .debug) mod.strip = true; b.installArtifact(exe); if (target.result.os.tag == .linux) installDesktopFiles(b);
// The installer, only when asked for. The step is there whatever the // target, so that asking for it on another says why there is none. const msi_step = b.step("msi", "Build grebe's Windows installer (an MSI)"); if (target.result.os.tag != .windows) msi_step.dependOn(&b.addFail("the MSI is built for a Windows target: pass -Dtarget=x86_64-windows-gnu").step) else addMsi(b, msi_step, exe, installNotices(b, exe, target, aerie), target);
const run = b.addRunArtifact(exe); run.addPassthruArgs(); const run_step = b.step("run", "Run grebe"); run_step.dependOn(&run.step);
const tests = b.addTest(.{ .name = "grebe-test", .root_module = mod, // The self-hosted backend emits no coverage instrumentation and no // debug info kcov can read; LLVM does both. .use_llvm = true, }); const run_tests = b.addRunArtifact(tests); const test_step = b.step("test", "Run the tests"); test_step.dependOn(&run_tests.step); // The build's own tools, for the machine running the build. for ([_]*std.Build.Step.Compile{ productCodeTool(b), hostTool(b, "ico"), hostTool(b, "notices") }) |tool| test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = tool.root_module })).step); // Again, with the repositories the tests make using SHA-256 object IDs, // which are longer than SHA-1's. const run_tests_sha256 = b.addRunArtifact(tests); run_tests_sha256.setEnvironmentVariable("GREBE_TEST_OBJECT_FORMAT", "sha256"); test_step.dependOn(&run_tests_sha256.step);
// Compiles everything without running anything. const check_step = b.step("check", "Check that everything compiles"); check_step.dependOn(&tests.step); check_step.dependOn(&exe.step); for ([_][]const u8{ "ico", "notices", "product_code" }) |tool| check_step.dependOn(&hostTool(b, tool).step);}
/// The icon, always; and the desktop entry when `-Dbindir` says where the/// binary is installed, since the entry names it by its absolute path and/// a build script is not told its install prefix.fn installDesktopFiles(b: *std.Build) void { for ([_]u32{ 16, 24, 32, 48, 64, 128, 256, 512 }) |n| { const sub = b.fmt("icons/hicolor/{d}x{d}/apps/dev.jcollie.grebe.png", .{ n, n }); b.getInstallStep().dependOn(&b.addInstallFile(b.path(b.fmt("dist/{s}", .{sub})), b.fmt("share/{s}", .{sub})).step); } const bindir = b.option([]const u8, "bindir", "Where grebe is installed, absolute; installs the desktop entry, which names it") orelse return; // Embedded rather than read: a build script cannot read files while it // configures. const template = @embedFile("dist/dev.jcollie.grebe.desktop.in"); const filled = std.mem.replaceOwned(u8, b.allocator, template, "@bindir@", bindir) catch @panic("OOM"); const out = b.addWriteFiles().add("dev.jcollie.grebe.desktop", filled); b.getInstallStep().dependOn(&b.addInstallFile(out, "share/applications/dev.jcollie.grebe.desktop").step);}
/// A program of `tools/`, for the machine running the build, never/// whatever `-Dtarget` grebe is built for.fn hostTool(b: *std.Build, name: []const u8) *std.Build.Step.Compile { return b.addExecutable(.{ .name = name, .root_module = b.createModule(.{ .root_source_file = b.path(b.fmt("tools/{s}.zig", .{name})), .target = b.graph.host, }), });}
fn productCodeTool(b: *std.Build) *std.Build.Step.Compile { return hostTool(b, "product_code");}
/// grebe's icon in the binary, as resource 1, which Explorer and the/// taskbar show: the PNGs of the hicolor theme up to 256 pixels, packed/// into an icon file by `tools/ico.zig`. And its version information,/// which Explorer shows in the file's properties and Task Manager names/// the process by.fn addWindowsResources(b: *std.Build, exe: *std.Build.Step.Compile) void { const run = b.addRunArtifact(hostTool(b, "ico")); const ico = run.addOutputFileArg("grebe.ico"); for ([_]u32{ 16, 24, 32, 48, 64, 128, 256 }) |n| run.addFileArg(b.path(b.fmt("dist/icons/hicolor/{d}x{d}/apps/dev.jcollie.grebe.png", .{ n, n }))); const wf = b.addWriteFiles(); _ = wf.addCopyFile(ico, "grebe.ico"); const v = std.SemanticVersion.parse(version) catch @panic("the version is not a semantic version"); const numbers = b.fmt("{d},{d},{d},0", .{ v.major, v.minor, v.patch }); const rc = wf.add("grebe.rc", b.fmt( \\#pragma code_page(65001) \\1 ICON "grebe.ico" \\ \\1 VERSIONINFO \\FILEVERSION {[numbers]s} \\PRODUCTVERSION {[numbers]s} \\FILEOS 0x40004 \\FILETYPE 0x1 \\BEGIN \\ BLOCK "StringFileInfo" \\ BEGIN \\ BLOCK "040904B0" \\ BEGIN \\ VALUE "CompanyName", "Jeffrey C. Ollie" \\ VALUE "FileDescription", "grebe" \\ VALUE "FileVersion", "{[version]s}" \\ VALUE "InternalName", "grebe" \\ VALUE "LegalCopyright", "© 2026 Jeffrey C. Ollie; MIT License" \\ VALUE "OriginalFilename", "grebe.exe" \\ VALUE "ProductName", "grebe" \\ VALUE "ProductVersion", "{[version]s}" \\ VALUE "Comments", "A history browser for git" \\ END \\ END \\ BLOCK "VarFileInfo" \\ BEGIN \\ VALUE "Translation", 0x409, 1200 \\ END \\END \\ , .{ .numbers = numbers, .version = version })); exe.root_module.addWin32ResourceFile(.{ .file = rc });}
/// The notices of the third-party software built into grebe.exe, written/// by `tools/notices.zig` from every package the build compiles in.fn installNotices(b: *std.Build, exe: *std.Build.Step.Compile, target: std.Build.ResolvedTarget, aerie: *std.Build.Dependency) std.Build.LazyPath { const run = b.addRunArtifact(hostTool(b, "notices")); const out = run.addOutputFileArg("THIRD-PARTY-NOTICES.txt");
var owners: std.AutoArrayHashMapUnmanaged(*std.Build, void) = .empty; var seen: std.AutoArrayHashMapUnmanaged(*std.Build.Module, void) = .empty; collectOwners(b, exe.root_module, &owners, &seen); // vk.xml is read only by vulkan-zig's generator, at build time, for // zig-aerie: what it makes of it is grebe's all the same. owners.put(b.allocator, aerie.builder.dependency("vulkan_headers", .{}).builder, {}) catch @panic("OOM"); // A package more than one dependency fetches, zigwin32 for one, is // one package: listed once. var hashes: std.StringArrayHashMapUnmanaged(void) = .empty; for (owners.keys()) |owner| { if (owner == b) continue; if ((hashes.getOrPut(b.allocator, owner.pkg_hash) catch @panic("OOM")).found_existing) continue; run.addArgs(&.{ "--package", ownerName(owner), owner.pkg_hash }); run.addDirectoryArg(owner.path("")); } run.addArg("--zon"); run.addFileArg(b.path("build.zig.zon")); // The texts of licenses a package names only in its headers: z2d's // MPL-2.0, which no file of grebe's is under, so it is not in // LICENSES, where REUSE would report it unused. run.addArg("--license-dir"); run.addDirectoryArg(b.path("dist/notices")); // Zig's standard library and runtime, in every program it builds; and // on Windows the MinGW-w64 C runtime. run.addArgs(&.{ "--text", "Zig" }); run.addFileArg(b.path("dist/notices/zig-LICENSE.txt")); if (target.result.os.tag == .windows) { run.addArgs(&.{ "--text", "MinGW-w64 runtime" }); run.addFileArg(b.path("dist/notices/mingw-w64-COPYING.txt")); } return out;}
/// The builders of `m`, and of every module, C source and header/// directory it brings in.fn collectOwners( b: *std.Build, m: *std.Build.Module, owners: *std.AutoArrayHashMapUnmanaged(*std.Build, void), seen: *std.AutoArrayHashMapUnmanaged(*std.Build.Module, void),) void { if ((seen.getOrPut(b.allocator, m) catch @panic("OOM")).found_existing) return; owners.put(b.allocator, m.owner, {}) catch @panic("OOM"); if (m.root_source_file) |p| addOwner(b, p, owners); for (m.import_table.values()) |dep| collectOwners(b, dep, owners, seen); for (m.include_dirs.items) |dir| switch (dir) { .path, .path_system, .path_after, .embed_path => |p| addOwner(b, p, owners), else => {}, }; for (m.link_objects.items) |lo| switch (lo) { .other_step => |c| collectOwners(b, c.root_module, owners, seen), .c_source_file => |f| addOwner(b, f.file, owners), .c_source_files => |f| addOwner(b, f.root, owners), .static_path => |p| addOwner(b, p, owners), else => {}, };}
fn addOwner(b: *std.Build, p: std.Build.LazyPath, owners: *std.AutoArrayHashMapUnmanaged(*std.Build, void)) void { const owner = switch (p) { .src_path => |s| s.owner, .dependency => |d| d.dependency.builder, else => return, }; owners.put(b.allocator, owner, {}) catch @panic("OOM");}
/// A dependency's name, as its parent knows it: the last of `dep_prefix`.fn ownerName(owner: *std.Build) []const u8 { const prefix = std.mem.trimEnd(u8, owner.dep_prefix, "."); const dot = std.mem.findScalarLast(u8, prefix, '.') orelse return prefix; return prefix[dot + 1 ..];}
/// grebe's MSI, `grebe-VERSION-ARCH.msi`, made by zixl from/// `dist/windows/grebe.wxs`, as harrier's is; and beside it/// `grebe-VERSION-ARCH.msi.ProductCode`, the GUID the winget manifest is/// given. What the MSI holds is gathered into one directory, laid out as/// it is installed, which the source reads as `$(var.SourceDir)`.fn addMsi(b: *std.Build, step: *std.Build.Step, exe: *std.Build.Step.Compile, notices: std.Build.LazyPath, target: std.Build.ResolvedTarget) void { const zixl = b.lazyImport(@This(), "zixl") orelse return; // The same zixl, for the machine running the build, as the one // addInstallerPackage runs. const zixl_dep = b.lazyDependency("zixl", .{ .optimize = .ReleaseSafe }) orelse return; const arch: []const u8 = switch (target.result.cpu.arch) { .x86_64 => "x64", .aarch64 => "arm64", else => @panic("no MSI for this architecture"), };
const stage = b.addWriteFiles(); _ = stage.addCopyFile(exe.getEmittedBin(), "grebe.exe"); // Only the licenses of what the MSI holds: grebe's, its icon's, and the // notices of what grebe.exe is built with. _ = stage.addCopyFile(b.path("LICENSES/MIT.txt"), "LICENSES/MIT.txt"); _ = stage.addCopyFile(b.path("LICENSES/CC-BY-4.0.txt"), "LICENSES/CC-BY-4.0.txt"); _ = stage.addCopyFile(notices, "LICENSES/THIRD-PARTY-NOTICES.txt");
const name = b.fmt("grebe-{s}-{s}.msi", .{ version, arch }); const msi = zixl.addInstallerPackage(b, .{ .sources = &.{b.path("dist/windows/grebe.wxs")}, .name = name, .arch = std.meta.stringToEnum(zixl.Arch, arch) orelse @panic("no zixl architecture for this one"), .defines = &.{ .{ .name = "Version", .value = .{ .text = version } }, .{ .name = "SourceDir", .value = .{ .directory = stage.getDirectory() } }, }, // Given here rather than left to zixl to read, so that a change to // it makes the MSI again. .timestamp = if (b.graph.environ_map.get("SOURCE_DATE_EPOCH")) |t| std.fmt.parseInt(i64, t, 10) catch std.debug.panic("SOURCE_DATE_EPOCH is not a number: {s}", .{t}) else null, }); step.dependOn(&b.addInstallFile(msi.package, name).step);
const export_property = b.addRunArtifact(zixl_dep.artifact("zixl-info")); export_property.addArg("export"); export_property.addFileArg(msi.package); export_property.addArg("Property"); const product_code = b.addRunArtifact(productCodeTool(b)); product_code.addFileArg(export_property.captureStdOut(.{})); const code = product_code.addOutputFileArg("ProductCode"); step.dependOn(&b.addInstallFile(code, b.fmt("{s}.ProductCode", .{name})).step);}
/// The translations, `locales/<tag>.ftl`, embedded: a module whose `all`/// lists each tag with its text, the source language first.fn localesModule(b: *std.Build) *std.Build.Module { // The source language, first, and then each translation. A new one is a // file in locales/ and a line here. const tags = [_][]const u8{"en-US"}; const wf = b.addWriteFiles(); var src: std.ArrayList(u8) = .empty; src.appendSlice(b.allocator, "pub const Entry = struct { tag: []const u8, source: []const u8 };\npub const all = [_]Entry{\n") catch @panic("OOM"); for (tags) |tag| { const name = b.fmt("{s}.ftl", .{tag}); _ = wf.addCopyFile(b.path(b.fmt("locales/{s}", .{name})), name); src.appendSlice(b.allocator, b.fmt(" .{{ .tag = \"{s}\", .source = @embedFile(\"{s}\") }},\n", .{ tag, name })) catch @panic("OOM"); } src.appendSlice(b.allocator, "};\n") catch @panic("OOM"); return b.createModule(.{ .root_source_file = wf.add("locales.zig", src.items) });}