const std = @import("std"); const builtin = @import("builtin"); const mupdf_build = @import("mupdf.zig"); const snap_build = @import("build/snap.zig"); const grammar_manifest = @import("src/grammar_manifest.zig"); /// The package manifest, imported UNTYPED on purpose — see the `version` /// option in `shellOptions` for why that word is load-bearing. const zon = @import("build.zig.zon"); /// Releases are numbered 0.N; the zon holds "0.N.0" because it must be semver, /// and what a user sees drops that ".0". const display_version = if (std.mem.endsWith(u8, zon.version, ".0") and std.mem.count(u8, zon.version, ".") == 2) zon.version[0 .. zon.version.len - 2] else zon.version; pub const Platform = enum { tty, gui, web, macos, esp32p4 }; const PerfBuild = struct { compiler: []const u8, target: []const u8, cpu: []const u8, driver_optimize: []const u8, core_optimize: []const u8, c_optimize: []const u8, terminal_optimize: []const u8, platform: []const u8, grammars: []const u8, mupdf: bool, jpx: bool, tracy: bool, quic: bool, theme_animation: bool, terminal_simd: bool, prebuilt_shaders: bool, }; pub const macos_min_version: std.SemanticVersion = .{ .major = 13, .minor = 0, .patch = 0 }; pub const TreeSitterGrammars = enum { disabled, zig, minimal, full }; const gui_shaders = [_][]const u8{ "ui.vert", "ui.frag", "decor.frag", "overlay.vert", "overlay.frag", "image.vert", "image.frag", "post.vert", // The bundled post passes: shaders/post/.glsl behind the // Shadertoy prefix (shaderSource). "post-crt.frag", "post-bloom.frag", "post-vignette.frag", "post-grain.frag", // Bloom's steps below the frame (src/gui/Post.zig bloomChain). "bloom-down.frag", "bloom-up.frag", }; pub fn build(b: *std.Build) void { const requested_platform = b.option(Platform, "platform", "which shell to build (tty, gui, web, macos, esp32p4); absent builds the tty cli and the SDL gui together"); const platform = requested_platform orelse .tty; const also_gui = requested_platform == null; const home_install = also_gui and prefixIsUntouched(b); if (home_install) { if (b.graph.environ_map.get("HOME")) |home| { b.resolveInstallPrefix(b.pathJoin(&.{ home, ".local" }), .{}); std.debug.print("pardes: installing into {s} (override with --prefix)\n", .{b.install_prefix}); } } const esp32p4_target: std.Target.Query = .{ .cpu_arch = .riscv32, .os_tag = .freestanding, .abi = .none, .cpu_model = .{ .explicit = &std.Target.riscv.cpu.generic_rv32 }, .cpu_features_add = riscvFeatures(&.{ .m, .a, .f, .c, .zicsr, .zifencei }), }; const requested_target = b.standardTargetOptions(.{ .default_target = switch (platform) { .macos => if (builtin.os.tag.isDarwin()) .{ .cpu_arch = builtin.target.cpu.arch, .os_tag = .macos, .os_version_min = .{ .semver = macos_min_version }, } else .{}, .esp32p4 => esp32p4_target, .tty, .gui, .web => .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .gnu, .glibc_version = .{ .major = 2, .minor = 38, .patch = 0 }, }, }, }); const target = if (platform == .esp32p4) b.resolveTargetQuery(esp32p4_target) else requested_target; const requested_optimize = b.standardOptimizeOption(.{}); const static = b.option(bool, "static", "statically link") orelse false; const dump_path = b.option([]const u8, "dump", "dump .zon embedded into the web shell (-Dplatform=web)"); // one substring, because that is what a dev loop needs: see the unit-test // step for why the whole-binary run is worth narrowing const test_filters: []const []const u8 = if (b.option([]const u8, "test-filter", "run only tests whose name contains this")) |f| &.{f} else &.{}; const is_web = platform == .web; const is_esp32p4 = platform == .esp32p4; const freestanding_core = is_web or is_esp32p4; const enable_quic = b.option(bool, "quic", "optional 9P-over-QUIC transport using system OpenSSL 3.6+ (default off)") orelse false; if (enable_quic and freestanding_core) @panic("-Dquic=true needs a native platform"); const ninep_options = b.addOptions(); ninep_options.addOption(bool, "quic", enable_quic); const enable_mupdf = b.option(bool, "mupdf", "native PDF rendering with MuPDF (AGPL/commercial; native default on, web/esp32p4 off; -Dmupdf=false disables)") orelse !freestanding_core; const enable_jpx = b.option(bool, "jpx", "JPEG 2000 in PDFs, for scanned documents (default on; -Djpx=false drops openjpeg)") orelse true; // Off on the board too: the sources are ~1.8 MB, more than the whole // 1.5 MiB app partition, and with them the image does not link. const embed_sources = b.option(bool, "embed-sources", "serve the editor's own sources under /src of its 9P tree (default off; the esp32p4 image has no room for them)") orelse false; const is_web_target = target.result.cpu.arch == .wasm32 and target.result.os.tag == .freestanding; const is_esp32p4_target = target.result.cpu.arch == .riscv32 and target.result.os.tag == .freestanding; const optimize = if (is_web) .ReleaseSmall else if (is_esp32p4_target and requested_optimize == .Debug) .ReleaseFast else requested_optimize; const test_build = b.step("test-build", "compile unit-test programs without running them"); const doc_typ_opts = b.addOptions(); doc_typ_opts.addOption([]const u8, "dir", b.pathFromRoot("docs/typ")); // The docs (docs/typ, docs/site): each output is one typst run, and each // reads docs/typ/builtins-*.json; `builtins-json` refreshes this build's from the // registry first (the tool is wired below, once the core module is). const typst_bin: ?[]const u8 = b.findProgram(&.{"typst"}, &.{}) catch null; var doc_renders: [3]?*std.Build.Step.Run = @splat(null); const cheatsheet_step = b.step("cheatsheet", "render docs/typ/cheatsheet-a4.typ to /share/doc/pardes/cheatsheet.pdf (needs typst)"); const book_step = b.step("book", "render docs/typ/book.typ to /share/doc/pardes/pardes.pdf (needs typst)"); // The website: one Typst bundle run writes every page, the stylesheet, // the fonts and the media (docs/site/site.typ). const site_step = b.step("site", "render the docs website to /share/doc/pardes/site (needs typst)"); if (typst_bin) |typst| { // They read all of docs/typ, docs/site and the tutor, so they always // run rather than track every file. const sheet = b.addSystemCommand(&.{ typst, "compile", "--ignore-system-fonts" }); sheet.addFileArg(b.path("docs/typ/cheatsheet-a4.typ")); sheet.has_side_effects = true; cheatsheet_step.dependOn(&b.addInstallFile(sheet.addOutputFileArg("cheatsheet.pdf"), "share/doc/pardes/cheatsheet.pdf").step); const book = b.addSystemCommand(&.{ typst, "compile", "--ignore-system-fonts", "--root", b.pathFromRoot(".") }); book.addFileArg(b.path("docs/typ/book.typ")); book.has_side_effects = true; book_step.dependOn(&b.addInstallFile(book.addOutputFileArg("pardes.pdf"), "share/doc/pardes/pardes.pdf").step); const site = b.addSystemCommand(&.{ typst, "compile", "--format", "bundle", "--features", "bundle,html", "--root", b.pathFromRoot(".") }); site.addFileArg(b.path("docs/site/site.typ")); const site_dir = site.addOutputDirectoryArg("site"); site.has_side_effects = true; site_step.dependOn(&b.addInstallDirectory(.{ .source_dir = site_dir, .install_dir = .prefix, .install_subdir = "share/doc/pardes/site" }).step); doc_renders = .{ sheet, book, site }; } else { cheatsheet_step.dependOn(&b.addFail("zig build cheatsheet needs typst on PATH").step); book_step.dependOn(&b.addFail("zig build book needs typst on PATH").step); site_step.dependOn(&b.addFail("zig build site needs typst on PATH").step); } const test_nonce: ?*std.Build.Module = if (b.option(bool, "test-rebuild", "force fresh Zig test compilation while retaining cached C dependencies") orelse false) blk: { const files = b.addTempFiles(); const source = files.add("test_nonce.zig", b.fmt("pub const nonce = {d};\n", .{std.Io.Clock.awake.now(b.graph.io).nanoseconds})); break :blk b.createModule(.{ .root_source_file = source, .target = target, .optimize = optimize }); } else null; if (freestanding_core) test_build.dependOn(&b.addFail("test-build needs a native platform").step); const unit_profile = b.addExecutable(.{ .name = "pardes-unit-profile", .root_module = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("test/unit_profile.zig"), }), }); const c_optimize: std.builtin.OptimizeMode = if (optimize == .Debug) .ReleaseFast else optimize; const default_grammars: TreeSitterGrammars = if (is_web) .zig else if (is_esp32p4) .disabled else .full; const requested_grammars = b.option(TreeSitterGrammars, "tree-sitter", "tree-sitter grammar set: disabled, zig, minimal (c/c++/zig), full"); const tree_sitter_grammars = requested_grammars orelse default_grammars; const tracy = b.option([]const u8, "tracy", "enable Tracy profiling; supply the path to a Tracy source checkout"); const macos_identity = b.option([]const u8, "macos-identity", "codesigning identity for pardes.app (default: ad-hoc)") orelse "-"; const prebuilt_shaders = b.option(bool, "prebuilt-shaders", "embed the committed shaders/prebuilt/*.spv instead of running glslc (default: on for a bare `zig build`, off when -Dplatform names a shell)") orelse also_gui; // The browser shell is a freestanding wasm core plus ordinary web files. // 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=)"); const mupdf_check = b.step("mupdf-check", "compile, link, render, and search docs/design.pdf with MuPDF"); const pdf_bench_step = b.step("pdf-bench", "benchmark real MuPDF page rendering (-- [--json] [--reps N] [--warmup N] [--path FILE] [--page N] [--pages N])"); const pdf_sections_bench_step = b.step("pdf-sections-bench", "benchmark PDF outline/sections paths in ReleaseFast (-Doptimize=ReleaseFast -- [--json] [--reps N] [--warmup N])"); const pdf_pointer_bench_step = b.step("pdf-pointer-bench", "time PDF hover and selection gestures and prove their pixels (-Doptimize=ReleaseFast -- [--label L] [--reps N] [--path FILE] [--cell WxH])"); const pdf_scroll_bench_step = b.step("pdf-scroll-bench", "benchmark fast continuous-strip PDF scrolling in ReleaseFast (-Doptimize=ReleaseFast -- [--json] [--reps N] [--warmup N] [--path FILE])"); if (!enable_mupdf) mupdf_check.dependOn(&b.addFail("mupdf-check is unavailable with -Dmupdf=false").step); if (!enable_mupdf) pdf_bench_step.dependOn(&b.addFail("pdf-bench is unavailable with -Dmupdf=false").step); if (!enable_mupdf) pdf_sections_bench_step.dependOn(&b.addFail("pdf-sections-bench is unavailable with -Dmupdf=false").step); if (!enable_mupdf) pdf_scroll_bench_step.dependOn(&b.addFail("pdf-scroll-bench is unavailable with -Dmupdf=false").step); if (!enable_mupdf) pdf_pointer_bench_step.dependOn(&b.addFail("pdf-pointer-bench is unavailable with -Dmupdf=false").step); const shaders_step = b.step("shaders", "refresh the committed shaders/prebuilt/*.spv (glslc)"); const update_shaders = b.addUpdateSourceFiles(); for (gui_shaders) |name| { update_shaders.addCopyFileToSource(compileGlsl(b, name), b.fmt("shaders/prebuilt/{s}.spv", .{name})); } shaders_step.dependOn(&update_shaders.step); snap_build.addWeb(b); 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= (the browser has no ptys; state replays from an embedded dump)"); if (enable_mupdf and is_web) return failBuild(b, web_step, "-Dmupdf=true is supported only by the native tty/Kitty and gui/SDL backends"); // The board's target is pinned (`target` above), so what is asked of // -Dtarget is checked, not what was resolved: another arch is refused, // not silently built for the board anyway. const requested_board = requested_target.result.cpu.arch == .riscv32 and requested_target.result.os.tag == .freestanding; if (is_esp32p4 and !requested_board) return failBuild(b, web_step, "-Dplatform=esp32p4 requires -Dtarget=riscv32-freestanding (ESP32-P4 firmware)"); if (enable_mupdf and is_esp32p4) return failBuild(b, web_step, "-Dmupdf=true is supported only by the native tty/Kitty and gui/SDL backends"); if (is_esp32p4 and requested_grammars != null and requested_grammars.? != .disabled) return failBuild(b, web_step, "-Dplatform=esp32p4 has no tree-sitter: the grammars' parse tables are megabytes and the flash partition is 1.5 MiB (-Dtree-sitter=disabled)"); // build-time IO: slurps the grammars' highlights.scm queries, and reads // vendor/themes to find the theme sources var threaded: std.Io.Threaded = .init(b.allocator, .{}); defer threaded.deinit(); const io = threaded.io(); const root_mod = b.createModule(.{ .target = target, .optimize = optimize, // The browser and macOS shells are libraries whose host owns main(): // each roots at its own flat C ABI instead of src/main.zig. .root_source_file = b.path(switch (platform) { .web => "src/web.zig", .macos => "src/macos.zig", .esp32p4 => "src/esp32p4.zig", .tty, .gui => "src/main.zig", }), .link_libc = !freestanding_core, }); const core_module = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("src/pardes.zig"), .link_libc = true, }); const isolated_mod: ?*std.Build.Module = if (platform == .tty) b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("src/main.zig"), .link_libc = true, }) else null; const gui_mod: ?*std.Build.Module = if (also_gui) b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("src/main.zig"), .link_libc = true, }) else null; const CoreMod = struct { mod: ?*std.Build.Module, shell: Platform }; var core_mods_buf: [4]*std.Build.Module = undefined; var core_shells_buf: [4]Platform = undefined; var core_mods_len: usize = 0; // root_mod FIRST: the module map below is folded out of its import table. for ([_]CoreMod{ .{ .mod = root_mod, .shell = platform }, // hxdiff's headless core and pardes-isolate are the primary shell's // configuration compiled two more ways, not two more frontends. .{ .mod = core_module, .shell = platform }, .{ .mod = isolated_mod, .shell = platform }, .{ .mod = gui_mod, .shell = .gui }, }) |entry| if (entry.mod) |m| { core_mods_buf[core_mods_len] = m; core_shells_buf[core_mods_len] = entry.shell; core_mods_len += 1; }; const core_mods = core_mods_buf[0..core_mods_len]; const cloud9 = b.dependency("cloud9", .{ .target = target, .optimize = optimize }).module("cloud9"); for (core_mods) |mod| mod.addImport("cloud9", cloud9); const core_shells = core_shells_buf[0..core_mods_len]; for (core_mods) |mod| mod.addOptions("9p_options", ninep_options); const quic_test_step = b.step("quic-test", "test optional QUIC transport (-Dquic=true)"); if (enable_quic) { const header = b.addWriteFiles().add("openssl.h", "#include \n#include \n#include \n"); const translated = b.addTranslateC(.{ .root_source_file = header, .target = target, .optimize = optimize, .link_libc = true }); const include_dir = std.mem.trim(u8, b.run(&.{ "pkg-config", "--variable=includedir", "openssl" }), " \t\r\n"); if (include_dir.len == 0) @panic("OpenSSL pkg-config metadata is missing its include directory"); translated.addSystemIncludePath(.{ .cwd_relative = include_dir }); const openssl = translated.createModule(); const library_dir = std.mem.trim(u8, b.run(&.{ "pkg-config", "--variable=libdir", "openssl" }), " \t\r\n"); if (library_dir.len == 0) @panic("OpenSSL pkg-config metadata is missing its library directory"); openssl.addLibraryPath(.{ .cwd_relative = library_dir }); openssl.linkSystemLibrary("ssl", .{}); openssl.linkSystemLibrary("crypto", .{}); for (core_mods) |mod| mod.addImport("openssl", openssl); const quic_module = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("src/9p_quic.zig"), .link_libc = true, }); quic_module.addImport("openssl", openssl); quic_module.addImport("cloud9", cloud9); const tests = b.addTest(.{ .root_module = quic_module, .filters = test_filters }); if (test_nonce) |nonce| tests.root_module.addImport("test_rebuild_nonce", nonce); test_build.dependOn(&tests.step); const run_tests = b.addRunArtifact(tests); run_tests.has_side_effects = true; quic_test_step.dependOn(&run_tests.step); if (testMatch(b, unit_profile, test_filters, quic_test_step)) |check| check.addArtifactArg(tests); } else quic_test_step.dependOn(&b.addFail("quic-test needs -Dquic=true and system OpenSSL 3.6+").step); // The conditional dependencies, hoisted so one `wireCore` call at the end // can name them all. The unconditional ones are ordinary consts below. var ts_mod: ?*std.Build.Module = null; var ts_queries_opts: ?*std.Build.Step.Options = null; var mupdf_core_mod: ?*std.Build.Module = null; var ghostty_core_vt: ?*std.Build.Module = null; var ts_libs: std.ArrayList(*std.Build.Step.Compile) = .empty; const theme_gen = b.addExecutable(.{ .name = "pardes-gen-themes", .root_module = b.createModule(.{ .target = b.graph.host, .optimize = .Debug, .root_source_file = b.path("tools/gen_themes.zig"), }), }); const run_theme_gen = b.addRunArtifact(theme_gen); const themes_out = run_theme_gen.addOutputDirectoryArg("themes"); for (vendoredThemes(b, io)) |name| run_theme_gen.addFileArg(b.path(b.fmt("vendor/themes/{s}", .{name}))); const themes_mod = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = themes_out.path(b, "list.zig"), }); // 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 esp32p4_cols = b.option(u16, "esp32p4-cols", "the board's grid width in cells (-Dplatform=esp32p4 only)") orelse 56; const esp32p4_rows = b.option(u16, "esp32p4-rows", "the board's grid height in cells (-Dplatform=esp32p4 only)") orelse 14; const theme_animation = b.option(bool, "theme-animation", "animate chrome colors across a theme change (default: off for esp32p4)"); // The macOS shell carries the workspace tag row's commands in its native // menu bar; the row itself goes away. `-Dworkspace-tag=true` puts it back. const workspace_tag = b.option(bool, "workspace-tag", "draw the workspace tag row (default: on except on macOS, whose menu bar carries its commands)"); // The commit is a build input: every `jj new` or `jj commit` moves HEAD, // changes the options module and so recompiles every binary that imports // it, which is all of them (~0.6 GB of fresh test objects per move). Only // builds someone keeps are stamped: release builds and the bare // `zig build` that installs into ~/.local. Every other Debug build (any // -Dplatform or --prefix, so every test and snapshot run) prints // `pardes ` alone, like a tarball build; -Dstamp-commit=true // stamps one anyway. const stamp_commit = b.option(bool, "stamp-commit", "embed the git commit in --version and crash records (default: release builds and the ~/.local install)") orelse (optimize != .Debug or home_install); const shell_cfg: ShellConfig = .{ .tree_sitter_grammars = tree_sitter_grammars, .tracy = tracy != null, .mupdf = enable_mupdf, .esp32p4_cols = esp32p4_cols, .esp32p4_rows = esp32p4_rows, .theme_animation = theme_animation, .workspace_tag = workspace_tag, .prebuilt_shaders = prebuilt_shaders, .embed_sources = embed_sources, .commit = if (stamp_commit) gitCommit(b) else null, }; const opts = shellOptions(b, shell_cfg, platform); const gui_opts: ?*std.Build.Step.Options = if (also_gui) shellOptions(b, shell_cfg, .gui) else null; if (tracy) |tracy_path| { for ([_]*std.Build.Module{ root_mod, core_module }) |mod| { mod.addIncludePath(.{ .cwd_relative = tracy_path }); mod.addCSourceFile(.{ .file = .{ .cwd_relative = b.pathJoin(&.{ tracy_path, "public", "TracyClient.cpp" }) }, .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; } } if (tree_sitter_grammars != .disabled) { const tree_sitter_mod = b.dependency("tree_sitter", .{ .target = target, .optimize = c_optimize }).module("tree_sitter"); if (is_web) { 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.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" }, }); } ts_mod = tree_sitter_mod; const ts_queries = b.addOptions(); inline for (grammar_manifest.all) |g| { if (tree_sitter_grammars == .full or (tree_sitter_grammars == .minimal and g.tier != .full) or (tree_sitter_grammars == .zig and g.tier == .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); // helix's textobject queries, vendored (vendor/queries); a // grammar without one gets none const objects = std.Io.Dir.cwd().readFileAlloc(io, b.pathFromRoot("vendor/queries/" ++ g.name ++ "/textobjects.scm"), b.allocator, .limited(0x100000)) catch ""; ts_queries.addOption([]const u8, g.name ++ "_textobjects", objects); const lib = b.addLibrary(.{ .name = "tree-sitter-" ++ g.name, .root_module = b.createModule(.{ .target = target, .optimize = c_optimize, .link_libc = !is_web }), .linkage = .static, }); // Every allocation a grammar's external scanner makes goes to the // runtime's allocator, which syntax.zig points at a Zig one: // TREE_SITTER_REUSE_ALLOCATOR for the scanners that use // ts_malloc, and src/tree_sitter_heap.h for the ones that call // malloc themselves. const ts_cflags = [_][]const u8{ "-fno-sanitize=function", "-DTREE_SITTER_REUSE_ALLOCATOR", "-include", b.path("src/tree_sitter_heap.h").getPath(b), }; 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")); ts_libs.append(b.allocator, lib) catch @panic("OOM"); } } ts_queries_opts = ts_queries; } const zstbi_dep = if (!freestanding_core) b.dependency("zstbi", .{ .target = target, .optimize = c_optimize }) else null; const zstbi_mod = if (zstbi_dep) |dep| dep.module("root") else b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("src/web/zstbi.zig"), }); // mvzr backtracks with no bound on its work (its own test "do not make // this test any longer": `a?` n times is 2^n steps), and pardes searches // with it holding the editor. Its matcher gets a step budget, patched into // the fetched source here at build time so that a fresh fetch keeps it: // each entry to matchPattern, which its backtracking recurses through, // spends a step, and with none left the match fails and says so // (src/regexp.zig). A bump that moves the anchor stops the build. const mvzr_mod = mvzr: { const dep = b.dependency("mvzr", .{ .target = target, .optimize = optimize }); const src = std.Io.Dir.cwd().readFileAlloc(io, dep.path("src/mvzr.zig").getPath(b), b.allocator, .limited(1 << 20)) catch @panic("read mvzr"); const anchor = "fn matchPattern(patt: []const RegOp, sets: []const CharSet, haystack: []const u8, i_in: usize) ?OpMatch {\n"; const at = (std.mem.indexOf(u8, src, anchor) orelse @panic("mvzr's matchPattern changed: redo its step budget in build.zig")) + anchor.len; const spend = \\ if (steps_left == 0) { \\ exhausted = true; \\ return null; \\ } \\ steps_left -= 1; \\ ; const budget = \\ \\/// pardes's patch (its build.zig): matchPattern entries left before a \\/// match gives up, and whether one did. Set by the caller per search. \\pub threadlocal var steps_left: u64 = std.math.maxInt(u64); \\pub threadlocal var exhausted: bool = false; \\ ; const budgeted = b.fmt("{s}{s}{s}{s}", .{ src[0..at], spend, src[at..], budget }); // A group's greedy match never gave back to what follows the group: // on `aab`, `(a+)ab` missed where `a+ab` matched. hasAlt counts the // group's own `)` as an alternative, so every group with pattern // after it takes matchGroup's alternatives branch, which tries each // alternative's one (greedy) match and gives up. When the rest fails // after it, try that alternative's shorter matches, longest first // (docs/mvzr-group-backtrack.md is the report for upstream). const group_anchor = \\ const next_match = matchPattern(next_patt, sets, haystack, m1.i); \\ if (next_match) |m2| { \\ // Whole group matches, and we can use m2.j \\ return m2; \\ } else { // Try our next pattern, if any. \\ ; const group_backoff = \\ const next_match = matchPattern(next_patt, sets, haystack, m1.i); \\ if (next_match) |m2| { \\ // Whole group matches, and we can use m2.j \\ return m2; \\ } else { // Try our next pattern, if any. \\ // pardes's patch (its build.zig): back the group off. \\ var e = m1.i; \\ while (e > i) { \\ e -= 1; \\ const shorter = matchAlt(remaining_patt, sets, haystack[0..e], i) orelse continue; \\ if (shorter.i != e) continue; \\ if (matchPattern(next_patt, sets, haystack, e)) |whole| return whole; \\ } \\ ; if (std.mem.count(u8, budgeted, group_anchor) != 1) @panic("mvzr's matchGroup changed: redo its group backtracking in build.zig"); const backed = std.mem.replaceOwned(u8, b.allocator, budgeted, group_anchor, group_backoff) catch @panic("OOM"); // `$` is refused anywhere but last, so `h$|zz` was a bad pattern // while `zz|h$` was fine. An alternative or a group ends at `|` and // `)`, and the matcher's `.end` ends the alternative it is in, so // `$` may stand before either (docs/mvzr-group-backtrack.md). const end_anchor = \\ '$' => { \\ if (i + 1 < in.len) { \\ ; const end_before_alt = \\ '$' => { \\ // pardes's patch (its build.zig): `$` may end an alternative. \\ if (i + 1 < in.len and in[i + 1] != '|' and in[i + 1] != ')') { \\ ; if (std.mem.count(u8, backed, end_anchor) != 1) @panic("mvzr's parser changed: redo its `$` before `|` in build.zig"); const patched = std.mem.replaceOwned(u8, b.allocator, backed, end_anchor, end_before_alt) catch @panic("OOM"); break :mvzr b.createModule(.{ .root_source_file = b.addWriteFiles().add("mvzr.zig", patched), .target = target, .optimize = optimize }); }; // The C heap every C library with an allocator hook is handed: the core's // tree-sitter and fonts, and pdf.zig's MuPDF, which is a module of its own. const c_heap_mod = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("src/c_heap.zig"), }); // One FreeType in a binary: the gui's glyph atlas links it and MuPDF is // built against it (see mupdf.zig). At c_optimize with the rest of the // vendored C, since MuPDF rasterizes a PDF's text through it. const freetype_dep: ?*std.Build.Dependency = if (freestanding_core) null else b.dependency("freetype", .{ .target = target, .optimize = c_optimize, .@"enable-libpng" = false, }); const freetype: ?*std.Build.Step.Compile = if (freetype_dep) |dep| dep.artifact("freetype") else null; // ...and one zlib, the one FreeType links for its gzip module. Asked of // FreeType's build with the options it asks with, the dependency cache // hands back that same artifact rather than a second copy. const zlib: ?*std.Build.Step.Compile = if (freetype_dep) |dep| dep.builder.dependency("zlib", .{ .target = target, .optimize = c_optimize }).artifact("z") else null; if (enable_mupdf) { if (b.lazyDependency("mupdf", .{})) |mupdf_dep| { const mupdf = mupdf_build.add(b, io, mupdf_dep, .{ .target = target, .optimize = c_optimize, .jpx = enable_jpx, .freetype = freetype.?, .zlib = zlib.?, }); const mupdf_mod = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("src/pdf.zig"), .link_libc = true, }); mupdf.linkTo(mupdf_mod); mupdf_mod.addImport("c_heap", c_heap_mod); mupdf_mod.addIncludePath(b.path("src")); mupdf_mod.addCSourceFile(.{ .file = b.path("src/pdf_bridge.c"), .flags = &.{"-std=gnu11"}, }); mupdf_core_mod = mupdf_mod; const mupdf_test = b.addTest(.{ .root_module = mupdf_mod }); const run_mupdf_test = b.addRunArtifact(mupdf_test); run_mupdf_test.has_side_effects = true; mupdf_check.dependOn(&run_mupdf_test.step); mupdf_check.dependOn(mupdf_build.addProbe(b, mupdf, .{ .target = target, .pdf = b.path("docs/design.pdf"), })); const pdf_bench_mod = b.createModule(.{ .target = target, .optimize = .ReleaseFast, .root_source_file = b.path("src/pdf.zig"), .link_libc = true, }); mupdf.linkTo(pdf_bench_mod); pdf_bench_mod.addImport("c_heap", c_heap_mod); pdf_bench_mod.addIncludePath(b.path("src")); pdf_bench_mod.addCSourceFile(.{ .file = b.path("src/pdf_bridge.c"), .flags = &.{"-std=gnu11"}, }); const pdf_bench = b.addExecutable(.{ .name = "pardes-pdf-bench", .root_module = b.createModule(.{ .target = target, .optimize = .ReleaseFast, .root_source_file = b.path("test/pdf_bench.zig"), .link_libc = true, }), }); pdf_bench.root_module.addImport("mupdf", pdf_bench_mod); const run_pdf_bench = b.addRunArtifact(pdf_bench); if (b.args) |args| run_pdf_bench.addArgs(args); run_pdf_bench.setCwd(b.path(".")); pdf_bench_step.dependOn(&run_pdf_bench.step); const pdf_sections_bench = b.addExecutable(.{ .name = "pardes-pdf-sections-bench", .root_module = b.createModule(.{ .target = target, .optimize = .ReleaseFast, .root_source_file = b.path("test/pdf_sections_bench.zig"), .link_libc = true, }), }); pdf_sections_bench.root_module.addImport("pardes", core_module); pdf_sections_bench.root_module.addImport("mupdf", mupdf_mod); const sections_bench_opts = b.addOptions(); sections_bench_opts.addOption(bool, "release_fast_core", optimize == .ReleaseFast); pdf_sections_bench.root_module.addOptions("pdf_sections_bench_config", sections_bench_opts); const run_pdf_sections_bench = b.addRunArtifact(pdf_sections_bench); if (b.args) |args| run_pdf_sections_bench.addArgs(args); run_pdf_sections_bench.setCwd(b.path(".")); pdf_sections_bench_step.dependOn(&run_pdf_sections_bench.step); const pdf_pointer_bench = b.addExecutable(.{ .name = "pardes-pdf-pointer-bench", .root_module = b.createModule(.{ .target = target, .optimize = .ReleaseFast, .root_source_file = b.path("test/pdf_pointer_bench.zig"), .link_libc = true, }), }); pdf_pointer_bench.root_module.addImport("pardes", core_module); pdf_pointer_bench.root_module.addImport("mupdf", mupdf_mod); const run_pdf_pointer_bench = b.addRunArtifact(pdf_pointer_bench); if (b.args) |args| run_pdf_pointer_bench.addArgs(args); run_pdf_pointer_bench.setCwd(b.path(".")); pdf_pointer_bench_step.dependOn(&run_pdf_pointer_bench.step); const pdf_scroll_bench = b.addExecutable(.{ .name = "pardes-pdf-scroll-bench", .root_module = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("test/pdf_scroll_bench.zig"), .link_libc = true, }), }); pdf_scroll_bench.root_module.addImport("pardes", core_module); pdf_scroll_bench.root_module.addImport("mupdf", mupdf_mod); const scroll_bench_opts = b.addOptions(); scroll_bench_opts.addOption(bool, "release_fast_core", optimize == .ReleaseFast); scroll_bench_opts.addOption([]const u8, "core_optimize", @tagName(optimize)); pdf_scroll_bench.root_module.addOptions("pdf_scroll_bench_config", scroll_bench_opts); const run_pdf_scroll_bench = b.addRunArtifact(pdf_scroll_bench); if (b.args) |args| run_pdf_scroll_bench.addArgs(args); run_pdf_scroll_bench.setCwd(b.path(".")); pdf_scroll_bench_step.dependOn(&run_pdf_scroll_bench.step); } } 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_dep = b.dependency("vaxis", .{ .target = target, .optimize = optimize, .external_uucode = true, }); const vaxis_mod = vaxis_dep.module("vaxis"); // vaxis's input parser, patched here at build time like mvzr so that a // fresh fetch keeps it: no reply, split read or malformed sequence may // reach the editor as typed text, and none may panic or end input. // - A cursor position report that no F3 could have sent is no key: the // startup width queries' `CSI 1;1R` arrived as two F3s, which a shell // pane took for text. // - ESC before a CSI or SS3 key (rxvt's Alt+arrow) is Alt on that key, // not Alt+Escape followed by `[A` as text. // - An X10 mouse report and a kitty graphics reply cut short by a read // wait for their rest: the first became three bytes of text, the // second sliced past its input. // - A malformed color or clipboard reply is dropped instead of failing // the parse, which stopped input; so is a byte no UTF-8 starts with. // - An OSC reply ends at its first terminator: one ended by BEL ran on to // the next sequence's ESC and left what lay between as text. // - A device report's fields are sought in the sequence, not past it. // A bump that moves an anchor stops the build. { const parser_path = vaxis_dep.path("src/Parser.zig").getPath(b); var src = std.Io.Dir.cwd().readFileAlloc(io, parser_path, b.allocator, .limited(1 << 20)) catch @panic("read vaxis Parser.zig"); const edits = [_]struct { anchor: []const u8, with: []const u8, count: usize = 1 }{ .{ .anchor = \\ 0x5F => return parseApc(input), \\ else => { , .with = \\ 0x5F => return parseApc(input), \\ // pardes's patch (its build.zig): ESC before a key's sequence is Alt on the key. \\ 0x1B => if (input.len > 2 and (input[2] == '[' or input[2] == 'O')) { \\ var inner = if (input[2] == '[') parseCsi(input[1..], &self.buf) else parseSs3(input[1..]); \\ if (inner.n == 0) return inner; \\ if (inner.event) |*event| switch (event.*) { \\ .key_press, .key_release => |*key| key.mods.alt = true, \\ else => {}, \\ }; \\ inner.n += 1; \\ return inner; \\ } else return .{ .event = .{ .key_press = .{ .codepoint = Key.escape, .mods = .{ .alt = true } } }, .n = 2 }, \\ else => { , }, .{ .anchor = " const first_cp = iter.next() orelse return error.InvalidUTF8;\n", .with = " const first_cp = iter.next() orelse return .{ .event = null, .n = 1 }; // pardes's patch (its build.zig)\n", }, .{ .anchor = " n = std.unicode.utf8CodepointSequenceLength(first_cp) catch return error.InvalidUTF8;\n", .with = " n = std.unicode.utf8CodepointSequenceLength(first_cp) catch return .{ .event = null, .n = 1 }; // pardes's patch (its build.zig)\n", }, .{ .anchor = \\ const sequence = input[0 .. end + 1 + 1]; \\ \\ switch (input[2]) { , .with = \\ // pardes's patch (its build.zig): an ESC that ends the input is half an ST. \\ if (input.len < end + 2) return .{ .event = null, .n = 0 }; \\ const sequence = input[0 .. end + 1 + 1]; \\ \\ switch (input[2]) { , }, .{ .anchor = \\ const esc_result = skipUntilST(input); \\ if (esc_result.n > 0) break :blk esc_result.n; \\ \\ // No escape, could be BEL terminated \\ const bel = std.mem.indexOfScalarPos(u8, input, 2, 0x07) orelse return .{ \\ .event = null, \\ .n = 0, \\ }; \\ bel_terminated = true; \\ break :blk bel + 1; , .with = \\ // pardes's patch (its build.zig): whichever terminator comes first ends it. \\ const esc = std.mem.indexOfScalarPos(u8, input, 2, 0x1b); \\ const bel = std.mem.indexOfScalarPos(u8, input, 2, 0x07); \\ if (bel != null and (esc == null or bel.? < esc.?)) { \\ bel_terminated = true; \\ break :blk bel.? + 1; \\ } \\ const esc_result = skipUntilST(input); \\ if (esc_result.n > 0) break :blk esc_result.n; \\ return .{ .event = null, .n = 0 }; , }, .{ .anchor = " const color = try Color.rgbFromSpec(color_spec);\n", .with = " const color = Color.rgbFromSpec(color_spec) catch return null_event; // pardes's patch (its build.zig)\n", .count = 2, }, .{ .anchor = \\ if (input[semicolon_idx + 1] != 'c') return null_event; \\ const payload = if (bel_terminated) \\ input[semicolon_idx + 3 .. sequence.len - 1] \\ else \\ input[semicolon_idx + 3 .. sequence.len - 2]; \\ const decoder = std.base64.standard.Decoder; \\ const text = try paste_allocator.?.alloc(u8, try decoder.calcSizeForSlice(payload)); \\ try decoder.decode(text, payload); , .with = \\ // pardes's patch (its build.zig): a malformed reply is dropped. \\ const body_end = if (bel_terminated) sequence.len - 1 else sequence.len - 2; \\ if (semicolon_idx + 3 > body_end or input[semicolon_idx + 1] != 'c') return null_event; \\ const payload = input[semicolon_idx + 3 .. body_end]; \\ const decoder = std.base64.standard.Decoder; \\ const gpa = paste_allocator orelse return null_event; \\ const text = gpa.alloc(u8, decoder.calcSizeForSlice(payload) catch return null_event) catch return null_event; \\ decoder.decode(text, payload) catch { \\ gpa.free(text); \\ return null_event; \\ }; , }, .{ .anchor = \\ 'A', 'B', 'C', 'D', 'E', 'F', 'H', 'P', 'Q', 'R', 'S' => { \\ , .with = \\ 'A', 'B', 'C', 'D', 'E', 'F', 'H', 'P', 'Q', 'R', 'S' => { \\ // pardes's patch (its build.zig): a cursor position report is no key. \\ if (final == 'R' and cursorReport(sequence)) return null_event; \\ , }, .{ .anchor = " std.debug.assert(sequence.len >= 4); // ESC [ ? c == 4 bytes\n", .with = " if (sequence.len < 4) return null_event; // pardes's patch (its build.zig)\n", }, .{ .anchor = "const delim_idx = std.mem.indexOfScalarPos(u8, input, 3, ';')", .with = "const delim_idx = std.mem.indexOfScalarPos(u8, sequence, 3, ';')", .count = 2, }, .{ .anchor = "input[delim_idx + 1 .. sequence.len - 2]", .with = "sequence[@min(delim_idx + 1, sequence.len - 2) .. sequence.len - 2]", }, .{ .anchor = " if (input.len == 3 and (input[2] == 'M') and full_input.len >= 6) {\n", .with = \\ // pardes's patch (its build.zig): an X10 report still arriving is incomplete, not text. \\ if (input.len == 3 and input[2] == 'M' and full_input.len < 6) return .{ .event = null, .n = 0 }; \\ if (input.len == 3 and (input[2] == 'M') and full_input.len >= 6) { \\ , }, }; for (edits) |edit| { if (std.mem.count(u8, src, edit.anchor) != edit.count) std.debug.panic("vaxis's Parser.zig changed at `{s}`: redo its patch in build.zig", .{edit.anchor}); src = std.mem.replaceOwned(u8, b.allocator, src, edit.anchor, edit.with) catch @panic("OOM"); } const cursor_report = \\ \\/// pardes's patch (its build.zig): whether `CSI ... R` answers a cursor \\/// position query. Only `CSI 1 ; mods R` with mods past 1 is a modified F3 \\/// (a bare F3 is `SS3 R`); a report has another row, or col 1 (mods 1). \\fn cursorReport(sequence: []const u8) bool { \\ const params = sequence[2 .. sequence.len - 1]; \\ if (params.len > 0 and params[0] == '?') return true; \\ var fields = std.mem.splitScalar(u8, params, ';'); \\ const row = fields.first(); \\ const col = fields.next() orelse return false; \\ if (fields.next() != null or row.len == 0 or col.len == 0) return false; \\ for (params) |c| if (c != ';' and !std.ascii.isDigit(c)) return false; \\ return !std.mem.eql(u8, row, "1") or std.mem.eql(u8, col, "1"); \\} \\ ; // vaxis's render, patched the same way: a blank whose ink alone // changed is not sent, since a blank's ink shows nowhere. A fade // (InactiveDim on a focus change) sent every blank of every pane it // touched again: 7 KB a click for three shell panes. The terminal // keeps the old ink there, and so does vaxis's last frame. const render_path = vaxis_dep.path("src/Vaxis.zig").getPath(b); var render_src = std.Io.Dir.cwd().readFileAlloc(io, render_path, b.allocator, .limited(1 << 20)) catch @panic("read vaxis Vaxis.zig"); const render_anchor = \\ if ((!self.refresh and \\ last.eql(cell) and ; if (std.mem.count(u8, render_src, render_anchor) != 1) std.debug.panic("vaxis's Vaxis.zig changed at `{s}`: redo its patch in build.zig", .{render_anchor}); render_src = std.mem.replaceOwned(u8, b.allocator, render_src, render_anchor, \\ if ((!self.refresh and \\ (last.eql(cell) or blankInkOnly(&last, &cell)) and // pardes's patch (its build.zig) \\ ) catch @panic("OOM"); // And the secondary cursors count as changed when they differ, not // when they are the same: read the other way round, every render // started a frame, ~40 bytes to the terminal with nothing changed. const secondary_anchor = " std.meta.eql(self.screen.cursor_secondary, self.state.cursor_secondary);\n"; if (std.mem.count(u8, render_src, secondary_anchor) != 1) std.debug.panic("vaxis's Vaxis.zig changed at `{s}`: redo its patch in build.zig", .{secondary_anchor}); render_src = std.mem.replaceOwned(u8, b.allocator, render_src, secondary_anchor, " !std.meta.eql(self.screen.cursor_secondary, self.state.cursor_secondary); // pardes's patch (its build.zig)\n") catch @panic("OOM"); const blank_ink = \\ \\/// pardes's patch (its build.zig): a blank cell (a space, no underline, \\/// strike or reverse) that differs from the last frame's blank in its \\/// ink alone, which shows nowhere. \\inline fn blankInkOnly(last: *const InternalScreen.InternalCell, cell: *const Cell) bool { \\ if (!(cell.char.grapheme.len == 1 and cell.char.grapheme[0] == ' ')) return false; \\ if (last.default or cell.default) return false; \\ if (!(last.char.items.len == 1 and last.char.items[0] == ' ')) return false; \\ const s = cell.style; \\ if (s.reverse or s.strikethrough or s.ul_style != .off) return false; \\ if (!std.mem.eql(u8, last.uri.items, cell.link.uri)) return false; \\ // Field by field: Style.eql is slow, and this runs for every blank \\ // a scrolling shell moves. \\ const l = last.style; \\ return sameColor(l.bg, s.bg) and sameColor(l.ul, s.ul) and l.ul_style == s.ul_style and \\ l.bold == s.bold and l.dim == s.dim and l.italic == s.italic and l.blink == s.blink and \\ !l.reverse and !l.strikethrough and l.invisible == s.invisible; \\} \\ \\inline fn sameColor(a: Cell.Color, b: Cell.Color) bool { \\ return switch (a) { \\ .default => b == .default, \\ .index => |i| b == .index and b.index == i, \\ .rgb => |rgb| b == .rgb and rgb[0] == b.rgb[0] and rgb[1] == b.rgb[1] and rgb[2] == b.rgb[2], \\ }; \\} \\ ; const files = b.addWriteFiles(); _ = files.add("Parser.zig", b.fmt("{s}{s}", .{ src, cursor_report })); _ = files.add("Vaxis.zig", b.fmt("{s}{s}", .{ render_src, blank_ink })); _ = files.addCopyFile(vaxis_dep.path("src/widgets/terminal/Parser.zig"), "widgets/terminal/Parser.zig"); // Suffixes: this leaves out both Parser.zig files and Vaxis.zig, // written above. _ = files.addCopyDirectory(vaxis_dep.path("src"), "", .{ .exclude_extensions = &.{ "Parser.zig", "Vaxis.zig" } }); vaxis_mod.root_source_file = files.getDirectory().path(b, "main.zig"); } var ghostty_vt_for_snap: ?*std.Build.Module = null; const ghostty_simd = !freestanding_core and (!target.result.os.tag.isDarwin() or b.graph.host.result.os.tag.isDarwin()); const ghostty_optimize: std.builtin.OptimizeMode = if (optimize == .Debug) .ReleaseSafe else optimize; const ghostty_dep = if (is_esp32p4) null else b.lazyDependency("ghostty", .{ .target = target, .optimize = ghostty_optimize, .simd = ghostty_simd, .@"app-runtime" = .none, .@"emit-xcframework" = false }); if (ghostty_dep) |dep| { const ghostty_vt = dep.module("ghostty-vt"); ghostty_vt_for_snap = ghostty_vt; ghostty_vt.addImport("uucode", uucode_mod); ghostty_core_vt = ghostty_vt; } // override uucode on vaxis LAST so it wins over ghostty's build() wiring. vaxis_mod.addImport("uucode", uucode_mod); // One call per module instead of one line per module per dependency, and // BEFORE the fold below, which reads what these imports put in the table. for (core_mods) |m| wireCore(m, .{ .themes = themes_mod, .tree_sitter = ts_mod, .ts_libs = ts_libs.items, .ts_queries = ts_queries_opts, .zstbi = zstbi_mod, .mvzr = mvzr_mod, .c_heap = c_heap_mod, .mupdf = mupdf_core_mod, .ghostty_vt = ghostty_core_vt, .vaxis = vaxis_mod, .uucode = uucode_mod, }); const iso_off = b.addOptions(); iso_off.addOption(bool, "isolated", false); const iso_on = b.addOptions(); iso_on.addOption(bool, "isolated", true); for (core_mods, core_shells) |mod, shell| { // config.zig's test checks the words and keys docs/typ names exist. mod.addOptions("doc_typ", doc_typ_opts); mod.addOptions("pardes_config", if (also_gui and shell == .gui) gui_opts.? else opts); mod.addOptions("pardes_isolation", if (isolated_mod == mod) iso_on else iso_off); if (shell == .gui) for (gui_shaders) |name| mod.addAnonymousImport( b.fmt("effect-source-{s}.glsl", .{name}), .{ .root_source_file = shaderSource(b, name) }, ); if (shell == .macos) mod.addAnonymousImport("effect-source-crt.ci.metal", .{ .root_source_file = b.path("shaders/crt.ci.metal"), }); } // docs/typ/builtins-.json: every builtin, from the registry and the Help // rows (src/builtins_json.zig), rewritten in the source tree so a plain // `typst compile` reads it too; a unit test fails when it is stale. // A terminal build writes builtins-tty.json and a window build // builtins-gui.json; style.typ merges the two into one glossary. const builtins_json_step = b.step("builtins-json", "rewrite docs/typ/builtins-.json from the builtin registry (-Dplatform=tty or gui)"); if (requested_platform == .tty or requested_platform == .gui) { const gen = b.addExecutable(.{ .name = "pardes-gen-builtins-json", .root_module = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("tools/gen_builtins_json.zig"), .imports = &.{.{ .name = "pardes", .module = core_module }}, }) }); const run_gen = b.addRunArtifact(gen); const json = run_gen.addOutputFileArg("builtins.json"); const update = b.addUpdateSourceFiles(); update.addCopyFileToSource(json, b.fmt("docs/typ/builtins-{s}.json", .{@tagName(platform)})); builtins_json_step.dependOn(&update.step); for (doc_renders) |r| if (r) |run| run.step.dependOn(&update.step); } else builtins_json_step.dependOn(&b.addFail("builtins-json writes one build's list: pass -Dplatform=tty or -Dplatform=gui").step); const gui_shell_mod: ?*std.Build.Module = gui_mod orelse if (platform == .gui) root_mod else null; if (gui_shell_mod) |gm| { 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"); gm.linkLibrary(sdl_lib); gm.addIncludePath(dep.path("include")); } gm.linkLibrary(freetype.?); // HarfBuzz from source, like FreeType: its FreeType integration // (HAVE_FREETYPE, hb-ft) links the freetype artifact above, so the two // share one FreeType. The flags are the ones Ghostty builds it with, // plus its allocator: gui.zig's ui_malloc and friends, over a Zig // allocator, as FreeType's is. if (b.lazyDependency("harfbuzz", .{})) |upstream| { const harfbuzz = b.addLibrary(.{ .name = "harfbuzz", .linkage = .static, .root_module = b.createModule(.{ .target = target, .optimize = optimize, .link_libc = true, .link_libcpp = true, }), }); harfbuzz.root_module.linkLibrary(freetype.?); harfbuzz.root_module.addIncludePath(upstream.path("src")); harfbuzz.root_module.addCSourceFile(.{ .file = upstream.path("src/harfbuzz.cc"), .flags = &.{ "-DHAVE_STDBOOL_H", "-DHAVE_UNISTD_H", "-DHAVE_SYS_MMAN_H", "-DHAVE_PTHREAD=1", "-DHAVE_FREETYPE=1", "-DHAVE_FT_GET_VAR_BLEND_COORDINATES=1", "-DHAVE_FT_SET_VAR_BLEND_COORDINATES=1", "-DHAVE_FT_DONE_MM_VAR=1", "-DHAVE_FT_GET_TRANSFORM=1", "-Dhb_malloc_impl=ui_malloc", "-Dhb_calloc_impl=ui_calloc", "-Dhb_realloc_impl=ui_realloc", "-Dhb_free_impl=ui_free", }, }); gm.linkLibrary(harfbuzz); gm.addIncludePath(upstream.path("src")); } if (target.result.os.tag.isDarwin()) gm.addRPath(.{ .cwd_relative = "/usr/local/lib" }); gm.addIncludePath(b.path("src/gui")); gm.addCSourceFile(.{ .file = b.path("src/gui/font.c") }); // the embedded UI font: provided as a build import since assets/ lives // outside the src/ module root (@embedFile can't escape it) gm.addAnonymousImport("AdwaitaMono-Regular.ttf", .{ .root_source_file = b.path("assets/AdwaitaMono-Regular.ttf"), }); // Put before a user's Shadertoy file when the gui compiles it. gm.addAnonymousImport("post-prefix.glsl", .{ .root_source_file = b.path("shaders/post/prefix.glsl") }); for (gui_shaders) |name| gm.addAnonymousImport(b.fmt("{s}.spv", .{name}), .{ .root_source_file = if (prebuilt_shaders) b.path(b.fmt("shaders/prebuilt/{s}.spv", .{name})) else compileGlsl(b, name), }); } // A detached session compiles its attached GUIs' Shadertoy files // (ShaderBuild.zig), whichever shell it was built for. if (gui_shell_mod != root_mod) root_mod.addAnonymousImport("post-prefix.glsl", .{ .root_source_file = b.path("shaders/post/prefix.glsl") }); 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), }); } if (platform == .web) { const wasm = b.addExecutable(.{ .name = "pardes", .root_module = root_mod }); wasm.entry = .disabled; wasm.rdynamic = true; wasm.export_memory = true; 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 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 if (is_esp32p4) { const obj = b.addObject(.{ .name = "pardes-esp32p4", .root_module = root_mod }); b.getInstallStep().dependOn(&b.addInstallFile(obj.getEmittedBin(), "pardes-esp32p4.o").step); web_step.dependOn(&b.addFail("web needs -Dplatform=web -Dtarget=wasm32-freestanding -Ddump=").step); } else if (platform == .macos) { const header = b.addTranslateC(.{ .root_source_file = b.path("src/macos/pardes.h"), .target = target, .optimize = optimize, }); root_mod.addImport("pardes.h", header.createModule()); const lib = b.addLibrary(.{ .name = "pardes", .linkage = .static, .root_module = root_mod, }); lib.bundle_compiler_rt = true; lib.bundle_ubsan_rt = true; const archive: ?std.Build.LazyPath = if (builtin.os.tag.isDarwin() and target.result.os.tag.isDarwin()) fatArchive(b, lib) else null; const install_lib: *std.Build.Step = if (archive) |a| &b.addInstallLibFile(a, "libpardes.a").step else &b.addInstallArtifact(lib, .{}).step; b.getInstallStep().dependOn(install_lib); b.installFile("src/macos/pardes.h", "include/pardes.h"); const app_step = b.step("macos-app", "assemble zig-out/pardes.app (needs macOS + swiftc)"); const dmg_step = b.step("macos-dmg", "package zig-out/pardes.dmg for distribution (needs macOS + swiftc)"); if (archive) |lib_archive| { const apple_arch: []const u8 = if (target.result.cpu.arch == .aarch64) "arm64" else @tagName(target.result.cpu.arch); const triple = b.fmt("{s}-apple-macos{d}.{d}", .{ apple_arch, macos_min_version.major, macos_min_version.minor, }); const min_version = b.fmt("{d}.{d}", .{ macos_min_version.major, macos_min_version.minor }); const swift_mode: []const u8 = switch (optimize) { .Debug => "-Onone", .ReleaseSmall => "-Osize", .ReleaseFast, .ReleaseSafe => "-O", }; const link = b.addSystemCommand(&.{ "swiftc", swift_mode, "-target", triple, "-import-objc-header" }); link.addFileArg(b.path("src/macos/pardes.h")); link.addArg("-o"); const app_bin = link.addOutputFileArg("pardes"); for ([_][]const u8{ "main.swift", "AppDelegate.swift", "PardesView.swift", "ScenePostprocessor.swift", "FileWatcher.swift", }) |src| link.addFileArg(b.path(b.fmt("src/macos/Sources/{s}", .{src}))); link.addFileArg(lib_archive); if (enable_quic) { const ssl_libdir = std.mem.trim(u8, b.run(&.{ "pkg-config", "--variable=libdir", "openssl" }), " \t\r\n"); link.addArgs(&.{ "-L", ssl_libdir, "-lssl", "-lcrypto" }); } link.addArgs(&.{ "-lc++", "-framework", "AppKit", "-framework", "CoreText", "-framework", "CoreGraphics", "-framework", "CoreImage", "-framework", "Metal", }); const icon_build = b.addSystemCommand(&.{ "swiftc", "-O", "-target", triple, "-o" }); const icon_bin = icon_build.addOutputFileArg("pardes-icon"); icon_build.addFileArg(b.path("src/macos/icon.swift")); const icon_run = std.Build.Step.Run.create(b, "pardes-icon"); icon_run.addFileArg(icon_bin); icon_run.addFileArg(b.path("src/macos/icon.png")); const icon_dir = icon_run.addOutputDirectoryArg("Resources"); const plist = b.addSystemCommand(&.{ "plutil", "-replace", "LSMinimumSystemVersion", "-string", min_version, "-o", }); const plist_out = plist.addOutputFileArg("Info.plist"); plist.addFileArg(b.path("src/macos/Info.plist")); const version_plist = b.addSystemCommand(&.{ "plutil", "-replace", "CFBundleShortVersionString", "-string", display_version, "-o", }); const version_plist_out = version_plist.addOutputFileArg("Info.plist"); version_plist.addFileArg(plist_out); const install_app_bin = b.addInstallFileWithDir(app_bin, .{ .custom = "pardes.app/Contents/MacOS" }, "pardes"); const install_plist = b.addInstallFileWithDir(version_plist_out, .{ .custom = "pardes.app/Contents" }, "Info.plist"); const install_scene_kernel = b.addInstallFileWithDir( b.path("shaders/crt.ci.metal"), .{ .custom = "pardes.app/Contents/Resources" }, "crt.ci.metal", ); // CFBundleIconFile names this without its extension; without both // halves the Dock shows the generic blank page. const install_icon = b.addInstallFileWithDir(icon_dir.path(b, "pardes.icns"), .{ .custom = "pardes.app/Contents/Resources" }, "pardes.icns"); const sign = b.addSystemCommand(&.{ "codesign", "--force", "--sign", macos_identity }); if (!std.mem.eql(u8, macos_identity, "-")) sign.addArgs(&.{ "--options", "runtime", "--timestamp" }); sign.addArg(b.getInstallPath(.prefix, "pardes.app")); sign.has_side_effects = true; sign.step.dependOn(&install_app_bin.step); sign.step.dependOn(&install_plist.step); sign.step.dependOn(&install_icon.step); sign.step.dependOn(&install_scene_kernel.step); app_step.dependOn(&sign.step); const dmg = b.addSystemCommand(&.{ "hdiutil", "create", "-volname", "pardes", "-ov", "-format", "UDZO", "-srcfolder" }); dmg.addArg(b.getInstallPath(.prefix, "pardes.app")); dmg.addArg(b.getInstallPath(.prefix, "pardes.dmg")); dmg.has_side_effects = true; dmg.step.dependOn(&sign.step); dmg_step.dependOn(&dmg.step); b.getInstallStep().dependOn(&sign.step); } else { const why = b.addFail("pardes.app needs a Darwin host and target; drop -Dtarget= or pass -Dtarget=native"); app_step.dependOn(&why.step); dmg_step.dependOn(&why.step); } const e2e_step = b.step("macos-e2e", "run the offscreen AppKit snapshot suite (needs macOS + swiftc)"); if (archive) |lib_archive| { const arch = if (target.result.cpu.arch == .aarch64) "arm64" else @tagName(target.result.cpu.arch); const triple = b.fmt("{s}-apple-macos{d}.{d}", .{ arch, macos_min_version.major, macos_min_version.minor }); const e2e = b.addSystemCommand(&.{ "swiftc", "-O", "-target", triple, "-import-objc-header" }); e2e.addFileArg(b.path("src/macos/pardes.h")); e2e.addArg("-o"); const e2e_bin = e2e.addOutputFileArg("pardes-macos-e2e"); for ([_][]const u8{ "src/macos/Sources/PardesView.swift", "src/macos/Sources/ScenePostprocessor.swift", "src/macos/Sources/FileWatcher.swift", "src/macos/Sources/AppDelegate.swift", "test/macos_e2e.swift", }) |source| e2e.addFileArg(b.path(source)); e2e.addFileArg(lib_archive); if (enable_quic) { const ssl_libdir = std.mem.trim(u8, b.run(&.{ "pkg-config", "--variable=libdir", "openssl" }), " \t\r\n"); e2e.addArgs(&.{ "-L", ssl_libdir, "-lssl", "-lcrypto" }); } e2e.addArgs(&.{ "-lc++", "-framework", "AppKit", "-framework", "CoreText", "-framework", "CoreGraphics", "-framework", "CoreImage", "-framework", "Metal" }); const files = b.addWriteFiles(); const runnable = files.addCopyFile(e2e_bin, "pardes-macos-e2e"); _ = files.addCopyFile(b.path("shaders/crt.ci.metal"), "crt.ci.metal"); const run_e2e = std.Build.Step.Run.create(b, "run macos-e2e"); run_e2e.addFileArg(runnable); run_e2e.setCwd(b.path(".")); if (b.args) |args| run_e2e.addArgs(args); run_e2e.has_side_effects = true; e2e_step.dependOn(&run_e2e.step); } else e2e_step.dependOn(&b.addFail("macos-e2e needs a Darwin host and target").step); const unit_step = b.step("unit-test", "run the C-ABI guard and core unit tests"); const unit_tests = b.addTest(.{ .root_module = root_mod, .filters = test_filters }); if (test_nonce) |nonce| unit_tests.root_module.addImport("test_rebuild_nonce", nonce); test_build.dependOn(&unit_tests.step); const run_unit = b.addRunArtifact(unit_tests); run_unit.has_side_effects = true; unit_step.dependOn(&run_unit.step); if (testMatch(b, unit_profile, test_filters, unit_step)) |check| check.addArtifactArg(unit_tests); web_step.dependOn(&b.addFail("web needs -Dplatform=web -Dtarget=wasm32-freestanding -Ddump=").step); } else { 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); if (gui_mod) |gm| { var gui_name: []const u8 = "pardes-gui"; if (target.result.os.tag != .linux or target.result.cpu.arch != .x86_64) gui_name = b.fmt("{s}-{s}-{s}", .{ gui_name, @tagName(target.result.os.tag), @tagName(target.result.cpu.arch) }); const gui_exe = b.addExecutable(.{ .name = gui_name, .linkage = if (static) .static else null, .root_module = gm, }); b.installArtifact(gui_exe); } const run = b.addRunArtifact(exe); b.step("run", "descend to the pardes").dependOn(&run.step); if (isolated_mod) |iso| { const iso_exe = b.addExecutable(.{ .name = "pardes-isolate", .linkage = if (static) .static else null, .root_module = iso, }); const run_iso = b.addRunArtifact(iso_exe); // Something to look at on arrival, read out of the binary itself. if (b.args) |args| run_iso.addArgs(args) else run_iso.addArg("src/pardes.zig"); const iso_step = b.step("run-isolated", "descend to a pardes with no host but the terminal"); iso_step.dependOn(&run_iso.step); } web_step.dependOn(&b.addFail("web needs -Dplatform=web -Dtarget=wasm32-freestanding -Ddump=").step); snap_build.addTty(b, .{ .exe = exe, .target = target, .optimize = optimize, .ghostty_vt = ghostty_vt_for_snap, .cloud9 = cloud9, }); const image_harness = b.addExecutable(.{ .name = "pardes-image-harness", .root_module = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("test/image_harness.zig"), .link_libc = true, }), }); if (ghostty_vt_for_snap) |vt| image_harness.root_module.addImport("ghostty-vt", vt); switch (target.result.os.tag) { .freebsd, .netbsd, .openbsd => image_harness.root_module.linkSystemLibrary("util", .{}), else => {}, } const run_image_harness = b.addRunArtifact(image_harness); run_image_harness.addArtifactArg(exe); run_image_harness.addArg(if (platform == .gui) "gui" else "tty"); run_image_harness.has_side_effects = true; b.step("image-harness", "exercise native Kitty/SDL image rendering end to end").dependOn(&run_image_harness.step); const pdf_harness_step = b.step("pdf-harness", "exercise continuous PDF rendering, search, and sections navigation end to end"); if (enable_mupdf) { const run_pdf_harness = b.addRunArtifact(image_harness); run_pdf_harness.addArtifactArg(exe); run_pdf_harness.addArg(if (platform == .gui) "gui" else "tty"); run_pdf_harness.addArg("pdf"); run_pdf_harness.has_side_effects = true; pdf_harness_step.dependOn(&run_pdf_harness.step); } else { pdf_harness_step.dependOn(&b.addFail("pdf-harness is unavailable with -Dmupdf=false").step); } 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", core_module); const run_hxdiff = b.addRunArtifact(hxdiff); run_hxdiff.has_side_effects = true; if (b.args) |args| run_hxdiff.addArgs(args) else { run_hxdiff.addArgs(&.{ "--strict", "test/hxcases/cases.jsonl", "test/hxcases/goldens.jsonl", "test/hxcases/waivers.jsonl", }); run_hxdiff.setCwd(b.path(".")); } const hxdiff_step = b.step("hxdiff", "compare Helix results (-- --strict cases reference [waivers]; cases alone emits)"); hxdiff_step.dependOn(&run_hxdiff.step); const helix_harness = b.option([]const u8, "helix-harness", "path to the reference hx-harness executable") orelse b.graph.environ_map.get("HX_HARNESS") orelse "hx-harness"; const reference = b.addSystemCommand(&.{helix_harness}); reference.addFileArg(b.path("test/hxcases/cases.jsonl")); reference.has_side_effects = true; const reference_results = reference.captureStdOut(.{}); const live_diff = b.addRunArtifact(hxdiff); live_diff.addArg("--strict"); live_diff.addFileArg(b.path("test/hxcases/cases.jsonl")); live_diff.addFileArg(reference_results); live_diff.addFileArg(b.path("test/hxcases/waivers.jsonl")); live_diff.has_side_effects = true; b.step("hxdiff-live", "compare the editor against a freshly run Helix reference").dependOn(&live_diff.step); const update_reference = b.addUpdateSourceFiles(); update_reference.addCopyFileToSource(reference_results, "test/hxcases/goldens.jsonl"); update_reference.step.dependOn(&live_diff.step); b.step("hxdiff-update", "replace checked-in Helix goldens after the live comparison passes").dependOn(&update_reference.step); const run_hxparity = b.addRunArtifact(hxdiff); run_hxparity.has_side_effects = true; run_hxparity.addArg("--parity"); if (b.args) |args| run_hxparity.addArgs(args) else { run_hxparity.addArgs(&.{ "--strict", "--waivers", "test/hxcases/parity-waivers.jsonl", "test/hxcases/cases.jsonl", "test/hxcases/parity.jsonl", }); run_hxparity.setCwd(b.path(".")); } const hxparity_step = b.step("hxparity", "run the file-vs-pty editing parity suite (-- [--waivers w.jsonl] ...)"); hxparity_step.dependOn(&run_hxparity.step); // every helix-golf example, one case per step of its walkthrough const run_hxgolf = b.addRunArtifact(hxdiff); run_hxgolf.has_side_effects = true; run_hxgolf.addArgs(&.{ "--strict", "test/hxcases/golf.jsonl", "test/hxcases/golf-goldens.jsonl", "test/hxcases/golf-waivers.jsonl" }); run_hxgolf.setCwd(b.path(".")); b.step("hxgolf", "compare the helix-golf examples against their Helix results").dependOn(&run_hxgolf.step); 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", core_module); const run_lspbench = b.addRunArtifact(lspbench); if (b.args) |args| run_lspbench.addArgs(args); run_lspbench.setCwd(b.path(".")); run_lspbench.has_side_effects = true; const lspbench_step = b.step("lspbench", "language-backend latency + feature matrix (-- [--json] [--check] [repo-root]), or one query (-- probe : [arg] [--reps N])"); lspbench_step.dependOn(&run_lspbench.step); const run_lspbench_check = b.addRunArtifact(lspbench); run_lspbench_check.addArg("--check"); if (b.args) |args| run_lspbench_check.addArgs(args); run_lspbench_check.setCwd(b.path(".")); run_lspbench_check.has_side_effects = true; const lspbench_check_step = b.step("lspbench-check", "require every configured language probe to complete correctly"); lspbench_check_step.dependOn(&run_lspbench_check.step); const locations_bench_step = b.step("locations-bench", "measure cold/warm result formatting (-Doptimize=ReleaseFast; CSV on stderr)"); if (optimize != .ReleaseFast) { locations_bench_step.dependOn(&b.addFail("locations-bench requires -Doptimize=ReleaseFast").step); } else { const locations_bench_module = b.createModule(.{ .target = target, .optimize = .ReleaseFast, .root_source_file = b.path("test/locations_bench.zig"), .link_libc = true, }); locations_bench_module.addImport("pardes", core_module); const locations_bench = b.addTest(.{ .root_module = locations_bench_module, .filters = &.{"locations benchmark cold and warm formatting"} }); const run_locations_bench = b.addRunArtifact(locations_bench); run_locations_bench.has_side_effects = true; locations_bench_step.dependOn(&run_locations_bench.step); } const perf_optimize: std.builtin.OptimizeMode = .ReleaseFast; const perf_options = b.addOptions(); const perf_target = std.Target.Query.fromTarget(&target.result); perf_options.addOption(PerfBuild, "identity", .{ .compiler = builtin.zig_version_string, .target = perf_target.zigTriple(b.allocator) catch @panic("out of memory"), .cpu = perf_target.serializeCpuAlloc(b.allocator) catch @panic("out of memory"), .driver_optimize = @tagName(perf_optimize), .core_optimize = @tagName(optimize), .c_optimize = @tagName(c_optimize), .terminal_optimize = @tagName(ghostty_optimize), .platform = @tagName(platform), .grammars = @tagName(tree_sitter_grammars), .mupdf = enable_mupdf, .jpx = enable_mupdf and enable_jpx, .tracy = tracy != null, .quic = enable_quic, .theme_animation = theme_animation orelse (platform != .esp32p4), .terminal_simd = ghostty_simd, .prebuilt_shaders = platform == .gui and prebuilt_shaders, }); const perf = b.addExecutable(.{ .name = "pardes-perf", .root_module = b.createModule(.{ .target = target, .optimize = perf_optimize, .root_source_file = b.path("test/perf.zig"), .link_libc = true, }), }); perf.root_module.addImport("pardes", core_module); perf.root_module.addOptions("perf_options", perf_options); const run_perf = b.addRunArtifact(perf); run_perf.has_side_effects = true; if (b.args) |args| run_perf.addArgs(args); run_perf.setCwd(b.path(".")); const perf_step = b.step("perf", "gesture latency or bounded terminal stress (-- [--json] [--reps N] [--base old.json] [--terminal-mib N])"); perf_step.dependOn(&run_perf.step); // The gate: the 50k-line file's gestures against the baseline this // build shape recorded, each within 3x of its best. Record one with // `zig build perf -Dplatform=P [-Doptimize=O] -- --only medium // --reps 10 --json > test/perf-baseline-P-O.json` (a build that is // known good). ponytail: the numbers are this machine's; another // machine records its own. const perf_gate = b.addRunArtifact(perf); perf_gate.has_side_effects = true; perf_gate.setCwd(b.path(".")); perf_gate.addArgs(&.{ "--only", "medium", "--reps", "10", "--base", b.fmt("test/perf-baseline-{t}-{t}.json", .{ platform, optimize }), "--max-ratio", "3" }); const perf_gate_step = b.step("perf-gate", "fail when a gesture on the 50k-line file takes over 3x its recorded baseline (test/perf-baseline--.json)"); perf_gate_step.dependOn(&perf_gate.step); const perf_tests = b.addTest(.{ .root_module = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("test/perf.zig"), .link_libc = true, }), .filters = test_filters, }); perf_tests.root_module.addOptions("perf_options", perf_options); if (test_nonce) |nonce| perf_tests.root_module.addImport("test_rebuild_nonce", nonce); const run_perf_test = b.addRunArtifact(perf_tests); run_perf_test.has_side_effects = true; const perf_test_step = b.step("perf-test", "test performance options and baselines without compiling the editor"); perf_test_step.dependOn(&run_perf_test.step); if (testMatch(b, unit_profile, test_filters, perf_test_step)) |check| check.addArtifactArg(perf_tests); const fs_bench = b.addExecutable(.{ .name = "pardes-fs-bench", .root_module = b.createModule(.{ .target = target, .optimize = .ReleaseFast, .root_source_file = b.path("test/fs_bench.zig"), .link_libc = true, }), }); fs_bench.root_module.addImport("pardes", core_module); const run_fs_bench = b.addRunArtifact(fs_bench); run_fs_bench.has_side_effects = true; if (b.args) |args| run_fs_bench.addArgs(args); run_fs_bench.setCwd(b.path(".")); const fs_bench_step = b.step("fs-bench", "9P filesystem request cost and allocation count (-- [--json] [--reps N])"); fs_bench_step.dependOn(&run_fs_bench.step); const fs_bench_test = b.addTest(.{ .root_module = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("test/fs_bench.zig"), }), .filters = test_filters, }); const run_fs_bench_test = b.addRunArtifact(fs_bench_test); run_fs_bench_test.has_side_effects = true; const fs_bench_test_step = b.step("fs-bench-test", "test filesystem benchmark options without compiling the editor"); fs_bench_test_step.dependOn(&run_fs_bench_test.step); if (testMatch(b, unit_profile, test_filters, fs_bench_test_step)) |check| check.addArtifactArg(fs_bench_test); const syntax_module = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("src/syntax.zig"), .link_libc = true, }); syntax_module.addOptions("pardes_config", opts); wireCore(syntax_module, .{ .tree_sitter = ts_mod, .ts_libs = ts_libs.items, .ts_queries = ts_queries_opts, .c_heap = c_heap_mod }); const syntax_runner = b.addExecutable(.{ .name = "pardes-syntax", .root_module = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("test/syntax.zig"), .link_libc = true, }), }); syntax_runner.root_module.addImport("syntax", syntax_module); const run_syntax = b.addRunArtifact(syntax_runner); if (b.args) |args| run_syntax.addArgs(args); b.step("syntax", "emit deterministic syntax snapshots").dependOn(&run_syntax.step); const run_syntax_bench = b.addRunArtifact(syntax_runner); run_syntax_bench.addArg("--bench"); if (b.args) |args| run_syntax_bench.addArgs(args); run_syntax_bench.has_side_effects = true; b.step("syntax-bench", "measure highlighting latency and allocations").dependOn(&run_syntax_bench.step); const syntax_test = b.addTest(.{ .root_module = syntax_module, .filters = if (test_filters.len == 0) &.{"syntax "} else test_filters, }); const run_syntax_test = b.addRunArtifact(syntax_test); run_syntax_test.has_side_effects = true; const syntax_test_step = b.step("syntax-test", "test syntax correctness without building the editor"); syntax_test_step.dependOn(&run_syntax_test.step); if (testMatch(b, unit_profile, test_filters, syntax_test_step)) |check| check.addArtifactArg(syntax_test); const run_fs_test = b.addSystemCommand(&.{ "python3", "-B", "test/fs.py" }); run_fs_test.addArtifactArg(exe); if (enable_quic) run_fs_test.addArg("--quic"); run_fs_test.setCwd(b.path(".")); run_fs_test.has_side_effects = true; const fs_test_step = b.step("fs-test", "exercise default 9P service and mounts with an independent client"); fs_test_step.dependOn(&run_fs_test.step); // Random 9P operations against a throwaway detached session, checking // the documented rules (test/monkey9p.py). The fixed-seed smoke run is // part of fs-test; the long run is manual. const run_monkey9p = b.addSystemCommand(&.{ "python3", "-B", "test/monkey9p.py" }); run_monkey9p.addArtifactArg(exe); if (b.args) |args| run_monkey9p.addArgs(args); run_monkey9p.setCwd(b.path(".")); run_monkey9p.has_side_effects = true; b.step("monkey-9p", "random 9P operations checking the documented rules (-- [--seed N]... [--steps M] [--replay F] [--shrink F] [--no-shrink])").dependOn(&run_monkey9p.step); // The GUI's effects under random input in a hidden test window, each // Motion x Lift x Bloom (test/gui_monkey.py): a gate, not part of // fs-test, since it needs a Wayland display. const monkey_gui_step = b.step("monkey-gui", "random input to hidden GUI test windows across the effect settings (-Dplatform=gui; -- [--seed N] [--steps M])"); if (platform == .gui) { const run_monkey_gui = b.addSystemCommand(&.{ "python3", "-B", "test/gui_monkey.py" }); run_monkey_gui.addArtifactArg(exe); if (b.args) |args| run_monkey_gui.addArgs(args); run_monkey_gui.setCwd(b.path(".")); run_monkey_gui.has_side_effects = true; monkey_gui_step.dependOn(&run_monkey_gui.step); } else monkey_gui_step.dependOn(&b.addFail("monkey-gui needs -Dplatform=gui").step); const run_monkey9p_smoke = b.addSystemCommand(&.{ "python3", "-B", "test/monkey9p.py" }); run_monkey9p_smoke.addArtifactArg(exe); run_monkey9p_smoke.addArg("--smoke"); run_monkey9p_smoke.setCwd(b.path(".")); run_monkey9p_smoke.has_side_effects = true; b.step("monkey-9p-smoke", "a short fixed-seed 9P monkey run (part of fs-test)").dependOn(&run_monkey9p_smoke.step); fs_test_step.dependOn(&run_monkey9p_smoke.step); const run_discovery = b.addSystemCommand(&.{ "python3", "-B", "test/fs.py" }); run_discovery.addArtifactArg(exe); run_discovery.addArg("--discovery"); run_discovery.setCwd(b.path(".")); run_discovery.has_side_effects = true; b.step("fs-discovery-test", "test browsable pane creation over 9P").dependOn(&run_discovery.step); if (target.result.os.tag == .linux) { const v9fs_helper = b.addExecutable(.{ .name = "pardes-v9fs", .root_module = b.createModule(.{ .root_source_file = b.path("src/linux/v9fs.zig"), .target = target, .optimize = requested_optimize, .link_libc = true, }), }); // Ordinary executable; each launch asks sudo inside its own PTY. b.installArtifact(v9fs_helper); const run_v9fs = b.addSystemCommand(&.{ "python3", "-B", "test/v9fs.py" }); run_v9fs.addArtifactArg(exe); run_v9fs.addArtifactArg(v9fs_helper); run_v9fs.setCwd(b.path(".")); run_v9fs.has_side_effects = true; b.step("v9fs-test", "probe Linux kernel 9P mounts in a private subprocess namespace (requires sudo -v)").dependOn(&run_v9fs.step); const run_v9fs_driver = b.addSystemCommand(&.{ "python3", "-B", "test/v9fs_driver_test.py" }); run_v9fs_driver.setCwd(b.path(".")); run_v9fs_driver.has_side_effects = true; b.step("v9fs-driver-test", "test kernel-mount probe launcher without privileges or compiling the editor").dependOn(&run_v9fs_driver.step); const run_v9fs_terminal = b.addSystemCommand(&.{ "python3", "-B", "test/v9fs_terminal.py" }); run_v9fs_terminal.addArtifactArg(exe); run_v9fs_terminal.addArtifactArg(v9fs_helper); run_v9fs_terminal.setCwd(b.path(".")); run_v9fs_terminal.has_side_effects = true; b.step("v9fs-terminal-test", "exercise Tty9p authentication routing and cleanup without mount privileges").dependOn(&run_v9fs_terminal.step); } const run_agent_session_test = b.addSystemCommand(&.{ "python3", "-B", "test/agent_session_test.py" }); run_agent_session_test.addArtifactArg(exe); if (platform == .gui) run_agent_session_test.addArg("--gui-grid"); run_agent_session_test.setCwd(b.path(".")); run_agent_session_test.has_side_effects = true; b.step("agent-session-test", "test interactive session readiness, input and cleanup through 9P").dependOn(&run_agent_session_test.step); const history = b.addExecutable(.{ .name = "pardes-history", .root_module = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("test/history.zig"), }), }); const run_history = b.addRunArtifact(history); if (b.args) |args| run_history.addArgs(args); run_history.has_side_effects = true; b.step("history", "compare command output and test/build runtime across jj revisions").dependOn(&run_history.step); const unit_step = b.step("unit-test", "run native shell and module unit tests"); // The perf gate runs with every unit-test (a regression like a // contrast search per grapheme costs 100x, far past the gate). unit_step.dependOn(perf_gate_step); const unit_match = testMatch(b, unit_profile, test_filters, unit_step); const lspbench_tests = b.addTest(.{ .root_module = lspbench.root_module, .filters = test_filters }); const run_lspbench_tests = b.addRunArtifact(lspbench_tests); run_lspbench_tests.has_side_effects = true; const lspbench_test_step = b.step("lspbench-test", "test language benchmark completeness and correctness gates"); lspbench_test_step.dependOn(&run_lspbench_tests.step); lspbench_check_step.dependOn(&run_lspbench_tests.step); if (testMatch(b, unit_profile, test_filters, lspbench_test_step)) |check| check.addArtifactArg(lspbench_tests); const diff_tests = b.addTest(.{ .root_module = hxdiff.root_module, .filters = test_filters }); const run_diff_tests = b.addRunArtifact(diff_tests); run_diff_tests.has_side_effects = true; const diff_test_step = b.step("hxdiff-test", "test differential comparison, allocation failures and CLI regression gates"); diff_test_step.dependOn(&run_diff_tests.step); if (testMatch(b, unit_profile, test_filters, diff_test_step)) |check| check.addArtifactArg(diff_tests); const run_diff_cli = b.addRunArtifact(hxdiff); run_diff_cli.addArg("--self-test"); run_diff_cli.has_side_effects = true; diff_test_step.dependOn(&run_diff_cli.step); const core_test = b.addTest(.{ .root_module = core_module, .filters = test_filters }); const run_core_test = b.addRunArtifact(core_test); run_core_test.has_side_effects = true; const core_test_step = b.step("core-test", "run core tests without native shell tests"); core_test_step.dependOn(&run_core_test.step); if (testMatch(b, unit_profile, test_filters, core_test_step)) |check| check.addArtifactArg(core_test); const pane_module = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("test/panes.zig"), .link_libc = true, }); pane_module.addImport("pardes", core_module); pane_module.addImport("pardes_config", core_module.import_table.get("pardes_config").?); const pane_tests = b.addTest(.{ .root_module = pane_module, .filters = test_filters }); if (test_nonce) |nonce| pane_tests.root_module.addImport("test_rebuild_nonce", nonce); test_build.dependOn(&pane_tests.step); const run_pane_tests = b.addRunArtifact(pane_tests); run_pane_tests.has_side_effects = true; unit_step.dependOn(&run_pane_tests.step); if (unit_match) |check| check.addArtifactArg(pane_tests); const pane_test_step = b.step("pane-test", "run pane, output, PDF and namespace integration tests"); pane_test_step.dependOn(&run_pane_tests.step); if (testMatch(b, unit_profile, test_filters, pane_test_step)) |check| check.addArtifactArg(pane_tests); // Keep native transport integration reachable even when the core's // anonymous test-import block is filtered out by Zig. const ninep_io_module = b.createModule(.{ .root_source_file = b.path("src/9p_io.zig"), .target = target, .optimize = optimize, .link_libc = true, }); ninep_io_module.link_objects.appendSlice(b.allocator, core_module.link_objects.items) catch @panic("OOM"); var ninep_imports = core_module.import_table.iterator(); while (ninep_imports.next()) |entry| ninep_io_module.addImport(entry.key_ptr.*, entry.value_ptr.*); if (enable_quic) { ninep_io_module.linkSystemLibrary("ssl", .{}); ninep_io_module.linkSystemLibrary("crypto", .{}); } const ninep_io_tests = b.addTest(.{ .root_module = ninep_io_module, .filters = &.{ "Unix TCP", "one fetch", "expired sessions", "posts itself" }, }); b.step("9p-io-test", "run native 9P transport and client integration tests") .dependOn(&b.addRunArtifact(ninep_io_tests).step); const fs_module = b.createModule(.{ .root_source_file = b.path("src/fs.zig"), .target = target, .optimize = optimize, .link_libc = true, }); fs_module.link_objects.appendSlice(b.allocator, core_module.link_objects.items) catch @panic("OOM"); var fs_imports = core_module.import_table.iterator(); while (fs_imports.next()) |entry| fs_module.addImport(entry.key_ptr.*, entry.value_ptr.*); if (enable_quic) { fs_module.linkSystemLibrary("ssl", .{}); fs_module.linkSystemLibrary("crypto", .{}); } const fs_tests = b.addTest(.{ .root_module = fs_module, .filters = test_filters }); b.step("fs-unit-test", "run filesystem unit tests").dependOn(&b.addRunArtifact(fs_tests).step); const protocol_step = b.step("9p-test", "run freestanding 9P protocol tests"); const history_step = b.step("history-test", "test historical comparisons and regression gates"); const protocol_match = testMatch(b, unit_profile, test_filters, protocol_step); const history_match = testMatch(b, unit_profile, test_filters, history_step); for ([_]struct { path: []const u8, libc: bool }{ .{ .path = "test/history.zig", .libc = false }, .{ .path = "src/9p.zig", .libc = false }, .{ .path = "src/esp32p4_gpio.zig", .libc = false }, .{ .path = "src/esp32p4/input_rescue.zig", .libc = false }, .{ .path = "src/fonts.zig", .libc = true }, .{ .path = "src/c_heap.zig", .libc = false }, }) |entry| { const tests = b.addTest(.{ .root_module = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path(entry.path), .link_libc = entry.libc, }), .filters = test_filters, }); tests.root_module.addImport("cloud9", cloud9); if (test_nonce) |nonce| tests.root_module.addImport("test_rebuild_nonce", nonce); test_build.dependOn(&tests.step); const run_tests = b.addRunArtifact(tests); run_tests.has_side_effects = true; unit_step.dependOn(&run_tests.step); if (unit_match) |check| check.addArtifactArg(tests); if (std.mem.eql(u8, entry.path, "src/9p.zig")) { protocol_step.dependOn(&run_tests.step); if (protocol_match) |check| check.addArtifactArg(tests); } if (std.mem.eql(u8, entry.path, "test/history.zig")) { history_step.dependOn(&run_tests.step); if (history_match) |check| check.addArtifactArg(tests); } } const deck_module = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("test/deck/replay.zig"), }); deck_module.addImport("deck", b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("src/gui/deck.zig"), })); const deck_test = b.addTest(.{ .root_module = deck_module, .filters = test_filters }); if (test_nonce) |nonce| deck_test.root_module.addImport("test_rebuild_nonce", nonce); test_build.dependOn(&deck_test.step); const run_deck_test = b.addRunArtifact(deck_test); run_deck_test.has_side_effects = true; unit_step.dependOn(&run_deck_test.step); if (unit_match) |check| check.addArtifactArg(deck_test); if (platform == .tty or platform == .gui) { const shell_test = b.addTest(.{ .root_module = root_mod, .filters = test_filters }); if (test_nonce) |nonce| shell_test.root_module.addImport("test_rebuild_nonce", nonce); test_build.dependOn(&shell_test.step); const run_shell_test = b.addRunArtifact(shell_test); run_shell_test.has_side_effects = true; unit_step.dependOn(&run_shell_test.step); if (unit_match) |check| check.addArtifactArg(shell_test); const run_profile = b.addRunArtifact(unit_profile); run_profile.addArtifactArg(shell_test); if (b.args) |args| run_profile.addArgs(args); run_profile.has_side_effects = true; b.step("unit-profile", "report test request time and process memory as JSONL (-- optional filter, --timeout-ms=N)").dependOn(&run_profile.step); const profile_test = b.addExecutable(.{ .name = "pardes-unit-profile-test", .root_module = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("test/unit_profile_test.zig"), }), }); const run_profile_test = b.addRunArtifact(profile_test); run_profile_test.addArtifactArg(unit_profile); run_profile_test.has_side_effects = true; b.step("unit-profile-test", "test profiler timing, failures and deadlines").dependOn(&run_profile_test.step); if (test_filters.len == 0) { unit_step.dependOn(&run_profile_test.step); test_build.dependOn(&unit_profile.step); test_build.dependOn(&profile_test.step); } } } } fn testMatch(b: *std.Build, profiler: *std.Build.Step.Compile, filters: []const []const u8, step: *std.Build.Step) ?*std.Build.Step.Run { if (filters.len == 0) return null; const check = b.addRunArtifact(profiler); check.addArgs(&.{ "--check-filter", filters[0] }); check.has_side_effects = true; step.dependOn(&check.step); return check; } fn prefixIsUntouched(b: *std.Build) bool { if (b.dest_dir != null) return false; const zig_out = b.build_root.join(b.allocator, &.{"zig-out"}) catch @panic("OOM"); if (!std.mem.eql(u8, b.install_prefix, zig_out)) return false; return std.mem.eql(u8, b.exe_dir, b.pathJoin(&.{ b.install_path, "bin" })) and std.mem.eql(u8, b.lib_dir, b.pathJoin(&.{ b.install_path, "lib" })) and std.mem.eql(u8, b.h_dir, b.pathJoin(&.{ b.install_path, "include" })); } fn gitCommit(b: *std.Build) ?[]const u8 { var code: u8 = 0; const out = b.runAllowFail( &.{ "git", "-C", b.build_root.path orelse ".", "rev-parse", "--short=12", "HEAD" }, &code, .ignore, ) catch return null; const trimmed = std.mem.trim(u8, out, " \t\r\n"); return if (trimmed.len == 0) null else trimmed; } const ShellConfig = struct { tree_sitter_grammars: TreeSitterGrammars, tracy: bool, mupdf: bool, esp32p4_cols: u16, esp32p4_rows: u16, /// null = "the platform's own default", resolved per shell below, because /// the default is off for the board and on everywhere else. theme_animation: ?bool, /// null = "the platform's own default": the macOS shell hands the row to /// its native menu bar, every other shell keeps the acme topbar. workspace_tag: ?bool, prebuilt_shaders: bool, embed_sources: bool, commit: ?[]const u8, }; fn shellOptions(b: *std.Build, cfg: ShellConfig, platform: Platform) *std.Build.Step.Options { const o = b.addOptions(); o.addOption(Platform, "platform", platform); o.addOption(bool, "syntax_highlighting", cfg.tree_sitter_grammars != .disabled); o.addOption(bool, "syntax_zig_grammar", cfg.tree_sitter_grammars != .disabled); o.addOption(bool, "syntax_minimal_grammars", cfg.tree_sitter_grammars == .minimal or cfg.tree_sitter_grammars == .full); o.addOption(bool, "syntax_full_grammars", cfg.tree_sitter_grammars == .full); o.addOption(bool, "enable_tracy", cfg.tracy); o.addOption(bool, "mupdf", cfg.mupdf); o.addOption(u16, "esp32p4_cols", cfg.esp32p4_cols); o.addOption(u16, "esp32p4_rows", cfg.esp32p4_rows); o.addOption(bool, "workspace_tag", cfg.workspace_tag orelse (platform != .macos)); o.addOption(bool, "theme_animation", cfg.theme_animation orelse (platform != .esp32p4)); o.addOption(bool, "embed_sources", cfg.embed_sources); o.addOption([]const u8, "version", display_version); // ...and the commit it was built from, when there is one and the build // is stamped at all (see `stamp_commit`). Read at configure time. o.addOption(?[]const u8, "commit", cfg.commit); return o; } // 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); } fn fatArchive(b: *std.Build, lib: *std.Build.Step.Compile) std.Build.LazyPath { const run = std.Build.Step.Run.create(b, "libtool libpardes.a"); run.addArgs(&.{ "libtool", "-static", "-o" }); const output = run.addOutputFileArg("libpardes.a"); for (lib.getCompileDependencies(false), 0..) |dep, i| { if (dep.kind != .lib) continue; run.addFileArg(reindexed(b, dep.getEmittedBin(), i)); } return output; } fn reindexed(b: *std.Build, archive: std.Build.LazyPath, index: usize) std.Build.LazyPath { const run = std.Build.Step.Run.create(b, b.fmt("ranlib #{d}", .{index})); run.addArgs(&.{ "/bin/sh", "-c", "/bin/cp \"$1\" \"$2\" && /usr/bin/ranlib \"$2\"", "_" }); run.addFileArg(archive); return run.addOutputFileArg(b.fmt("{d}.a", .{index})); } // glslc shaders/.glsl -fshader-stage= -o .spv, // returned as a LazyPath for @embedFile. The stage is the name's own suffix. /// The GLSL a shader is compiled from: shaders/.glsl, or for a bundled /// post pass (post-.frag) shaders/post/.glsl behind the Shadertoy /// prefix, the one the gui puts before a user's file at run time. fn shaderSource(b: *std.Build, name: []const u8) std.Build.LazyPath { if (!std.mem.startsWith(u8, name, "post-")) return b.path(b.fmt("shaders/{s}.glsl", .{name})); const joined = b.addSystemCommand(&.{"cat"}); joined.addFileArg(b.path("shaders/post/prefix.glsl")); joined.addFileArg(b.path(b.fmt("shaders/post/{s}.glsl", .{name["post-".len .. name.len - ".frag".len]}))); return joined.captureStdOut(.{}); } /// The one glslc invocation every shader is compiled with, at build time here /// and at run time for a user's post pass (src/gui/Post.zig). fn compileGlsl(b: *std.Build, name: []const u8) std.Build.LazyPath { const stage = if (std.mem.endsWith(u8, name, ".vert")) "vertex" else "fragment"; const cmd = b.addSystemCommand(&.{ "glslc", b.fmt("-fshader-stage={s}", .{stage}) }); cmd.addFileArg(shaderSource(b, name)); cmd.addArg("-o"); return cmd.addOutputFileArg(b.fmt("{s}.spv", .{name})); } fn vendoredThemes(b: *std.Build, io: std.Io) []const []const u8 { var count: usize = 0; { var dir = b.build_root.handle.openDir(io, "vendor/themes", .{ .iterate = true }) catch @panic("open vendor/themes"); defer dir.close(io); var it = dir.iterate(); while (it.next(io) catch @panic("read vendor/themes")) |e| { if (!std.mem.endsWith(u8, e.name, ".toml") and !std.mem.endsWith(u8, e.name, ".json")) continue; count += 1; } } const names = b.allocator.alloc([]const u8, count) catch @panic("OOM"); var dir = b.build_root.handle.openDir(io, "vendor/themes", .{ .iterate = true }) catch @panic("open vendor/themes"); defer dir.close(io); var it = dir.iterate(); var index: usize = 0; while (it.next(io) catch @panic("read vendor/themes")) |e| { if (!std.mem.endsWith(u8, e.name, ".toml") and !std.mem.endsWith(u8, e.name, ".json")) continue; names[index] = b.dupe(e.name); index += 1; } std.debug.assert(index == names.len); std.mem.sort([]const u8, names, {}, struct { fn lt(_: void, a: []const u8, c: []const u8) bool { return std.mem.order(u8, a, c) == .lt; } }.lt); return names; } fn wireCore(m: *std.Build.Module, d: struct { themes: ?*std.Build.Module = null, tree_sitter: ?*std.Build.Module = null, ts_libs: []const *std.Build.Step.Compile = &.{}, ts_queries: ?*std.Build.Step.Options = null, zstbi: ?*std.Build.Module = null, mvzr: ?*std.Build.Module = null, c_heap: ?*std.Build.Module = null, mupdf: ?*std.Build.Module = null, ghostty_vt: ?*std.Build.Module = null, vaxis: ?*std.Build.Module = null, uucode: ?*std.Build.Module = null, }) void { if (d.themes) |x| m.addImport("generated_themes", x); if (d.tree_sitter) |x| m.addImport("tree-sitter", x); for (d.ts_libs) |lib| m.linkLibrary(lib); if (d.ts_queries) |x| m.addOptions("ts_queries", x); if (d.zstbi) |x| m.addImport("zstbi", x); if (d.mvzr) |x| m.addImport("mvzr", x); if (d.c_heap) |x| m.addImport("c_heap", x); if (d.mupdf) |x| m.addImport("mupdf", x); if (d.ghostty_vt) |x| m.addImport("ghostty-vt", x); if (d.vaxis) |x| m.addImport("vaxis", x); if (d.uucode) |x| m.addImport("uucode", x); } /// The CPU feature set for the ESP32-P4 firmware target, as a `Cpu.Feature.Set`. Spelled as a /// helper because `cpu_features_add` wants a set and there is no literal syntax for one. fn riscvFeatures(comptime features: []const std.Target.riscv.Feature) std.Target.Cpu.Feature.Set { var set = std.Target.Cpu.Feature.Set.empty; for (features) |f| set.addFeature(@intFromEnum(f)); return set; }