summaryrefslogtreecommitdiff
path: root/build.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-01 22:44:19 -0300
committerGabriel Schneider <[email protected]>2026-08-10 09:17:07 -0300
commitd59397d4146ffa23adbbe6dc99c64ebb22a93b2d (patch)
treea06984bf172a6c894c1586d58aad02ec302b6c2d /build.zig
parentcbcdf01764e2e144a821643c3a8e9a5ad39eda2e (diff)
downloadpardes-d59397d4146ffa23adbbe6dc99c64ebb22a93b2d.tar.gz
pardes-d59397d4146ffa23adbbe6dc99c64ebb22a93b2d.zip
add vanilla DOM web backend
Diffstat (limited to 'build.zig')
-rw-r--r--build.zig306
1 files changed, 103 insertions, 203 deletions
diff --git a/build.zig b/build.zig
index 781cc848..b089a9c9 100644
--- a/build.zig
+++ b/build.zig
@@ -9,7 +9,6 @@ pub const Platform = enum { tty, gui, web };
/// 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
@@ -27,10 +26,10 @@ pub fn build(b: *std.Build) void {
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;
+ const is_web = platform == .web;
+ const is_web_target = target.result.cpu.arch == .wasm32 and target.result.os.tag == .freestanding;
// wasm: size is the budget
- const optimize = if (is_emscripten) .ReleaseSmall else requested_optimize;
+ const optimize = if (is_web) .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
@@ -38,12 +37,16 @@ pub fn build(b: *std.Build) void {
// 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;
+ // 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 .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=<dump.zon>)");
+ // The browser shell is a freestanding wasm core plus ordinary web files.
+ // JavaScript owns the loop and IO; HTML/CSS own rendering.
+ const web_step = b.step("web", "build the DOM browser shell into zig-out/web (-Dplatform=web -Dtarget=wasm32-freestanding -Ddump=<dump.zon>)");
// 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
@@ -68,30 +71,9 @@ pub fn build(b: *std.Build) void {
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 (is_web and !is_web_target) return failBuild(b, web_step, "-Dplatform=web requires -Dtarget=wasm32-freestanding");
+ if (!is_web and (is_web_target or target.result.os.tag == .emscripten)) return failBuild(b, web_step, "wasm browser targets require -Dplatform=web");
if (platform == .web and dump_path == null) return failBuild(b, web_step, "-Dplatform=web requires -Ddump=<dump.zon> (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 <emscripten-cache>/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: slurps the grammars' highlights.scm queries, and reads
// vendor/themes to find the theme sources
@@ -102,8 +84,8 @@ pub fn build(b: *std.Build) void {
const root_mod = b.createModule(.{
.target = target,
.optimize = optimize,
- .root_source_file = b.path("src/main.zig"),
- .link_libc = true,
+ .root_source_file = b.path(if (is_web) "src/web.zig" else "src/main.zig"),
+ .link_libc = !is_web,
});
// helix differential harness (test/hxdiff.zig): a second compilation of
// the sans-IO core, driven headlessly. Mirrors root_mod's wiring for
@@ -150,19 +132,16 @@ pub fn build(b: *std.Build) void {
root_mod.addImport("generated_themes", themes_mod);
hx_core_mod.addImport("generated_themes", themes_mod);
- // 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/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
+ // 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 = !is_emscripten;
+ const zls_backend = !is_web;
const opts = b.addOptions();
opts.addOption(Platform, "platform", platform);
@@ -236,16 +215,26 @@ pub fn build(b: *std.Build) void {
// 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) {
+ 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),
+ };
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");
+ lib.root_module.link_libc = false;
+ lib.root_module.addIncludePath(web_libc_include);
+ lib.root_module.addCMacro("NDEBUG", "1");
},
else => {},
};
+ root_mod.addIncludePath(b.path("src/web/libc/include"));
+ root_mod.addCSourceFile(.{
+ .file = b.path("src/web/libc.c"),
+ .flags = &.{ "-std=c11", "-DNDEBUG=1" },
+ });
}
root_mod.addImport("tree-sitter", tree_sitter_mod);
hx_core_mod.addImport("tree-sitter", tree_sitter_mod);
@@ -299,10 +288,9 @@ pub fn build(b: *std.Build) void {
const lib = b.addLibrary(.{
.name = "tree-sitter-" ++ g.name,
- .root_module = b.createModule(.{ .target = target, .optimize = c_optimize, .link_libc = true }),
+ .root_module = b.createModule(.{ .target = target, .optimize = c_optimize, .link_libc = !is_web }),
.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
@@ -315,19 +303,26 @@ pub fn build(b: *std.Build) void {
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));
+ if (is_web) lib.root_module.addIncludePath(b.path("src/web/libc/include"));
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"));
+ // 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 (!is_web) 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,
+ .optimize = optimize,
+ .root_source_file = b.path("src/web/zstbi.zig"),
+ });
+ root_mod.addImport("zstbi", zstbi_mod);
+ hx_core_mod.addImport("zstbi", zstbi_mod);
// 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
@@ -353,11 +348,11 @@ pub fn build(b: *std.Build) void {
.tables_path = uucode_tables,
}).module("uucode");
- const vaxis_mod = b.dependency("vaxis", .{
+ const vaxis_mod: ?*std.Build.Module = if (!is_web) b.dependency("vaxis", .{
.target = target,
.optimize = optimize,
.external_uucode = true,
- }).module("vaxis");
+ }).module("vaxis") else null;
var ghostty_vt_for_snap: ?*std.Build.Module = null;
// ghostty's simd libs (simdutf/highway, C++) locate the Apple SDK via
@@ -379,7 +374,7 @@ pub fn build(b: *std.Build) void {
// 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();
+ const ghostty_simd = !is_web 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,
@@ -394,39 +389,19 @@ pub fn build(b: *std.Build) void {
// 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 });
+ const ghostty_dep = 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);
+ if (vaxis_mod) |vaxis| {
+ vaxis.addImport("uucode", uucode_mod);
+ root_mod.addImport("vaxis", vaxis);
+ }
root_mod.addImport("uucode", uucode_mod);
hx_core_mod.addImport("uucode", uucode_mod);
@@ -467,29 +442,22 @@ pub fn build(b: *std.Build) void {
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
+ // SDL3 shell wiring (-Dplatform=gui). 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) {
+ // the SPIR-V compiled at build time from shaders/*.glsl.
+ if (platform == .gui) {
// 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 });
+ 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");
- 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 +
@@ -505,71 +473,56 @@ pub fn build(b: *std.Build) void {
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"),
- });
- }
+ 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 (is_web) {
+ 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 });
}
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 wasm = b.addExecutable(.{ .name = "pardes", .root_module = root_mod });
+ 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);
+ inline for (.{
+ .{ "src/web/index.html", "index.html" },
+ .{ "src/web/app.mjs", "app.mjs" },
+ .{ "src/web/pardes.css", "pardes.css" },
+ .{ "assets/AdwaitaMono-Regular.ttf", "AdwaitaMono-Regular.ttf" },
+ }) |file| web_step.dependOn(&b.addInstallFileWithDir(b.path(file[0]), .{ .custom = "web" }, file[1]).step);
- 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);
+ const run_web_harness = b.addSystemCommand(&.{ "node", "test/web_harness.mjs" });
+ run_web_harness.addArg(b.getInstallPath(.{ .custom = "web" }, ""));
+ run_web_harness.step.dependOn(web_step);
+ b.step("web-harness", "run the dependency-free JS/WASM DOM harness").dependOn(&run_web_harness.step);
b.getInstallStep().dependOn(web_step);
} else {
// binary name: the native-shaped linux-x86_64 tty build stays `pardes`
@@ -587,7 +540,7 @@ pub fn build(b: *std.Build) void {
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=<dump.zon>").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
@@ -736,45 +689,6 @@ pub fn build(b: *std.Build) void {
}
}
-// the html shell: a full-viewport canvas wired into Module (from the prototype)
-const html_shell =
- \\<!doctype html>
- \\<html>
- \\<head>
- \\ <meta charset="utf-8">
- \\ <meta name="viewport" content="width=device-width, initial-scale=1">
- \\ <title>pardes</title>
- \\ <style>
- \\ html, body { margin: 0; width: 100%; height: 100%; background: #121212; }
- \\ body { overflow: hidden; }
- \\ canvas { display: block; width: 100vw; height: 100vh; outline: none;
- \\ image-rendering: crisp-edges; image-rendering: pixelated;
- \\ -ms-interpolation-mode: nearest-neighbor;
- \\ transform: translateZ(0); backface-visibility: hidden;
- \\ touch-action: none; }
- \\ </style>
- \\</head>
- \\<body>
- \\ <canvas id="canvas" tabindex="0"></canvas>
- \\ <script>
- \\ const canvas = document.getElementById('canvas');
- \\ canvas.addEventListener('contextmenu', (event) => event.preventDefault());
- \\ canvas.addEventListener('pointerdown', (event) => {
- \\ canvas.focus({ preventScroll: true });
- \\ canvas.setPointerCapture?.(event.pointerId);
- \\ });
- \\ const releasePointer = (event) => {
- \\ if (canvas.hasPointerCapture?.(event.pointerId)) canvas.releasePointerCapture(event.pointerId);
- \\ };
- \\ canvas.addEventListener('pointerup', releasePointer);
- \\ canvas.addEventListener('pointercancel', releasePointer);
- \\ window.Module = { canvas };
- \\ </script>
- \\ <script src="pardes.js"></script>
- \\</body>
- \\</html>
-;
-
// 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;
@@ -812,17 +726,3 @@ fn vendoredThemes(b: *std.Build, io: std.Io) []const []const u8 {
}.lt);
return names.items;
}
-
-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);
-}