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"); 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 }; const zls_version = "0.16.1-dev+3e0d0820"; comptime { const plus = std.mem.indexOfScalar(u8, zls_version, '+') orelse @compileError("zls_version must end in +"); const short = zls_version[plus + 1 ..]; const url = zon.dependencies.zls.url; const hash = std.mem.indexOfScalar(u8, url, '#') orelse @compileError("the zls dependency url must pin a commit with #"); if (!std.mem.startsWith(u8, url[hash + 1 ..], short)) @compileError( "zls_version pins commit " ++ short ++ ", build.zig.zon pins " ++ url[hash + 1 ..], ); } pub const TreeSitterGrammars = enum { disabled, zig, minimal, full }; const gui_shaders = [_][]const u8{ "ui.vert", "ui.frag", "overlay.vert", "overlay.frag", "image.vert", "image.frag", "crt.vert", "crt.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; if (also_gui and prefixIsUntouched(b)) { 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; 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 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_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); const shaders_step = b.step("shaders", "refresh paired shaders/prebuilt/*.spv + *.glsl snapshots (glslc)"); const update_shaders = b.addUpdateSourceFiles(); for (gui_shaders) |name| { update_shaders.addCopyFileToSource(compileGlsl(b, name), b.fmt("shaders/prebuilt/{s}.spv", .{name})); update_shaders.addCopyFileToSource( b.path(b.fmt("shaders/{s}.glsl", .{name})), b.fmt("shaders/prebuilt/{s}.glsl", .{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"); if (is_esp32p4 and !is_esp32p4_target) 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 zls_mod: ?*std.Build.Module = null; 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 zls_backend = !freestanding_core; 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)"); const shell_cfg: ShellConfig = .{ .tree_sitter_grammars = tree_sitter_grammars, .tracy = tracy != null, .zls_backend = zls_backend, .mupdf = enable_mupdf, .esp32p4_cols = esp32p4_cols, .esp32p4_rows = esp32p4_rows, .theme_animation = theme_animation, .prebuilt_shaders = prebuilt_shaders, .zig_lib_dir = b.graph.zig_lib_directory.path orelse "", .commit = gitCommit(b), }; 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 (zls_backend) { const zls_dep = b.dependency("zls", .{ .target = target, .optimize = optimize, .@"version-string" = @as([]const u8, zls_version), }); zls_mod = zls_dep.module("zls"); } 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); const lib = b.addLibrary(.{ .name = "tree-sitter-" ++ g.name, .root_module = b.createModule(.{ .target = target, .optimize = c_optimize, .link_libc = !is_web }), .linkage = .static, }); const ts_cflags = [_][]const u8{"-fno-sanitize=function"}; lib.root_module.addCSourceFile(.{ .file = dep.path(g.src ++ "/parser.c"), .flags = &ts_cflags }); if (g.scanner) lib.root_module.addCSourceFile(.{ .file = dep.path(g.src ++ "/scanner.c"), .flags = &ts_cflags }); lib.root_module.addIncludePath(dep.path(g.src)); 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"), }); const mvzr_mod = b.dependency("mvzr", .{ .target = target, .optimize = optimize }).module("mvzr"); 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, }); 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.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.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_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_mod = b.dependency("vaxis", .{ .target = target, .optimize = optimize, .external_uucode = true, }).module("vaxis"); 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, .zls = zls_mod, .tree_sitter = ts_mod, .ts_libs = ts_libs.items, .ts_queries = ts_queries_opts, .zstbi = zstbi_mod, .mvzr = mvzr_mod, .mupdf = mupdf_core_mod, .ghostty_vt = ghostty_core_vt, .vaxis = vaxis_mod, .uucode = uucode_mod, }); var module_count: usize = 0; { var it = root_mod.import_table.iterator(); while (it.next()) |e| { const lp = e.value_ptr.*.root_source_file orelse continue; switch (lp) { .src_path, .cwd_relative => module_count += 1, else => continue, } } } const mod_names = b.allocator.alloc([]const u8, module_count) catch @panic("OOM"); const mod_roots = b.allocator.alloc([]const u8, module_count) catch @panic("OOM"); var module_index: usize = 0; { var it = root_mod.import_table.iterator(); while (it.next()) |e| { const lp = e.value_ptr.*.root_source_file orelse continue; // `generated` is a build artifact (the options modules): it has no // path until make() runs, and pointing an editor at one is useless const abs = switch (lp) { .src_path => |sp| sp.owner.pathFromRoot(sp.sub_path), .cwd_relative => |cr| cr, else => continue, }; mod_names[module_index] = e.key_ptr.*; mod_roots[module_index] = abs; module_index += 1; } } std.debug.assert(module_index == module_count); for ([_]?*std.Build.Step.Options{ opts, gui_opts }) |maybe| if (maybe) |o| { o.addOption([]const []const u8, "module_names", mod_names); o.addOption([]const []const u8, "module_roots", mod_roots); }; const iso_off = b.addOptions(); iso_off.addOption(bool, "isolated", false); const iso_on = b.addOptions(); iso_on.addOption(bool, "isolated", true); // Consulted only for a gui shell, so `-Dprebuilt-shaders` alone decides it // here; the platform half of that condition is the branch it sits in. const gui_effect_source_dir = if (prebuilt_shaders) "shaders/prebuilt" else "shaders"; for (core_mods, core_shells) |mod, shell| { 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 = b.path(b.fmt("{s}/{s}.glsl", .{ gui_effect_source_dir, name })) }, ); if (shell == .macos) mod.addAnonymousImport("effect-source-crt.ci.metal", .{ .root_source_file = b.path("shaders/crt.ci.metal"), }); mod.addAnonymousImport("root-build.zig", .{ .root_source_file = b.path("build.zig") }); mod.addAnonymousImport("root-build.zig.zon", .{ .root_source_file = b.path("build.zig.zon") }); } 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")); } const freetype_dep = b.dependency("freetype", .{ .target = target, .optimize = optimize, .@"enable-libpng" = false, }); gm.linkLibrary(freetype_dep.artifact("freetype")); 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"), }); 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), }); } 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", zon.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); 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])"); 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 lspprobe = b.addExecutable(.{ .name = "pardes-lspprobe", .root_module = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("tools/lspprobe.zig"), .link_libc = true, }), }); lspprobe.root_module.addImport("pardes", core_module); const run_lspprobe = b.addRunArtifact(lspprobe); if (b.args) |args| run_lspprobe.addArgs(args); const lspprobe_step = b.step("lspprobe", "one language query against the real seam (-- : [arg] [--reps N])"); lspprobe_step.dependOn(&run_lspprobe.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); 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 }); 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; b.step("fs-test", "exercise default 9P service and mounts with an independent client").dependOn(&run_fs_test.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"); 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 config_module = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("src/config.zig"), }); config_module.addImport("pardes_config", core_module.import_table.get("pardes_config").?); const config_tests = b.addTest(.{ .root_module = config_module, .filters = test_filters }); const run_config_tests = b.addRunArtifact(config_tests); run_config_tests.has_side_effects = true; const config_test_step = b.step("config-test", "run configuration tests without compiling the editor"); config_test_step.dependOn(&run_config_tests.step); if (testMatch(b, unit_profile, test_filters, config_test_step)) |check| check.addArtifactArg(config_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" }, }); 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/gui/crt.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, zls_backend: 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, prebuilt_shaders: bool, zig_lib_dir: []const u8, 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, "zls_backend", cfg.zls_backend); 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, "theme_animation", cfg.theme_animation orelse (platform != .esp32p4)); // Meaningful only for the SDL shell. Keeping the platform condition here // prevents Config/EffectCode from describing tty/macOS/web as "prebuilt". o.addOption(bool, "gui_shader_sources_prebuilt", platform == .gui and cfg.prebuilt_shaders); o.addOption([]const u8, "zls_version", if (cfg.zls_backend) zls_version else "none"); o.addOption([]const u8, "version", zon.version); // ...and the commit it was built from, when there is one. See `gitCommit` // for why this is optional and why it is read at configure time. o.addOption(?[]const u8, "commit", cfg.commit); o.addOption([]const u8, "zig_lib_dir", cfg.zig_lib_dir); 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. 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(b.path(b.fmt("shaders/{s}.glsl", .{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, zls: ?*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, 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.zls) |x| m.addImport("zls", 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.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; }