diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-10 09:58:31 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-11 09:58:59 -0300 |
| commit | eb11ab331b4e13e2b9e5a673a4c012d22fdd1d9c (patch) | |
| tree | cc993ad239451adb6f23645ee5ca001802832ed0 /build.zig | |
| parent | 38e9919a9ea9055538409b388d580c4e4c838434 (diff) | |
| download | pardes-eb11ab331b4e13e2b9e5a673a4c012d22fdd1d9c.tar.gz pardes-eb11ab331b4e13e2b9e5a673a4c012d22fdd1d9c.zip | |
macos: the AppKit shell, its icon, and the offscreen e2e harness
Diffstat (limited to 'build.zig')
| -rw-r--r-- | build.zig | 133 |
1 files changed, 125 insertions, 8 deletions
@@ -1,10 +1,19 @@ const std = @import("std"); +const builtin = @import("builtin"); const mupdf_build = @import("mupdf.zig"); const snap_build = @import("build/snap.zig"); const grammar_manifest = @import("src/grammar_manifest.zig"); pub const Platform = enum { tty, gui, web, macos }; +/// The oldest macOS pardes.app claims to run on, spelled ONCE. Three things +/// have to agree about it or the bundle is a lie: the target this build gives +/// the static library, the `-target` build-app.sh passes swiftc (which is what +/// writes LC_BUILD_VERSION, the thing dyld actually enforces), and the plist's +/// LSMinimumSystemVersion. The script gets it as an argument and sets the last +/// two from it, so there is one string and no drift. +pub const macos_min_version: std.SemanticVersion = .{ .major = 13, .minor = 0, .patch = 0 }; + /// The ZLS the language backend links, spelled once. It names the commit /// build.zig.zon pins (0.16.x branch) and is passed BOTH to ZLS's own /// `-Dversion-string` (its build.zig otherwise shells out to `git describe`, @@ -13,19 +22,41 @@ const zls_version = "0.16.1-dev+3e0d0820"; pub const TreeSitterGrammars = enum { disabled, zig, minimal, full }; pub fn build(b: *std.Build) void { + const platform = b.option(Platform, "platform", "which shell to build (tty, gui, web, macos)") orelse .tty; // Default target is the Steam Deck (deckcap's trick): x86_64 linux-gnu // with the glibc version pinned low, so a binary built on a rolling- // release host runs on SteamOS — a native build references the host's // newer versioned libm/libc symbols and dies with "GLIBC_2.4x not found" // on the deck. Override with -Dtarget= as usual. - const target = b.standardTargetOptions(.{ .default_target = .{ - .cpu_arch = .x86_64, - .os_tag = .linux, - .abi = .gnu, - .glibc_version = .{ .major = 2, .minor = 38, .patch = 0 }, + // + // -Dplatform=macos cannot take that default, and the failure is not + // subtle: swiftc links this archive, so a Steam Deck build hands ld64 ELF + // objects inside a GNU archive and the app link dies with "archive member + // '/SYM64/' not a mach-o file". On a Mac it therefore targets the host + // arch at macos_min_version — the same triple build-app.sh gives swiftc, + // so neither half of the app can disagree with the other about how old a + // macOS it supports. Naming the arch rather than leaving it null is + // ghostty's workaround (Config.genericMacOSTarget): a spelled arch + // resolves the CPU model to generic, where a bare native query would bake + // in apple_m2 and everything its LLVM backend has opinions about. + // + // Anywhere else it stays plain native, which is the whole point of the + // Linux dev loop: `zig build unit-test -Dplatform=macos` has to produce a + // binary that machine can actually execute. + const target = b.standardTargetOptions(.{ .default_target = switch (platform) { + .macos => if (builtin.os.tag.isDarwin()) .{ + .cpu_arch = builtin.target.cpu.arch, + .os_tag = .macos, + .os_version_min = .{ .semver = macos_min_version }, + } else .{}, + .tty, .gui, .web => .{ + .cpu_arch = .x86_64, + .os_tag = .linux, + .abi = .gnu, + .glibc_version = .{ .major = 2, .minor = 38, .patch = 0 }, + }, } }); const requested_optimize = b.standardOptimizeOption(.{}); - const platform = b.option(Platform, "platform", "which shell to build (tty, gui, web, macos)") orelse .tty; const static = b.option(bool, "static", "statically link") orelse false; const dump_path = b.option([]const u8, "dump", "dump .zon embedded into the web shell (-Dplatform=web)"); const is_web = platform == .web; @@ -655,14 +686,70 @@ pub fn build(b: *std.Build) void { // it or every build ends in undefined symbols at the swiftc link. lib.bundle_compiler_rt = true; lib.bundle_ubsan_rt = true; - b.installArtifact(lib); + // ...and neither does bundling stop at compiler_rt. addLibrary emits + // ONLY this module's own objects; MuPDF, tree-sitter, zstbi, ZLS and + // ghostty-vt's simdutf/highway stay in archives of their own that zig + // would hand a linker it drives itself. swiftc drives this one, is + // given one file, and fails with a page of undefined C++ symbols. So + // everything reachable is folded into a single archive first — this is + // ghostty's CombineArchivesStep, minus the non-Darwin half. + // libtool and ranlib are Apple's, so this needs a Darwin host as well + // as a Darwin target; a cross-build installs the plain archive, which + // is all the Linux dev loop ever links (nothing). + if (builtin.os.tag.isDarwin() and target.result.os.tag.isDarwin()) + b.getInstallStep().dependOn(&b.addInstallLibFile(fatArchive(b, lib), "libpardes.a").step) + else + b.installArtifact(lib); b.installFile("src/macos/pardes.h", "include/pardes.h"); const app = b.addSystemCommand(&.{"src/macos/build-app.sh"}); app.addArg(b.getInstallPath(.prefix, "")); + // The deployment target travels as an argument rather than being + // written twice: see macos_min_version. + app.addArg(b.fmt("{d}.{d}", .{ macos_min_version.major, macos_min_version.minor })); + // ...and so does the optimize mode, so swiftc compiles the shell the + // same way zig compiled the core. Hardcoding -O there meant the default + // `zig build macos-app` shipped an optimized shell around a Debug core, + // which costs 5x a frame and looks like a slow backend rather than a + // Debug build. See the note in build-app.sh. + app.addArg(@tagName(optimize)); app.has_side_effects = true; // writes a bundle outside the cache app.step.dependOn(b.getInstallStep()); - b.step("macos-app", "assemble zig-out/pardes.app (needs macOS + swiftc)").dependOn(&app.step); + // The library the script links has to BE a Mach-O one. Cross-building + // the ABI for another host is supported and tested (see the Linux dev + // loop in docs/macos.md); assembling a bundle out of it is not. + const app_step = b.step("macos-app", "assemble zig-out/pardes.app (needs macOS + swiftc)"); + if (target.result.os.tag.isDarwin()) + app_step.dependOn(&app.step) + else + app_step.dependOn(&b.addFail("macos-app needs a Darwin target; drop -Dtarget= or pass -Dtarget=native").step); + + // The offscreen AppKit suite. A second link rather than a flag on the + // app: test scaffolding does not ship in the product, and main.swift's + // top-level code is already an entry point (see src/macos/build-e2e.sh). + // Same two arguments as the app, because it must be the same link. + const e2e = b.addSystemCommand(&.{"src/macos/build-e2e.sh"}); + e2e.addArg(b.getInstallPath(.prefix, "")); + e2e.addArg(b.fmt("{d}.{d}", .{ macos_min_version.major, macos_min_version.minor })); + e2e.has_side_effects = true; // writes a binary outside the cache + e2e.step.dependOn(b.getInstallStep()); + const run_e2e = b.addSystemCommand(&.{b.getInstallPath(.prefix, "bin/pardes-macos-e2e")}); + // Relative, and pinned to the build root: the scripts and their goldens + // are source, not an install artifact, and the harness chdirs into a + // hermetic /tmp world per script — so it resolves both up front and + // must start somewhere it knows. + run_e2e.setCwd(b.path(".")); + run_e2e.addArg("test/macos-snapshots"); + // `-- --update` reaches the harness this way, exactly as the tty suite + // takes it: regenerating goldens is the same binary with one more flag. + if (b.args) |args| run_e2e.addArgs(args); + run_e2e.has_side_effects = true; // spawns shells, writes /tmp and goldens + run_e2e.step.dependOn(&e2e.step); + const e2e_step = b.step("macos-e2e", "run the offscreen AppKit snapshot suite (needs macOS + swiftc)"); + if (target.result.os.tag.isDarwin()) + e2e_step.dependOn(&run_e2e.step) + else + e2e_step.dependOn(&b.addFail("macos-e2e needs a Darwin target; drop -Dtarget= or pass -Dtarget=native").step); // Same step name the other native platforms use, because in this // configuration theirs is not declared: the ABI guard and the core's @@ -923,6 +1010,36 @@ fn failBuild(b: *std.Build, web_step: *std.Build.Step, msg: []const u8) void { b.getInstallStep().dependOn(fail); } +/// Fold `lib` and every static archive it transitively links into one file, +/// because swiftc is handed exactly one. getCompileDependencies walks the +/// module graph, so this stays correct as dependencies come and go — nothing +/// here names MuPDF or tree-sitter, and adding a third C library needs no edit. +fn fatArchive(b: *std.Build, lib: *std.Build.Step.Compile) std.Build.LazyPath { + const run = std.Build.Step.Run.create(b, "libtool libpardes.a"); + run.addArgs(&.{ "libtool", "-static", "-o" }); + const output = run.addOutputFileArg("libpardes.a"); + for (lib.getCompileDependencies(false), 0..) |dep, i| { + if (dep.kind != .lib) continue; + run.addFileArg(reindexed(b, dep.getEmittedBin(), i)); + } + return output; +} + +/// Rewrite one archive's index with Apple's ranlib, on the way past. Two of +/// Xcode's tools disagree with zig's archive layout and neither says so +/// usefully: ld64 refuses it outright ("64-bit mach-o member 'compiler_rt.o' +/// not 8-byte aligned"), and libtool silently DROPS members — a 15 MB input +/// came back as a 13 MB output with half the objects missing, which links +/// almost far enough to look like a source problem. ranlib rewrites both +/// complaints away. Ghostty hit the same two (src/build/LibtoolStep.zig); the +/// copy is because ranlib works in place and a build-cache input is not ours. +fn reindexed(b: *std.Build, archive: std.Build.LazyPath, index: usize) std.Build.LazyPath { + const run = std.Build.Step.Run.create(b, b.fmt("ranlib #{d}", .{index})); + run.addArgs(&.{ "/bin/sh", "-c", "/bin/cp \"$1\" \"$2\" && /usr/bin/ranlib \"$2\"", "_" }); + run.addFileArg(archive); + return run.addOutputFileArg(b.fmt("{d}.a", .{index})); +} + // glslc <src> -fshader-stage=<stage> -o <out> -> .spv, returned as a LazyPath // for @embedFile. Only used by the SDL3 GPU shell (-Dplatform=gui). fn compileGlsl(b: *std.Build, src: []const u8, stage: []const u8, out: []const u8) std.Build.LazyPath { |
