summaryrefslogtreecommitdiff
path: root/build.zig
diff options
context:
space:
mode:
Diffstat (limited to 'build.zig')
-rw-r--r--build.zig1355
1 files changed, 397 insertions, 958 deletions
diff --git a/build.zig b/build.zig
index f94a4bcf..98ba2901 100644
--- a/build.zig
+++ b/build.zig
@@ -7,30 +7,29 @@ const grammar_manifest = @import("src/grammar_manifest.zig");
/// option in `shellOptions` for why that word is load-bearing.
const zon = @import("build.zig.zon");
-/// `esp32p4` is not a shell in this package at all: it is one freestanding
-/// OBJECT, compiled for riscv32-freestanding, which the `zig_p4` firmware
-/// package links beside its own `_start`. See the esp32p4 branch below.
pub const Platform = enum { tty, gui, web, macos, esp32p4 };
-/// 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` the app's swiftc link is given (which is
-/// what writes LC_BUILD_VERSION, the thing dyld actually enforces), and the
-/// plist's LSMinimumSystemVersion. The macos branch below derives the last two
-/// from this, so there is one string and no drift.
+const PerfBuild = struct {
+ compiler: []const u8,
+ target: []const u8,
+ cpu: []const u8,
+ driver_optimize: []const u8,
+ core_optimize: []const u8,
+ c_optimize: []const u8,
+ terminal_optimize: []const u8,
+ platform: []const u8,
+ grammars: []const u8,
+ mupdf: bool,
+ jpx: bool,
+ tracy: bool,
+ quic: bool,
+ theme_animation: bool,
+ terminal_simd: bool,
+ prebuilt_shaders: bool,
+};
+
pub const macos_min_version: std.SemanticVersion = .{ .major = 13, .minor = 0, .patch = 0 };
-/// The ZLS the language backend links. 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`, which fails on a fetched
-/// package that has no .git) and to `SPC l i`.
-///
-/// It is spelled TWICE — here and in `.dependencies.zls.url` — and the comment
-/// that used to claim "spelled once" was wrong. The duplication is unavoidable
-/// (the semver half exists nowhere in the manifest), so the `comptime` block
-/// below makes the two AGREE by construction: a .zon bump that forgets this
-/// line is now a build error instead of a `SPC l i` that names a build nobody
-/// linked.
const zls_version = "0.16.1-dev+3e0d0820";
comptime {
@@ -46,12 +45,6 @@ comptime {
}
pub const TreeSitterGrammars = enum { disabled, zig, minimal, full };
-/// The GUI shell's shaders, spelled ONCE. The runtime SPIR-V imports, their
-/// EffectCode source imports, and `zig build shaders` all read this list. That
-/// last step writes BOTH the bytecode and its exact source snapshot into the
-/// tracked shaders/prebuilt/ pair. Each name is `shaders/<name>.glsl`, and the
-/// `.vert`/`.frag` in it is also the glslc shader stage — adding a shader is
-/// adding a name here plus the @embedFile in src/gui/gui.zig.
const gui_shaders = [_][]const u8{
"ui.vert", "ui.frag",
"overlay.vert", "overlay.frag",
@@ -59,75 +52,16 @@ const gui_shaders = [_][]const u8{
"crt.vert", "crt.frag",
};
-/// Every GUI shader is also a source import for EffectCode. Live builds import
-/// shaders/*.glsl; prebuilt builds import the source snapshot paired with the
-/// committed SPIR-V, so the builtin cannot print code other than what produced
-/// the bytecode that build executes.
pub fn build(b: *std.Build) void {
- // `-Dplatform` names ONE shell. Absent, this build makes BOTH native
- // shells — the tty cli and the SDL gui — because those two together are
- // what installing pardes means, and asking for them one at a time is two
- // invocations that a person has to remember are two.
- //
- // Everything below derives from `platform`, which is the PRIMARY shell:
- // the one rooted at `root_mod`, the one `unit-test` runs, and the one the
- // snapshot/harness suites drive. `also_gui` adds the second beside it and
- // changes nothing about the first.
const requested_platform = b.option(Platform, "platform", "which shell to build (tty, gui, web, macos, esp32p4); absent builds the tty cli and the SDL gui together");
const platform = requested_platform orelse .tty;
const also_gui = requested_platform == null;
- // WHERE A BARE `zig build` PUTS THE BINARIES: `~/.local/bin`, not
- // `zig-out/bin`. The default build IS the install — a `pardes` that is not
- // on PATH afterwards is one more command to remember — and the two shells
- // it makes are exactly the two a person runs.
- //
- // Two conditions, and the second one is not the obvious one:
- // * no shell was NAMED. `-Dplatform=web` keeps writing `zig-out/web`,
- // which its docs name, and `-Dplatform=macos` keeps its lib/include
- // layout.
- // * nothing else has already said where to install: no DESTDIR, no
- // `--prefix`, no `--prefix-*dir`. See `prefixIsUntouched`, which also
- // records the one spelling it cannot detect.
- //
- // It does NOT depend on which step was asked for, because build.zig cannot
- // know that (build_runner keeps the step names in a local and resolves them
- // after `build()` returns). That is why the dev binaries below install to
- // `<prefix>/dev` rather than `<prefix>/bin`: `zig build perf` redirects the
- // prefix too, and a 200 MB Debug benchmark must not land on a PATH.
- //
- // `resolveInstallPrefix` is what recomputes the derived lib/bin/include
- // directories; assigning `install_prefix` alone would leave `exe_dir`
- // pointing into zig-out.
if (also_gui and prefixIsUntouched(b)) {
if (b.graph.environ_map.get("HOME")) |home| {
b.resolveInstallPrefix(b.pathJoin(&.{ home, ".local" }), .{});
- // Said out loud, because a build that moves a file somewhere the
- // command line did not mention should not be silent about it — and
- // because it is the only signal in the one case this cannot detect,
- // `--prefix` given as the default path spelled absolutely.
std.debug.print("pardes: installing into {s} (override with --prefix)\n", .{b.install_prefix});
}
}
- // 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.
- //
- // -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 esp32p4_target: std.Target.Query = .{
.cpu_arch = .riscv32,
.os_tag = .freestanding,
@@ -142,16 +76,6 @@ pub fn build(b: *std.Build) void {
.os_tag = .macos,
.os_version_min = .{ .semver = macos_min_version },
} else .{},
- // The P4 firmware target, spelled out here so `-Dplatform=esp32p4` alone is a
- // working command line. The CPU FEATURES are part of that spelling and
- // are not optional: the object this build emits is linked into an image
- // whose other halves are compiled `generic_rv32+m+a+f+c+zicsr+zifencei`,
- // and `f` decides the float ABI. Leaving the model implicit produced a
- // soft-float object and `ld.lld: cannot link object files with different
- // floating-point ABI` — at LINK time in the other repo, far from here.
- // Espressif's GCC adds the vendor extensions xesploop/xespv2p1 on top;
- // upstream LLVM has neither and ordinary code never emits them, so this
- // matches the base ISA the firmware uses exactly.
.esp32p4 => esp32p4_target,
.tty, .gui, .web => .{
.cpu_arch = .x86_64,
@@ -161,13 +85,6 @@ pub fn build(b: *std.Build) void {
},
},
});
- // `standardTargetOptions` honours `default_target` ONLY when `-Dtarget` is absent, so the
- // documented `-Dplatform=esp32p4 -Dtarget=riscv32-freestanding` discarded the CPU features above and
- // silently produced a soft-float object. The features are not a preference here - `f` decides
- // the float ABI, and the object is linked into an image whose other halves have it - so esp32p4 takes
- // the pinned query whatever was asked for. `-Dtarget` stays accepted, and the check further down
- // still rejects anything that is not riscv32-freestanding, so a wrong `-Dtarget` is an error
- // rather than something quietly ignored.
const target = if (platform == .esp32p4) b.resolveTargetQuery(esp32p4_target) else requested_target;
const requested_optimize = b.standardOptimizeOption(.{});
const static = b.option(bool, "static", "statically link") orelse false;
@@ -180,78 +97,42 @@ pub fn build(b: *std.Build) void {
&.{};
const is_web = platform == .web;
const is_esp32p4 = platform == .esp32p4;
- // Platforms with no host libc: the browser and the P4 firmware. Every
- // dependency below that exists only because a target links libc — the
- // image decoder, ZLS, ghostty's C++ simd, MuPDF — is off for both, and the
- // reason is freestanding-ness rather than the browser.
const freestanding_core = is_web or is_esp32p4;
+ const enable_quic = b.option(bool, "quic", "optional 9P-over-QUIC transport using system OpenSSL 3.6+ (default off)") orelse false;
+ if (enable_quic and freestanding_core) @panic("-Dquic=true needs a native platform");
+ const ninep_options = b.addOptions();
+ ninep_options.addOption(bool, "quic", enable_quic);
const enable_mupdf = b.option(bool, "mupdf", "native PDF rendering with MuPDF (AGPL/commercial; native default on, web/esp32p4 off; -Dmupdf=false disables)") orelse !freestanding_core;
- // JPEG 2000, and with it scanned PDFs: a scan is one /JPXDecode image per
- // page, so without this MuPDF decodes nothing and every page comes back
- // blank. On by default — a viewer that cannot open scans is the more
- // surprising default — and a switch at all because it is 31 files of
- // third-party C parsing untrusted input. See the OPENJPEG block in
- // mupdf.zig.
const enable_jpx = b.option(bool, "jpx", "JPEG 2000 in PDFs, for scanned documents (default on; -Djpx=false drops openjpeg)") orelse true;
const is_web_target = target.result.cpu.arch == .wasm32 and target.result.os.tag == .freestanding;
const is_esp32p4_target = target.result.cpu.arch == .riscv32 and target.result.os.tag == .freestanding;
- // wasm: size is the budget.
- //
- // esp32p4: Debug is not a supported mode, and `-Doptimize` defaulting to it made the naive
- // `zig build -Dplatform=esp32p4` produce an object that cannot run. Debug wraps every tier in
- // `allocators.zig` in a `DebugAllocator`, whose metadata is page-granular; the board hands the
- // editor a 384 KiB heap and one 4 KiB page per size class does not fit in it, so the image
- // links and then dies in `Pardes.init`. ReleaseFast rather than ReleaseSmall because it was
- // measured on the die and not chosen: against ReleaseSmall it is 13% off the fixed
- // per-keystroke cost and 36% off the per-character cost, for 35% more flash on a partition
- // that is 39% used. See experiments/report.typ. An explicit `-Doptimize=` still wins, so
- // ReleaseSmall remains one flag away when flash matters more than latency.
const optimize = if (is_web)
.ReleaseSmall
else if (is_esp32p4_target and requested_optimize == .Debug)
.ReleaseFast
else
requested_optimize;
- // The vendored C is never what we are debugging, and at -O0 it dominates
- // the app: 90% of a Debug startup is tree-sitter's query analyser
- // (perf: ts_query__perform_analysis + ts_lookahead_iterator__next), and
- // stb_image decodes at a crawl. Building the C optimized whatever the Zig
- // mode takes a Debug boot from ~710ms to ~210ms — the same treatment
- // ghostty's simdutf/highway already get here. Zig code keeps its mode.
+ const test_build = b.step("test-build", "compile unit-test programs without running them");
+ const test_nonce: ?*std.Build.Module = if (b.option(bool, "test-rebuild", "force fresh Zig test compilation while retaining cached C dependencies") orelse false) blk: {
+ const files = b.addTempFiles();
+ const source = files.add("test_nonce.zig", b.fmt("pub const nonce = {d};\n", .{std.Io.Clock.awake.now(b.graph.io).nanoseconds}));
+ break :blk b.createModule(.{ .root_source_file = source, .target = target, .optimize = optimize });
+ } else null;
+ if (freestanding_core) test_build.dependOn(&b.addFail("test-build needs a native platform").step);
+ const unit_profile = b.addExecutable(.{
+ .name = "pardes-unit-profile",
+ .root_module = b.createModule(.{
+ .target = target,
+ .optimize = optimize,
+ .root_source_file = b.path("test/unit_profile.zig"),
+ }),
+ });
const c_optimize: std.builtin.OptimizeMode = if (optimize == .Debug) .ReleaseFast else optimize;
- // The browser keeps its useful default grammar without acquiring a host
- // libc contract: Tree-sitter and the generated Zig parser are linked into
- // the freestanding module against src/web/libc's tiny in-module shim.
const default_grammars: TreeSitterGrammars = if (is_web) .zig else if (is_esp32p4) .disabled else .full;
const requested_grammars = b.option(TreeSitterGrammars, "tree-sitter", "tree-sitter grammar set: disabled, zig, minimal (c/c++/zig), full");
const tree_sitter_grammars = requested_grammars orelse default_grammars;
const tracy = b.option([]const u8, "tracy", "enable Tracy profiling; supply the path to a Tracy source checkout");
- // Who signs pardes.app. Ad-hoc ("-") is what makes a bundle launchable on
- // the machine that built it and needs no keychain; a Developer ID here is
- // what makes one launchable on someone else's. See the macos branch below.
const macos_identity = b.option([]const u8, "macos-identity", "codesigning identity for pardes.app (default: ad-hoc)") orelse "-";
- // Shader compilation is the one build input that needs a tool nothing else
- // here needs: glslc, which ships with the Vulkan SDK / shaderc and is not
- // on a stock machine. It is also the input that changes least often, so
- // -Dprebuilt-shaders decouples the two: the SPIR-V compiled from
- // shaders/*.glsl and its exact GLSL snapshot are committed together under
- // shaders/prebuilt/, and this flag embeds that pair instead of shelling
- // out. `-Dplatform=gui` then builds with nothing but a C toolchain.
- //
- // Off by default when a shell was NAMED, because it trades a dependency for
- // a freshness problem: with the flag on, the live .glsl sources are not
- // build inputs, so editing one changes neither runtime bytecode nor
- // EffectCode until someone runs `zig build shaders` (see below). Somebody
- // who typed `-Dplatform=gui` is working on the gui and wants the shaders
- // that are actually in the tree.
- //
- // ON by default for the bare `zig build`, which is a different question
- // with a different right answer. That build makes the gui BESIDE the cli,
- // for a person who asked for pardes rather than for a graphics toolchain,
- // and compiling shaders live would make `zig build` fail on any machine
- // without a Vulkan SDK — a dependency the cli never needed and that this
- // build did not have before the gui joined it. The committed SPIR-V exists
- // exactly so that arrangement is possible.
const prebuilt_shaders = b.option(bool, "prebuilt-shaders", "embed the committed shaders/prebuilt/*.spv instead of running glslc (default: on for a bare `zig build`, off when -Dplatform names a shell)") orelse also_gui;
// The browser shell is a freestanding wasm core plus ordinary web files.
@@ -270,12 +151,6 @@ pub fn build(b: *std.Build) void {
if (!enable_mupdf)
pdf_scroll_bench_step.dependOn(&b.addFail("pdf-scroll-bench is unavailable with -Dmupdf=false").step);
- // The other half of -Dprebuilt-shaders: recompile every shader and update
- // BOTH tracked artifacts — SPIR-V plus the exact GLSL EffectCode will print.
- // A machine without glslc can then consume the pair without consulting the
- // live source. Deliberately independent of -Dplatform: whoever changes a
- // shader runs this on a machine that has the compiler and commits the diff
- // (`jj diff shaders/prebuilt` says whether the pair drifted).
const shaders_step = b.step("shaders", "refresh paired shaders/prebuilt/*.spv + *.glsl snapshots (glslc)");
const update_shaders = b.addUpdateSourceFiles();
for (gui_shaders) |name| {
@@ -311,33 +186,18 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path(switch (platform) {
.web => "src/web.zig",
.macos => "src/macos.zig",
- // esp32p4 roots at its own flat C ABI too, for the same reason web and
- // macOS do: the firmware's `_start`, its linker script and its UART
- // live in the zig-p4 package, which LINKS the object this emits.
.esp32p4 => "src/esp32p4.zig",
.tty, .gui => "src/main.zig",
}),
.link_libc = !freestanding_core,
});
- // helix differential harness (test/hxdiff.zig): a second compilation of
- // the core, driven headlessly. Mirrors root_mod's wiring for
- // everything src/pardes.zig pulls in (ghostty-vt, tree-sitter, zstbi, the
- // option modules); imported as "pardes" by the harness exe below.
- const hx_core_mod = b.createModule(.{
+ const core_module = b.createModule(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/pardes.zig"),
.link_libc = true,
});
- // `pardes-isolate`: a THIRD compilation of the same graph whose only
- // difference is one comptime bool. It cannot be the same binary with a
- // flag, because the point is that the isolated build does not CONTAIN the
- // filesystem: look.zig's libc paths compile away, so no runtime mistake
- // can reach a disk that a `--flag` build still links.
- //
- // Only the tty platform has one. gui adds a GPU it would still need, and
- // web/macos are libraries whose host owns main().
const isolated_mod: ?*std.Build.Module = if (platform == .tty) b.createModule(.{
.target = target,
.optimize = optimize,
@@ -345,10 +205,6 @@ pub fn build(b: *std.Build) void {
.link_libc = true,
}) else null;
- // The SECOND shell of a default build: the same src/main.zig, compiled
- // with `platform = .gui`. A separate module and not a flag on the first
- // one for the reason pardes-isolate is separate — `pardes.platform` is
- // comptime, and the gui shell @cImports an SDL the cli must not link.
const gui_mod: ?*std.Build.Module = if (also_gui) b.createModule(.{
.target = target,
.optimize = optimize,
@@ -356,10 +212,6 @@ pub fn build(b: *std.Build) void {
.link_libc = true,
}) else null;
- // Every module that compiles src/pardes.zig, so its wiring is applied once
- // per dependency instead of once per module per dependency — each paired
- // with the frontend it IS, because that is the single thing their
- // `pardes_config` modules are allowed to disagree about.
const CoreMod = struct { mod: ?*std.Build.Module, shell: Platform };
var core_mods_buf: [4]*std.Build.Module = undefined;
var core_shells_buf: [4]Platform = undefined;
@@ -369,7 +221,7 @@ pub fn build(b: *std.Build) void {
.{ .mod = root_mod, .shell = platform },
// hxdiff's headless core and pardes-isolate are the primary shell's
// configuration compiled two more ways, not two more frontends.
- .{ .mod = hx_core_mod, .shell = platform },
+ .{ .mod = core_module, .shell = platform },
.{ .mod = isolated_mod, .shell = platform },
.{ .mod = gui_mod, .shell = .gui },
}) |entry| if (entry.mod) |m| {
@@ -379,6 +231,36 @@ pub fn build(b: *std.Build) void {
};
const core_mods = core_mods_buf[0..core_mods_len];
const core_shells = core_shells_buf[0..core_mods_len];
+ for (core_mods) |mod| mod.addOptions("9p_options", ninep_options);
+ const quic_test_step = b.step("quic-test", "test optional QUIC transport (-Dquic=true)");
+ if (enable_quic) {
+ const header = b.addWriteFiles().add("openssl.h", "#include <openssl/ssl.h>\n#include <openssl/quic.h>\n#include <openssl/err.h>\n");
+ const translated = b.addTranslateC(.{ .root_source_file = header, .target = target, .optimize = optimize, .link_libc = true });
+ const include_dir = std.mem.trim(u8, b.run(&.{ "pkg-config", "--variable=includedir", "openssl" }), " \t\r\n");
+ if (include_dir.len == 0) @panic("OpenSSL pkg-config metadata is missing its include directory");
+ translated.addSystemIncludePath(.{ .cwd_relative = include_dir });
+ const openssl = translated.createModule();
+ const library_dir = std.mem.trim(u8, b.run(&.{ "pkg-config", "--variable=libdir", "openssl" }), " \t\r\n");
+ if (library_dir.len == 0) @panic("OpenSSL pkg-config metadata is missing its library directory");
+ openssl.addLibraryPath(.{ .cwd_relative = library_dir });
+ openssl.linkSystemLibrary("ssl", .{});
+ openssl.linkSystemLibrary("crypto", .{});
+ for (core_mods) |mod| mod.addImport("openssl", openssl);
+ const quic_module = b.createModule(.{
+ .target = target,
+ .optimize = optimize,
+ .root_source_file = b.path("src/9p_quic.zig"),
+ .link_libc = true,
+ });
+ quic_module.addImport("openssl", openssl);
+ const tests = b.addTest(.{ .root_module = quic_module, .filters = test_filters });
+ if (test_nonce) |nonce| tests.root_module.addImport("test_rebuild_nonce", nonce);
+ test_build.dependOn(&tests.step);
+ const run_tests = b.addRunArtifact(tests);
+ run_tests.has_side_effects = true;
+ quic_test_step.dependOn(&run_tests.step);
+ if (testMatch(b, unit_profile, test_filters, quic_test_step)) |check| check.addArtifactArg(tests);
+ } else quic_test_step.dependOn(&b.addFail("quic-test needs -Dquic=true and system OpenSSL 3.6+").step);
// The conditional dependencies, hoisted so one `wireCore` call at the end
// can name them all. The unconditional ones are ordinary consts below.
@@ -389,30 +271,10 @@ pub fn build(b: *std.Build) void {
var ghostty_core_vt: ?*std.Build.Module = null;
var ts_libs: std.ArrayList(*std.Build.Step.Compile) = .empty;
- // THEMES. tools/gen_themes.zig turns each vendored helix .toml / zed .json
- // into one .zig file per theme plus a list.zig that imports them all;
- // src/pardes.zig folds that list into the ring beside the three it ships.
- //
- // Straight into the build cache and handed over as a module, rather than
- // written back into src/: the output then has no freshness problem to own
- // (zig re-runs the generator only when a vendored file changes, and a
- // cached run leaves the compile's inputs byte-identical, so `zig build`
- // with nothing touched still does nothing), there is nothing to gitignore
- // and no stale .zig can survive a deleted source.
- //
- // The INPUT list is read from the directory here rather than written out,
- // so adding a theme is dropping a file in — and each file goes in as a
- // content-hashed argument, which is what makes that new file re-run the
- // step and nothing else.
const theme_gen = b.addExecutable(.{
.name = "pardes-gen-themes",
.root_module = b.createModule(.{
.target = b.graph.host,
- // Debug on purpose: it runs for ~40ms, and every core module
- // imports what it generates, so its compile is the first link of
- // every cold build. ReleaseSafe cost 16s of compile to save 47ms of
- // run time, and the 226 files it writes are byte-identical either
- // way.
.optimize = .Debug,
.root_source_file = b.path("tools/gen_themes.zig"),
}),
@@ -430,62 +292,11 @@ pub fn build(b: *std.Build) void {
if (target.result.os.tag == .freebsd or target.result.os.tag == .netbsd or target.result.os.tag == .openbsd)
root_mod.linkSystemLibrary("util", .{});
- // The language backend (src/lsp/lsp_zls.zig) links ZLS as a MODULE — no zls
- // binary, no JSON-RPC, no protocol. Everything except the browser gets it;
- // the web shell does not, because it has no threads and no-ops the
- // lsp effect anyway, so paying to compile an analyser it can never call
- // would be pure wasm.
const zls_backend = !freestanding_core;
- // THE BOARD'S GRID, a build option because the right size is a measurement rather than a
- // constant, and because two independent things limit it.
- //
- // LATENCY binds first, and linearly: every frame walks the whole grid, at 0.87 us per cell
- // measured on the die. 56x14 is 784 cells and a 3,930 us round trip - 63% more area and 40% more
- // width than the 40x12 it replaces, and still inside the 4 ms this port was built to hold. 56x16
- // was tried first and measured 4,029, which is over; 80x24 works and costs 4,809. The full curve
- // is in `05-zig-p4/experiments/report.typ`.
- //
- // MEMORY binds much later, and only since vaxis's two unused grids stopped being allocated:
- // 140x42 runs, 160x48 links and then traps, and 200x60 does not link at all - `.bss will not fit
- // in region l2mem, overflowed by 76036 bytes`, that being the shell's shadow copy of the grid.
const esp32p4_cols = b.option(u16, "esp32p4-cols", "the board's grid width in cells (-Dplatform=esp32p4 only)") orelse 56;
const esp32p4_rows = b.option(u16, "esp32p4-rows", "the board's grid height in cells (-Dplatform=esp32p4 only)") orelse 14;
- // `-Desp32p4-port`, `-Desp32p4-prof` and `-Desp32p4-cpu-mhz` used to be
- // declared here, for the block that linked and flashed the image. That
- // block is gone — see "where the FLASHABLE image comes from" below — and so
- // are they: an option this build cannot honour is worse than no option,
- // because it accepts the flag and then ignores it. All three belong to the
- // toolchain repository and are spelled the same way there.
- // ANIMATED THEME CHANGES, off on the board because there the animation is not an animation.
- //
- // A theme change moves the anchored chrome palette - taglines, boxes, line numbers, scroll bars -
- // from the old colors to the new ones over `animation.transition_steps` display frames, which is
- // ten. On a screen that repaints in microseconds that is a short legible fade, and it is the
- // reason the transition exists: a palette that teleports reads as a glitch.
- //
- // On a 115200 serial line a frame is not free. Every one of those ten steps recolors every
- // anchored cell, so the shell's diff finds the whole chrome dirty and spends a frame's worth of
- // wire on it, ten times over, with nothing else to look at in between. Measured on the die, one
- // `NextColor`:
- //
- // fade on 12,593 bytes 1,097 ms of saturated wire
- // fade off 2,425 bytes 215 ms
- //
- // A second of the editor talking to itself about a color, on the one transport where a second is
- // noticeable, for a gradient nobody can see arrive gradually at 11.5 KB/s.
- //
- // COMPTIME, not a runtime flag: `pardes.ChromeAnimation` selects `animation.Immediate` over
- // `animation.Transition`, which leaves `ChromeTheme.interpolate` unreachable and out of the image
- // (2,336 bytes of it). A bool tested at runtime would have kept every one of those bytes.
- //
- // A build option rather than a platform test, because "is a frame expensive" is a property of the
- // transport and not of the target: a P4 driven over something faster than a UART would want it on,
- // and it is off here only as the default that matches the wire this port actually has.
const theme_animation = b.option(bool, "theme-animation", "animate chrome colors across a theme change (default: off for esp32p4)");
- // Everything `pardes_config` carries that is a property of the BUILD
- // rather than of one frontend, gathered into one value so the two options
- // modules a default build makes cannot drift apart in any other field.
const shell_cfg: ShellConfig = .{
.tree_sitter_grammars = tree_sitter_grammars,
.tracy = tracy != null,
@@ -498,26 +309,10 @@ pub fn build(b: *std.Build) void {
.zig_lib_dir = b.graph.zig_lib_directory.path orelse "",
.commit = gitCommit(b),
};
- // ONE `pardes_config` per distinct frontend in this build: two when the
- // cli and the gui are made together, one otherwise. hxdiff's core and
- // pardes-isolate share the primary shell's, being the same frontend.
const opts = shellOptions(b, shell_cfg, platform);
const gui_opts: ?*std.Build.Step.Options = if (also_gui) shellOptions(b, shell_cfg, .gui) else null;
- // NOTE: these are attached to the modules at the BOTTOM of this function,
- // after every addImport — the module-import table below is folded out of
- // root_mod.import_table and would be empty if we attached them here.
if (zls_backend) {
- // .target/.optimize are mandatory: createZLSModule bakes them into the
- // module it registers, so a mismatch here is a second compilation of
- // the whole analyser rather than an error.
- // `.target`/`.optimize` are MANDATORY: createZLSModule bakes them in,
- // so a module built without them mismatches ours at link time.
- // `version-string` is not cosmetic either — ZLS's build.zig shells out
- // to `git describe` to name itself, and a package the build system
- // fetched is an extracted tarball with no .git, so every single build
- // printed a "Failed to run git describe" warning. We pin the commit in
- // build.zig.zon, so we already know the answer.
const zls_dep = b.dependency("zls", .{
.target = target,
.optimize = optimize,
@@ -526,14 +321,8 @@ pub fn build(b: *std.Build) void {
zls_mod = zls_dep.module("zls");
}
- // Tracy zones (src/tracy.zig): compile the client into the binary only when
- // -Dtracy=<path> names a Tracy checkout; otherwise every zone is a no-op.
- // Sampling/callstacks/system tracing stay off: tracy's symbol worker
- // SIGSEGVs on this binary's debug info, and its crash handler then parks
- // every thread — the app wedges before the first frame. Zones don't need
- // any of it.
if (tracy) |tracy_path| {
- for ([_]*std.Build.Module{ root_mod, hx_core_mod }) |mod| {
+ for ([_]*std.Build.Module{ root_mod, core_module }) |mod| {
mod.addIncludePath(.{ .cwd_relative = tracy_path });
mod.addCSourceFile(.{
.file = .{ .cwd_relative = b.pathJoin(&.{ tracy_path, "public", "TracyClient.cpp" }) },
@@ -551,15 +340,9 @@ pub fn build(b: *std.Build) void {
}
}
- // tree-sitter: the zig bindings + C runtime, one static lib per grammar,
- // and each grammar's highlights.scm slurped at build time into the
- // ts_queries options module (codegen consumed by comptime in syntax.zig).
if (tree_sitter_grammars != .disabled) {
const tree_sitter_mod = b.dependency("tree_sitter", .{ .target = target, .optimize = c_optimize }).module("tree_sitter");
if (is_web) {
- // zig-tree-sitter carries its C runtime as a linked static library.
- // Retarget that library away from libc and put the compatibility
- // headers before any unavailable freestanding system headers.
const web_libc_include: std.Build.LazyPath = .{
.cwd_relative = b.path("src/web/libc/include").getPath(b),
};
@@ -595,14 +378,6 @@ pub fn build(b: *std.Build) void {
.root_module = b.createModule(.{ .target = target, .optimize = c_optimize, .link_libc = !is_web }),
.linkage = .static,
});
- // grammars declare external_scanner_create() with EMPTY PARENS
- // (a K&R non-prototype, not (void)): under clang's
- // -fsanitize=function the callee's type hash differs from the
- // runtime's void*(*)(void) call through the pointer, so the
- // first scanner call of ANY parse traps (function_type_mismatch,
- // an ud1 blamed on a random inlined line). Uninstrumented
- // callees make the runtime's call-site checks skip; the rest
- // of UBSan stays live for the grammar code.
const ts_cflags = [_][]const u8{"-fno-sanitize=function"};
lib.root_module.addCSourceFile(.{ .file = dep.path(g.src ++ "/parser.c"), .flags = &ts_cflags });
if (g.scanner) lib.root_module.addCSourceFile(.{ .file = dep.path(g.src ++ "/scanner.c"), .flags = &ts_cflags });
@@ -614,9 +389,6 @@ pub fn build(b: *std.Build) void {
ts_queries_opts = ts_queries;
}
- // zstbi (C stb_image): image pane decode. The freestanding core has no C
- // allocator ABI, so its module is a compatible no-decode shim; browser IO
- // can grow native image decoding independently of the core.
const zstbi_dep = if (!freestanding_core) b.dependency("zstbi", .{ .target = target, .optimize = c_optimize }) else null;
const zstbi_mod = if (zstbi_dep) |dep| dep.module("root") else b.createModule(.{
.target = target,
@@ -624,18 +396,8 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("src/web/zstbi.zig"),
});
- // mvzr: the regex engine behind `s` / `S` (helix select/split on a regex).
- // A bytecode VM that compiles a RUNTIME pattern into a fixed-size struct
- // with no allocator at all, which is exactly the shape an interactive
- // prompt needs — see applySelRegex in src/pardes.zig.
const mvzr_mod = b.dependency("mvzr", .{ .target = target, .optimize = optimize }).module("mvzr");
- // MuPDF is the default native PDF engine, but remains a genuinely optional
- // dependency: with -Dmupdf=false (and by default on web), the lazy source
- // archive is not fetched, compiled, linked, or exposed to runtime code.
- // mupdf.zig mirrors the 1.27.0 Makefile source graph with Zig's C compiler
- // and deliberately enables only the PDF document handler for this first
- // integration.
if (enable_mupdf) {
if (b.lazyDependency("mupdf", .{})) |mupdf_dep| {
const mupdf = mupdf_build.add(b, io, mupdf_dep, .{
@@ -656,20 +418,15 @@ pub fn build(b: *std.Build) void {
.flags = &.{"-std=gnu11"},
});
mupdf_core_mod = mupdf_mod;
- // Compile the @cImport + exception bridge and exercise the Zig
- // wrapper's caller-owned RGBA boundary alongside the C end-to-end
- // document probe below.
const mupdf_test = b.addTest(.{ .root_module = mupdf_mod });
- mupdf_check.dependOn(&b.addRunArtifact(mupdf_test).step);
+ const run_mupdf_test = b.addRunArtifact(mupdf_test);
+ run_mupdf_test.has_side_effects = true;
+ mupdf_check.dependOn(&run_mupdf_test.step);
mupdf_check.dependOn(mupdf_build.addProbe(b, mupdf, .{
.target = target,
.pdf = b.path("docs/design.pdf"),
}));
- // A headless renderer benchmark with its own ReleaseFast wrapper
- // module. Reusing root_mod's Debug-mode MuPDF import here would
- // benchmark Zig safety checks around optimized C rather than the
- // production-cost boundary the scoreboard is meant to expose.
const pdf_bench_mod = b.createModule(.{
.target = target,
.optimize = .ReleaseFast,
@@ -697,10 +454,6 @@ pub fn build(b: *std.Build) void {
run_pdf_bench.setCwd(b.path("."));
pdf_bench_step.dependOn(&run_pdf_bench.step);
- // Unlike the raster-only scoreboard above, this one drives the
- // real core so cached +PdfSections reopen and n/N include their
- // production state transitions. Pass -Doptimize=ReleaseFast so
- // hx_core_mod and its MuPDF wrapper share the executable's mode.
const pdf_sections_bench = b.addExecutable(.{
.name = "pardes-pdf-sections-bench",
.root_module = b.createModule(.{
@@ -710,7 +463,7 @@ pub fn build(b: *std.Build) void {
.link_libc = true,
}),
});
- pdf_sections_bench.root_module.addImport("pardes", hx_core_mod);
+ pdf_sections_bench.root_module.addImport("pardes", core_module);
pdf_sections_bench.root_module.addImport("mupdf", mupdf_mod);
const sections_bench_opts = b.addOptions();
sections_bench_opts.addOption(bool, "release_fast_core", optimize == .ReleaseFast);
@@ -720,14 +473,6 @@ pub fn build(b: *std.Build) void {
run_pdf_sections_bench.setCwd(b.path("."));
pdf_sections_bench_step.dependOn(&run_pdf_sections_bench.step);
- // The fling scoreboard. Same real-core wiring as the sections
- // bench: what it measures is one shell frame — a whole batch of
- // wheel notches through update() followed by a single render() —
- // so it needs the production core, not the raster wrapper alone.
- // The exe follows -Doptimize with the core rather than pinning
- // ReleaseFast: a profile or a crash in the harness itself needs the
- // same line numbers and un-inlined frames as one in the core, and a
- // scoreboard run passes ReleaseFast anyway (see the file header).
const pdf_scroll_bench = b.addExecutable(.{
.name = "pardes-pdf-scroll-bench",
.root_module = b.createModule(.{
@@ -737,19 +482,12 @@ pub fn build(b: *std.Build) void {
.link_libc = true,
}),
});
- pdf_scroll_bench.root_module.addImport("pardes", hx_core_mod);
+ pdf_scroll_bench.root_module.addImport("pardes", core_module);
pdf_scroll_bench.root_module.addImport("mupdf", mupdf_mod);
const scroll_bench_opts = b.addOptions();
scroll_bench_opts.addOption(bool, "release_fast_core", optimize == .ReleaseFast);
scroll_bench_opts.addOption([]const u8, "core_optimize", @tagName(optimize));
pdf_scroll_bench.root_module.addOptions("pdf_scroll_bench_config", scroll_bench_opts);
- // Installed, unlike the other two: `perf record` needs a stable
- // path and a cache hash is not one. By ITS OWN step, and into
- // `<prefix>/dev` rather than `<prefix>/bin`: a bare `zig build`
- // installs into the user's own bin directory now, and build.zig
- // cannot tell which step was asked for, so the only way a 200 MB
- // Debug benchmark is kept off a PATH is by never targeting `bin`.
- pdf_scroll_bench_step.dependOn(&b.addInstallArtifact(pdf_scroll_bench, .{ .dest_dir = .{ .override = .{ .custom = "dev" } } }).step);
const run_pdf_scroll_bench = b.addRunArtifact(pdf_scroll_bench);
if (b.args) |args| run_pdf_scroll_bench.addArgs(args);
run_pdf_scroll_bench.setCwd(b.path("."));
@@ -757,9 +495,6 @@ pub fn build(b: *std.Build) void {
}
}
- // ghostty-vt and vaxis both depend on uucode, but Zig forbids one source
- // file in two modules — build a single uucode (our config, unpacked tables)
- // and hand it to both. Same trick as the prototype; see uucode_config.zig.
const uucode_config = b.path("uucode_config.zig");
const uucode_tables = b.dependency("uucode", .{
.target = target,
@@ -773,10 +508,6 @@ pub fn build(b: *std.Build) void {
.tables_path = uucode_tables,
}).module("uucode");
- // Core grapheme/display-width code uses vaxis.gwidth on every platform.
- // The web build consumes only that lazy Zig path; terminal IO remains
- // unreachable, while omitting the module makes the shared core fail at
- // @import("vaxis") before the browser snapshot can render terminal panes.
const vaxis_mod = b.dependency("vaxis", .{
.target = target,
.optimize = optimize,
@@ -784,46 +515,8 @@ pub fn build(b: *std.Build) void {
}).module("vaxis");
var ghostty_vt_for_snap: ?*std.Build.Module = null;
- // ghostty's simd libs (simdutf/highway, C++) locate the Apple SDK via
- // xcrun on darwin targets, so cross-compiling to macOS from elsewhere
- // uses the scalar fallback (the same configuration the web shell ships).
- // Native/cross builds hand ghostty the real target (it defaults to the
- // host otherwise); the web path keeps its original no-target fetch, whose
- // zig object never uses ghostty's artifacts.
- // NOTE: darwin targets also need two one-line zig-0.16 fixes in the
- // pinned ghostty (applied in the zig-pkg cache; re-apply after a fresh
- // fetch, or bump the pin once upstream carries them):
- // src/os/mach.zig — std.heap.next_mmap_addr_hint is gone (make the hint
- // var module-local) and posix.mmap prot is now a packed struct
- // (.{ .READ = true, .WRITE = true });
- // src/terminal/kitty/graphics_image.zig:185 — shm_open's variadic mode
- // literal 0 must be @as(std.c.mode_t, 0).
- // Darwin HOSTS need a third: src/build/GhosttyDist.zig:28 — the dist
- // tarball's GTK resources run pkg-config for libadwaita eagerly (panics
- // without it); gate that block on `b.graph.host.result.os.tag == .linux`.
- // Ghostty's pinned translate-c tarball also 404s now (codeberg dropped
- // the archive endpoint) — seed zig-pkg/ from a machine that has it.
const ghostty_simd = !freestanding_core and (!target.result.os.tag.isDarwin() or b.graph.host.result.os.tag.isDarwin());
- // app-runtime none + emit-xcframework off: only the ghostty-vt module is
- // consumed, and the defaults otherwise drag ghostty's app graph into the
- // build — gtk4 header translation via host pkg-config for linux targets,
- // the Xcode app graph (iOS SDK, xcodebuild) on darwin hosts.
- // NEVER hand ghostty `.Debug`: that flips its `slow_runtime_safety`, which
- // walks the whole PageList after every mutation and PANICS the app on a
- // transient state its own next lines repair — `PageList.resizeCols` grows
- // rows BEFORE it moves a history viewport pin back into the active area,
- // so widening a window whose scrollback holds wrapped lines dies with
- // "PageList integrity check failed: ViewportPinInsufficientRows". Those
- // checks are a ghostty-development tool (upstream ships them off); Zig's
- // own safety checks come from OUR optimize mode and are unaffected, since
- // ghostty-vt is a module compiled into this binary. See reflow.snap.
const ghostty_optimize: std.builtin.OptimizeMode = if (optimize == .Debug) .ReleaseSafe else optimize;
- // The P4 firmware has no terminal panes at all (see `terminal_panes` in
- // src/pardes.zig), so src/term_pane.zig's `@import("ghostty-vt")` sits in a
- // dead comptime branch and `wireCore` below is handed a null. The
- // dependency is therefore not merely unused, it is never REQUESTED: this is
- // a lazyDependency, so an esp32p4 build does not need the ghostty package (nor
- // its translate-c tarball, nor its simd C++) present at all.
const ghostty_dep = if (is_esp32p4) null else b.lazyDependency("ghostty", .{ .target = target, .optimize = ghostty_optimize, .simd = ghostty_simd, .@"app-runtime" = .none, .@"emit-xcframework" = false });
if (ghostty_dep) |dep| {
const ghostty_vt = dep.module("ghostty-vt");
@@ -850,20 +543,6 @@ pub fn build(b: *std.Build) void {
.uucode = uucode_mod,
});
- // --- the dependency module map, for `gd` on `@import("vaxis")` ---
- //
- // ZLS resolves an import string three ways: a relative `.zig` path, `std`
- // (via zig_lib_dir), and everything else — which it can only answer by
- // running `zig build --build-runner` to learn the module graph. This
- // backend sets `zig_exe_path = null` on purpose, so that last branch
- // returns nothing and every `@import("<dependency>")` is a silent miss
- // while `std` works perfectly. That asymmetry is the whole bug report.
- //
- // We do not need the compiler to answer it: THIS FILE *is* the module
- // graph. Fold the imports we just wired into a name->root-source table and
- // hand it to the backend, which consults it exactly where ZLS gave up.
- // Costs one build option and stays correct by construction — a dependency
- // that is added or renamed above cannot forget to update it.
var module_count: usize = 0;
{
var it = root_mod.import_table.iterator();
@@ -895,21 +574,11 @@ pub fn build(b: *std.Build) void {
}
}
std.debug.assert(module_index == module_count);
- // Both options modules get the same map: the shells' import tables are
- // identical at this point, because every import that differs between them
- // is an anonymous FILE added below rather than a module.
for ([_]?*std.Build.Step.Options{ opts, gui_opts }) |maybe| if (maybe) |o| {
o.addOption([]const []const u8, "module_names", mod_names);
o.addOption([]const []const u8, "module_roots", mod_roots);
};
- // The one setting that produces a SECOND executable rather than changing
- // this one: its own tiny options module, because duplicating `opts` to
- // flip a single bool would be twenty lines that must then agree forever.
- // Both are handed out in the loop below rather than module by module:
- // every compilation of src/pardes.zig imports it unconditionally (:89),
- // and naming the modules one at a time is exactly what left the second
- // shell without one.
const iso_off = b.addOptions();
iso_off.addOption(bool, "isolated", false);
const iso_on = b.addOptions();
@@ -918,9 +587,6 @@ pub fn build(b: *std.Build) void {
// Consulted only for a gui shell, so `-Dprebuilt-shaders` alone decides it
// here; the platform half of that condition is the branch it sits in.
const gui_effect_source_dir = if (prebuilt_shaders) "shaders/prebuilt" else "shaders";
- // Attached after the fold above, for the reason `opts` names, and with the
- // embedded FILES: those are bytes rather than modules, and adding them
- // earlier would list them in the module map as if they were importable.
for (core_mods, core_shells) |mod, shell| {
mod.addOptions("pardes_config", if (also_gui and shell == .gui) gui_opts.? else opts);
mod.addOptions("pardes_isolation", if (isolated_mod == mod) iso_on else iso_off);
@@ -931,29 +597,13 @@ pub fn build(b: *std.Build) void {
if (shell == .macos) mod.addAnonymousImport("effect-source-crt.ci.metal", .{
.root_source_file = b.path("shaders/crt.ci.metal"),
});
- // The virtual filesystem's two repo-root entries: `@embedFile` cannot
- // reach outside the module's own directory, so they arrive by name.
- // See src/source_manifest.zig.
mod.addAnonymousImport("root-build.zig", .{ .root_source_file = b.path("build.zig") });
mod.addAnonymousImport("root-build.zig.zon", .{ .root_source_file = b.path("build.zig.zon") });
}
- // SDL3 shell wiring. SDL3 and FreeType are both built from source as
- // static libraries; gui.zig @cImports their headers plus the small
- // FreeType policy shim, and embeds SPIR-V compiled from GLSL.
- //
- // The module it lands on is whichever one IS the SDL shell here: the
- // second shell of a default build, or the only shell under
- // `-Dplatform=gui`. Never both, and never the cli.
const gui_shell_mod: ?*std.Build.Module = gui_mod orelse
if (platform == .gui) root_mod else null;
if (gui_shell_mod) |gm| {
- // sanitize_c MUST stay off: zig cc's UBSan (on for C in Debug AND
- // ReleaseSafe) traps hidapi's mismatched fn-pointer calls the moment
- // a HID gamepad is enumerated — SDL_Init SIGILLs on the deck itself
- // (ud2 in SDL_hid_set_nonblocking_REAL); headless boxes never see it.
- // SDL3 always statically linked (like deckcap): the gui binary must
- // only need system libc/libm — never a shared libSDL3.
const sdl_dep = b.lazyDependency("sdl", .{ .target = target, .optimize = optimize, .sanitize_c = .off, .preferred_linkage = .static });
if (sdl_dep) |dep| {
const sdl_lib = dep.artifact("SDL3");
@@ -966,10 +616,6 @@ pub fn build(b: *std.Build) void {
.@"enable-libpng" = false,
});
gm.linkLibrary(freetype_dep.artifact("freetype"));
- // darwin: SDL_GPU only speaks SPIRV here (shaders/*.glsl -> glslc), so
- // it must pick its vulkan backend via the Vulkan SDK's loader +
- // MoltenVK in /usr/local/lib — a path dyld no longer searches for
- // bare dlopen names. The rpath restores that lookup.
if (target.result.os.tag.isDarwin())
gm.addRPath(.{ .cwd_relative = "/usr/local/lib" });
gm.addIncludePath(b.path("src/gui"));
@@ -999,9 +645,6 @@ pub fn build(b: *std.Build) void {
wasm.entry = .disabled;
wasm.rdynamic = true;
wasm.export_memory = true;
- // The full grammar tier carries ~48 MiB of generated parse tables.
- // Keep the compact default at 32 MiB, but leave enough static address
- // space for an explicitly requested all-language build to link.
wasm.initial_memory = @as(u64, if (tree_sitter_grammars == .full) 64 else 32) * 1024 * 1024;
wasm.max_memory = 512 * 1024 * 1024;
web_step.dependOn(&b.addInstallFileWithDir(wasm.getEmittedBin(), .{ .custom = "web" }, "pardes.wasm").step);
@@ -1018,84 +661,16 @@ pub fn build(b: *std.Build) void {
b.step("web-harness", "run the dependency-free JS/WASM DOM harness").dependOn(&run_web_harness.step);
b.getInstallStep().dependOn(web_step);
} else if (is_esp32p4) {
- // ONE freestanding object, exporting the C ABI in src/esp32p4.zig. Not an executable, because
- // the firmware's `_start`, its generated linker script and its UART driver all live in the
- // zig-p4 package; not a library, because `addLibrary` bundles compiler_rt and the firmware
- // already has its own; and not a MODULE exposed through build.zig.zon, which is what this
- // was first and is the interesting part.
- //
- // A dependency in the OTHER DIRECTION was tried and reverted. Nesting this package's
- // ~30-package graph under zig-p4's broke every build in that repo, not just the firmware
- // one: `std/Build.zig:2091` exceeded its 1000-branch comptime quota through ghostty's
- // `SharedDeps.zig:874` `lazyImport`, seven cached tree-sitter versions failed to compile
- // because their build.zig uses APIs removed in 0.16, and the fetch materialised 2.6 GB
- // across 42,736 files into a repo whose entire claim is that Zig is its only dependency.
- //
- // THIS direction is free, and that is why build.zig.zon now names `.zig_p4`: that package
- // declares no dependencies at all, so it enlarges nothing here, and its `build()`
- // early-returns when it is not the root. What did NOT change is the seam. The editor still
- // crosses to the firmware as this one object over eight C functions, because bytes are the
- // right seam for a serial line and because that is the arrangement the board was measured
- // through — see the `-Desp32p4-firmware` block below, which links this exact object.
- //
- // The object is also the compile probe: rooted at src/esp32p4.zig it drags the whole core through
- // the riscv32 backend by actually calling it, so `llvm-size` on the result is a real number
- // to hold against the board's 1.5 MiB factory partition.
const obj = b.addObject(.{ .name = "pardes-esp32p4", .root_module = root_mod });
b.getInstallStep().dependOn(&b.addInstallFile(obj.getEmittedBin(), "pardes-esp32p4.o").step);
- // ------------------------------------------------- and where the FLASHABLE image comes from
- //
- // Not from here, and this is the one deliberate asymmetry in the board build.
- // `-Dplatform=esp32p4` emits the object above and needs no toolchain at all. Linking that
- // object into an image needs the linker script, the app descriptor, the heap, the HAL and
- // the flash tooling, all of which live in the `05-zig-p4` checkout — so the image is built
- // THERE, by the repository that owns them:
- //
- // zig build -Dpardes # the console firmware
- // zig build -Dpardes -Dapp=<pardes>/src/esp32p4_9p.zig # the 9P server firmware
- //
- // both taking `-Dpardes-obj=<pardes>/zig-out/pardes-esp32p4.o`, which is exactly the file
- // installed above.
- //
- // This build tree used to do it too, under `-Desp32p4-firmware`, by declaring `.zig_p4` as
- // a path dependency on that sibling checkout. That cost far more than the duplication was
- // worth: `@import` in a build script is resolved when the SCRIPT is compiled and not when
- // the branch that needs it is taken, so naming the package at all meant that anyone
- // without a `../05-zig-p4` beside their pardes could not build pardes AT ALL — not the
- // firmware, not the SDL shell, not the terminal one. `zig build` failed with
- // `no module named 'zig_p4'` from a line inside an `if` that was false.
- //
- // Neither `.lazy = true` nor `b.lazyImport` rescues it: laziness is about FETCHING, and a
- // path dependency whose directory is absent is generated as a package with no `build.zig`
- // rather than as one marked unavailable, so `lazyImport` reaches a `@compileError` instead
- // of returning null. Both were tried. The toolchain has no remote to make it a fetched
- // dependency instead.
- //
- // So the arrangement is the one that repository had already arrived at from its own side,
- // where its build.zig records the same lesson: the object is the seam, it crosses by PATH
- // and never by package, and each repository builds what it owns the pieces of.
web_step.dependOn(&b.addFail("web needs -Dplatform=web -Dtarget=wasm32-freestanding -Ddump=<dump.zon>").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 the bundle needs
- // one. Deliberately NOT here: an xcframework, lipo and an Xcode
- // project. The first two are for a UNIVERSAL binary and this ships
- // arm64; the third is a second build system to keep in step for what
- // a plist and a few install steps already do. ghostty's
- // src/build/GhosttyXCFramework.zig is the map if that day comes.
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(.{
@@ -1103,26 +678,10 @@ pub fn build(b: *std.Build) void {
.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;
- // ...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).
const archive: ?std.Build.LazyPath =
if (builtin.os.tag.isDarwin() and target.result.os.tag.isDarwin()) fatArchive(b, lib) else null;
- // The one artifact everything downstream links. Named as a step rather
- // than folded into the install step so the e2e link below can wait for
- // the LIBRARY without also waiting for the app bundle.
const install_lib: *std.Build.Step = if (archive) |a|
&b.addInstallLibFile(a, "libpardes.a").step
else
@@ -1130,24 +689,9 @@ pub fn build(b: *std.Build) void {
b.getInstallStep().dependOn(install_lib);
b.installFile("src/macos/pardes.h", "include/pardes.h");
- // ---- pardes.app ----
- //
- // A bundle is a directory with a plist, a binary and an icon in it, so
- // it is assembled HERE rather than by a script the build shells out to.
- // Every input is a file the graph knows — the archive, the four Swift
- // sources, the header, the plist, the icon generator — which is what
- // makes the app rebuild when one of them moves and stay untouched when
- // none does. The script this replaced took an install PREFIX and
- // re-derived its inputs from whatever happened to be sitting in
- // zig-out, so it could neither be cached nor be wrong out loud.
const app_step = b.step("macos-app", "assemble zig-out/pardes.app (needs macOS + swiftc)");
const dmg_step = b.step("macos-dmg", "package zig-out/pardes.dmg for distribution (needs macOS + swiftc)");
if (archive) |lib_archive| {
- // The deployment target, spelled once (macos_min_version) and given
- // to all three things that have to agree about it: the archive was
- // built for it, this triple is what writes LC_BUILD_VERSION — the
- // thing dyld actually enforces — and the plist key below is the
- // claim Finder reads. Apple spells aarch64 "arm64".
const apple_arch: []const u8 =
if (target.result.cpu.arch == .aarch64) "arm64" else @tagName(target.result.cpu.arch);
const triple = b.fmt("{s}-apple-macos{d}.{d}", .{
@@ -1155,24 +699,12 @@ pub fn build(b: *std.Build) void {
});
const min_version = b.fmt("{d}.{d}", .{ macos_min_version.major, macos_min_version.minor });
- // swiftc compiles the shell the way zig compiled the core. Pinning
- // -O here meant the ordinary build shipped an optimized shell
- // around a Debug core, which costs 5x a frame and reads as "the mac
- // backend is slow" rather than "you built Debug": one frame at
- // 190x56 measured 4.5 ms against a Debug core and 0.88 ms against a
- // ReleaseFast one, 4.1 ms of it in pardes_frame alone.
const swift_mode: []const u8 = switch (optimize) {
.Debug => "-Onone",
.ReleaseSmall => "-Osize",
.ReleaseFast, .ReleaseSafe => "-O",
};
- // -import-objc-header rather than a module map: the header is
- // consumed straight from the source tree, so there is nothing to
- // stage and nothing to keep in sync. -lc++ because ghostty-vt pulls
- // in simdutf and highway; the Zig side bundles compiler_rt and
- // ubsan_rt into the archive above, so the C++ runtime is all this
- // link still has to supply.
const link = b.addSystemCommand(&.{ "swiftc", swift_mode, "-target", triple, "-import-objc-header" });
link.addFileArg(b.path("src/macos/pardes.h"));
link.addArg("-o");
@@ -1200,12 +732,6 @@ pub fn build(b: *std.Build) void {
"Metal",
});
- // The ICNS is generated, not committed: it is a ten-size container
- // macOS reads, and a binary blob in the tree is one nobody can
- // review. What IS committed is src/macos/icon.png, the drawing it
- // is cut from — passed as a file arg so a new picture invalidates
- // the cached icon instead of shipping the old one. Compiled alone
- // because the file is top-level code — one file, one module.
const icon_build = b.addSystemCommand(&.{ "swiftc", "-O", "-target", triple, "-o" });
const icon_bin = icon_build.addOutputFileArg("pardes-icon");
icon_build.addFileArg(b.path("src/macos/icon.swift"));
@@ -1214,11 +740,6 @@ pub fn build(b: *std.Build) void {
icon_run.addFileArg(b.path("src/macos/icon.png"));
const icon_dir = icon_run.addOutputDirectoryArg("Resources");
- // One version, two consumers: LSMinimumSystemVersion is stamped
- // from the same string the link above enforces, so a bumped
- // deployment target cannot leave a stale claim behind. plutil reads
- // the committed plist and writes a new one into the cache — the
- // source file is never mutated, which is what PlistBuddy did.
const plist = b.addSystemCommand(&.{
"plutil", "-replace", "LSMinimumSystemVersion", "-string", min_version, "-o",
});
@@ -1227,9 +748,6 @@ pub fn build(b: *std.Build) void {
const install_app_bin = b.addInstallFileWithDir(app_bin, .{ .custom = "pardes.app/Contents/MacOS" }, "pardes");
const install_plist = b.addInstallFileWithDir(plist_out, .{ .custom = "pardes.app/Contents" }, "Info.plist");
- // Runtime Metal compilation keeps this exact source shared with
- // EffectCode's build-time embedding; no generated Swift literal
- // or second shader copy can drift from what the app executes.
const install_scene_kernel = b.addInstallFileWithDir(
b.path("shaders/crt.ci.metal"),
.{ .custom = "pardes.app/Contents/Resources" },
@@ -1239,17 +757,6 @@ pub fn build(b: *std.Build) void {
// halves the Dock shows the generic blank page.
const install_icon = b.addInstallFileWithDir(icon_dir.path(b, "pardes.icns"), .{ .custom = "pardes.app/Contents/Resources" }, "pardes.icns");
- // Gatekeeper. Ad-hoc by default, which is what an arm64 bundle
- // needs to launch at all and needs no keychain; -Dmacos-identity=
- // names a Developer ID for a bundle that leaves this machine, and
- // only then are the hardened runtime and a trusted timestamp worth
- // asking for — both are notarization's requirements rather than a
- // signature's, and --timestamp on an ad-hoc signature is an error.
- //
- // It signs the DIRECTORY, so it must follow all four installs: a
- // signature taken before the icon lands is a signature the icon
- // then breaks. Always runs, because the bundle it edits lives
- // outside the cache and zig cannot know what is in it.
const sign = b.addSystemCommand(&.{ "codesign", "--force", "--sign", macos_identity });
if (!std.mem.eql(u8, macos_identity, "-")) sign.addArgs(&.{ "--options", "runtime", "--timestamp" });
sign.addArg(b.getInstallPath(.prefix, "pardes.app"));
@@ -1260,10 +767,6 @@ pub fn build(b: *std.Build) void {
sign.step.dependOn(&install_scene_kernel.step);
app_step.dependOn(&sign.step);
- // ...and the thing you hand someone. UDZO is the compressed
- // read-only image every mac already knows how to open; the app
- // inside it carries the signature made above, which is what
- // survives the copy out.
const dmg = b.addSystemCommand(&.{ "hdiutil", "create", "-volname", "pardes", "-ov", "-format", "UDZO", "-srcfolder" });
dmg.addArg(b.getInstallPath(.prefix, "pardes.app"));
dmg.addArg(b.getInstallPath(.prefix, "pardes.dmg"));
@@ -1271,62 +774,51 @@ pub fn build(b: *std.Build) void {
dmg.step.dependOn(&sign.step);
dmg_step.dependOn(&dmg.step);
- // The app is part of an ORDINARY build rather than a verb to
- // remember: `zig build -Dplatform=macos` leaves a launchable,
- // signed bundle in zig-out beside the library it was linked from.
- // The dmg stays opt-in — it is for handing over, not for running.
b.getInstallStep().dependOn(&sign.step);
} else {
- // The library a bundle links has to BE a Mach-O one, and libtool
- // is Apple's. Cross-building the ABI for another host is supported
- // and tested (the Linux dev loop in docs/macos.md); assembling a
- // bundle out of it is not.
const why = b.addFail("pardes.app needs a Darwin host and target; drop -Dtarget= or pass -Dtarget=native");
app_step.dependOn(&why.step);
dmg_step.dependOn(&why.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 the app's own link uses, because it must be the
- // same link — it waits on the LIBRARY rather than the whole install, so
- // running the suite does not also assemble and sign a bundle it never
- // opens.
- 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(install_lib);
- 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);
+ if (archive) |lib_archive| {
+ const arch = if (target.result.cpu.arch == .aarch64) "arm64" else @tagName(target.result.cpu.arch);
+ const triple = b.fmt("{s}-apple-macos{d}.{d}", .{ arch, macos_min_version.major, macos_min_version.minor });
+ const e2e = b.addSystemCommand(&.{ "swiftc", "-O", "-target", triple, "-import-objc-header" });
+ e2e.addFileArg(b.path("src/macos/pardes.h"));
+ e2e.addArg("-o");
+ const e2e_bin = e2e.addOutputFileArg("pardes-macos-e2e");
+ for ([_][]const u8{
+ "src/macos/Sources/PardesView.swift",
+ "src/macos/Sources/ScenePostprocessor.swift",
+ "src/macos/Sources/FileWatcher.swift",
+ "src/macos/Sources/AppDelegate.swift",
+ "test/macos_e2e.swift",
+ }) |source| e2e.addFileArg(b.path(source));
+ e2e.addFileArg(lib_archive);
+ e2e.addArgs(&.{ "-lc++", "-framework", "AppKit", "-framework", "CoreText", "-framework", "CoreGraphics", "-framework", "CoreImage", "-framework", "Metal" });
+ const files = b.addWriteFiles();
+ const runnable = files.addCopyFile(e2e_bin, "pardes-macos-e2e");
+ _ = files.addCopyFile(b.path("shaders/crt.ci.metal"), "crt.ci.metal");
+ const run_e2e = b.addSystemCommand(&.{});
+ run_e2e.addFileArg(runnable);
+ run_e2e.setCwd(b.path("."));
+ if (b.args) |args| run_e2e.addArgs(args);
+ run_e2e.has_side_effects = true;
+ e2e_step.dependOn(&run_e2e.step);
+ } else e2e_step.dependOn(&b.addFail("macos-e2e needs a Darwin host and target").step);
- // Same step name the other native platforms use, because in this
- // configuration theirs is not declared. macos.zig imports the core, so
- // this one aggregate reaches both the C-ABI guard and the core tests.
const unit_step = b.step("unit-test", "run the C-ABI guard and core unit tests");
- unit_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = root_mod, .filters = test_filters })).step);
+ const unit_tests = b.addTest(.{ .root_module = root_mod, .filters = test_filters });
+ if (test_nonce) |nonce| unit_tests.root_module.addImport("test_rebuild_nonce", nonce);
+ test_build.dependOn(&unit_tests.step);
+ const run_unit = b.addRunArtifact(unit_tests);
+ run_unit.has_side_effects = true;
+ unit_step.dependOn(&run_unit.step);
+ if (testMatch(b, unit_profile, test_filters, unit_step)) |check| check.addArtifactArg(unit_tests);
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
- // shell gets its own name so it no longer clobbers the tty binary,
- // and any non linux-x86_64 target carries os-arch in the name.
var exe_name: []const u8 = if (platform == .gui) "pardes-gui" else "pardes";
if (target.result.os.tag != .linux or target.result.cpu.arch != .x86_64)
exe_name = b.fmt("{s}-{s}-{s}", .{ exe_name, @tagName(target.result.os.tag), @tagName(target.result.cpu.arch) });
@@ -1336,10 +828,6 @@ pub fn build(b: *std.Build) void {
.root_module = root_mod,
});
b.installArtifact(exe);
- // The gui beside the cli when neither was named. Same source, its own
- // module, its own binary name — `pardes-gui`, which is the spelling
- // nested.zig's executable-family test already expects to find next to
- // `pardes` (see samePardesExecutable).
if (gui_mod) |gm| {
var gui_name: []const u8 = "pardes-gui";
if (target.result.os.tag != .linux or target.result.cpu.arch != .x86_64)
@@ -1353,11 +841,6 @@ pub fn build(b: *std.Build) void {
}
const run = b.addRunArtifact(exe);
b.step("run", "descend to the pardes").dependOn(&run.step);
- // `pardes-isolate`: the SAME source on the core's own defaults — the
- // embedded source filesystem, the in-process clipboard, silent ptys.
- // Its host keeps only the terminal it draws on, so nothing is forked
- // and there is no filesystem in the binary to reach. A pardes that can
- // only read itself.
if (isolated_mod) |iso| {
const iso_exe = b.addExecutable(.{
.name = "pardes-isolate",
@@ -1367,17 +850,11 @@ pub fn build(b: *std.Build) void {
const run_iso = b.addRunArtifact(iso_exe);
// Something to look at on arrival, read out of the binary itself.
if (b.args) |args| run_iso.addArgs(args) else run_iso.addArg("src/pardes.zig");
- // Installed by its own step, like every other binary here that is
- // not one of the two shells `zig build` exists to produce.
const iso_step = b.step("run-isolated", "descend to a pardes with no host but the terminal");
- iso_step.dependOn(&b.addInstallArtifact(iso_exe, .{ .dest_dir = .{ .override = .{ .custom = "dev" } } }).step);
iso_step.dependOn(&run_iso.step);
}
web_step.dependOn(&b.addFail("web needs -Dplatform=web -Dtarget=wasm32-freestanding -Ddump=<dump.zon>").step);
- // snapshot parity suite: scripted input traces in a pty, captured grids
- // diffed against the goldens in snapshots/ — the frozen record of parity
- // with the prototype this program replaced. See build/snap.zig.
snap_build.addTty(b, .{
.exe = exe,
.target = target,
@@ -1385,11 +862,6 @@ pub fn build(b: *std.Build) void {
.ghostty_vt = ghostty_vt_for_snap,
});
- // Native image regression harness. TTY builds emulate Kitty over a
- // pty and inspect the real APC stream; GUI builds drive PARDES_TEST's
- // real SDL GPU readback and inspect source-colored pixels. It is a
- // separate explicit step because the GUI arm needs a graphical/GPU
- // session, while unit-test remains safe on headless builders.
const image_harness = b.addExecutable(.{
.name = "pardes-image-harness",
.root_module = b.createModule(.{
@@ -1410,10 +882,6 @@ pub fn build(b: *std.Build) void {
run_image_harness.has_side_effects = true;
b.step("image-harness", "exercise native Kitty/SDL image rendering end to end").dependOn(&run_image_harness.step);
- // The PDF arm uses the same native-pixel observer but a generated,
- // searchable two-page document. Keep it a distinct opt-in step: the
- // ordinary image harness stays identical when MuPDF is disabled, and
- // asking for PDF coverage without the feature gets an explicit error.
const pdf_harness_step = b.step("pdf-harness", "exercise continuous PDF rendering, search, and sections navigation end to end");
if (enable_mupdf) {
const run_pdf_harness = b.addRunArtifact(image_harness);
@@ -1426,10 +894,6 @@ pub fn build(b: *std.Build) void {
pdf_harness_step.dependOn(&b.addFail("pdf-harness is unavailable with -Dmupdf=false").step);
}
- // helix differential harness: drive the core headlessly over
- // JSON-Lines cases, one contract result line per case on stdout —
- // the pardes half of the pardes-vs-helix diff (helix's hx-harness on
- // its pardes-harness branch speaks the same contract).
const hxdiff = b.addExecutable(.{
.name = "pardes-hxdiff",
.root_module = b.createModule(.{
@@ -1439,54 +903,55 @@ pub fn build(b: *std.Build) void {
.link_libc = true,
}),
});
- hxdiff.root_module.addImport("pardes", hx_core_mod);
- // Installed by the steps that run it, not by the default install: the
- // two shells are what `zig build` puts in the user's bin directory.
- const install_hxdiff = &b.addInstallArtifact(hxdiff, .{ .dest_dir = .{ .override = .{ .custom = "dev" } } }).step;
+ hxdiff.root_module.addImport("pardes", core_module);
const run_hxdiff = b.addRunArtifact(hxdiff);
+ run_hxdiff.has_side_effects = true;
if (b.args) |args| run_hxdiff.addArgs(args) else {
- // no args: run the checked-in differential suite offline —
- // cases vs the helix goldens, waivers exempting the documented
- // pardes-isms. Nonzero exit on any unwaivered mismatch.
run_hxdiff.addArgs(&.{
+ "--strict",
"test/hxcases/cases.jsonl",
"test/hxcases/goldens.jsonl",
"test/hxcases/waivers.jsonl",
});
run_hxdiff.setCwd(b.path("."));
}
- const hxdiff_step = b.step("hxdiff", "run the helix differential suite (-- <cases.jsonl> [goldens.jsonl [waivers.jsonl]])");
- hxdiff_step.dependOn(install_hxdiff);
+ const hxdiff_step = b.step("hxdiff", "compare Helix results (-- --strict cases reference [waivers]; cases alone emits)");
hxdiff_step.dependOn(&run_hxdiff.step);
- // file-vs-pty parity: the SAME harness binary, run in --parity mode.
- // Each case runs twice over the same text and keys, once in a file
- // pane and once in a pty pane, and the two result lines must be
- // identical — the file pane is the oracle, so editing a shell pane
- // cannot drift away from editing a document.
+ const helix_harness = b.option([]const u8, "helix-harness", "path to the reference hx-harness executable") orelse
+ b.graph.environ_map.get("HX_HARNESS") orelse "hx-harness";
+ const reference = b.addSystemCommand(&.{helix_harness});
+ reference.addFileArg(b.path("test/hxcases/cases.jsonl"));
+ reference.has_side_effects = true;
+ const reference_results = reference.captureStdOut(.{});
+ const live_diff = b.addRunArtifact(hxdiff);
+ live_diff.addArg("--strict");
+ live_diff.addFileArg(b.path("test/hxcases/cases.jsonl"));
+ live_diff.addFileArg(reference_results);
+ live_diff.addFileArg(b.path("test/hxcases/waivers.jsonl"));
+ live_diff.has_side_effects = true;
+ b.step("hxdiff-live", "compare the editor against a freshly run Helix reference").dependOn(&live_diff.step);
+ const update_reference = b.addUpdateSourceFiles();
+ update_reference.addCopyFileToSource(reference_results, "test/hxcases/goldens.jsonl");
+ update_reference.step.dependOn(&live_diff.step);
+ b.step("hxdiff-update", "replace checked-in Helix goldens after the live comparison passes").dependOn(&update_reference.step);
+
const run_hxparity = b.addRunArtifact(hxdiff);
+ run_hxparity.has_side_effects = true;
run_hxparity.addArg("--parity");
if (b.args) |args| run_hxparity.addArgs(args) else {
- // the WHOLE helix corpus plus the editing extras: a parity gate
- // that only ran the cases its author wrote could not catch the
- // next regression. parity-waivers names the divergences that are
- // not editing (pardes bindings, pty viewport geometry, a tab the
- // emulator expands), each with its reason.
run_hxparity.addArgs(&.{
- "--waivers", "test/hxcases/parity-waivers.jsonl",
- "test/hxcases/cases.jsonl", "test/hxcases/parity.jsonl",
+ "--strict",
+ "--waivers",
+ "test/hxcases/parity-waivers.jsonl",
+ "test/hxcases/cases.jsonl",
+ "test/hxcases/parity.jsonl",
});
run_hxparity.setCwd(b.path("."));
}
const hxparity_step = b.step("hxparity", "run the file-vs-pty editing parity suite (-- [--waivers w.jsonl] <cases.jsonl>...)");
- hxparity_step.dependOn(install_hxdiff);
hxparity_step.dependOn(&run_hxparity.step);
- // the language-backend scoreboard. ReleaseFast on purpose: the point
- // is to compare backends' real cost, and a Debug build measures the
- // safety checks of whichever one allocates most. It links the same
- // core module as hxdiff, so `lsp.query` here is the one the editor
- // runs.
const lspbench = b.addExecutable(.{
.name = "pardes-lspbench",
.root_module = b.createModule(.{
@@ -1496,19 +961,21 @@ pub fn build(b: *std.Build) void {
.link_libc = true,
}),
});
- lspbench.root_module.addImport("pardes", hx_core_mod);
- const install_lspbench = &b.addInstallArtifact(lspbench, .{ .dest_dir = .{ .override = .{ .custom = "dev" } } }).step;
+ lspbench.root_module.addImport("pardes", core_module);
const run_lspbench = b.addRunArtifact(lspbench);
if (b.args) |args| run_lspbench.addArgs(args);
run_lspbench.setCwd(b.path("."));
- const lspbench_step = b.step("lspbench", "language-backend latency + feature matrix (-- [--json] [repo-root])");
- lspbench_step.dependOn(install_lspbench);
+ run_lspbench.has_side_effects = true;
+ const lspbench_step = b.step("lspbench", "language-backend latency + feature matrix (-- [--json] [--check] [repo-root])");
lspbench_step.dependOn(&run_lspbench.step);
+ const run_lspbench_check = b.addRunArtifact(lspbench);
+ run_lspbench_check.addArg("--check");
+ if (b.args) |args| run_lspbench_check.addArgs(args);
+ run_lspbench_check.setCwd(b.path("."));
+ run_lspbench_check.has_side_effects = true;
+ const lspbench_check_step = b.step("lspbench-check", "require every configured language probe to complete correctly");
+ lspbench_check_step.dependOn(&run_lspbench_check.step);
- // The client's one-shot probe: the same seam, one query, any
- // workspace — how the protocol client is exercised against real
- // rust/C/go trees during development without driving the editor.
- // Debug is fine: the latency measured is the child server's.
const lspprobe = b.addExecutable(.{
.name = "pardes-lspprobe",
.root_module = b.createModule(.{
@@ -1518,43 +985,67 @@ pub fn build(b: *std.Build) void {
.link_libc = true,
}),
});
- lspprobe.root_module.addImport("pardes", hx_core_mod);
- const install_lspprobe = &b.addInstallArtifact(lspprobe, .{ .dest_dir = .{ .override = .{ .custom = "dev" } } }).step;
+ lspprobe.root_module.addImport("pardes", core_module);
const run_lspprobe = b.addRunArtifact(lspprobe);
if (b.args) |args| run_lspprobe.addArgs(args);
const lspprobe_step = b.step("lspprobe", "one language query against the real seam (-- <kind> <file> <line>:<col> [arg] [--reps N])");
- lspprobe_step.dependOn(install_lspprobe);
lspprobe_step.dependOn(&run_lspprobe.step);
- // the editing scoreboard: one gesture, one file size, one number.
- // ReleaseFast for the same reason lspbench is — a Debug build measures
- // safety checks, and the question here is what the algorithm costs.
- // Same core module again, so the `render` this times is the editor's.
+ const perf_optimize: std.builtin.OptimizeMode = .ReleaseFast;
+ const perf_options = b.addOptions();
+ const perf_target = std.Target.Query.fromTarget(&target.result);
+ perf_options.addOption(PerfBuild, "identity", .{
+ .compiler = builtin.zig_version_string,
+ .target = perf_target.zigTriple(b.allocator) catch @panic("out of memory"),
+ .cpu = perf_target.serializeCpuAlloc(b.allocator) catch @panic("out of memory"),
+ .driver_optimize = @tagName(perf_optimize),
+ .core_optimize = @tagName(optimize),
+ .c_optimize = @tagName(c_optimize),
+ .terminal_optimize = @tagName(ghostty_optimize),
+ .platform = @tagName(platform),
+ .grammars = @tagName(tree_sitter_grammars),
+ .mupdf = enable_mupdf,
+ .jpx = enable_mupdf and enable_jpx,
+ .tracy = tracy != null,
+ .quic = enable_quic,
+ .theme_animation = theme_animation orelse (platform != .esp32p4),
+ .terminal_simd = ghostty_simd,
+ .prebuilt_shaders = platform == .gui and prebuilt_shaders,
+ });
const perf = b.addExecutable(.{
.name = "pardes-perf",
.root_module = b.createModule(.{
.target = target,
- .optimize = .ReleaseFast,
+ .optimize = perf_optimize,
.root_source_file = b.path("test/perf.zig"),
.link_libc = true,
}),
});
- perf.root_module.addImport("pardes", hx_core_mod);
- const install_perf = &b.addInstallArtifact(perf, .{ .dest_dir = .{ .override = .{ .custom = "dev" } } }).step;
+ perf.root_module.addImport("pardes", core_module);
+ perf.root_module.addOptions("perf_options", perf_options);
const run_perf = b.addRunArtifact(perf);
+ run_perf.has_side_effects = true;
if (b.args) |args| run_perf.addArgs(args);
run_perf.setCwd(b.path("."));
- const perf_step = b.step("perf", "large-file / long-line latency table (-- [--json] [--reps N] [--base old.json])");
- perf_step.dependOn(install_perf);
+ const perf_step = b.step("perf", "gesture latency or bounded terminal stress (-- [--json] [--reps N] [--base old.json] [--terminal-mib N])");
perf_step.dependOn(&run_perf.step);
+ const perf_tests = b.addTest(.{
+ .root_module = b.createModule(.{
+ .target = target,
+ .optimize = optimize,
+ .root_source_file = b.path("test/perf.zig"),
+ .link_libc = true,
+ }),
+ .filters = test_filters,
+ });
+ perf_tests.root_module.addOptions("perf_options", perf_options);
+ if (test_nonce) |nonce| perf_tests.root_module.addImport("test_rebuild_nonce", nonce);
+ const run_perf_test = b.addRunArtifact(perf_tests);
+ run_perf_test.has_side_effects = true;
+ const perf_test_step = b.step("perf-test", "test performance options and baselines without compiling the editor");
+ perf_test_step.dependOn(&run_perf_test.step);
+ if (testMatch(b, unit_profile, test_filters, perf_test_step)) |check| check.addArtifactArg(perf_tests);
- // The filesystem scoreboard. Same shape as `perf` and for the same
- // reason: it drives `acmefs.handle` through the real core, so it wants
- // the core module rather than a second compilation of it, and
- // ReleaseFast because a Debug run measures safety checks. It takes no
- // mount point — there is no FUSE and no thread in it, which is exactly
- // the claim the split makes and the thing worth measuring separately
- // from the kernel's read/write/wake.
const fs_bench = b.addExecutable(.{
.name = "pardes-fs-bench",
.root_module = b.createModule(.{
@@ -1564,259 +1055,255 @@ pub fn build(b: *std.Build) void {
.link_libc = true,
}),
});
- fs_bench.root_module.addImport("pardes", hx_core_mod);
- const install_fs_bench = &b.addInstallArtifact(fs_bench, .{ .dest_dir = .{ .override = .{ .custom = "dev" } } }).step;
+ fs_bench.root_module.addImport("pardes", core_module);
const run_fs_bench = b.addRunArtifact(fs_bench);
+ run_fs_bench.has_side_effects = true;
if (b.args) |args| run_fs_bench.addArgs(args);
run_fs_bench.setCwd(b.path("."));
- const fs_bench_step = b.step("fs-bench", "acme-fs per-request cost and allocation count (-- [--json] [--reps N])");
- fs_bench_step.dependOn(install_fs_bench);
+ const fs_bench_step = b.step("fs-bench", "9P filesystem request cost and allocation count (-- [--json] [--reps N])");
fs_bench_step.dependOn(&run_fs_bench.step);
+ const fs_bench_test = b.addTest(.{
+ .root_module = b.createModule(.{
+ .target = target,
+ .optimize = optimize,
+ .root_source_file = b.path("test/fs_bench.zig"),
+ }),
+ .filters = test_filters,
+ });
+ const run_fs_bench_test = b.addRunArtifact(fs_bench_test);
+ run_fs_bench_test.has_side_effects = true;
+ const fs_bench_test_step = b.step("fs-bench-test", "test filesystem benchmark options without compiling the editor");
+ fs_bench_test_step.dependOn(&run_fs_bench_test.step);
+ if (testMatch(b, unit_profile, test_filters, fs_bench_test_step)) |check| check.addArtifactArg(fs_bench_test);
- const unit_step = b.step("unit-test", "run native shell and module unit tests");
- // Secure tempfile creation is native-shell IO, isolated from the core
- // and tested as its own libc-linked module.
- const temp_file_test = b.addTest(.{ .root_module = b.createModule(.{
- .target = target,
- .optimize = optimize,
- .root_source_file = b.path("src/temp_file.zig"),
- .link_libc = true,
- }) });
- unit_step.dependOn(&b.addRunArtifact(temp_file_test).step);
- // Resolving a shell binary and picking its prompt integration is the
- // same shape: native-shell IO, no core imports, its own libc-linked
- // module. (A test file the core merely re-exported would compile and
- // silently never run — zig only collects tests from what it analyses.)
- const shell_bin_test = b.addTest(.{ .root_module = b.createModule(.{
+ const syntax_module = b.createModule(.{
.target = target,
.optimize = optimize,
- .root_source_file = b.path("src/shell_bin.zig"),
+ .root_source_file = b.path("src/syntax.zig"),
.link_libc = true,
- }) });
- unit_step.dependOn(&b.addRunArtifact(shell_bin_test).step);
- // Nested-instance detection and its socket path: same shape again —
- // native-shell IO, no core imports, its own libc-linked module.
- const nested_test = b.addTest(.{ .root_module = b.createModule(.{
- .target = target,
- .optimize = optimize,
- .root_source_file = b.path("src/nested.zig"),
- .link_libc = true,
- }) });
- unit_step.dependOn(&b.addRunArtifact(nested_test).step);
- // The LSP protocol client. Its own module for the reason its
- // neighbours have, and it earned the comment above the hard way: the
- // core DOES import src/lsp/lsp.zig, which imports this file, and that
- // is not enough — zig collects tests from a module's ROOT SOURCE FILE
- // only, so every test in src/lsp/ compiled and none of them ran. What
- // went unnoticed behind that is the whole reason this entry exists: the
- // transport asked for SOCK_CLOEXEC, which darwin's socketpair(2)
- // rejects, so no language server had ever spawned on macOS. The
- // end-to-end cover (test/snapshots/lsp-client.snap) could not catch it
- // either — `snap` runs the x86_64-linux target, where the flag is real.
- //
- // `pardes_config` and `zls` are attached because lsp.zig's `backends`
- // names the in-process analyser; this test module compiles the same
- // graph the shells do rather than a second, luckier one.
- const lsp_client_test = b.addTest(.{ .root_module = blk: {
- const m = b.createModule(.{
+ });
+ syntax_module.addOptions("pardes_config", opts);
+ wireCore(syntax_module, .{ .tree_sitter = ts_mod, .ts_libs = ts_libs.items, .ts_queries = ts_queries_opts });
+ const syntax_runner = b.addExecutable(.{
+ .name = "pardes-syntax",
+ .root_module = b.createModule(.{
.target = target,
.optimize = optimize,
- .root_source_file = b.path("src/lsp/lsp_client.zig"),
+ .root_source_file = b.path("test/syntax.zig"),
.link_libc = true,
- });
- m.addOptions("pardes_config", opts);
- if (zls_mod) |x| m.addImport("zls", x);
- break :blk m;
- } });
- unit_step.dependOn(&b.addRunArtifact(lsp_client_test).step);
- // The shared LSP host worker. Rooted here for the reason every module
- // above it is: zig collects tests from a module's ROOT FILE only, and
- // this one is reached through src/pardes.zig by three shells that would
- // each have compiled its tests and run none of them. It needs the whole
- // core graph because it imports the core — the same wiring hx_core_mod
- // gets, in one call, so a dependency added there is not missed here.
- const lsp_host_test = b.addTest(.{ .root_module = blk: {
- const m = b.createModule(.{
+ }),
+ });
+ syntax_runner.root_module.addImport("syntax", syntax_module);
+ const run_syntax = b.addRunArtifact(syntax_runner);
+ if (b.args) |args| run_syntax.addArgs(args);
+ b.step("syntax", "emit deterministic syntax snapshots").dependOn(&run_syntax.step);
+ const run_syntax_bench = b.addRunArtifact(syntax_runner);
+ run_syntax_bench.addArg("--bench");
+ if (b.args) |args| run_syntax_bench.addArgs(args);
+ run_syntax_bench.has_side_effects = true;
+ b.step("syntax-bench", "measure highlighting latency and allocations").dependOn(&run_syntax_bench.step);
+ const syntax_test = b.addTest(.{
+ .root_module = syntax_module,
+ .filters = if (test_filters.len == 0) &.{"syntax "} else test_filters,
+ });
+ const run_syntax_test = b.addRunArtifact(syntax_test);
+ run_syntax_test.has_side_effects = true;
+ const syntax_test_step = b.step("syntax-test", "test syntax correctness without building the editor");
+ syntax_test_step.dependOn(&run_syntax_test.step);
+ if (testMatch(b, unit_profile, test_filters, syntax_test_step)) |check| check.addArtifactArg(syntax_test);
+
+ const run_fs_test = b.addSystemCommand(&.{ "python3", "-B", "test/fs.py" });
+ run_fs_test.addArtifactArg(exe);
+ if (enable_quic) run_fs_test.addArg("--quic");
+ run_fs_test.setCwd(b.path("."));
+ run_fs_test.has_side_effects = true;
+ b.step("fs-test", "exercise default 9P service and mounts with an independent client").dependOn(&run_fs_test.step);
+
+ const run_agent_session_test = b.addSystemCommand(&.{ "python3", "-B", "test/agent_session_test.py" });
+ run_agent_session_test.addArtifactArg(exe);
+ if (platform == .gui) run_agent_session_test.addArg("--gui-grid");
+ run_agent_session_test.setCwd(b.path("."));
+ run_agent_session_test.has_side_effects = true;
+ b.step("agent-session-test", "test interactive session readiness, input and cleanup through 9P").dependOn(&run_agent_session_test.step);
+
+ const history = b.addExecutable(.{
+ .name = "pardes-history",
+ .root_module = b.createModule(.{
.target = target,
.optimize = optimize,
- .root_source_file = b.path("src/lsp_host.zig"),
- .link_libc = true,
- });
- m.addOptions("pardes_config", opts);
- m.addOptions("pardes_isolation", iso_off);
- wireCore(m, .{
- .themes = themes_mod,
- .zls = zls_mod,
- .tree_sitter = ts_mod,
- .ts_libs = ts_libs.items,
- .ts_queries = ts_queries_opts,
- .zstbi = zstbi_mod,
- .mvzr = mvzr_mod,
- .mupdf = mupdf_core_mod,
- .ghostty_vt = ghostty_core_vt,
- .vaxis = vaxis_mod,
- .uucode = uucode_mod,
- });
- // src/source_manifest.zig embeds these two by name; every core
- // compilation gets them beside `pardes_config` (see the core_mods
- // loop), and a module rooted inside the core needs them too.
- m.addAnonymousImport("root-build.zig", .{ .root_source_file = b.path("build.zig") });
- m.addAnonymousImport("root-build.zig.zon", .{ .root_source_file = b.path("build.zig.zon") });
- break :blk m;
- } });
- unit_step.dependOn(&b.addRunArtifact(lsp_host_test).step);
- // The 9P2000 codec and its sans-io server. Its own module for the
- // reason spelled out above rather than as a convention, and a stronger
- // one than its neighbours have: 9p.zig is FREESTANDING — it imports
- // `std` and nothing else, so that the same source compiles for
- // wasm32- and riscv32-freestanding — and its only host-side importer
- // is src/fs9_service.zig, which reaches pardes.zig. Compiling it here
- // as its own root is therefore also the check that the freestanding
- // promise still holds: NO `link_libc`, and an import of anything
- // OS-shaped would fail this step rather than passing quietly inside
- // the core's graph.
- const ninep_test = b.addTest(.{ .root_module = b.createModule(.{
- .target = target,
- .optimize = optimize,
- .root_source_file = b.path("src/9p.zig"),
- }) });
- unit_step.dependOn(&b.addRunArtifact(ninep_test).step);
- // The board's own 9P tree, and the comptime table that generates it. Its own module for the
- // reason its neighbours have: nothing `unit-test` compiles reaches src/board9p.zig — the
- // core does not import it, because the whole point of it is that it does NOT import the
- // core — so its eight tests would otherwise silently not exist. No `link_libc`: it imports
- // `std`, `src/board_pins.zig` and, in its tests only, `ninep`, which is what lets the same
- // source serve a real client here and drive `hal.gpio` on the die.
- //
- // `ninep` used to be injected here as a named module over src/9p.zig.
- // It is a plain path import now (`src/board9p.zig` says why), so this
- // test needs no module map at all — which is the same property that
- // lets the toolchain repository root a firmware image at
- // src/esp32p4_9p.zig without being told what to inject.
- const board9p_test = b.addTest(.{ .root_module = b.createModule(.{
- .target = target,
- .optimize = optimize,
- .root_source_file = b.path("src/board9p.zig"),
- }) });
- unit_step.dependOn(&b.addRunArtifact(board9p_test).step);
- // The board's input-rescue policy: drain the receiver while spinning on a full transmitter.
- // A measured bug — a 200-byte burst typed into a long frame lost 88 bytes on the die — so
- // it gets a test that fails without the fix, and it runs HERE rather than only on hardware.
- // Its own module for the reason spelled out above rather than as a convention: nothing
- // `unit-test` compiles reaches src/esp32p4/, because the firmware root is its own module graph,
- // so these five tests would otherwise silently not exist. No `link_libc`, unlike its
- // neighbours: input_rescue.zig imports `std` and nothing else, which is exactly what lets
- // the same source run against a fake FIFO here and against UART0 on the board.
- const esp32p4_rescue_test = b.addTest(.{ .root_module = b.createModule(.{
+ .root_source_file = b.path("test/history.zig"),
+ }),
+ });
+ const run_history = b.addRunArtifact(history);
+ if (b.args) |args| run_history.addArgs(args);
+ run_history.has_side_effects = true;
+ b.step("history", "compare command output and test/build runtime across jj revisions").dependOn(&run_history.step);
+
+ const unit_step = b.step("unit-test", "run native shell and module unit tests");
+ const unit_match = testMatch(b, unit_profile, test_filters, unit_step);
+ const lspbench_tests = b.addTest(.{ .root_module = lspbench.root_module, .filters = test_filters });
+ const run_lspbench_tests = b.addRunArtifact(lspbench_tests);
+ run_lspbench_tests.has_side_effects = true;
+ const lspbench_test_step = b.step("lspbench-test", "test language benchmark completeness and correctness gates");
+ lspbench_test_step.dependOn(&run_lspbench_tests.step);
+ lspbench_check_step.dependOn(&run_lspbench_tests.step);
+ if (testMatch(b, unit_profile, test_filters, lspbench_test_step)) |check| check.addArtifactArg(lspbench_tests);
+ const config_module = b.createModule(.{
.target = target,
.optimize = optimize,
- .root_source_file = b.path("src/esp32p4/input_rescue.zig"),
- }) });
- unit_step.dependOn(&b.addRunArtifact(esp32p4_rescue_test).step);
- // fonts.zig is the same shape once more, and it needs its own module
- // for the reason spelled out above rather than as a convention: the
- // core imports it behind `platform == .gui or .macos`, so on this build
- // nothing analyses it and its tests would silently not exist. That is
- // how a picker capped at 512 faces on a machine with a thousand of them
- // went unnoticed.
- const fonts_test = b.addTest(.{ .root_module = b.createModule(.{
+ .root_source_file = b.path("src/config.zig"),
+ });
+ config_module.addImport("pardes_config", core_module.import_table.get("pardes_config").?);
+ const config_tests = b.addTest(.{ .root_module = config_module, .filters = test_filters });
+ const run_config_tests = b.addRunArtifact(config_tests);
+ run_config_tests.has_side_effects = true;
+ const config_test_step = b.step("config-test", "run configuration tests without compiling the editor");
+ config_test_step.dependOn(&run_config_tests.step);
+ if (testMatch(b, unit_profile, test_filters, config_test_step)) |check| check.addArtifactArg(config_tests);
+ const diff_tests = b.addTest(.{ .root_module = hxdiff.root_module, .filters = test_filters });
+ const run_diff_tests = b.addRunArtifact(diff_tests);
+ run_diff_tests.has_side_effects = true;
+ const diff_test_step = b.step("hxdiff-test", "test differential comparison, allocation failures and CLI regression gates");
+ diff_test_step.dependOn(&run_diff_tests.step);
+ if (testMatch(b, unit_profile, test_filters, diff_test_step)) |check| check.addArtifactArg(diff_tests);
+ const run_diff_cli = b.addRunArtifact(hxdiff);
+ run_diff_cli.addArg("--self-test");
+ run_diff_cli.has_side_effects = true;
+ diff_test_step.dependOn(&run_diff_cli.step);
+ const core_test = b.addTest(.{ .root_module = core_module, .filters = test_filters });
+ const run_core_test = b.addRunArtifact(core_test);
+ run_core_test.has_side_effects = true;
+ const core_test_step = b.step("core-test", "run core tests without native shell tests");
+ core_test_step.dependOn(&run_core_test.step);
+ if (testMatch(b, unit_profile, test_filters, core_test_step)) |check| check.addArtifactArg(core_test);
+ const pane_module = b.createModule(.{
.target = target,
.optimize = optimize,
- .root_source_file = b.path("src/fonts.zig"),
+ .root_source_file = b.path("test/panes.zig"),
.link_libc = true,
- }) });
- unit_step.dependOn(&b.addRunArtifact(fonts_test).step);
- // crt.zig is pure std — the mouse-mapping-vs-shader-formula test
- const crt_test = b.addTest(.{ .root_module = b.createModule(.{
- .target = target,
- .optimize = optimize,
- .root_source_file = b.path("src/gui/crt.zig"),
- }) });
- unit_step.dependOn(&b.addRunArtifact(crt_test).step);
- // deck.zig is pure std too; replay.zig drives it with slices of real
- // Steam Deck recordings (test/deck/*.zon, deckcap capture format)
- const deck_test = b.addTest(.{ .root_module = b.createModule(.{
+ });
+ pane_module.addImport("pardes", core_module);
+ pane_module.addImport("pardes_config", core_module.import_table.get("pardes_config").?);
+ const pane_tests = b.addTest(.{ .root_module = pane_module, .filters = test_filters });
+ if (test_nonce) |nonce| pane_tests.root_module.addImport("test_rebuild_nonce", nonce);
+ test_build.dependOn(&pane_tests.step);
+ const run_pane_tests = b.addRunArtifact(pane_tests);
+ run_pane_tests.has_side_effects = true;
+ unit_step.dependOn(&run_pane_tests.step);
+ if (unit_match) |check| check.addArtifactArg(pane_tests);
+ const pane_test_step = b.step("pane-test", "run pane, output, PDF and namespace integration tests");
+ pane_test_step.dependOn(&run_pane_tests.step);
+ if (testMatch(b, unit_profile, test_filters, pane_test_step)) |check| check.addArtifactArg(pane_tests);
+
+ const protocol_step = b.step("9p-test", "run freestanding 9P protocol tests");
+ const history_step = b.step("history-test", "test historical comparisons and regression gates");
+ const protocol_match = testMatch(b, unit_profile, test_filters, protocol_step);
+ const history_match = testMatch(b, unit_profile, test_filters, history_step);
+ for ([_]struct { path: []const u8, libc: bool }{
+ .{ .path = "test/history.zig", .libc = false },
+ .{ .path = "src/9p.zig", .libc = false },
+ .{ .path = "src/esp32p4_gpio.zig", .libc = false },
+ .{ .path = "src/esp32p4/input_rescue.zig", .libc = false },
+ .{ .path = "src/fonts.zig", .libc = true },
+ .{ .path = "src/gui/crt.zig", .libc = false },
+ }) |entry| {
+ const tests = b.addTest(.{
+ .root_module = b.createModule(.{
+ .target = target,
+ .optimize = optimize,
+ .root_source_file = b.path(entry.path),
+ .link_libc = entry.libc,
+ }),
+ .filters = test_filters,
+ });
+ if (test_nonce) |nonce| tests.root_module.addImport("test_rebuild_nonce", nonce);
+ test_build.dependOn(&tests.step);
+ const run_tests = b.addRunArtifact(tests);
+ run_tests.has_side_effects = true;
+ unit_step.dependOn(&run_tests.step);
+ if (unit_match) |check| check.addArtifactArg(tests);
+ if (std.mem.eql(u8, entry.path, "src/9p.zig")) {
+ protocol_step.dependOn(&run_tests.step);
+ if (protocol_match) |check| check.addArtifactArg(tests);
+ }
+ if (std.mem.eql(u8, entry.path, "test/history.zig")) {
+ history_step.dependOn(&run_tests.step);
+ if (history_match) |check| check.addArtifactArg(tests);
+ }
+ }
+
+ const deck_module = b.createModule(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("test/deck/replay.zig"),
- }) });
- deck_test.root_module.addImport("deck", b.createModule(.{
+ });
+ deck_module.addImport("deck", b.createModule(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/gui/deck.zig"),
}));
- unit_step.dependOn(&b.addRunArtifact(deck_test).step);
- // Shell-specific inline tests. main() imports a shell inside its
- // runtime switch, which test analysis never enters. main.zig's test
- // block names the selected shell, user config, and platform-only
- // helpers; its ordinary core import reaches pardes.zig and modal.zig.
- //
- // `-Dtest-filter=<substring>` narrows it. The whole binary is 14s and
- // two tests are 8.6s of that, so a one-line change to a module with a
- // three-millisecond test used to cost the full run: `zig build
- // unit-test -Dtest-filter="an untouched tagline"` is ~1s. The test
- // runner's own `--test-filter` never worked here - nothing forwarded
- // `--` args to the run step, so it was parsed as a script name.
+ const deck_test = b.addTest(.{ .root_module = deck_module, .filters = test_filters });
+ if (test_nonce) |nonce| deck_test.root_module.addImport("test_rebuild_nonce", nonce);
+ test_build.dependOn(&deck_test.step);
+ const run_deck_test = b.addRunArtifact(deck_test);
+ run_deck_test.has_side_effects = true;
+ unit_step.dependOn(&run_deck_test.step);
+ if (unit_match) |check| check.addArtifactArg(deck_test);
+
if (platform == .tty or platform == .gui) {
const shell_test = b.addTest(.{ .root_module = root_mod, .filters = test_filters });
- unit_step.dependOn(&b.addRunArtifact(shell_test).step);
+ if (test_nonce) |nonce| shell_test.root_module.addImport("test_rebuild_nonce", nonce);
+ test_build.dependOn(&shell_test.step);
+ const run_shell_test = b.addRunArtifact(shell_test);
+ run_shell_test.has_side_effects = true;
+ unit_step.dependOn(&run_shell_test.step);
+ if (unit_match) |check| check.addArtifactArg(shell_test);
+ const run_profile = b.addRunArtifact(unit_profile);
+ run_profile.addArtifactArg(shell_test);
+ if (b.args) |args| run_profile.addArgs(args);
+ run_profile.has_side_effects = true;
+ b.step("unit-profile", "report test request time and process memory as JSONL (-- optional filter, --timeout-ms=N)").dependOn(&run_profile.step);
+ const profile_test = b.addExecutable(.{
+ .name = "pardes-unit-profile-test",
+ .root_module = b.createModule(.{
+ .target = target,
+ .optimize = optimize,
+ .root_source_file = b.path("test/unit_profile_test.zig"),
+ }),
+ });
+ const run_profile_test = b.addRunArtifact(profile_test);
+ run_profile_test.addArtifactArg(unit_profile);
+ run_profile_test.has_side_effects = true;
+ b.step("unit-profile-test", "test profiler timing, failures and deadlines").dependOn(&run_profile_test.step);
+ if (test_filters.len == 0) {
+ unit_step.dependOn(&run_profile_test.step);
+ test_build.dependOn(&unit_profile.step);
+ test_build.dependOn(&profile_test.step);
+ }
}
}
}
-/// Is this build free to choose where it installs? Only when nothing else has
-/// said: no DESTDIR, no `--prefix`, and no `--prefix-lib-dir`/`--prefix-exe-dir`
-/// /`--prefix-include-dir`.
-///
-/// Answered by INSPECTING WHAT THE RUNNER RESOLVED, because the runner records
-/// nothing else: `lib/build_runner.zig` keeps the requested step names in a
-/// local, and calls `resolveInstallPrefix(install_prefix, dir_list)` (its
-/// line 459) before `runBuild`, which substitutes the `zig-out` default for a
-/// null prefix and folds the three directory overrides into `lib_dir`/
-/// `exe_dir`/`h_dir`. So by the time `build()` runs, "was it given" survives
-/// only as "does it still look exactly like the default".
-///
-/// KNOWN LIMIT, stated because it cannot be closed from here: `--prefix` given
-/// as the default path spelled absolutely (`--prefix "$PWD/zig-out"`) is
-/// indistinguishable from no prefix at all. `announceInstallPrefix` below is
-/// the mitigation — the redirect says out loud where it put things, so the case
-/// is visible rather than silent.
+fn testMatch(b: *std.Build, profiler: *std.Build.Step.Compile, filters: []const []const u8, step: *std.Build.Step) ?*std.Build.Step.Run {
+ if (filters.len == 0) return null;
+ const check = b.addRunArtifact(profiler);
+ check.addArgs(&.{ "--check-filter", filters[0] });
+ check.has_side_effects = true;
+ step.dependOn(&check.step);
+ return check;
+}
+
fn prefixIsUntouched(b: *std.Build) bool {
if (b.dest_dir != null) return false;
const zig_out = b.build_root.join(b.allocator, &.{"zig-out"}) catch @panic("OOM");
if (!std.mem.eql(u8, b.install_prefix, zig_out)) return false;
- // The three directory overrides do NOT touch `install_prefix`, so without
- // this they would pass the test above and then be discarded by the
- // `resolveInstallPrefix` call that follows it.
return std.mem.eql(u8, b.exe_dir, b.pathJoin(&.{ b.install_path, "bin" })) and
std.mem.eql(u8, b.lib_dir, b.pathJoin(&.{ b.install_path, "lib" })) and
std.mem.eql(u8, b.h_dir, b.pathJoin(&.{ b.install_path, "include" }));
}
-/// The commit this build came from, or null when there is nothing to say.
-///
-/// Read at CONFIGURE time and handed to `pardes_config`, so the binary carries
-/// a string rather than the ability to shell out. That is the whole point: a
-/// `--version` that runs `git` itself reports the tree it happens to be
-/// standing in rather than the one it was built from, and on the board there is
-/// no `git` to run and no process to run it with.
-///
-/// ABSENCE IS NOT AN ERROR, and must not be. A release tarball has no `.git`, a
-/// container may have no `git` binary, and a source drop is not a repository —
-/// none of those is a reason to refuse to build. Every way of having no answer
-/// (no binary, no repository, a git that exits non-zero, a git that prints
-/// nothing) lands on the same `null`, and the frontends print the version alone.
-///
-/// Deliberately NOT `--dirty`. Marking a dirty tree costs a worktree stat on
-/// every configure, and — the real cost — it would change `pardes_config` on
-/// every file edit. Every module in this build imports that options module, so
-/// a dirty marker means editing one line rebuilds the world. The commit alone
-/// changes only when a commit does.
fn gitCommit(b: *std.Build) ?[]const u8 {
var code: u8 = 0;
- // `-C` the build root rather than trusting the cwd: `zig build` may be run
- // from anywhere, and a `git` resolved against the wrong directory would
- // cheerfully answer about a DIFFERENT repository.
const out = b.runAllowFail(
&.{ "git", "-C", b.build_root.path orelse ".", "rev-parse", "--short=12", "HEAD" },
&code,
@@ -1826,11 +1313,6 @@ fn gitCommit(b: *std.Build) ?[]const u8 {
return if (trimmed.len == 0) null else trimmed;
}
-/// Everything `pardes_config` says that does NOT depend on which frontend is
-/// being built. Gathered into one value so the two options modules a default
-/// build makes cannot drift apart in any field but the one that is supposed to
-/// differ. Adding a field here is additive for both shells at once, which is
-/// the property that makes two shells in one build cheap to keep honest.
const ShellConfig = struct {
tree_sitter_grammars: TreeSitterGrammars,
tracy: bool,
@@ -1843,15 +1325,9 @@ const ShellConfig = struct {
theme_animation: ?bool,
prebuilt_shaders: bool,
zig_lib_dir: []const u8,
- /// Which commit this build came from, or null when there is no answer. Read
- /// once in `build()` so the two shells of a default build cannot disagree,
- /// and so `git` is spawned once rather than per frontend.
commit: ?[]const u8,
};
-/// One `pardes_config` options module, for one frontend. `module_names` and
-/// `module_roots` are NOT here: they are folded out of the module import table
-/// long after this runs, and are added to every returned module then.
fn shellOptions(b: *std.Build, cfg: ShellConfig, platform: Platform) *std.Build.Step.Options {
const o = b.addOptions();
o.addOption(Platform, "platform", platform);
@@ -1868,24 +1344,11 @@ fn shellOptions(b: *std.Build, cfg: ShellConfig, platform: Platform) *std.Build.
// Meaningful only for the SDL shell. Keeping the platform condition here
// prevents Config/EffectCode from describing tty/macOS/web as "prebuilt".
o.addOption(bool, "gui_shader_sources_prebuilt", platform == .gui and cfg.prebuilt_shaders);
- // Which ZLS is compiled in, for `SPC l i`. Kept next to the dependency it
- // names: the .zon pins a commit, and a status screen that cannot say WHICH
- // analyser answered is not worth opening.
o.addOption([]const u8, "zls_version", if (cfg.zls_backend) zls_version else "none");
- // THE version, read from the manifest rather than copied beside it. The
- // `@import` being UNTYPED is what makes that possible: annotating its type
- // would demand an exact field match and reject `.dependencies`, `.paths`
- // and the rest, which is the failure the hand-synced literal that used to
- // sit here was working around. Verified against this exact manifest.
o.addOption([]const u8, "version", zon.version);
// ...and the commit it was built from, when there is one. See `gitCommit`
// for why this is optional and why it is read at configure time.
o.addOption(?[]const u8, "commit", cfg.commit);
- // The stdlib this binary was compiled against, so `gd` on `std.mem.count`
- // can open the same mem.zig the compiler used. ZLS resolves `@import("std")`
- // through `zig_lib_dir` and nothing else; without it every std symbol is a
- // silent miss, and asking the `zig` binary for it is the subprocess this
- // whole backend exists to avoid. ZIG_LIB_DIR overrides it at runtime.
o.addOption([]const u8, "zig_lib_dir", cfg.zig_lib_dir);
return o;
}
@@ -1897,10 +1360,6 @@ 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" });
@@ -1912,14 +1371,6 @@ fn fatArchive(b: *std.Build, lib: *std.Build.Step.Compile) std.Build.LazyPath {
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\"", "_" });
@@ -1937,11 +1388,6 @@ fn compileGlsl(b: *std.Build, name: []const u8) std.Build.LazyPath {
return cmd.addOutputFileArg(b.fmt("{s}.spv", .{name}));
}
-/// The theme sources, sorted. SORTED because the run step is cached by its
-/// argv: readdir order is whatever the filesystem feels like, and an argv that
-/// shuffles is a cache miss and a rebuild every time. Anything that is not a
-/// .toml or a .json is skipped, which is what lets the upstream LICENSE files
-/// sit beside the themes they cover.
fn vendoredThemes(b: *std.Build, io: std.Io) []const []const u8 {
var count: usize = 0;
{
@@ -1973,13 +1419,6 @@ fn vendoredThemes(b: *std.Build, io: std.Io) []const []const u8 {
return names;
}
-/// Everything every compilation of src/pardes.zig needs, in one call per module
-/// rather than a line per module beside every dependency — which is the shape
-/// that let a third module quietly miss two of them.
-///
-/// Fields are in the order these used to be attached in, because that order IS
-/// the module map `Debug` prints (import_table iterates by insertion), and a
-/// refactor that renumbers a user-visible list changed something.
fn wireCore(m: *std.Build.Module, d: struct {
themes: ?*std.Build.Module = null,
zls: ?*std.Build.Module = null,