summaryrefslogtreecommitdiff
path: root/build.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-08 10:44:56 -0300
committerGabriel Schneider <[email protected]>2026-08-10 09:17:07 -0300
commitc3c8bbd8d8add99088c774c54bc1acf1e39ec895 (patch)
treea602212f59134909532093765a80c87f219c6c4b /build.zig
parent8aafc3fa24c7475a07259eb06cd5d217f510df98 (diff)
downloadpardes-c3c8bbd8d8add99088c774c54bc1acf1e39ec895.tar.gz
pardes-c3c8bbd8d8add99088c774c54bc1acf1e39ec895.zip
a native macOS backend: libpardes plus an AppKit shell
Adds -Dplatform=macos, a fourth backend beside tty, gui and web. Zig keeps the core, the ptys, every effect and the worker threads; Swift owns NSApplication, the window, input translation, and drawing the cell grid with CoreText. They meet at a hand-written C ABI in src/macos/pardes.h, built as a static library the app links. The ABI is src/web.zig's boundary with the wasm removed, because both hosts are the same animal: someone else owns the clock, feeds events in through flat functions, and reads one packed cell buffer out. The browser proved the shape. The one divergence is that the browser has no processes and forwards every effect to JavaScript, whereas forkpty is right here, so src/macos.zig performs them — spawn, write, resize_pty, save_file, new_file, write_dump, open_link, set_clipboard. lsp, pipe and watch are answered with nothing and marked; the core already tolerates that, since the browser answers none of them either. This deliberately inverts ghostty's split, which was studied first and is written up in docs/ghostty-macos-notes.md. Ghostty hands Zig a bare NSView*, installs its own CALayer and owns the frame clock; Swift never renders. Pardes does the opposite because its frame is already a cell grid and CoreText draws one natively — the alternative is a second hand-rolled glyph atlas, which is what most of gui.zig's 4,300 lines already are. It would also have been written blind: the Swift half cannot be compiled here. What makes the scaffold verifiable rather than dead code is that the Zig half is ordinary POSIX and builds and tests on Linux. Borrowing ghostty's best trick, build.zig translate-C's the header into the test build and src/macos.zig asserts every constant, struct layout, and exported function's arity and widths against it. That guard earned its place immediately: pardes_scroll grew a cell coordinate after the Swift view had been written against the older form. Skipped, and named as the upgrade path in docs/macos.md: the Xcode project, xcframework, lipo and codesigning ghostty needs. All four exist for distribution; a dev build is a swiftc invocation and a directory with a plist. The Swift app is a scaffold and says so — every uncertain API spelling carries an UNVERIFIED marker, and no part of it has been compiled. tty is unaffected: 75/75 snapshot scripts and both unit suites pass.
Diffstat (limited to 'build.zig')
-rw-r--r--build.zig59
1 files changed, 56 insertions, 3 deletions
diff --git a/build.zig b/build.zig
index feefecdc..8a07cf16 100644
--- a/build.zig
+++ b/build.zig
@@ -2,7 +2,7 @@ const std = @import("std");
const mupdf_build = @import("mupdf.zig");
const snap_build = @import("build/snap.zig");
-pub const Platform = enum { tty, gui, web };
+pub const Platform = enum { tty, gui, web, macos };
/// 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
@@ -24,7 +24,7 @@ pub fn build(b: *std.Build) void {
.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)") orelse .tty;
+ 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;
@@ -96,7 +96,13 @@ pub fn build(b: *std.Build) void {
const root_mod = b.createModule(.{
.target = target,
.optimize = optimize,
- .root_source_file = b.path(if (is_web) "src/web.zig" else "src/main.zig"),
+ // The browser and macOS shells are libraries whose host owns main():
+ // each roots at its own flat C ABI instead of src/main.zig.
+ .root_source_file = b.path(switch (platform) {
+ .web => "src/web.zig",
+ .macos => "src/macos.zig",
+ .tty, .gui => "src/main.zig",
+ }),
.link_libc = !is_web,
});
// helix differential harness (test/hxdiff.zig): a second compilation of
@@ -635,6 +641,53 @@ pub fn build(b: *std.Build) void {
run_web_harness.step.dependOn(web_step);
b.step("web-harness", "run the dependency-free JS/WASM DOM harness").dependOn(&run_web_harness.step);
b.getInstallStep().dependOn(web_step);
+ } else if (platform == .macos) {
+ // The native macOS shell is a static library plus a Swift app: Zig
+ // keeps the core, the ptys and every effect; Swift owns AppKit and
+ // draws the cell grid with CoreText. See docs/macos.md.
+ //
+ // The Zig half is ordinary POSIX and builds anywhere, which is what
+ // makes the boundary testable without a Mac — only `macos-app` needs
+ // one. Deliberately NOT here: an xcframework, lipo, an Xcode project
+ // and codesigning. Those exist to ship a signed universal bundle, and
+ // this is a dev build; ghostty's src/build/GhosttyXCFramework.zig is
+ // the map when distribution matters.
+ const header = b.addTranslateC(.{
+ .root_source_file = b.path("src/macos/pardes.h"),
+ .target = target,
+ .optimize = optimize,
+ });
+ // The header is hand-written, so only a test keeps it in step with the
+ // Zig side. Importing the translated header makes it a checkable
+ // artifact — see the ABI guard at the bottom of src/macos.zig.
+ root_mod.addImport("pardes.h", header.createModule());
+
+ const lib = b.addLibrary(.{
+ .name = "pardes",
+ .linkage = .static,
+ .root_module = root_mod,
+ });
+ // Swift links this archive directly, so the runtime support Zig would
+ // otherwise expect from a Zig-linked executable has to travel inside
+ // 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);
+ b.installFile("src/macos/pardes.h", "include/pardes.h");
+
+ const app = b.addSystemCommand(&.{"src/macos/build-app.sh"});
+ app.addArg(b.getInstallPath(.prefix, ""));
+ 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);
+
+ // Same step name the other native platforms use, because in this
+ // configuration theirs is not declared: the ABI guard and the core's
+ // own inline tests are what `-Dplatform=macos` has to keep green.
+ const unit_step = b.step("unit-test", "run the C-ABI guard and the core unit tests");
+ unit_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = root_mod })).step);
+ unit_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = hx_core_mod })).step);
+ web_step.dependOn(&b.addFail("web needs -Dplatform=web -Dtarget=wasm32-freestanding -Ddump=<dump.zon>").step);
} else {
// binary name: the native-shaped linux-x86_64 tty build stays `pardes`
// (the snap suite and e2e harness drive zig-out/bin/pardes); the gui