diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-02 21:56:01 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | 589269c0b3ca74a29f5889fa6e781e59cef7efdb (patch) | |
| tree | ecd2ce3060d06353f3b6302e70abf784295ff40f /build.zig | |
| parent | 81992cc92743a98066ee1a9025bc74f91cf0e967 (diff) | |
| download | pardes-589269c0b3ca74a29f5889fa6e781e59cef7efdb.tar.gz pardes-589269c0b3ca74a29f5889fa6e781e59cef7efdb.zip | |
wasm replay ui
Diffstat (limited to 'build.zig')
| -rw-r--r-- | build.zig | 327 |
1 files changed, 250 insertions, 77 deletions
@@ -1,10 +1,14 @@ const std = @import("std"); const AppKind = enum { main, replay }; +const GhosttyArtifact = enum { ghostty, lib }; pub fn build(b: *std.Build) void { - const optimize = b.standardOptimizeOption(.{}); + const requested_optimize = b.standardOptimizeOption(.{}); const target = b.standardTargetOptions(.{}); + const is_emscripten = target.result.os.tag == .emscripten; + const optimize = if (is_emscripten) .ReleaseSmall else requested_optimize; + const emscripten_sysroot = if (is_emscripten) detectEmscriptenSysroot(b) else null; // build-time IO, only used to slurp the grammars' highlights.scm queries. var threaded: std.Io.Threaded = .init(b.allocator, .{}); @@ -24,6 +28,25 @@ pub fn build(b: *std.Build) void { // pulled in when this flag is set. Use `zig build test-sdl -Dsdl` for the // rendered-image SDL e2e suite. const sdl = b.option(bool, "sdl", "build the SDL3 GPU UI frontend instead of the terminal backend") orelse false; + const sdl_backend = sdl or is_emscripten; + const replay_dump = b.option([]const u8, "replay-dump", "embed this replay dump .zon into pardes-replay"); + const syntax_highlighting = !is_emscripten; + const emscripten_include: ?std.Build.LazyPath = if (emscripten_sysroot) |sysroot| + .{ .cwd_relative = b.pathJoin(&.{ sysroot, "include" }) } + else + null; + const emscripten_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 exe = b.addExecutable(.{ .name = "pardes", @@ -45,6 +68,10 @@ pub fn build(b: *std.Build) void { .strip = strip, }), }); + if (emscripten_libc_file) |libc_file| { + exe.setLibCFile(libc_file); + replay.setLibCFile(libc_file); + } const tests = b.addExecutable(.{ .name = "pardes-e2e", .linkage = exe_linkage, @@ -58,11 +85,21 @@ pub fn build(b: *std.Build) void { const main_app_config = b.addOptions(); main_app_config.addOption(AppKind, "app_kind", .main); + main_app_config.addOption(bool, "syntax_highlighting", syntax_highlighting); exe.root_module.addOptions("app_config", main_app_config); const replay_app_config = b.addOptions(); replay_app_config.addOption(AppKind, "app_kind", .replay); + replay_app_config.addOption(bool, "syntax_highlighting", syntax_highlighting); replay.root_module.addOptions("app_config", replay_app_config); + const replay_config = b.addOptions(); + replay_config.addOption(bool, "embedded_dump", replay_dump != null); + replay.root_module.addOptions("replay_config", replay_config); + if (replay_dump) |path| { + replay.root_module.addAnonymousImport("replay.dump.zon", .{ + .root_source_file = if (std.fs.path.isAbsolute(path)) .{ .cwd_relative = path } else b.path(path), + }); + } // ghostty-vt and vaxis both depend on uucode, but Zig 0.16 forbids the // same source file from belonging to two modules, so they can't each have @@ -100,7 +137,7 @@ pub fn build(b: *std.Build) void { // doesn't even require libc, but it has a significant performance // penalty. If your embedding app requires libc anyway, you should // always keep simd enabled. - // .simd = false, + .simd = !is_emscripten, })) |dep| { // Accessing ghostty's modules runs its build(), which (because vaxis is // a shared dependency, deduplicated to the same module) wires ghostty's @@ -108,6 +145,23 @@ pub fn build(b: *std.Build) void { // with ours — order matters, so this must happen after this point. const ghostty_vt = dep.module("ghostty-vt"); ghostty_vt.addImport("uucode", uucode_mod); + 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); + } exe.root_module.addImport("ghostty-vt", ghostty_vt); replay.root_module.addImport("ghostty-vt", ghostty_vt); // The e2e test harness parses the app's pty output with its own ghostty @@ -124,7 +178,7 @@ pub fn build(b: *std.Build) void { // `"vaxis"` directly; the chosen backend file re-exports the shared UI types // (Key/Mouse/Window/Cell/…) and provides the renderer/loop (`Vaxis`/`Tty`/ // `Loop`/`init`). vaxis = transparent passthrough; sdl = SDL3 GPU frontend. - const ui_root = if (sdl) b.path("ui_sdl.zig") else b.path("ui_vaxis.zig"); + const ui_root = if (sdl_backend) b.path("ui_sdl.zig") else b.path("ui_vaxis.zig"); const ui_mod = b.createModule(.{ .target = target, .optimize = optimize, @@ -134,19 +188,30 @@ pub fn build(b: *std.Build) void { exe.root_module.addImport("ui", ui_mod); replay.root_module.addImport("ui", ui_mod); - // SDL3 GPU backend wiring (`-Dsdl`). The sdl package builds SDL3 from source + // SDL3 backend wiring (`-Dsdl`, or Emscripten replay builds). The sdl package builds SDL3 from source // as a static lib + emits the SDL3/ headers; we @cImport those headers in // ui_sdl.zig, link the lib, and pull in stb_truetype (for the glyph atlas) and // the embedded DejaVuSansMono font. All gated on the flag so a plain `zig build` // (vaxis) never fetches or compiles SDL. - if (sdl) { - const sdl_dep = b.lazyDependency("sdl", .{ - .target = target, - .optimize = optimize, - }) orelse return; // not fetched (offline?) — skip SDL wiring + var sdl_lib_for_wasm: ?*std.Build.Step.Compile = null; + if (sdl_backend) { + const sdl_dep = if (is_emscripten and emscripten_include != null) + b.lazyDependency("sdl", .{ + .target = target, + .optimize = optimize, + .system_include_path = emscripten_include.?, + }) orelse return + else + b.lazyDependency("sdl", .{ + .target = target, + .optimize = optimize, + }) orelse return; // not fetched (offline?) — skip SDL wiring const sdl_lib = sdl_dep.artifact("SDL3"); + if (emscripten_libc_file) |libc_file| sdl_lib.setLibCFile(libc_file); + if (is_emscripten) sdl_lib_for_wasm = sdl_lib; ui_mod.linkLibrary(sdl_lib); ui_mod.addIncludePath(sdl_dep.path("include")); + if (emscripten_include) |include| ui_mod.addSystemIncludePath(include); ui_mod.link_libc = true; // stb_truetype: the header (declarations, via include path) + the impl TU @@ -156,13 +221,18 @@ pub fn build(b: *std.Build) void { ui_mod.addCSourceFile(.{ .file = b.path("vendor/stb/stb_truetype_impl.c") }); ui_mod.addCSourceFile(.{ .file = b.path("vendor/stb/font.c") }); - // GLSL -> SPIR-V at build time, embedded into ui_sdl.zig via @embedFile. - ui_mod.addAnonymousImport("ui.vert.spv", .{ - .root_source_file = compileGlsl(b, "shaders/ui.vert.glsl", "vertex", "ui.vert.spv"), - }); - ui_mod.addAnonymousImport("ui.frag.spv", .{ - .root_source_file = compileGlsl(b, "shaders/ui.frag.glsl", "fragment", "ui.frag.spv"), - }); + if (is_emscripten) { + ui_mod.addAnonymousImport("ui.vert.es.glsl", .{ .root_source_file = b.path("shaders/ui.vert.es.glsl") }); + ui_mod.addAnonymousImport("ui.frag.es.glsl", .{ .root_source_file = b.path("shaders/ui.frag.es.glsl") }); + } else { + // GLSL -> SPIR-V at build time, embedded into ui_sdl.zig via @embedFile. + ui_mod.addAnonymousImport("ui.vert.spv", .{ + .root_source_file = compileGlsl(b, "shaders/ui.vert.glsl", "vertex", "ui.vert.spv"), + }); + ui_mod.addAnonymousImport("ui.frag.spv", .{ + .root_source_file = compileGlsl(b, "shaders/ui.frag.glsl", "fragment", "ui.frag.spv"), + }); + } } // zstbi: C (stb_image)-backed image decoder for the image-viewer panes. We use @@ -170,73 +240,77 @@ pub fn build(b: *std.Build) void { // compiler here; stb_image is plain C (extern fns), so it compiles fine, decodes // every common format, and offers downscaling. Its module ("root") carries the // C source + include path + libc, so importing it is all that's needed. - const zstbi_mod = b.dependency("zstbi", .{ .target = target, .optimize = optimize }).module("root"); + const zstbi_dep = b.dependency("zstbi", .{ .target = target, .optimize = optimize }); + const zstbi_mod = zstbi_dep.module("root"); exe.root_module.addImport("zstbi", zstbi_mod); replay.root_module.addImport("zstbi", zstbi_mod); - // tree-sitter syntax highlighting for file panes: the Zig bindings + C runtime - // (the `tree-sitter` module), each grammar's parser.c linked as a static lib, - // and its highlights.scm read at build time into the `ts_queries` options module. - const tree_sitter = b.dependency("tree_sitter", .{ .target = target, .optimize = optimize }); - exe.root_module.addImport("tree-sitter", tree_sitter.module("tree_sitter")); - replay.root_module.addImport("tree-sitter", tree_sitter.module("tree_sitter")); + if (syntax_highlighting) { + // tree-sitter syntax highlighting for file panes: the Zig bindings + C runtime + // (the `tree-sitter` module), each grammar's parser.c linked as a static lib, + // and its highlights.scm read at build time into the `ts_queries` options module. + const tree_sitter = b.dependency("tree_sitter", .{ .target = target, .optimize = optimize }); + const tree_sitter_mod = tree_sitter.module("tree_sitter"); + exe.root_module.addImport("tree-sitter", tree_sitter_mod); + replay.root_module.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", - local_query: ?[]const u8 = null, - }; - const grammars = [_]Grammar{ - .{ .name = "ada", .dep = "ts_ada" }, - .{ .name = "bash", .dep = "ts_bash", .scanner = true }, - .{ .name = "c", .dep = "ts_c" }, - .{ .name = "c_sharp", .dep = "ts_c_sharp", .scanner = true }, - .{ .name = "clojure", .dep = "ts_clojure" }, - .{ .name = "cpp", .dep = "ts_cpp", .scanner = 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" }, - }; - const ts_queries = b.addOptions(); - inline for (grammars) |g| { - const dep = b.dependency(g.dep, .{}); - const query_path = if (g.local_query) |p| b.path(p) else 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 Grammar = struct { + name: []const u8, + dep: []const u8, + src: []const u8 = "src", + scanner: bool = false, + query: []const u8 = "queries/highlights.scm", + local_query: ?[]const u8 = null, + }; + const grammars = [_]Grammar{ + .{ .name = "ada", .dep = "ts_ada" }, + .{ .name = "bash", .dep = "ts_bash", .scanner = true }, + .{ .name = "c", .dep = "ts_c" }, + .{ .name = "c_sharp", .dep = "ts_c_sharp", .scanner = true }, + .{ .name = "clojure", .dep = "ts_clojure" }, + .{ .name = "cpp", .dep = "ts_cpp", .scanner = 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" }, + }; + const ts_queries = b.addOptions(); + inline for (grammars) |g| { + const dep = b.dependency(g.dep, .{}); + const query_path = if (g.local_query) |p| b.path(p) else 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, - }); - lib.root_module.addCSourceFile(.{ .file = dep.path(g.src ++ "/parser.c") }); - if (g.scanner) lib.root_module.addCSourceFile(.{ .file = dep.path(g.src ++ "/scanner.c") }); - lib.root_module.addIncludePath(dep.path(g.src)); - exe.root_module.linkLibrary(lib); - replay.root_module.linkLibrary(lib); + const lib = b.addLibrary(.{ + .name = "tree-sitter-" ++ g.name, + .root_module = b.createModule(.{ .target = target, .optimize = optimize, .link_libc = true }), + .linkage = .static, + }); + lib.root_module.addCSourceFile(.{ .file = dep.path(g.src ++ "/parser.c") }); + if (g.scanner) lib.root_module.addCSourceFile(.{ .file = dep.path(g.src ++ "/scanner.c") }); + lib.root_module.addIncludePath(dep.path(g.src)); + exe.root_module.linkLibrary(lib); + replay.root_module.linkLibrary(lib); + } + exe.root_module.addOptions("ts_queries", ts_queries); + replay.root_module.addOptions("ts_queries", ts_queries); } - exe.root_module.addOptions("ts_queries", ts_queries); - replay.root_module.addOptions("ts_queries", ts_queries); // Install the exe so `zig build` (and `zig build install`) produces it in // zig-out/bin for global install, not just as a hidden dep of `run`/`test`. @@ -253,6 +327,85 @@ pub fn build(b: *std.Build) void { const replay_step = b.step("replay", "replay a pardes dump: zig build replay -- <dump.zon>"); replay_step.dependOn(&run_replay.step); + const replay_wasm_step = b.step("replay-wasm", "build browser SDL replay (requires -Dtarget=wasm32-emscripten -Dreplay-dump=<dump.zon>)"); + if (!is_emscripten) { + replay_wasm_step.dependOn(&b.addFail("replay-wasm requires -Dtarget=wasm32-emscripten").step); + } else if (replay_dump == null) { + replay_wasm_step.dependOn(&b.addFail("replay-wasm requires -Dreplay-dump=<dump.zon>").step); + } else if (emscripten_sysroot == null) { + replay_wasm_step.dependOn(&b.addFail("replay-wasm requires an Emscripten sysroot; pass --sysroot <emscripten>/cache/sysroot or set EM_CACHE/EMSDK").step); + } else { + const replay_obj = b.addObject(.{ + .name = "pardes-replay", + .root_module = replay.root_module, + }); + if (emscripten_libc_file) |libc_file| replay_obj.setLibCFile(libc_file); + + const emcc_path = b.option([]const u8, "emcc", "path to emcc for replay-wasm final link") orelse "/usr/lib/emscripten/emcc"; + const emcc = b.addSystemCommand(&.{emcc_path}); + emcc.addFileArg(replay_obj.getEmittedBinDirectory().path(b, "pardes-replay_zcu.o")); + emcc.addFileArg(replay_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")); + if (sdl_lib_for_wasm) |sdl_lib| emcc.addFileArg(sdl_lib.getEmittedBin()); + 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=2147483648", + "-sEXIT_RUNTIME=0", + "-sFULL_ES3=1", + "-sMIN_WEBGL_VERSION=2", + "-sMAX_WEBGL_VERSION=2", + "-sINCOMING_MODULE_JS_API=canvas", + "-o", + }); + const replay_js = emcc.addOutputFileArg("pardes-replay.js"); + replay_wasm_step.dependOn(&b.addInstallFileWithDir(replay_js, .{ .custom = "web" }, "pardes-replay.js").step); + replay_wasm_step.dependOn(&b.addInstallFileWithDir(replay_js.dirname().path(b, "pardes-replay.wasm"), .{ .custom = "web" }, "pardes-replay.wasm").step); + + const web = b.addWriteFiles(); + const html = web.add("replay.html", + \\<!doctype html> + \\<html> + \\<head> + \\ <meta charset="utf-8"> + \\ <meta name="viewport" content="width=device-width, initial-scale=1"> + \\ <title>pardes replay</title> + \\ <style> + \\ html, body { margin: 0; min-width: 100%; min-height: 100%; background: #121212; } + \\ body { overflow: auto; } + \\ canvas { display: block; } + \\ </style> + \\</head> + \\<body> + \\ <canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas> + \\ <script> + \\ window.Module = { canvas: document.getElementById('canvas') }; + \\ </script> + \\ <script src="pardes-replay.js"></script> + \\</body> + \\</html> + ); + replay_wasm_step.dependOn(&b.addInstallFileWithDir(html, .{ .custom = "web" }, "replay.html").step); + } + + const test_browser_step = b.step("test-browser", "run browser SDL replay screenshot e2e (requires -Dtarget=wasm32-emscripten -Dreplay-dump=<dump.zon>)"); + if (!is_emscripten) { + test_browser_step.dependOn(&b.addFail("test-browser requires -Dtarget=wasm32-emscripten").step); + } else if (replay_dump == null) { + test_browser_step.dependOn(&b.addFail("test-browser requires -Dreplay-dump=<dump.zon>").step); + } else { + const run_browser_e2e = b.addSystemCommand(&.{ "node", "tests/browser_replay_e2e.mjs" }); + run_browser_e2e.addArg("zig-out/web"); + run_browser_e2e.addArg("zig-out/browser-replay.png"); + run_browser_e2e.step.dependOn(replay_wasm_step); + test_browser_step.dependOn(&run_browser_e2e.step); + } + // tutor_gen: a separate binary that parses tutor.txt and writes // tutor_cases.zig (a file of `test` blocks, one per practice block). // `zig build tutor-gen` runs it; the unit-test step depends on the output. @@ -380,3 +533,23 @@ fn compileGlsl(b: *std.Build, src: []const u8, stage: []const u8, out: []const u 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); +} |
