const std = @import("std"); pub const Platform = enum { tty, gui, web }; pub const TreeSitterGrammars = enum { disabled, minimal, full }; const GhosttyArtifact = enum { ghostty, lib }; pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const requested_optimize = b.standardOptimizeOption(.{}); const platform = b.option(Platform, "platform", "which shell to build (tty, gui, web)") orelse .tty; const static = b.option(bool, "static", "statically link") orelse false; const dump_path = b.option([]const u8, "dump", "dump .zon embedded into the web shell (-Dplatform=web)"); const emcc_path = b.option([]const u8, "emcc", "emcc for the web shell's final link") orelse "/usr/lib/emscripten/emcc"; const is_emscripten = target.result.os.tag == .emscripten; // wasm: size is the budget const optimize = if (is_emscripten) .ReleaseSmall else requested_optimize; const default_grammars: TreeSitterGrammars = if (is_emscripten) .minimal else .full; const tree_sitter_grammars = b.option(TreeSitterGrammars, "tree-sitter", "tree-sitter grammar set: disabled, minimal, full") orelse default_grammars; const tracy = b.option([]const u8, "tracy", "enable Tracy profiling; supply the path to a Tracy source checkout"); // the browser shell: zig object + emcc link + html, installed to zig-out/web const web_step = b.step("web", "build the browser shell into zig-out/web (-Dplatform=web -Dtarget=wasm32-emscripten -Ddump=)"); const emscripten_sysroot = if (is_emscripten) detectEmscriptenSysroot(b) else null; if (platform == .web and !is_emscripten) return failBuild(b, web_step, "-Dplatform=web requires -Dtarget=wasm32-emscripten"); if (platform != .web and is_emscripten) return failBuild(b, web_step, "-Dtarget=wasm32-emscripten requires -Dplatform=web"); if (platform == .web and dump_path == null) return failBuild(b, web_step, "-Dplatform=web requires -Ddump= (the browser has no ptys; state replays from an embedded dump)"); if (platform == .web and emscripten_sysroot == null) return failBuild(b, web_step, "no emscripten sysroot found; pass --sysroot /sysroot or set EM_CACHE/EMSDK"); // emscripten "libc": point zig at the sysroot headers/crt via a libc.txt // (the prototype's WriteFiles trick) — applied to EVERY compile step. const libc_file: ?std.Build.LazyPath = if (emscripten_sysroot) |sysroot| blk: { const files = b.addWriteFiles(); break :blk files.add("emscripten-libc.txt", b.fmt( \\include_dir={s}/include \\sys_include_dir={s}/include \\crt_dir={s}/lib/wasm32-emscripten \\msvc_lib_dir= \\kernel32_lib_dir= \\gcc_dir= \\ , .{ sysroot, sysroot, sysroot })); } else null; const emscripten_include: ?std.Build.LazyPath = if (emscripten_sysroot) |sysroot| .{ .cwd_relative = b.pathJoin(&.{ sysroot, "include" }) } else null; // build-time IO, only used to slurp the grammars' highlights.scm queries var threaded: std.Io.Threaded = .init(b.allocator, .{}); defer threaded.deinit(); const io = threaded.io(); const root_mod = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("src/main.zig"), .link_libc = true, }); // helix differential harness (test/hxdiff.zig): a second compilation of // the sans-IO 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(.{ .target = target, .optimize = optimize, .root_source_file = b.path("src/pardes.zig"), .link_libc = true, }); // static archives the emcc link needs beyond the zig object (grammars, SDL) var wasm_libs: std.ArrayList(std.Build.LazyPath) = .empty; // forkpty: in libc proper on glibc>=2.34 and darwin; the BSDs keep it in libutil if (target.result.os.tag == .freebsd or target.result.os.tag == .netbsd or target.result.os.tag == .openbsd) root_mod.linkSystemLibrary("util", .{}); const opts = b.addOptions(); opts.addOption(Platform, "platform", platform); opts.addOption(bool, "syntax_highlighting", tree_sitter_grammars != .disabled); opts.addOption(bool, "syntax_minimal_grammars", tree_sitter_grammars != .disabled); opts.addOption(bool, "syntax_full_grammars", tree_sitter_grammars == .full); opts.addOption(bool, "enable_tracy", tracy != null); root_mod.addOptions("pardes_config", opts); hx_core_mod.addOptions("pardes_config", opts); // Tracy zones (src/tracy.zig): compile the client into the binary only when // -Dtracy= 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| { mod.addIncludePath(.{ .cwd_relative = tracy_path }); mod.addCSourceFile(.{ .file = .{ .cwd_relative = b.pathJoin(&.{ tracy_path, "public", "TracyClient.cpp" }) }, .flags = &.{ "-DTRACY_ENABLE=1", "-DTRACY_NO_SAMPLING", "-DTRACY_NO_CALLSTACK", "-DTRACY_NO_SYSTEM_TRACING", "-DTRACY_NO_CRASH_HANDLER", "-DTRACY_NO_CODE_TRANSFER", "-fno-sanitize=undefined", }, }); mod.link_libcpp = true; } } // 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 = optimize }).module("tree_sitter"); // the C runtime rides the module as a static lib; give it the wasm // libc and hand its archive to the emcc link if (is_emscripten) { for (tree_sitter_mod.link_objects.items) |link_object| switch (link_object) { .other_step => |lib| { lib.setLibCFile(libc_file.?); wasm_libs.append(b.allocator, lib.getEmittedBin()) catch @panic("OOM"); }, else => {}, }; } root_mod.addImport("tree-sitter", tree_sitter_mod); hx_core_mod.addImport("tree-sitter", tree_sitter_mod); const Grammar = struct { name: []const u8, dep: []const u8, src: []const u8 = "src", scanner: bool = false, query: []const u8 = "queries/highlights.scm", minimal: bool = false, }; const grammars = [_]Grammar{ .{ .name = "ada", .dep = "ts_ada" }, .{ .name = "bash", .dep = "ts_bash", .scanner = true }, .{ .name = "c", .dep = "ts_c", .minimal = true }, .{ .name = "c_sharp", .dep = "ts_c_sharp", .scanner = true }, .{ .name = "clojure", .dep = "ts_clojure" }, .{ .name = "cpp", .dep = "ts_cpp", .scanner = true, .minimal = true }, .{ .name = "css", .dep = "ts_css", .scanner = true }, .{ .name = "elixir", .dep = "ts_elixir", .scanner = true }, .{ .name = "erlang", .dep = "ts_erlang", .scanner = true }, .{ .name = "fortran", .dep = "ts_fortran", .scanner = true }, .{ .name = "go", .dep = "ts_go" }, .{ .name = "haskell", .dep = "ts_haskell", .scanner = true }, .{ .name = "html", .dep = "ts_html", .scanner = true }, .{ .name = "java", .dep = "ts_java" }, .{ .name = "javascript", .dep = "ts_javascript", .scanner = true }, .{ .name = "json", .dep = "ts_json" }, .{ .name = "kotlin", .dep = "ts_kotlin", .scanner = true }, .{ .name = "ocaml", .dep = "ts_ocaml", .src = "grammars/ocaml/src", .scanner = true }, .{ .name = "pascal", .dep = "ts_pascal" }, .{ .name = "php", .dep = "ts_php", .src = "php/src", .scanner = true }, .{ .name = "powershell", .dep = "ts_powershell", .scanner = true }, .{ .name = "python", .dep = "ts_python", .scanner = true }, .{ .name = "ruby", .dep = "ts_ruby", .scanner = true }, .{ .name = "rust", .dep = "ts_rust", .scanner = true }, .{ .name = "scala", .dep = "ts_scala", .scanner = true }, .{ .name = "zig", .dep = "ts_zig", .minimal = true }, }; const ts_queries = b.addOptions(); inline for (grammars) |g| { if (tree_sitter_grammars == .full or g.minimal) { const dep = b.dependency(g.dep, .{}); const query_path = dep.path(g.query); const query = std.Io.Dir.cwd().readFileAlloc(io, query_path.getPath(b), b.allocator, .limited(0x100000)) catch @panic("read " ++ g.name ++ " highlights"); ts_queries.addOption([]const u8, g.name ++ "_highlights", query); const lib = b.addLibrary(.{ .name = "tree-sitter-" ++ g.name, .root_module = b.createModule(.{ .target = target, .optimize = optimize, .link_libc = true }), .linkage = .static, }); if (libc_file) |f| lib.setLibCFile(f); // 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 }); lib.root_module.addIncludePath(dep.path(g.src)); root_mod.linkLibrary(lib); hx_core_mod.linkLibrary(lib); if (is_emscripten) wasm_libs.append(b.allocator, lib.getEmittedBin()) catch @panic("OOM"); } } root_mod.addOptions("ts_queries", ts_queries); hx_core_mod.addOptions("ts_queries", ts_queries); } // zstbi (C stb_image): image pane decode. zigimg crashes the 0.16 compiler. const zstbi_dep = b.dependency("zstbi", .{ .target = target, .optimize = optimize }); root_mod.addImport("zstbi", zstbi_dep.module("root")); hx_core_mod.addImport("zstbi", zstbi_dep.module("root")); // 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, .optimize = optimize, .build_config_path = uucode_config, }).namedLazyPath("tables.zig"); const uucode_mod = b.dependency("uucode", .{ .target = target, .optimize = optimize, .build_config_path = uucode_config, .tables_path = uucode_tables, }).module("uucode"); const vaxis_mod = b.dependency("vaxis", .{ .target = target, .optimize = optimize, .external_uucode = true, }).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). const ghostty_simd = !target.result.os.tag.isDarwin() or b.graph.host.result.os.tag.isDarwin(); const ghostty_dep = if (is_emscripten) b.lazyDependency("ghostty", .{ .simd = false }) else b.lazyDependency("ghostty", .{ .target = target, .optimize = optimize, .simd = ghostty_simd }); if (ghostty_dep) |dep| { const ghostty_vt = dep.module("ghostty-vt"); ghostty_vt_for_snap = ghostty_vt; ghostty_vt.addImport("uucode", uucode_mod); // emscripten: hand ghostty-vt the terminal_options module its build() // would otherwise provide (copied verbatim from the prototype build) if (is_emscripten) { const term_opts = b.addOptions(); term_opts.addOption(GhosttyArtifact, "artifact", .lib); term_opts.addOption(bool, "c_abi", false); term_opts.addOption(bool, "oniguruma", false); term_opts.addOption(bool, "simd", false); term_opts.addOption(bool, "slow_runtime_safety", false); term_opts.addOption(bool, "kitty_graphics", false); term_opts.addOption(bool, "tmux_control_mode", false); term_opts.addOption([]const u8, "version_string", "0.0.0"); term_opts.addOption(usize, "version_major", 0); term_opts.addOption(usize, "version_minor", 0); term_opts.addOption(usize, "version_patch", 0); term_opts.addOption(?[]const u8, "version_pre", null); term_opts.addOption(?[]const u8, "version_build", null); ghostty_vt.addOptions("terminal_options", term_opts); } root_mod.addImport("ghostty-vt", ghostty_vt); hx_core_mod.addImport("ghostty-vt", ghostty_vt); } // override uucode on vaxis LAST so it wins over ghostty's build() wiring. vaxis_mod.addImport("uucode", uucode_mod); root_mod.addImport("vaxis", vaxis_mod); root_mod.addImport("uucode", uucode_mod); hx_core_mod.addImport("uucode", uucode_mod); // SDL3 shell wiring (-Dplatform=gui and web). The sdl package builds SDL3 // from source as a static lib + emits the SDL3/ headers; gui.zig @cImports // those, plus a small stb_truetype C shim for the glyph atlas, and embeds // the SPIR-V compiled at build time from shaders/*.glsl — or, on web, the // GL ES shader sources compiled at runtime by WebGL. if (platform == .gui or platform == .web) { const sdl_dep = if (emscripten_include) |include| b.lazyDependency("sdl", .{ .target = target, .optimize = optimize, .system_include_path = include }) else b.lazyDependency("sdl", .{ .target = target, .optimize = optimize }); if (sdl_dep) |dep| { const sdl_lib = dep.artifact("SDL3"); if (libc_file) |f| sdl_lib.setLibCFile(f); root_mod.linkLibrary(sdl_lib); root_mod.addIncludePath(dep.path("include")); if (emscripten_include) |include| root_mod.addSystemIncludePath(include); if (is_emscripten) wasm_libs.append(b.allocator, sdl_lib.getEmittedBin()) catch @panic("OOM"); } root_mod.addIncludePath(b.path("vendor/stb")); root_mod.addCSourceFile(.{ .file = b.path("vendor/stb/stb_truetype_impl.c") }); root_mod.addCSourceFile(.{ .file = b.path("vendor/stb/font.c") }); // the embedded UI font: provided as a build import since assets/ lives // outside the src/ module root (@embedFile can't escape it) root_mod.addAnonymousImport("AdwaitaMono-Regular.ttf", .{ .root_source_file = b.path("assets/AdwaitaMono-Regular.ttf"), }); if (platform == .web) { root_mod.addAnonymousImport("ui.vert.es.glsl", .{ .root_source_file = b.path("shaders/ui.vert.es.glsl") }); root_mod.addAnonymousImport("ui.frag.es.glsl", .{ .root_source_file = b.path("shaders/ui.frag.es.glsl") }); root_mod.addAnonymousImport("overlay.vert.es.glsl", .{ .root_source_file = b.path("shaders/overlay.vert.es.glsl") }); root_mod.addAnonymousImport("overlay.frag.es.glsl", .{ .root_source_file = b.path("shaders/overlay.frag.es.glsl") }); const dp = dump_path.?; root_mod.addAnonymousImport("embedded.dump.zon", .{ .root_source_file = if (std.fs.path.isAbsolute(dp)) .{ .cwd_relative = dp } else b.path(dp), }); } else { root_mod.addAnonymousImport("ui.vert.spv", .{ .root_source_file = compileGlsl(b, "shaders/ui.vert.glsl", "vertex", "ui.vert.spv"), }); root_mod.addAnonymousImport("ui.frag.spv", .{ .root_source_file = compileGlsl(b, "shaders/ui.frag.glsl", "fragment", "ui.frag.spv"), }); root_mod.addAnonymousImport("overlay.vert.spv", .{ .root_source_file = compileGlsl(b, "shaders/overlay.vert.glsl", "vertex", "overlay.vert.spv"), }); root_mod.addAnonymousImport("overlay.frag.spv", .{ .root_source_file = compileGlsl(b, "shaders/overlay.frag.glsl", "fragment", "overlay.frag.spv"), }); } } if (platform == .web) { // zig emits an object; emcc does the final link (wasm + js glue), // compiling the C shims itself and pulling in every static archive. const obj = b.addObject(.{ .name = "pardes", .root_module = root_mod }); obj.setLibCFile(libc_file.?); const emcc = b.addSystemCommand(&.{emcc_path}); emcc.addFileArg(obj.getEmittedBinDirectory().path(b, "pardes_zcu.o")); emcc.addFileArg(obj.getEmittedBin()); emcc.addFileArg(b.path("vendor/stb/stb_truetype_impl.c")); emcc.addFileArg(b.path("vendor/stb/font.c")); emcc.addFileArg(zstbi_dep.path("src/zstbi.c")); for (wasm_libs.items) |lib| emcc.addFileArg(lib); emcc.addPrefixedDirectoryArg("-I", b.path("vendor/stb")); emcc.addPrefixedDirectoryArg("-I", zstbi_dep.path("libs/stbi")); emcc.addArgs(&.{ "-std=c99", "-fno-sanitize=undefined", "-Oz", "-sINITIAL_MEMORY=1073741824", // the 512 MiB web heap + code/stack "-sEXIT_RUNTIME=0", // run() returns; the rAF loop keeps living "-sFULL_ES3=1", "-sMIN_WEBGL_VERSION=2", "-sMAX_WEBGL_VERSION=2", "-sINCOMING_MODULE_JS_API=canvas", "-o", }); const js = emcc.addOutputFileArg("pardes.js"); web_step.dependOn(&b.addInstallFileWithDir(js, .{ .custom = "web" }, "pardes.js").step); web_step.dependOn(&b.addInstallFileWithDir(js.dirname().path(b, "pardes.wasm"), .{ .custom = "web" }, "pardes.wasm").step); const html = b.addWriteFiles().add("index.html", html_shell); web_step.dependOn(&b.addInstallFileWithDir(html, .{ .custom = "web" }, "index.html").step); b.getInstallStep().dependOn(web_step); } else { const exe = b.addExecutable(.{ .name = "pardes", .linkage = if (static) .static else null, .root_module = root_mod, }); b.installArtifact(exe); const run = b.addRunArtifact(exe); b.step("run", "descend to the pardes").dependOn(&run.step); web_step.dependOn(&b.addFail("web needs -Dplatform=web -Dtarget=wasm32-emscripten -Ddump=").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. `-- --update` regenerates. const snap = b.addExecutable(.{ .name = "pardes-snap", .root_module = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("test/snapshot.zig"), .link_libc = true, }), }); if (ghostty_vt_for_snap) |vt| snap.root_module.addImport("ghostty-vt", vt); if (target.result.os.tag == .freebsd or target.result.os.tag == .netbsd or target.result.os.tag == .openbsd) snap.root_module.linkSystemLibrary("util", .{}); b.installArtifact(snap); const run_snap = b.addRunArtifact(snap); run_snap.addArtifactArg(exe); if (b.args) |args| run_snap.addArgs(args); b.step("snap", "run the snapshot parity suite (-- --update regenerates goldens)").dependOn(&run_snap.step); // helix differential harness: drive the sans-IO 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(.{ .target = target, .optimize = optimize, .root_source_file = b.path("test/hxdiff.zig"), .link_libc = true, }), }); hxdiff.root_module.addImport("pardes", hx_core_mod); b.installArtifact(hxdiff); const run_hxdiff = b.addRunArtifact(hxdiff); 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(&.{ "test/hxcases/cases.jsonl", "test/hxcases/goldens.jsonl", "test/hxcases/waivers.jsonl", }); run_hxdiff.setCwd(b.path(".")); } b.step("hxdiff", "run the helix differential suite (-- [goldens.jsonl [waivers.jsonl]])").dependOn(&run_hxdiff.step); // modal.zig is pure std — its inline unit tests run here const unit = b.addTest(.{ .root_module = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("src/modal.zig"), }) }); b.step("unit-test", "run the modal.zig unit tests").dependOn(&b.addRunArtifact(unit).step); } } // the html shell: a full-viewport canvas wired into Module (from the prototype) const html_shell = \\ \\ \\ \\ \\ \\ pardes \\ \\ \\ \\ \\ \\ \\ \\ ; // a bad -D combination: fail both `zig build` and `zig build web` with the why fn failBuild(b: *std.Build, web_step: *std.Build.Step, msg: []const u8) void { const fail = &b.addFail(msg).step; web_step.dependOn(fail); b.getInstallStep().dependOn(fail); } // glslc -fshader-stage= -o -> .spv, returned as a LazyPath // for @embedFile. Only used by the SDL3 GPU shell (-Dplatform=gui). fn compileGlsl(b: *std.Build, src: []const u8, stage: []const u8, out: []const u8) std.Build.LazyPath { const cmd = b.addSystemCommand(&.{ "glslc", b.fmt("-fshader-stage={s}", .{stage}) }); cmd.addFileArg(b.path(src)); cmd.addArg("-o"); return cmd.addOutputFileArg(out); } fn detectEmscriptenSysroot(b: *std.Build) ?[]const u8 { if (b.option([]const u8, "emscripten-sysroot", "path to the Emscripten sysroot")) |path| return path; if (b.sysroot) |path| return path; if (envPath(b, "EMSCRIPTEN_SYSROOT")) |path| return path; if (envPath(b, "EM_CACHE")) |cache| return b.pathJoin(&.{ cache, "sysroot" }); if (envPath(b, "EMSDK")) |emsdk| return b.pathJoin(&.{ emsdk, "upstream", "emscripten", "cache", "sysroot" }); if (envPath(b, "HOME")) |home| return b.pathJoin(&.{ home, ".cache", "emscripten", "sysroot" }); return null; } fn envPath(b: *std.Build, name: []const u8) ?[]const u8 { return b.graph.environ_map.get(name); }