Something went wrong. Try again.
A GPU terminal emulator for Wayland and Windows on libghostty-vt, drawing with Vulkan, running a program on a pty or a serial port.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584// 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 = gnuOnWindows(b, b.standardTargetOptions(.{})); const optimize = b.standardOptimizeOption(.{});
const build_options = b.addOptions(); build_options.addOption([]const u8, "version", version);
// libghostty-vt, built Debug, checks a whole page's integrity after // every change to it -- every character printed with a hyperlink, say // -- which turns a burst of output into minutes of a core at full // tilt. Those checks are for its own tests; a Debug harrier builds it // ReleaseSafe, which keeps the runtime safety and drops them, unless // -Dvt-debug asks for them. const vt_debug = b.option(bool, "vt-debug", "Build libghostty-vt as Debug too, with its slow integrity checks") orelse false; const vt_optimize: @TypeOf(optimize) = if (optimize == .debug and !vt_debug) .safe else optimize; const ghostty = b.dependency("ghostty", .{ .target = target, .optimize = vt_optimize, }); const fluent = b.dependency("fluent", .{ .target = target, .optimize = optimize, }); const serial = b.dependency("serial", .{ .target = target, .optimize = optimize, }); // The font libraries are built optimized even in a Debug build, as // zig-aerie builds them: reading fontconfig's 6000-odd cached patterns // takes seven seconds in Debug and half of one optimized, and nobody // debugging harrier is stepping through a font parser. What harrier // imports of theirs, or of what they import, is built the same way, so // that each is one module with zig-aerie's. const font_optimize: @TypeOf(optimize) = if (optimize == .debug) .safe else optimize; // Built as zig-font-config builds it. const known_folders = b.dependency("known_folders", .{ .target = target, .optimize = font_optimize, }); // For its z2dimg. const font_renderer = b.dependency("zig_font_renderer", .{ .target = target, .optimize = font_optimize, }); // The same z2d zig-font-renderer draws with, for the glyphs harrier // draws itself. const z2d = b.dependency("z2d", .{ .target = target, .optimize = font_optimize, }); const font_shaper = b.dependency("zig_font_shaper", .{ .target = target, .optimize = font_optimize, }); // Built as the font libraries are, since zig-font-renderer's SVG glyphs // and struthio's `@include` both use uri, and the two must be one // module. const struthio = b.dependency("struthio", .{ .target = target, .optimize = font_optimize, }); // The decoders are built optimized for the same reason as the font // libraries: a sound file is decoded whole when the configuration is // loaded. const chime = b.dependency("chime", .{ .target = target, .optimize = font_optimize, });
// zf's fuzzy matching, for shell search and the profile selector: its // library alone, not its finder, which would ask for libvaxis. const zf = b.dependency("zf", .{ .target = target, .optimize = optimize, .with_tui = false, });
// The window system, the Vulkan renderer and the font stack, which // harrier shares with other programs. const aerie = b.dependency("aerie", .{ .target = target, .optimize = optimize, });
const exe_mod = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, .imports = &.{ .{ .name = "ghostty-vt", .module = ghostty.module("ghostty-vt") }, .{ .name = "serial", .module = serial.module("serial") }, .{ .name = "fluent", .module = fluent.module("fluent") }, .{ .name = "locales", .module = localesModule(b) }, .{ .name = "struthio", .module = struthio.module("struthio") }, .{ .name = "chime", .module = chime.module("chime") }, .{ .name = "zf", .module = zf.module("zf") }, .{ .name = "known_folders", .module = known_folders.module("known-folders") }, .{ .name = "z2d", .module = z2d.module("z2d") }, .{ .name = "font_shaper", .module = font_shaper.module("font_shaper") }, .{ .name = "build_options", .module = build_options.createModule() }, .{ .name = "aerie", .module = aerie.module("aerie") }, }, });
// What each window system wants: Wayland and D-Bus on Linux, the Win32 // API on Windows. Only the target's are fetched. const xkb: ?*std.Build.Dependency = switch (target.result.os.tag) { .linux => addLinux(b, exe_mod, target, optimize, font_optimize), .windows => windows: { if (b.lazyDependency("zigwin32", .{})) |zigwin32| exe_mod.addImport("win32", zigwin32.module("win32")); break :windows null; }, else => null, };
// The icon Windows shows on harrier's notifications, which it reads // from a file harrier writes. if (target.result.os.tag == .windows) exe_mod.addAnonymousImport("icon.png", .{ .root_source_file = b.path("dist/icons/hicolor/256x256/apps/dev.jcollie.harrier.png") });
// PNG decoding for kitty's graphics protocol, which libghostty-vt leaves // to whoever embeds it: the same wuffs it builds for its own pixel work, // so the C is compiled once. On Windows, z2dimg, the decoder the font // renderer already carries: translate-c turns the fortified string // functions of an optimized mingw build into Zig that does not compile, // and wuffs' header pulls them in. // z2dimg also converts the clipboard's bitmaps on Windows, and is // imported everywhere so that those conversions are tested anywhere. const z2dimg = font_renderer.builder.dependency("z2dimg", .{ .target = target, .optimize = font_optimize }); exe_mod.addImport("z2dimg", z2dimg.module("z2dimg")); if (target.result.os.tag != .windows) if (ghostty.builder.lazyDependency("wuffs", .{ .target = target, .optimize = vt_optimize, })) |wuffs| exe_mod.addImport("wuffs", wuffs.module("wuffs"));
const exe = b.addExecutable(.{ .name = "harrier", .root_module = exe_mod, }); // A GUI program on Windows, so that one started from Explorer or a // shortcut opens no console window beside its own. What it writes to // standard output and error is then seen only where they are // redirected: not in the console it was started from. 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 // harrier.exe and so one MSI (see ADR 0004). lld names the PDB by a // hash of its contents and stamps that into the exe, and the PDB holds // the paths of the C objects, which Zig compiles to randomly named // temporary files. The MSI never shipped the PDB, so a release's panic // traces lose nothing; a Debug build keeps one. if (target.result.os.tag == .windows and optimize != .debug) exe_mod.strip = true; b.installArtifact(exe); // Windows has no terminfo, and msys2's tic lays its directory out // differently anyway. if (target.result.os.tag != .windows) installTerminfo(b); const conpty = if (target.result.os.tag == .windows) installConPty(b, target) else null; const notices = installNotices(b, exe, target, xkb, aerie); if (target.result.os.tag == .linux) installDesktopFiles(b); // libX11's Compose tables, where zig-xkb looks beside the binary: // without them dead keys and the Compose key type nothing. if (xkb) |dep| b.installDirectory(.{ .source_dir = dep.namedLazyPath("x11_locale"), .install_dir = .prefix, .install_subdir = "share/X11/locale", });
// The installer, only when asked for: a plain Windows build installs // no MSI. The step is there whatever the target, so that asking for it // on another says why there is none rather than that there is no step. const msi_step = b.step("msi", "Build harrier'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 if (conpty) |c| addMsi(b, msi_step, exe, notices, c);
const run = b.addRunArtifact(exe); run.step.dependOn(b.getInstallStep()); run.addPassthruArgs(); b.step("run", "Run harrier").dependOn(&run.step);
const test_filter = b.option([]const u8, "test-filter", "Run only the tests whose name contains this"); const test_step = b.step("test", "Run tests"); test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = productCodeTool(b).root_module, })).step); test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = exe_mod, .filters = if (test_filter) |f| &.{f} else &.{}, // kcov and the fuzzer read nothing from the self-hosted backend. .use_llvm = true, })).step);}
/// The GNU ABI for a Windows target that names none, so that building/// needs only Zig and not Visual Studio. Asked for explicitly, because/// Ghostty's build otherwise takes MSVC for itself, and the two halves of/// harrier would be built for different ABIs.fn gnuOnWindows(b: *std.Build, target: std.Build.ResolvedTarget) std.Build.ResolvedTarget { if (target.result.os.tag != .windows or target.query.abi != null) return target; var query = target.query; query.abi = .gnu; return b.resolveTargetQuery(query);}
/// The keyboard's names for keys, and D-Bus, imported into harrier's/// module. Returns zig-xkb, whose Compose tables are installed beside the/// binary.fn addLinux( b: *std.Build, exe_mod: *std.Build.Module, target: std.Build.ResolvedTarget, optimize: std.lang.Optimize, font_optimize: std.lang.Optimize,) ?*std.Build.Dependency { const dbus_build = b.lazyImport(@This(), "dbus_service") orelse return null;
// As zig-aerie builds it, so that the two are one module. const xkb = b.lazyDependency("xkb", .{ .target = target, .optimize = font_optimize }) orelse return null;
// D-Bus: the session-bus service that makes harrier one process with // many windows, and the client a second launch hands its window to. const dbus_service = b.lazyDependency("dbus_service", .{ .target = target, .optimize = optimize }) orelse return null; const mdbus = b.lazyDependency("mdbus", .{ .target = target, .optimize = optimize }) orelse return null; const dbus_interfaces = dbus_build.addInterfaces(b, dbus_service, &.{ b.path("dbus/org.freedesktop.Application.xml"), b.path("dbus/org.freedesktop.Terminal1.xml"), b.path("dbus/org.gnome.Shell.SearchProvider2.xml"), b.path("dbus/org.freedesktop.portal.Settings.xml"), b.path("dbus/org.freedesktop.portal.Notification.xml"), b.path("dbus/org.freedesktop.host.portal.Registry.xml"), b.path("dbus/org.freedesktop.portal.OpenURI.xml"), b.path("dbus/org.freedesktop.systemd1.Manager.xml"), b.path("dbus/com.canonical.Unity.LauncherEntry.xml"), b.path("dbus/org.freedesktop.portal.GlobalShortcuts.xml"), b.path("dbus/org.freedesktop.portal.Request.xml"), });
exe_mod.addImport("xkb", xkb.module("xkb")); exe_mod.addImport("dbus_service", dbus_service.module("dbus_service")); exe_mod.addImport("dbus_interfaces", dbus_interfaces); exe_mod.addImport("mdbus", mdbus.module("mdbus")); return xkb;}
/// 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) });}
/// harrier's terminfo entry: its source, always, and, when ncurses' `tic`/// is on the path, the entry compiled into `share/terminfo` and written/// out as termcap./// The notices of the third-party software built into harrier, installed/// as `share/licenses/harrier/THIRD-PARTY-NOTICES.txt`: each package a/// module or a C source of harrier's comes from, found by walking what/// harrier is compiled from, so that it is the target's own set; and what/// is not a package but is in harrier all the same.fn installNotices(b: *std.Build, exe: *std.Build.Step.Compile, target: std.Build.ResolvedTarget, xkb: ?*std.Build.Dependency, aerie: *std.Build.Dependency) std.Build.LazyPath { const tool = b.addExecutable(.{ .name = "notices", .root_module = b.createModule(.{ .root_source_file = b.path("tools/notices.zig"), .target = b.graph.host, }), }); const run = b.addRunArtifact(tool); 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 harrier's all the same. owners.put(b.allocator, aerie.builder.dependency("vulkan_headers", .{}).builder, {}) catch @panic("OOM"); // libX11's Compose tables, installed beside harrier, are its data. if (xkb) |dep| owners.put(b.allocator, dep.builder.dependency("libx11", .{}).builder, {}) catch @panic("OOM"); for (owners.keys()) |owner| { if (owner == b) continue; run.addArgs(&.{ "--package", ownerName(owner), owner.pkg_hash }); run.addDirectoryArg(owner.path("")); } run.addArg("--zon"); run.addFileArg(b.path("build.zig.zon")); // Ghostty vendors simdutf with no notice of its own. run.addArgs(&.{ "--supplement", "simdutf" }); run.addFileArg(b.path("dist/notices/simdutf-LICENSE-MIT.txt")); // The texts of licenses a package names only in its headers: z2d's // MPL-2.0, which no file of harrier'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. libc++, for highway and simdutf, // is Apache-2.0 with the LLVM exception, which asks for no notice. 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")); } b.getInstallStep().dependOn(&b.addInstallFile(out, "share/licenses/harrier/THIRD-PARTY-NOTICES.txt").step); 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); // Headers count: highway's templates are in them. 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`,/// `zig_font_renderer.z2d.`.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 ..];}
/// Our own pseudoconsole, beside harrier.exe, as ConPty.zig looks for it:/// `conpty.dll`, and the `OpenConsole.exe` it runs in the directory named/// for the architecture, as the package's own MSBuild targets lay them out./// See ADR 0003.fn installConPty(b: *std.Build, target: std.Build.ResolvedTarget) ?ConPty { const dep = b.lazyDependency("conpty", .{}) orelse return null; const arch = switch (target.result.cpu.arch) { .x86_64 => "x64", .aarch64 => "arm64", .x86 => "x86", else => return null, }; const conpty: ConPty = .{ .arch = arch, .dll = dep.path(b.fmt("runtimes/win-{s}/native/conpty.dll", .{arch})), .host = dep.path(b.fmt("build/native/runtimes/{s}/OpenConsole.exe", .{arch})), }; b.getInstallStep().dependOn(&b.addInstallBinFile(conpty.dll, "conpty.dll").step); b.getInstallStep().dependOn(&b.addInstallBinFile(conpty.host, b.fmt("{s}/OpenConsole.exe", .{arch})).step); return conpty;}
/// Our own pseudoconsole, for one architecture: `arch` is Windows' name/// for it, which the console host's directory is named for.const ConPty = struct { arch: []const u8, dll: std.Build.LazyPath, host: std.Build.LazyPath,};
/// harrier's MSI, `harrier-VERSION-ARCH.msi`, made by zixl from/// `dist/windows/harrier.wxs` (see ADR 0004), and beside it/// `harrier-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, conpty: ConPty) 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 stage = b.addWriteFiles(); _ = stage.addCopyFile(exe.getEmittedBin(), "harrier.exe"); _ = stage.addCopyFile(conpty.dll, "conpty.dll"); _ = stage.addCopyFile(conpty.host, b.fmt("{s}/OpenConsole.exe", .{conpty.arch})); // Only the licenses of what the MSI holds: harrier's, Microsoft's for // the pseudoconsole, and the notices of what harrier.exe is built with. _ = stage.addCopyFile(b.path("LICENSES/MIT.txt"), "LICENSES/MIT.txt"); _ = stage.addCopyFile(b.path("LICENSES/CC-BY-2.0.txt"), "LICENSES/CC-BY-2.0.txt"); _ = stage.addCopyFile(b.path("dist/windows/conpty-LICENSE.txt"), "LICENSES/conpty.txt"); _ = stage.addCopyFile(notices, "LICENSES/THIRD-PARTY-NOTICES.txt");
const name = b.fmt("harrier-{s}-{s}.msi", .{ version, conpty.arch }); const msi = zixl.addInstallerPackage(b, .{ .sources = &.{b.path("dist/windows/harrier.wxs")}, .name = name, .arch = std.meta.stringToEnum(zixl.Arch, conpty.arch) orelse @panic("no zixl architecture for the pseudoconsole's"), .defines = &.{ .{ .name = "Version", .value = .{ .text = version } }, .{ .name = "ConPtyArch", .value = .{ .text = conpty.arch } }, .{ .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);}
fn productCodeTool(b: *std.Build) *std.Build.Step.Compile { return b.addExecutable(.{ .name = "product_code", .root_module = b.createModule(.{ .root_source_file = b.path("tools/product_code.zig"), .target = b.graph.host, }), });}
fn installTerminfo(b: *std.Build) void { const source = b.path("dist/terminfo/harrier.terminfo"); b.getInstallStep().dependOn(&b.addInstallFile(source, "share/terminfo/harrier.terminfo").step); // msys2's tic, on Windows, files entries by a hex digit rather than by // their first letter, as a case-insensitive file system needs. const tic = (if (b.graph.host.result.os.tag == .windows) null else b.findProgram(.{ .names = &.{"tic"} })) orelse { std.log.info("no tic: installing harrier's terminfo source only", .{}); return; }; const compile = b.addSystemCommand(&.{ tic, "-x", "-o" }); const dir = compile.addOutputDirectoryArg("terminfo"); compile.addFileArg(source); // tic makes the second name a symlink to the first, which installing // the directory would leave out: each is installed as a file. const entry = dir.path(b, "h/harrier"); for ([_][]const u8{ "h/harrier", "x/xterm-harrier" }) |name| b.getInstallStep().dependOn(&b.addInstallFile(entry, b.fmt("share/terminfo/{s}", .{name})).step); const termcap = b.addSystemCommand(&.{ tic, "-C", "-x" }); termcap.addFileArg(source); b.getInstallStep().dependOn(&b.addInstallFile(termcap.captureStdOut(.{}), "share/terminfo/harrier.termcap").step);}
/// harrier's icon in the binary, as resource 1, which Explorer and the/// taskbar show and the window class loads: 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 tool = b.addExecutable(.{ .name = "ico", .root_module = b.createModule(.{ .root_source_file = b.path("tools/ico.zig"), .target = b.graph.host, }), }); const run = b.addRunArtifact(tool); const ico = run.addOutputFileArg("harrier.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.harrier.png", .{ n, n }))); const wf = b.addWriteFiles(); _ = wf.addCopyFile(ico, "harrier.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 }); // The message table Event Viewer reads the Application log's events // through (see src/windows/eventlog.zig): one block of one message, ID // 1, the UTF-16 text `%1`, padded to four bytes. _ = wf.add("messages.bin", &[_]u8{ 1, 0, 0, 0, // blocks 1, 0, 0, 0, 1, 0, 0, 0, 16, 0, 0, 0, // IDs 1 to 1, at 16 12, 0, 1, 0, // its length, and Unicode '%', 0, '1', 0, 0, 0, 0, 0, }); const rc = wf.add("harrier.rc", b.fmt( \\#pragma code_page(65001) \\1 ICON "harrier.ico" \\1 MESSAGETABLE "messages.bin" \\ \\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", "harrier" \\ VALUE "FileVersion", "{[version]s}" \\ VALUE "InternalName", "harrier" \\ VALUE "LegalCopyright", "© 2026 Jeffrey C. Ollie; MIT License" \\ VALUE "OriginalFilename", "harrier.exe" \\ VALUE "ProductName", "harrier" \\ VALUE "ProductVersion", "{[version]s}" \\ VALUE "Comments", "A GPU terminal emulator" \\ END \\ END \\ BLOCK "VarFileInfo" \\ BEGIN \\ VALUE "Translation", 0x409, 1200 \\ END \\END \\ , .{ .numbers = numbers, .version = version })); exe.root_module.addWin32ResourceFile(.{ .file = rc });}
/// The icon and the GNOME Shell search provider, always; and the desktop/// entry, the D-Bus service file and the/// systemd user unit when `-Dbindir` says where the binary is installed,/// since each of them has to name it by its absolute path and Zig 0.17/// does not tell a build script 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.harrier.png", .{ n, n }); b.getInstallStep().dependOn(&b.addInstallFile(b.path(b.fmt("dist/{s}", .{sub})), b.fmt("share/{s}", .{sub})).step); } b.getInstallStep().dependOn(&b.addInstallFile( b.path("dist/dev.jcollie.harrier.search-provider.ini"), "share/gnome-shell/search-providers/dev.jcollie.harrier.search-provider.ini", ).step);
const bindir = b.option([]const u8, "bindir", "Where harrier is installed, absolute; installs the desktop, D-Bus and systemd files that name it") orelse return; // Embedded rather than read: a build script cannot read files while it // configures. const files = [_]struct { []const u8, []const u8, []const u8 }{ .{ "dev.jcollie.harrier.desktop", "share/applications", @embedFile("dist/dev.jcollie.harrier.desktop.in") }, .{ "dev.jcollie.harrier.service", "share/dbus-1/services", @embedFile("dist/dev.jcollie.harrier.service.in") }, .{ "app-dev.jcollie.harrier.service", "share/systemd/user", @embedFile("dist/app-dev.jcollie.harrier.service.in") }, }; const wf = b.addWriteFiles(); for (files) |f| { const template = f[2]; const filled = std.mem.replaceOwned(u8, b.allocator, template, "@bindir@", bindir) catch @panic("OOM"); const out = wf.add(f[0], filled); b.getInstallStep().dependOn(&b.addInstallFile(out, b.fmt("{s}/{s}", .{ f[1], f[0] })).step); }}