diff options
Diffstat (limited to 'build.zig')
| -rw-r--r-- | build.zig | 102 |
1 files changed, 91 insertions, 11 deletions
@@ -4,7 +4,10 @@ const mupdf_build = @import("mupdf.zig"); const snap_build = @import("build/snap.zig"); const grammar_manifest = @import("src/grammar_manifest.zig"); -pub const Platform = enum { tty, gui, web, macos }; +/// `p4` is not a shell in this package at all: it is a MODULE (`pardes_p4`) +/// compiled for riscv32-freestanding, which the zig-p4 firmware package +/// imports and gives a serial host. See the p4 branch below. +pub const Platform = enum { tty, gui, web, macos, p4 }; /// The oldest macOS pardes.app claims to run on, spelled ONCE. Three things /// have to agree about it or the bundle is a lie: the target this build gives @@ -39,7 +42,7 @@ const gui_shaders = [_][]const u8{ /// committed SPIR-V, so the builtin cannot print code other than what produced /// the bytecode that build executes. pub fn build(b: *std.Build) void { - const platform = b.option(Platform, "platform", "which shell to build (tty, gui, web, macos)") orelse .tty; + const platform = b.option(Platform, "platform", "which shell to build (tty, gui, web, macos, p4)") orelse .tty; // Default target is the Steam Deck (deckcap's trick): x86_64 linux-gnu // with the glibc version pinned low, so a binary built on a rolling- // release host runs on SteamOS — a native build references the host's @@ -60,12 +63,30 @@ pub fn build(b: *std.Build) void { // Anywhere else it stays plain native, which is the whole point of the // Linux dev loop: `zig build unit-test -Dplatform=macos` has to produce a // binary that machine can actually execute. - const target = b.standardTargetOptions(.{ .default_target = switch (platform) { + const p4_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 .{}, + // The P4 firmware target, spelled out here so `-Dplatform=p4` alone is a + // working command line. The CPU FEATURES are part of that spelling and + // are not optional: the object this build emits is linked into an image + // whose other halves are compiled `generic_rv32+m+a+f+c+zicsr+zifencei`, + // and `f` decides the float ABI. Leaving the model implicit produced a + // soft-float object and `ld.lld: cannot link object files with different + // floating-point ABI` — at LINK time in the other repo, far from here. + // Espressif's GCC adds the vendor extensions xesploop/xespv2p1 on top; + // upstream LLVM has neither and ordinary code never emits them, so this + // matches the base ISA the firmware uses exactly. + .p4 => p4_target, .tty, .gui, .web => .{ .cpu_arch = .x86_64, .os_tag = .linux, @@ -73,6 +94,14 @@ pub fn build(b: *std.Build) void { .glibc_version = .{ .major = 2, .minor = 38, .patch = 0 }, }, } }); + // `standardTargetOptions` honours `default_target` ONLY when `-Dtarget` is absent, so the + // documented `-Dplatform=p4 -Dtarget=riscv32-freestanding` discarded the CPU features above and + // silently produced a soft-float object. The features are not a preference here - `f` decides + // the float ABI, and the object is linked into an image whose other halves have it - so p4 takes + // the pinned query whatever was asked for. `-Dtarget` stays accepted, and the check further down + // still rejects anything that is not riscv32-freestanding, so a wrong `-Dtarget` is an error + // rather than something quietly ignored. + const target = if (platform == .p4) b.resolveTargetQuery(p4_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)"); @@ -83,7 +112,13 @@ pub fn build(b: *std.Build) void { else &.{}; const is_web = platform == .web; - const enable_mupdf = b.option(bool, "mupdf", "native PDF rendering with MuPDF (AGPL/commercial; native default on, web off; -Dmupdf=false disables)") orelse !is_web; + const is_p4 = platform == .p4; + // Platforms with no host libc: the browser and the P4 firmware. Every + // dependency below that exists only because a target links libc — the + // image decoder, ZLS, ghostty's C++ simd, MuPDF — is off for both, and the + // reason is freestanding-ness rather than the browser. + const freestanding_core = is_web or is_p4; + const enable_mupdf = b.option(bool, "mupdf", "native PDF rendering with MuPDF (AGPL/commercial; native default on, web/p4 off; -Dmupdf=false disables)") orelse !freestanding_core; // JPEG 2000, and with it scanned PDFs: a scan is one /JPXDecode image per // page, so without this MuPDF decodes nothing and every page comes back // blank. On by default — a viewer that cannot open scans is the more @@ -92,6 +127,7 @@ pub fn build(b: *std.Build) void { // mupdf.zig. 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_p4_target = target.result.cpu.arch == .riscv32 and target.result.os.tag == .freestanding; // wasm: size is the budget const optimize = if (is_web) .ReleaseSmall else requested_optimize; // The vendored C is never what we are debugging, and at -O0 it dominates @@ -104,8 +140,9 @@ pub fn build(b: *std.Build) void { // The browser keeps its useful default grammar without acquiring a host // libc contract: Tree-sitter and the generated Zig parser are linked into // the freestanding module against src/web/libc's tiny in-module shim. - const default_grammars: TreeSitterGrammars = if (is_web) .zig else .full; - const tree_sitter_grammars = b.option(TreeSitterGrammars, "tree-sitter", "tree-sitter grammar set: disabled, zig, minimal (c/c++/zig), full") orelse default_grammars; + const default_grammars: TreeSitterGrammars = if (is_web) .zig else if (is_p4) .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"); // Who signs pardes.app. Ad-hoc ("-") is what makes a bundle launchable on // the machine that built it and needs no keychain; a Developer ID here is @@ -166,6 +203,9 @@ pub fn build(b: *std.Build) void { if (!is_web and (is_web_target or target.result.os.tag == .emscripten)) return failBuild(b, web_step, "wasm browser targets require -Dplatform=web"); if (platform == .web and dump_path == null) return failBuild(b, web_step, "-Dplatform=web requires -Ddump=<dump.zon> (the browser has no ptys; state replays from an embedded dump)"); if (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_p4 and !is_p4_target) return failBuild(b, web_step, "-Dplatform=p4 requires -Dtarget=riscv32-freestanding (ESP32-P4 firmware)"); + if (enable_mupdf and is_p4) return failBuild(b, web_step, "-Dmupdf=true is supported only by the native tty/Kitty and gui/SDL backends"); + if (is_p4 and requested_grammars != null and requested_grammars.? != .disabled) return failBuild(b, web_step, "-Dplatform=p4 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 @@ -181,9 +221,13 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path(switch (platform) { .web => "src/web.zig", .macos => "src/macos.zig", + // p4 roots at its own flat C ABI too, for the same reason web and + // macOS do: the firmware's `_start`, its linker script and its UART + // live in the zig-p4 package, which LINKS the object this emits. + .p4 => "src/p4.zig", .tty, .gui => "src/main.zig", }), - .link_libc = !is_web, + .link_libc = !freestanding_core, }); // helix differential harness (test/hxdiff.zig): a second compilation of // the core, driven headlessly. Mirrors root_mod's wiring for @@ -276,7 +320,7 @@ pub fn build(b: *std.Build) void { // the web shell does not, because it has no threads and no-ops the // lsp effect anyway, so paying to compile an analyser it can never call // would be pure wasm. - const zls_backend = !is_web; + const zls_backend = !freestanding_core; const opts = b.addOptions(); opts.addOption(Platform, "platform", platform); @@ -417,7 +461,7 @@ pub fn build(b: *std.Build) void { // zstbi (C stb_image): image pane decode. The freestanding core has no C // allocator ABI, so its module is a compatible no-decode shim; browser IO // can grow native image decoding independently of the core. - const zstbi_dep = if (!is_web) b.dependency("zstbi", .{ .target = target, .optimize = c_optimize }) else null; + const zstbi_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, @@ -599,7 +643,7 @@ pub fn build(b: *std.Build) void { // without it); gate that block on `b.graph.host.result.os.tag == .linux`. // Ghostty's pinned translate-c tarball also 404s now (codeberg dropped // the archive endpoint) — seed zig-pkg/ from a machine that has it. - const ghostty_simd = !is_web and (!target.result.os.tag.isDarwin() or b.graph.host.result.os.tag.isDarwin()); + const ghostty_simd = !freestanding_core and (!target.result.os.tag.isDarwin() or b.graph.host.result.os.tag.isDarwin()); // app-runtime none + emit-xcframework off: only the ghostty-vt module is // consumed, and the defaults otherwise drag ghostty's app graph into the // build — gtk4 header translation via host pkg-config for linux targets, @@ -614,7 +658,13 @@ pub fn build(b: *std.Build) void { // own safety checks come from OUR optimize mode and are unaffected, since // ghostty-vt is a module compiled into this binary. See reflow.snap. const ghostty_optimize: std.builtin.OptimizeMode = if (optimize == .Debug) .ReleaseSafe else optimize; - const ghostty_dep = b.lazyDependency("ghostty", .{ .target = target, .optimize = ghostty_optimize, .simd = ghostty_simd, .@"app-runtime" = .none, .@"emit-xcframework" = false }); + // The P4 firmware has no terminal panes at all (see `terminal_panes` in + // src/pardes.zig), so src/term_pane.zig's `@import("ghostty-vt")` sits in a + // dead comptime branch and `wireCore` below is handed a null. The + // dependency is therefore not merely unused, it is never REQUESTED: this is + // a lazyDependency, so a p4 build does not need the ghostty package (nor + // its translate-c tarball, nor its simd C++) present at all. + const ghostty_dep = if (is_p4) 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; @@ -794,6 +844,28 @@ pub fn build(b: *std.Build) void { 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_p4) { + // ONE freestanding object, exporting the C ABI in src/p4.zig. Not an executable, because + // the firmware's `_start`, its generated linker script and its UART driver all live in the + // zig-p4 package; not a library, because `addLibrary` bundles compiler_rt and the firmware + // already has its own; and not a MODULE exposed through build.zig.zon, which is what this + // was first and is the interesting part. + // + // A path dependency was tried and reverted. Nesting this package's ~30-package graph under + // zig-p4's broke every build in that repo, not just the firmware one: `std/Build.zig:2091` + // exceeded its 1000-branch comptime quota through ghostty's `SharedDeps.zig:874` + // `lazyImport`, seven cached tree-sitter versions failed to compile because their build.zig + // uses APIs removed in 0.16, and the fetch materialised 2.6 GB across 42,736 files into a + // repo whose entire claim is that Zig is its only dependency. An object has none of that, + // and the seam it leaves is bytes rather than types, which is the right seam for a serial + // line anyway. + // + // The object is also the compile probe: rooted at src/p4.zig it drags the whole core through + // the riscv32 backend by actually calling it, so `llvm-size` on the result is a real number + // to hold against the board's 1.5 MiB factory partition. + const obj = b.addObject(.{ .name = "pardes-p4", .root_module = root_mod }); + b.getInstallStep().dependOn(&b.addInstallFile(obj.getEmittedBin(), "pardes-p4.o").step); + web_step.dependOn(&b.addFail("web needs -Dplatform=web -Dtarget=wasm32-freestanding -Ddump=<dump.zon>").step); } else if (platform == .macos) { // The native macOS shell is a static library plus a Swift app: Zig // keeps the core, the ptys and every effect; Swift owns AppKit and @@ -1435,3 +1507,11 @@ fn wireCore(m: *std.Build.Module, d: struct { 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; +} |
