const std = @import("std"); const snap_build = @import("build/snap.zig"); pub const Platform = enum { tty, gui, web }; /// The ZLS the language backend links, spelled once. It names the commit /// build.zig.zon pins (0.16.x branch) and is passed BOTH to ZLS's own /// `-Dversion-string` (its build.zig otherwise shells out to `git describe`, /// which fails on a fetched package that has no .git) and to `SPC l i`. const zls_version = "0.16.1-dev+3e0d0820"; pub const TreeSitterGrammars = enum { disabled, zig, minimal, full }; const GhosttyArtifact = enum { ghostty, lib }; pub fn build(b: *std.Build) void { // Default target is the Steam Deck (deckcap's trick): x86_64 linux-gnu // with the glibc version pinned low, so a binary built on a rolling- // release host runs on SteamOS — a native build references the host's // newer versioned libm/libc symbols and dies with "GLIBC_2.4x not found" // on the deck. Override with -Dtarget= as usual. const target = b.standardTargetOptions(.{ .default_target = .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .gnu, .glibc_version = .{ .major = 2, .minor = 38, .patch = 0 }, } }); 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; // 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 c_optimize: std.builtin.OptimizeMode = if (optimize == .Debug) .ReleaseFast else optimize; const default_grammars: TreeSitterGrammars = if (is_emscripten) .zig else .full; const tree_sitter_grammars = b.option(TreeSitterGrammars, "tree-sitter", "tree-sitter grammar set: disabled, zig, minimal (c/c++/zig), 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=)"); // Web LOOK has a tiny read-only source filesystem. Generate one Zig module // on the host from every tracked .zig path + its current working-tree // contents. The generator prefers Git as the tracked-file filter and has a // conservative filesystem fallback for source archives without `.git`. const source_gen = b.addExecutable(.{ .name = "pardes-embed-zig-sources", .root_module = b.createModule(.{ .target = b.graph.host, .optimize = .ReleaseSafe, .root_source_file = b.path("tools/embed_zig_sources.zig"), }), }); const run_source_gen = b.addRunArtifact(source_gen); run_source_gen.addDirectoryArg(b.path(".")); const embedded_sources = run_source_gen.addOutputFileArg("embedded_sources.zig"); // The tracked set can change without any already-known input changing, so // discovery intentionally runs whenever a consumer builds it. run_source_gen.has_side_effects = true; const inspect_sources = b.addInstallFileWithDir(embedded_sources, .{ .custom = "web" }, "embedded_sources.zig"); b.step("embed-web-sources", "generate zig-out/web/embedded_sources.zig from tracked .zig files").dependOn(&inspect_sources.step); snap_build.addWeb(b); 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", .{}); // The language backend (src/lsp_zls.zig) links ZLS as a MODULE — no zls // binary, no JSON-RPC, no protocol. Everything except the browser gets it; // emscripten does not, because the web shell 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 = !is_emscripten; const opts = b.addOptions(); opts.addOption(Platform, "platform", platform); opts.addOption(bool, "syntax_highlighting", tree_sitter_grammars != .disabled); opts.addOption(bool, "syntax_zig_grammar", tree_sitter_grammars != .disabled); opts.addOption(bool, "syntax_minimal_grammars", tree_sitter_grammars == .minimal or tree_sitter_grammars == .full); opts.addOption(bool, "syntax_full_grammars", tree_sitter_grammars == .full); opts.addOption(bool, "enable_tracy", tracy != null); opts.addOption(bool, "zls_backend", zls_backend); // 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. opts.addOption([]const u8, "zls_version", if (zls_backend) zls_version else "none"); // 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. opts.addOption([]const u8, "zig_lib_dir", b.graph.zig_lib_directory.path orelse ""); // NOTE: `opts` is 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 it 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, .@"version-string" = @as([]const u8, zls_version), }); root_mod.addImport("zls", zls_dep.module("zls")); hx_core_mod.addImport("zls", zls_dep.module("zls")); } // 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 = c_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 (tree_sitter_grammars == .minimal and g.minimal) or (tree_sitter_grammars == .zig and std.mem.eql(u8, g.name, "zig"))) { 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 = c_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 = c_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). // 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 = !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; const ghostty_dep = if (is_emscripten) b.lazyDependency("ghostty", .{ .simd = false }) 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"); 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); // --- 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("")` 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 mod_names: std.ArrayList([]const u8) = .empty; var mod_roots: std.ArrayList([]const u8) = .empty; { var it = root_mod.import_table.iterator(); while (it.next()) |e| { const lp = e.value_ptr.*.root_source_file orelse continue; // `generated` is a build artifact (the options modules): it has no // path until make() runs, and pointing an editor at one is useless const abs = switch (lp) { .src_path => |sp| sp.owner.pathFromRoot(sp.sub_path), .cwd_relative => |cr| cr, else => continue, }; mod_names.append(b.allocator, e.key_ptr.*) catch @panic("OOM"); mod_roots.append(b.allocator, abs) catch @panic("OOM"); } } opts.addOption([]const []const u8, "module_names", mod_names.items); opts.addOption([]const []const u8, "module_roots", mod_roots.items); root_mod.addOptions("pardes_config", opts); hx_core_mod.addOptions("pardes_config", opts); // 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) { // 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 = if (emscripten_include) |include| b.lazyDependency("sdl", .{ .target = target, .optimize = optimize, .sanitize_c = .off, .preferred_linkage = .static, .system_include_path = include }) else b.lazyDependency("sdl", .{ .target = target, .optimize = optimize, .sanitize_c = .off, .preferred_linkage = .static }); 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"); } // 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()) root_mod.addRPath(.{ .cwd_relative = "/usr/local/lib" }); 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), }); root_mod.addAnonymousImport("embedded_sources", .{ .root_source_file = embedded_sources }); } 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"), }); root_mod.addAnonymousImport("crt.vert.spv", .{ .root_source_file = compileGlsl(b, "shaders/crt.vert.glsl", "vertex", "crt.vert.spv"), }); root_mod.addAnonymousImport("crt.frag.spv", .{ .root_source_file = compileGlsl(b, "shaders/crt.frag.glsl", "fragment", "crt.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", "-sEXPORTED_FUNCTIONS=['_main','_pardes_web_test_cols','_pardes_web_test_rows','_pardes_web_test_cursor_x','_pardes_web_test_cursor_y','_pardes_web_test_cell','_pardes_web_test_cell_fg','_pardes_web_test_cell_bg','_pardes_web_test_cell_attrs','_pardes_web_test_cell_x','_pardes_web_test_cell_y']", "-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 { // 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) }); const exe = b.addExecutable(.{ .name = exe_name, .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. See build/snap.zig. snap_build.addTty(b, .{ .exe = exe, .target = target, .optimize = optimize, .ghostty_vt = ghostty_vt_for_snap, }); // 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); // 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 run_hxparity = b.addRunArtifact(hxdiff); 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", }); run_hxparity.setCwd(b.path(".")); } b.step("hxparity", "run the file-vs-pty editing parity suite (-- [--waivers w.jsonl] ...)").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(.{ .target = target, .optimize = .ReleaseFast, .root_source_file = b.path("test/lspbench.zig"), .link_libc = true, }), }); lspbench.root_module.addImport("pardes", hx_core_mod); b.installArtifact(lspbench); const run_lspbench = b.addRunArtifact(lspbench); if (b.args) |args| run_lspbench.addArgs(args); run_lspbench.setCwd(b.path(".")); b.step("lspbench", "language-backend latency + feature matrix (-- [--json] [repo-root])").dependOn(&run_lspbench.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"), }) }); const unit_step = b.step("unit-test", "run the modal.zig + crt.zig + deck replay unit tests"); unit_step.dependOn(&b.addRunArtifact(unit).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/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(.{ .target = target, .optimize = optimize, .root_source_file = b.path("test/deck/replay.zig"), }) }); deck_test.root_module.addImport("deck", b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("src/deck.zig"), })); unit_step.dependOn(&b.addRunArtifact(deck_test).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); }