summaryrefslogtreecommitdiff
path: root/build.zig
diff options
context:
space:
mode:
Diffstat (limited to 'build.zig')
-rw-r--r--build.zig102
1 files changed, 91 insertions, 11 deletions
diff --git a/build.zig b/build.zig
index 061d1f2c..01fed2b6 100644
--- a/build.zig
+++ b/build.zig
@@ -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;
+}