b2a07df2
build: the import graph as a declared, layer-checked table
a73x 2026-08-14 08:04
Commit message
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -72,366 +72,294 @@ fn linkQuic(b: *std.Build, c: *std.Build.Step.Compile, deps: anytype) void { | |||
| 72 | c.linkSystemLibrary2("wolfssl", .{ .preferred_link_mode = .static }); | 72 | c.linkSystemLibrary2("wolfssl", .{ .preferred_link_mode = .static }); |
| 73 | } | 73 | } |
| 74 | 74 | ||
| 75 | pub fn build(b: *std.Build) void { | 75 | /// One row of the module table: the import graph as declared data. |
| 76 | // Single source for both binaries' --version. Bumped at tag time. | 76 | /// The wiring loop below derives every addImport from it, so an import |
| 77 | const version = "0.0.1-5"; | 77 | /// the table does not declare cannot exist — violations are impossible, |
| 78 | const version_opts = b.addOptions(); | 78 | /// not detected. Layers are the topological strata of the production |
| 79 | version_opts.addOption([]const u8, "version", version); | 79 | /// graph, computed 2026-08-14 and FROZEN: a new import that would |
| 80 | 80 | /// flatten or invert a stratum fails at comptime, and re-stratifying | |
| 81 | const target = b.standardTargetOptions(.{}); | 81 | /// requires editing this table, which is the point. |
| 82 | const optimize = b.standardOptimizeOption(.{}); | 82 | const ModSpec = struct { |
| 83 | const quic = quicDeps(b, target); | 83 | name: []const u8, |
| 84 | 84 | path: []const u8, | |
| 85 | const ghostty_dep = b.lazyDependency("ghostty", .{ | 85 | layer: u8, |
| 86 | .target = target, | 86 | /// Production imports: must point at a strictly lower layer. |
| 87 | .optimize = optimize, | 87 | imports: []const []const u8 = &.{}, |
| 88 | }); | 88 | /// Test-only imports (the testtmp pattern): wired identically — lazy |
| 89 | 89 | /// compilation keeps them out of release binaries — but declared | |
| 90 | const protocol_mod = b.createModule(.{ | 90 | /// apart, because "test scaffolding never ships" is a stated rule. |
| 91 | .root_source_file = b.path("src/protocol.zig"), | 91 | /// Excluded from the strata computation. |
| 92 | .target = target, | 92 | test_imports: []const []const u8 = &.{}, |
| 93 | .optimize = optimize, | 93 | link_libc: bool = false, |
| 94 | }); | 94 | /// Also instantiated against wasm32 (the muxweb core's twins). |
| 95 | 95 | wasm: bool = false, | |
| 96 | const engine_mod = b.createModule(.{ | 96 | /// This module's test binary needs the QUIC archives. |
| 97 | .root_source_file = b.path("src/engine.zig"), | 97 | quic_tests: bool = false, |
| 98 | .target = target, | 98 | }; |
| 99 | .optimize = optimize, | 99 | |
| 100 | }); | 100 | const mod_table = [_]ModSpec{ |
| 101 | if (ghostty_dep) |dep| { | 101 | // ---- layer 0: imports nothing internal in production ---- |
| 102 | engine_mod.addImport("ghostty-vt", dep.module("ghostty-vt")); | 102 | .{ .name = "protocol", .path = "src/protocol.zig", .layer = 0, .wasm = true }, |
| 103 | } | 103 | .{ .name = "engine", .path = "src/engine.zig", .layer = 0, .wasm = true }, |
| 104 | 104 | .{ .name = "pty", .path = "src/pty.zig", .layer = 0, .link_libc = true }, | |
| 105 | const pty_mod = b.createModule(.{ | ||
| 106 | .root_source_file = b.path("src/pty.zig"), | ||
| 107 | .target = target, | ||
| 108 | .optimize = optimize, | ||
| 109 | .link_libc = true, | ||
| 110 | }); | ||
| 111 | |||
| 112 | // The QUIC vocabulary both ends share: the one @cImport of the vendored | 105 | // The QUIC vocabulary both ends share: the one @cImport of the vendored |
| 113 | // stack, the key, the wire constants, the egress ring. It has to be ONE | 106 | // stack, the key, the wire constants, the egress ring. It has to be ONE |
| 114 | // module — two @cImport blocks over the same headers are two distinct | 107 | // module — two @cImport blocks over the same headers are two distinct |
| 115 | // type universes, and `ngtcp2_vec`s cross between listener and client. | 108 | // type universes, and `ngtcp2_vec`s cross between listener and client. |
| 116 | const quic_mod = b.createModule(.{ | 109 | .{ .name = "quic", .path = "src/quic.zig", .layer = 0, .link_libc = true, .quic_tests = true }, |
| 117 | .root_source_file = b.path("src/quic.zig"), | 110 | // Test-only: short temp paths for the tests that bind unix sockets. |
| 118 | .target = target, | 111 | // Imported by every module that has such a test, which is why it is a |
| 119 | .optimize = optimize, | 112 | // module rather than three copies. |
| 120 | .link_libc = true, | 113 | .{ .name = "testtmp", .path = "src/testtmp.zig", .layer = 0 }, |
| 121 | }); | 114 | // Normalized key events -> VT bytes. A leaf with no platform imports — |
| 122 | 115 | // each shell (browser, later xkb) produces the normalized form and this | |
| 116 | // owns the bytes; must compile for wasm32. | ||
| 117 | .{ .name = "keymap", .path = "src/keymap.zig", .layer = 0, .wasm = true }, | ||
| 118 | // What the two scripted fixtures share: the escape table and the exit | ||
| 119 | // codes. One copy, so ptyclient and wsclient cannot disagree about | ||
| 120 | // what a scenario's heredoc sent. | ||
| 121 | .{ .name = "script", .path = "test/script.zig", .layer = 0 }, | ||
| 122 | // Test helpers, built as real binaries because that is how the suite | ||
| 123 | // uses them: rawmode is a deterministic stand-in for an editor (nvim's | ||
| 124 | // redraw timing is its own business and it is not installed everywhere), | ||
| 125 | // and delaypipe makes a slow round trip out of a shell pipeline instead | ||
| 126 | // of out of netem and root. | ||
| 127 | .{ .name = "rawmode", .path = "test/rawmode.zig", .layer = 0 }, | ||
| 128 | .{ .name = "delaypipe", .path = "test/delaypipe.zig", .layer = 0 }, | ||
| 129 | // XDG-derived paths (key file, daemon log), shared by both binaries. | ||
| 130 | .{ .name = "xdg", .path = "src/xdg.zig", .layer = 0, .link_libc = true, .test_imports = &.{"testtmp"} }, | ||
| 131 | // The socket path's identity and the right to bind it: the stale-socket | ||
| 132 | // claim and the dev+ino record teardown compares against. A leaf — it | ||
| 133 | // takes a path and nothing else, and knows no Server exists. | ||
| 134 | .{ .name = "sockpath", .path = "src/sockpath.zig", .layer = 0, .test_imports = &.{"testtmp"} }, | ||
| 135 | // No imports that teach it anything, deliberately: the proxy is a byte | ||
| 136 | // pump that knows nothing about the protocol it carries. `testtmp` is | ||
| 137 | // the one exception and does not weaken that — it hands its tests a | ||
| 138 | // short directory to put a socket in and knows nothing about the bytes. | ||
| 139 | .{ .name = "proxy", .path = "src/proxy.zig", .layer = 0, .link_libc = true, .test_imports = &.{"testtmp"} }, | ||
| 140 | // ---- layer 1: single-hop over the leaves ---- | ||
| 123 | // The QUIC listener: the vocabulary plus a UDP socket and a connection | 141 | // The QUIC listener: the vocabulary plus a UDP socket and a connection |
| 124 | // table, and deliberately no protocol import — it carries opaque bytes, | 142 | // table, and deliberately no protocol import — it carries opaque bytes, |
| 125 | // exactly as proxy.zig does. | 143 | // exactly as proxy.zig does. |
| 126 | const quic_server_mod = b.createModule(.{ | 144 | .{ .name = "quic_server", .path = "src/quic_server.zig", .layer = 1, .link_libc = true, .imports = &.{"quic"}, .quic_tests = true }, |
| 127 | .root_source_file = b.path("src/quic_server.zig"), | 145 | // The client's QUIC transport. Imports the vocabulary module — NOT the |
| 128 | .target = target, | 146 | // listener — for the pieces both ends must agree on (the key, the egress |
| 129 | .optimize = optimize, | 147 | // ring's lifetime discipline, the PSK identity and ALPN); duplicating |
| 130 | .link_libc = true, | 148 | // those would make a handshake failure the first sign they had drifted. |
| 131 | }); | 149 | // It has no business knowing a listener exists, and now it cannot. |
| 132 | quic_server_mod.addImport("quic", quic_mod); | 150 | .{ .name = "quic_client", .path = "src/quic_client.zig", .layer = 1, .link_libc = true, .imports = &.{"quic"}, .quic_tests = true }, |
| 133 | |||
| 134 | // Speculative local echo: the overlay and its policy, and deliberately | 151 | // Speculative local echo: the overlay and its policy, and deliberately |
| 135 | // nothing else. No engine import, which is what lets the whole state | 152 | // nothing else. No engine import, which is what lets the whole state |
| 136 | // machine be exercised without a terminal — or a daemon — anywhere in | 153 | // machine be exercised without a terminal — or a daemon — anywhere in |
| 137 | // the picture; reconcile takes its grid duck-typed instead. | 154 | // the picture; reconcile takes its grid duck-typed instead. |
| 138 | const predict_mod = b.createModule(.{ | 155 | .{ .name = "predict", .path = "src/predict.zig", .layer = 1, .imports = &.{"protocol"} }, |
| 139 | .root_source_file = b.path("src/predict.zig"), | ||
| 140 | .target = target, | ||
| 141 | .optimize = optimize, | ||
| 142 | }); | ||
| 143 | predict_mod.addImport("protocol", protocol_mod); | ||
| 144 | |||
| 145 | // Test-only: short temp paths for the tests that bind unix sockets. | ||
| 146 | // Imported by every module that has such a test, which is why it is a | ||
| 147 | // module rather than three copies. | ||
| 148 | const testtmp_mod = b.createModule(.{ | ||
| 149 | .root_source_file = b.path("src/testtmp.zig"), | ||
| 150 | .target = target, | ||
| 151 | .optimize = optimize, | ||
| 152 | }); | ||
| 153 | |||
| 154 | // XDG-derived paths (key file, daemon log), shared by both binaries. | ||
| 155 | const xdg_mod = b.createModule(.{ | ||
| 156 | .root_source_file = b.path("src/xdg.zig"), | ||
| 157 | .target = target, | ||
| 158 | .optimize = optimize, | ||
| 159 | .link_libc = true, | ||
| 160 | }); | ||
| 161 | xdg_mod.addImport("testtmp", testtmp_mod); | ||
| 162 | |||
| 163 | // Daemon spawning (probe / detach / poll). muxd start today; attach | 156 | // Daemon spawning (probe / detach / poll). muxd start today; attach |
| 164 | // auto-start is a banked second call site. | 157 | // auto-start is a banked second call site. |
| 165 | const spawn_mod = b.createModule(.{ | 158 | .{ .name = "spawn", .path = "src/spawn.zig", .layer = 1, .link_libc = true, .imports = &.{"xdg"}, .test_imports = &.{"testtmp"} }, |
| 166 | .root_source_file = b.path("src/spawn.zig"), | ||
| 167 | .target = target, | ||
| 168 | .optimize = optimize, | ||
| 169 | .link_libc = true, | ||
| 170 | }); | ||
| 171 | spawn_mod.addImport("xdg", xdg_mod); | ||
| 172 | spawn_mod.addImport("testtmp", testtmp_mod); | ||
| 173 | |||
| 174 | // The ssh→QUIC handoff's shared vocabulary: the announce line, the | 159 | // The ssh→QUIC handoff's shared vocabulary: the announce line, the |
| 175 | // per-host cache, the dial-host strip. Both binaries import it — | 160 | // per-host cache, the dial-host strip. Both binaries import it — |
| 176 | // `muxd endpoint` writes the line, `mux HOST` reads it — and it is | 161 | // `muxd endpoint` writes the line, `mux HOST` reads it. xdg is for |
| 177 | // pure enough to need nothing but testtmp for its cache tests. | 162 | // makePrivateParent only — the 0700-parent discipline the cache file |
| 178 | const handoff_mod = b.createModule(.{ | 163 | // shares with the key file; xdg is a leaf, so no cycle. |
| 179 | .root_source_file = b.path("src/handoff.zig"), | 164 | .{ .name = "handoff", .path = "src/handoff.zig", .layer = 1, .link_libc = true, .imports = &.{"xdg"}, .test_imports = &.{"testtmp"} }, |
| 180 | .target = target, | ||
| 181 | .optimize = optimize, | ||
| 182 | .link_libc = true, | ||
| 183 | }); | ||
| 184 | handoff_mod.addImport("testtmp", testtmp_mod); | ||
| 185 | // For makePrivateParent only — the 0700-parent discipline the cache | ||
| 186 | // file shares with the key file. xdg is a leaf, so no cycle. | ||
| 187 | handoff_mod.addImport("xdg", xdg_mod); | ||
| 188 | |||
| 189 | // Row-level change tracking behind the delta stream. Engine plus | 165 | // Row-level change tracking behind the delta stream. Engine plus |
| 190 | // protocol and nothing else — no daemon, no clients — so the tracker's | 166 | // protocol and nothing else — no daemon, no clients — so the tracker's |
| 191 | // own tests drive it with an engine and no socket in sight. | 167 | // own tests drive it with an engine and no socket in sight. |
| 192 | const delta_mod = b.createModule(.{ | 168 | .{ .name = "delta", .path = "src/delta.zig", .layer = 1, .imports = &.{ "engine", "protocol" } }, |
| 193 | .root_source_file = b.path("src/delta.zig"), | ||
| 194 | .target = target, | ||
| 195 | .optimize = optimize, | ||
| 196 | }); | ||
| 197 | delta_mod.addImport("engine", engine_mod); | ||
| 198 | delta_mod.addImport("protocol", protocol_mod); | ||
| 199 | |||
| 200 | // The session's command state machine: MarkEvents in, transitions out. | 169 | // The session's command state machine: MarkEvents in, transitions out. |
| 201 | // Engine plus protocol and nothing else, same shape as delta_mod — pure, | 170 | // Engine plus protocol and nothing else, same shape as delta — pure, |
| 202 | // socket-free, and its own tests drive it with no daemon in sight. | 171 | // socket-free, and its own tests drive it with no daemon in sight. |
| 203 | const cmd_mod = b.createModule(.{ | 172 | .{ .name = "cmd", .path = "src/cmd.zig", .layer = 1, .imports = &.{ "engine", "protocol" } }, |
| 204 | .root_source_file = b.path("src/cmd.zig"), | ||
| 205 | .target = target, | ||
| 206 | .optimize = optimize, | ||
| 207 | }); | ||
| 208 | cmd_mod.addImport("engine", engine_mod); | ||
| 209 | cmd_mod.addImport("protocol", protocol_mod); | ||
| 210 | |||
| 211 | // Shell integration: the OSC 133 mark scripts and what a spawn must add | 173 | // Shell integration: the OSC 133 mark scripts and what a spawn must add |
| 212 | // to hand them to a shell. Near-leaf on purpose — it writes files and | 174 | // to hand them to a shell. Near-leaf on purpose — it writes files and |
| 213 | // reads the environment, and knows nothing of ptys, servers or the | 175 | // reads the environment, and knows nothing of ptys, servers or the |
| 214 | // protocol, so its tests need no daemon and no socket. The one import | 176 | // protocol, so its tests need no daemon and no socket. The one import |
| 215 | // is xdg, for the private-directory policy the shim directory shares | 177 | // is xdg, for the private-directory policy the shim directory shares |
| 216 | // with the key file's parent; xdg is itself a leaf, so no cycle. | 178 | // with the key file's parent; xdg is itself a leaf, so no cycle. |
| 217 | const shellint_mod = b.createModule(.{ | 179 | .{ .name = "shellint", .path = "src/shellint.zig", .layer = 1, .imports = &.{"xdg"} }, |
| 218 | .root_source_file = b.path("src/shellint.zig"), | ||
| 219 | .target = target, | ||
| 220 | .optimize = optimize, | ||
| 221 | }); | ||
| 222 | shellint_mod.addImport("xdg", xdg_mod); | ||
| 223 | |||
| 224 | // The replay core: snapshot/delta application and the resume | 180 | // The replay core: snapshot/delta application and the resume |
| 225 | // coordinates, shared by the CLI client, the wasm core, and the | 181 | // coordinates, shared by the CLI client, the wasm core, and the |
| 226 | // server's test fixtures. Engine plus protocol and nothing else, and | 182 | // server's test fixtures. Engine plus protocol and nothing else, and |
| 227 | // deliberately platform-free — it must compile for wasm32. | 183 | // deliberately platform-free — it must compile for wasm32. |
| 228 | const replica_mod = b.createModule(.{ | 184 | .{ .name = "replica", .path = "src/replica.zig", .layer = 1, .wasm = true, .imports = &.{ "engine", "protocol" } }, |
| 229 | .root_source_file = b.path("src/replica.zig"), | 185 | // Painting the replica onto a tty. Takes an fd out and replica/engine |
| 230 | .target = target, | 186 | // types in, and knows nothing about transports — which is what lets its |
| 231 | .optimize = optimize, | 187 | // tests drive every painter through a pipe with no daemon anywhere. |
| 232 | }); | 188 | .{ .name = "paint", .path = "src/paint.zig", .layer = 1, .imports = &.{ "engine", "protocol" } }, |
| 233 | replica_mod.addImport("engine", engine_mod); | 189 | // Replays a captured client stdout stream and prints the final grid in |
| 234 | replica_mod.addImport("protocol", protocol_mod); | 190 | // `muxd dump`'s formats — the client half of the M11 render-vs-dump |
| 235 | 191 | // convergence check. Imports the engine module so both sides of the | |
| 236 | // Normalized key events -> VT bytes. A leaf with no platform imports — | 192 | // diff go through the same ghostty-vt and the same formatter. |
| 237 | // each shell (browser, later xkb) produces the normalized form and this | 193 | .{ .name = "render", .path = "test/render.zig", .layer = 1, .imports = &.{"engine"} }, |
| 238 | // owns the bytes; must compile for wasm32. | 194 | // The pty-driving e2e fixture: real client on a pty slave, scripted |
| 239 | const keymap_mod = b.createModule(.{ | 195 | // from stdin (M12). Imports pty so the product's own module is the one |
| 240 | .root_source_file = b.path("src/keymap.zig"), | 196 | // under it. |
| 241 | .target = target, | 197 | .{ .name = "ptyclient", .path = "test/ptyclient.zig", .layer = 1, .link_libc = true, .imports = &.{ "pty", "script" } }, |
| 242 | .optimize = optimize, | 198 | // ---- layer 2 ---- |
| 243 | }); | 199 | // quic and quic_server both: the listener it owns, and the vocabulary |
| 244 | 200 | // it names directly (the key it loads, the idle default it falls back | |
| 201 | // to). xdg is for endpoint_req's lazy bind — the default key path, | ||
| 202 | // resolved by the daemon itself when nobody handed it a --key. | ||
| 203 | .{ .name = "server", .path = "src/server.zig", .layer = 2, .link_libc = true, .imports = &.{ "engine", "pty", "protocol", "delta", "cmd", "shellint", "sockpath", "quic", "quic_server", "xdg" }, .test_imports = &.{ "replica", "testtmp" }, .quic_tests = true }, | ||
| 204 | // The client is the only thing that predicts: the overlay is a local | ||
| 205 | // display decision and never becomes state anybody else can see. It | ||
| 206 | // also borrows ignoreSigpipe, which proxy owns — proxy is a leaf, so | ||
| 207 | // this adds no cycle and teaches the proxy nothing. | ||
| 208 | .{ .name = "client", .path = "src/client.zig", .layer = 2, .link_libc = true, .imports = &.{ "engine", "protocol", "replica", "quic_client", "quic", "predict", "handoff", "proxy", "paint" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | ||
| 209 | // The agent-facing client. It speaks frames and owns no terminal, which | ||
| 210 | // is the whole point — it attaches at 0x0 and never claims the grid. | ||
| 211 | // The transport modules are the CLI client's, minus everything that | ||
| 212 | // renders: `quic_client` for the remote arm and `xdg` for the one | ||
| 213 | // key-resolution rule all three binaries obey. Deliberately still no | ||
| 214 | // engine and no replica — muxa has nothing to draw. | ||
| 215 | .{ .name = "muxa", .path = "src/muxa.zig", .layer = 2, .link_libc = true, .imports = &.{ "protocol", "sockpath", "quic_client", "quic", "xdg" }, .quic_tests = true }, | ||
| 216 | .{ .name = "wsclient", .path = "test/wsclient.zig", .layer = 2, .link_libc = true, .imports = &.{ "engine", "replica", "protocol", "script" } }, | ||
| 217 | // ---- layer 3 ---- | ||
| 245 | // The muxweb hub's HTTP/WebSocket decisions: Origin gate, route | 218 | // The muxweb hub's HTTP/WebSocket decisions: Origin gate, route |
| 246 | // table, WS endpoint naming. Assets are injected (the exe root | 219 | // table, WS endpoint naming. Assets are injected (the exe root |
| 247 | // @embedFiles them), so its tests build no artifacts. | 220 | // @embedFiles them), so its tests build no artifacts. |
| 248 | const webhub_mod = b.createModule(.{ | 221 | .{ .name = "webhub", .path = "src/webhub.zig", .layer = 3, .imports = &.{ "protocol", "client" }, .quic_tests = true }, |
| 249 | .root_source_file = b.path("src/webhub.zig"), | 222 | // sockpath is the sun_path bound only; the client binds no socket itself. |
| 250 | .target = target, | 223 | .{ .name = "mux", .path = "src/mux_main.zig", .layer = 3, .link_libc = true, .imports = &.{ "client", "xdg", "spawn", "handoff", "sockpath" }, .quic_tests = true }, |
| 251 | .optimize = optimize, | 224 | // The daemon entrypoint loads the key and constructs the listener, so |
| 252 | }); | 225 | // it needs quic/quic_server directly rather than through the server. |
| 253 | webhub_mod.addImport("protocol", protocol_mod); | 226 | // `muxd endpoint` prints the announce line handoff spells; sockpath is |
| 254 | 227 | // the sun_path bound, checked before any verb acts on the path; and the | |
| 255 | // The socket path's identity and the right to bind it: the stale-socket | 228 | // keygen round-trip test needs a directory to generate into, which the |
| 256 | // claim and the dev+ino record teardown compares against. A leaf — it | 229 | // daemon itself never touches. |
| 257 | // takes a path and nothing else, and knows no Server exists. | 230 | .{ .name = "exe", .path = "src/main.zig", .layer = 3, .link_libc = true, .imports = &.{ "server", "protocol", "cmd", "proxy", "quic", "quic_server", "xdg", "spawn", "handoff", "sockpath" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, |
| 258 | const sockpath_mod = b.createModule(.{ | 231 | // ---- layer 4 ---- |
| 259 | .root_source_file = b.path("src/sockpath.zig"), | 232 | // The HOST tile's recipe comes from the same owner mux_main uses, and |
| 260 | .target = target, | 233 | // sockpath is the sun_path bound its --sock tiles are refused against. |
| 261 | .optimize = optimize, | 234 | .{ .name = "webhub_main", .path = "src/webhub_main.zig", .layer = 4, .link_libc = true, .imports = &.{ "client", "webhub", "xdg", "handoff", "sockpath" }, .quic_tests = true }, |
| 262 | }); | 235 | }; |
| 263 | sockpath_mod.addImport("testtmp", testtmp_mod); | 236 | |
| 264 | 237 | fn layerOf(comptime name: []const u8) u8 { | |
| 265 | const server_mod = b.createModule(.{ | 238 | for (mod_table) |m| { |
| 266 | .root_source_file = b.path("src/server.zig"), | 239 | if (std.mem.eql(u8, m.name, name)) return m.layer; |
| 267 | .target = target, | 240 | } |
| 268 | .optimize = optimize, | 241 | @compileError("module table: unknown module '" ++ name ++ "'"); |
| 269 | .link_libc = true, | 242 | } |
| 270 | }); | ||
| 271 | server_mod.addImport("engine", engine_mod); | ||
| 272 | server_mod.addImport("pty", pty_mod); | ||
| 273 | server_mod.addImport("protocol", protocol_mod); | ||
| 274 | server_mod.addImport("delta", delta_mod); | ||
| 275 | server_mod.addImport("cmd", cmd_mod); | ||
| 276 | server_mod.addImport("shellint", shellint_mod); | ||
| 277 | server_mod.addImport("replica", replica_mod); | ||
| 278 | server_mod.addImport("sockpath", sockpath_mod); | ||
| 279 | // Both: the listener it owns, and the vocabulary it names directly | ||
| 280 | // (the key it loads, the idle default it falls back to). | ||
| 281 | server_mod.addImport("quic", quic_mod); | ||
| 282 | server_mod.addImport("quic_server", quic_server_mod); | ||
| 283 | server_mod.addImport("testtmp", testtmp_mod); | ||
| 284 | // For endpoint_req's lazy bind: the default key path, resolved by the | ||
| 285 | // daemon itself when nobody handed it a --key. | ||
| 286 | server_mod.addImport("xdg", xdg_mod); | ||
| 287 | 243 | ||
| 288 | // The client's QUIC transport. Imports the vocabulary module — NOT the | 244 | comptime { |
| 289 | // listener — for the pieces both ends must agree on (the key, the egress | 245 | // Both checks are O(edges x rows) name comparisons; the default 1000 |
| 290 | // ring's lifetime discipline, the PSK identity and ALPN); duplicating | 246 | // backwards branches does not cover a 32-row, 76-edge table. |
| 291 | // those would make a handshake failure the first sign they had drifted. | 247 | @setEvalBranchQuota(20_000); |
| 292 | // It has no business knowing a listener exists, and now it cannot. | 248 | for (mod_table) |m| { |
| 293 | const quic_client_mod = b.createModule(.{ | 249 | for (m.imports) |dep| { |
| 294 | .root_source_file = b.path("src/quic_client.zig"), | 250 | if (layerOf(dep) >= m.layer) @compileError(std.fmt.comptimePrint( |
| 295 | .target = target, | 251 | "layer violation: {s} (layer {d}) imports {s} (layer {d}) — " ++ |
| 296 | .optimize = optimize, | 252 | "production imports point strictly downward; re-stratifying " ++ |
| 297 | .link_libc = true, | 253 | "is a deliberate edit to mod_table, never an accident", |
| 298 | }); | 254 | .{ m.name, m.layer, dep, layerOf(dep) }, |
| 299 | quic_client_mod.addImport("quic", quic_mod); | 255 | )); |
| 256 | } | ||
| 257 | // Test-only imports skip the direction rule but must name real rows. | ||
| 258 | for (m.test_imports) |dep| _ = layerOf(dep); | ||
| 259 | } | ||
| 260 | } | ||
| 300 | 261 | ||
| 301 | // No imports that teach it anything, deliberately: the proxy is a byte | 262 | /// Test registration order. Doctrine-laden and deliberately NOT derived |
| 302 | // pump that knows nothing about the protocol it carries. `testtmp` is | 263 | /// from the layers: delta, cmd, shellint and sockpath run BEFORE server |
| 303 | // the one exception and does not weaken that — it hands its tests a | 264 | /// because their tests are seconds-long and socket-free, while a |
| 304 | // short directory to put a socket in and knows nothing about the bytes. | 265 | /// regression in any of them can wedge a server test that waits on a |
| 305 | const proxy_mod = b.createModule(.{ | 266 | /// client forever — and a wedged step prints nothing at all. Failing |
| 306 | .root_source_file = b.path("src/proxy.zig"), | 267 | /// first is what makes the catch legible. script leads for the same |
| 307 | .target = target, | 268 | /// reason: instant, allocation-only, and both fixtures inherit its |
| 308 | .optimize = optimize, | 269 | /// escape pins. mux and exe are executable roots but carry the argument |
| 309 | .link_libc = true, | 270 | /// parsers — a test that is never built is not a test (decisions.md). |
| 310 | }); | 271 | const test_order = [_][]const u8{ |
| 311 | proxy_mod.addImport("testtmp", testtmp_mod); | 272 | "script", "protocol", "engine", "pty", "delta", |
| 273 | "cmd", "shellint", "replica", "keymap", "webhub", | ||
| 274 | "sockpath", "muxa", "server", "client", "proxy", | ||
| 275 | "mux", "quic", "quic_server", "exe", "testtmp", | ||
| 276 | "quic_client", "predict", "rawmode", "delaypipe", "xdg", | ||
| 277 | "spawn", "handoff", "paint", "render", "ptyclient", | ||
| 278 | "webhub_main", "wsclient", | ||
| 279 | }; | ||
| 280 | |||
| 281 | comptime { | ||
| 282 | @setEvalBranchQuota(20_000); | ||
| 283 | // Every table row appears in the test loop exactly once. A module in | ||
| 284 | // the table but not the loop is the silent-module-loss hazard with a | ||
| 285 | // new spelling; a duplicate runs a suite twice and skews timings. | ||
| 286 | if (test_order.len != mod_table.len) | ||
| 287 | @compileError("test_order must cover every mod_table row exactly once"); | ||
| 288 | for (test_order, 0..) |n, i| { | ||
| 289 | _ = layerOf(n); | ||
| 290 | for (test_order[i + 1 ..]) |n2| { | ||
| 291 | if (std.mem.eql(u8, n, n2)) @compileError("duplicate in test_order: " ++ n); | ||
| 292 | } | ||
| 293 | } | ||
| 294 | } | ||
| 312 | 295 | ||
| 313 | // Painting the replica onto a tty. Takes an fd out and replica/engine | 296 | pub fn build(b: *std.Build) void { |
| 314 | // types in, and knows nothing about transports — which is what lets its | 297 | // Single source for both binaries' --version. Bumped at tag time. |
| 315 | // tests drive every painter through a pipe with no daemon anywhere. | 298 | const version = "0.0.1-5"; |
| 316 | const paint_mod = b.createModule(.{ | 299 | const version_opts = b.addOptions(); |
| 317 | .root_source_file = b.path("src/paint.zig"), | 300 | version_opts.addOption([]const u8, "version", version); |
| 318 | .target = target, | ||
| 319 | .optimize = optimize, | ||
| 320 | }); | ||
| 321 | paint_mod.addImport("engine", engine_mod); | ||
| 322 | paint_mod.addImport("protocol", protocol_mod); | ||
| 323 | 301 | ||
| 324 | const client_mod = b.createModule(.{ | 302 | const target = b.standardTargetOptions(.{}); |
| 325 | .root_source_file = b.path("src/client.zig"), | 303 | const optimize = b.standardOptimizeOption(.{}); |
| 326 | .target = target, | 304 | const quic = quicDeps(b, target); |
| 327 | .optimize = optimize, | ||
| 328 | .link_libc = true, | ||
| 329 | }); | ||
| 330 | client_mod.addImport("engine", engine_mod); | ||
| 331 | client_mod.addImport("protocol", protocol_mod); | ||
| 332 | client_mod.addImport("replica", replica_mod); | ||
| 333 | client_mod.addImport("testtmp", testtmp_mod); | ||
| 334 | client_mod.addImport("quic_client", quic_client_mod); | ||
| 335 | client_mod.addImport("quic", quic_mod); | ||
| 336 | // The client is the only thing that predicts: the overlay is a local | ||
| 337 | // display decision and never becomes state anybody else can see. | ||
| 338 | client_mod.addImport("predict", predict_mod); | ||
| 339 | client_mod.addImport("handoff", handoff_mod); | ||
| 340 | // The client borrows ignoreSigpipe, which proxy owns. proxy is a leaf, | ||
| 341 | // so this adds no cycle and teaches the proxy nothing. | ||
| 342 | client_mod.addImport("proxy", proxy_mod); | ||
| 343 | client_mod.addImport("paint", paint_mod); | ||
| 344 | |||
| 345 | // Late-bound: webhub is defined before client in this file, but the | ||
| 346 | // import graph only needs both to exist by the time the exes compile. | ||
| 347 | webhub_mod.addImport("client", client_mod); | ||
| 348 | |||
| 349 | const mux_mod = b.createModule(.{ | ||
| 350 | .root_source_file = b.path("src/mux_main.zig"), | ||
| 351 | .target = target, | ||
| 352 | .optimize = optimize, | ||
| 353 | .link_libc = true, | ||
| 354 | }); | ||
| 355 | mux_mod.addImport("client", client_mod); | ||
| 356 | mux_mod.addImport("build_options", version_opts.createModule()); | ||
| 357 | mux_mod.addImport("xdg", xdg_mod); | ||
| 358 | mux_mod.addImport("spawn", spawn_mod); | ||
| 359 | mux_mod.addImport("handoff", handoff_mod); | ||
| 360 | // The sun_path bound only; the client binds no socket itself. | ||
| 361 | mux_mod.addImport("sockpath", sockpath_mod); | ||
| 362 | 305 | ||
| 363 | // Test helpers, built as real binaries because that is how the suite | 306 | const ghostty_dep = b.lazyDependency("ghostty", .{ |
| 364 | // uses them: rawmode is a deterministic stand-in for an editor (nvim's | ||
| 365 | // redraw timing is its own business and it is not installed everywhere), | ||
| 366 | // and delaypipe makes a slow round trip out of a shell pipeline instead | ||
| 367 | // of out of netem and root. | ||
| 368 | const rawmode_mod = b.createModule(.{ | ||
| 369 | .root_source_file = b.path("test/rawmode.zig"), | ||
| 370 | .target = target, | ||
| 371 | .optimize = optimize, | ||
| 372 | }); | ||
| 373 | const delaypipe_mod = b.createModule(.{ | ||
| 374 | .root_source_file = b.path("test/delaypipe.zig"), | ||
| 375 | .target = target, | 307 | .target = target, |
| 376 | .optimize = optimize, | 308 | .optimize = optimize, |
| 377 | }); | 309 | }); |
| 378 | 310 | ||
| 379 | // Replays a captured client stdout stream and prints the final grid in | 311 | // The table is the law; this loop can only wire what it declares. |
| 380 | // `muxd dump`'s formats — the client half of the M11 render-vs-dump | 312 | const idx = struct { |
| 381 | // convergence check. Imports the engine module so both sides of the | 313 | fn of(name: []const u8) usize { |
| 382 | // diff go through the same ghostty-vt and the same formatter. | 314 | for (&mod_table, 0..) |m, i| { |
| 383 | const render_mod = b.createModule(.{ | 315 | if (std.mem.eql(u8, m.name, name)) return i; |
| 384 | .root_source_file = b.path("test/render.zig"), | 316 | } |
| 385 | .target = target, | 317 | unreachable; // every lookup below is comptime-checked against the table |
| 386 | .optimize = optimize, | 318 | } |
| 387 | }); | 319 | }; |
| 388 | render_mod.addImport("engine", engine_mod); | 320 | var mods: [mod_table.len]*std.Build.Module = undefined; |
| 321 | for (&mod_table, 0..) |spec, i| { | ||
| 322 | mods[i] = b.createModule(.{ | ||
| 323 | .root_source_file = b.path(spec.path), | ||
| 324 | .target = target, | ||
| 325 | .optimize = optimize, | ||
| 326 | .link_libc = if (spec.link_libc) true else null, | ||
| 327 | }); | ||
| 328 | } | ||
| 329 | for (&mod_table, 0..) |spec, i| { | ||
| 330 | for (spec.imports) |dep| mods[i].addImport(dep, mods[idx.of(dep)]); | ||
| 331 | for (spec.test_imports) |dep| mods[i].addImport(dep, mods[idx.of(dep)]); | ||
| 332 | } | ||
| 389 | 333 | ||
| 390 | // What the two scripted fixtures share: the escape table and the exit | 334 | // -Dgraph: dump the declared edges for the extract-and-diff proof. |
| 391 | // codes. One copy, so ptyclient and wsclient cannot disagree about | 335 | if (b.option(bool, "graph", "print the module import graph and continue") orelse false) { |
| 392 | // what a scenario's heredoc sent. | 336 | for (&mod_table) |spec| { |
| 393 | const script_mod = b.createModule(.{ | 337 | for (spec.imports) |dep| std.debug.print("edge {s} {s}\n", .{ spec.name, dep }); |
| 394 | .root_source_file = b.path("test/script.zig"), | 338 | for (spec.test_imports) |dep| std.debug.print("edge {s} {s}\n", .{ spec.name, dep }); |
| 395 | .target = target, | 339 | } |
| 396 | .optimize = optimize, | 340 | } |
| 397 | }); | ||
| 398 | 341 | ||
| 399 | // The pty-driving e2e fixture: real client on a pty slave, scripted | 342 | // Named handles for the exe/wasm/step wiring below — only the ones |
| 400 | // from stdin (M12). Imports pty so the product's own module is the one | 343 | // that wiring actually uses (an unused local is a compile error). |
| 401 | // under it. | 344 | // Dep edges (ghostty) and build_options stay outside the table's |
| 402 | const ptyclient_mod = b.createModule(.{ | 345 | // jurisdiction, explicit. |
| 403 | .root_source_file = b.path("test/ptyclient.zig"), | 346 | const engine_mod = mods[idx.of("engine")]; |
| 404 | .target = target, | 347 | const mux_mod = mods[idx.of("mux")]; |
| 405 | .optimize = optimize, | 348 | const exe_mod = mods[idx.of("exe")]; |
| 406 | .link_libc = true, | 349 | const muxa_mod = mods[idx.of("muxa")]; |
| 407 | }); | 350 | const rawmode_mod = mods[idx.of("rawmode")]; |
| 408 | ptyclient_mod.addImport("pty", pty_mod); | 351 | const delaypipe_mod = mods[idx.of("delaypipe")]; |
| 409 | ptyclient_mod.addImport("script", script_mod); | 352 | const render_mod = mods[idx.of("render")]; |
| 353 | const ptyclient_mod = mods[idx.of("ptyclient")]; | ||
| 354 | const wsclient_mod = mods[idx.of("wsclient")]; | ||
| 355 | const webhub_main_mod = mods[idx.of("webhub_main")]; | ||
| 410 | 356 | ||
| 411 | const exe_mod = b.createModule(.{ | 357 | if (ghostty_dep) |dep| { |
| 412 | .root_source_file = b.path("src/main.zig"), | 358 | engine_mod.addImport("ghostty-vt", dep.module("ghostty-vt")); |
| 413 | .target = target, | 359 | } |
| 414 | .optimize = optimize, | 360 | mux_mod.addImport("build_options", version_opts.createModule()); |
| 415 | .link_libc = true, | ||
| 416 | }); | ||
| 417 | exe_mod.addImport("server", server_mod); | ||
| 418 | exe_mod.addImport("protocol", protocol_mod); | ||
| 419 | exe_mod.addImport("cmd", cmd_mod); | ||
| 420 | exe_mod.addImport("proxy", proxy_mod); | ||
| 421 | // The daemon entrypoint loads the key and constructs the listener, so it | ||
| 422 | // needs the modules directly rather than through the server. | ||
| 423 | exe_mod.addImport("quic", quic_mod); | ||
| 424 | exe_mod.addImport("quic_server", quic_server_mod); | ||
| 425 | exe_mod.addImport("build_options", version_opts.createModule()); | 361 | exe_mod.addImport("build_options", version_opts.createModule()); |
| 426 | exe_mod.addImport("xdg", xdg_mod); | 362 | webhub_main_mod.addImport("build_options", version_opts.createModule()); |
| 427 | // The keygen round-trip test needs a directory to generate into; the | ||
| 428 | // daemon itself never touches this. | ||
| 429 | exe_mod.addImport("testtmp", testtmp_mod); | ||
| 430 | exe_mod.addImport("spawn", spawn_mod); | ||
| 431 | // `muxd endpoint` prints the announce line this module spells. | ||
| 432 | exe_mod.addImport("handoff", handoff_mod); | ||
| 433 | // The sun_path bound, checked before any verb acts on the path. | ||
| 434 | exe_mod.addImport("sockpath", sockpath_mod); | ||
| 435 | 363 | ||
| 436 | const exe = b.addExecutable(.{ .name = "muxd", .root_module = exe_mod }); | 364 | const exe = b.addExecutable(.{ .name = "muxd", .root_module = exe_mod }); |
| 437 | // Zig 0.15's self-hosted x86_64 linker can't handle the .sframe | 365 | // Zig 0.15's self-hosted x86_64 linker can't handle the .sframe |
| @@ -443,24 +371,6 @@ pub fn build(b: *std.Build) void { | |||
| 443 | linkQuic(b, exe, quic); | 371 | linkQuic(b, exe, quic); |
| 444 | b.installArtifact(exe); | 372 | b.installArtifact(exe); |
| 445 | 373 | ||
| 446 | // The agent-facing client. It speaks frames and owns no terminal, which | ||
| 447 | // is the whole point — it attaches at 0x0 and never claims the grid. | ||
| 448 | // The transport modules are the CLI client's, minus everything that | ||
| 449 | // renders: `quic_client` for the remote arm and `xdg` for the one | ||
| 450 | // key-resolution rule all three binaries obey. Deliberately still no | ||
| 451 | // engine and no replica — muxa has nothing to draw. | ||
| 452 | const muxa_mod = b.createModule(.{ | ||
| 453 | .root_source_file = b.path("src/muxa.zig"), | ||
| 454 | .target = target, | ||
| 455 | .optimize = optimize, | ||
| 456 | .link_libc = true, | ||
| 457 | }); | ||
| 458 | muxa_mod.addImport("protocol", protocol_mod); | ||
| 459 | muxa_mod.addImport("sockpath", sockpath_mod); | ||
| 460 | muxa_mod.addImport("quic_client", quic_client_mod); | ||
| 461 | muxa_mod.addImport("quic", quic_mod); | ||
| 462 | muxa_mod.addImport("xdg", xdg_mod); | ||
| 463 | |||
| 464 | const mux_exe = b.addExecutable(.{ .name = "mux", .root_module = mux_mod }); | 374 | const mux_exe = b.addExecutable(.{ .name = "mux", .root_module = mux_mod }); |
| 465 | mux_exe.use_llvm = true; | 375 | mux_exe.use_llvm = true; |
| 466 | mux_exe.use_lld = true; | 376 | mux_exe.use_lld = true; |
| @@ -511,20 +421,30 @@ pub fn build(b: *std.Build) void { | |||
| 511 | .target = wasm_target, | 421 | .target = wasm_target, |
| 512 | .optimize = .ReleaseSmall, | 422 | .optimize = .ReleaseSmall, |
| 513 | }); | 423 | }); |
| 514 | const engine_wasm_mod = wasmMod(b, wasm_target, "src/engine.zig"); | 424 | // Rows with .wasm get wasm32 twins, imports rewired from the SAME table |
| 425 | // rows — one source of truth for both instantiations. wasm_core itself | ||
| 426 | // stays explicit: it is wasm-only, never in the native test loop. | ||
| 427 | var wasm_mods = [_]?*std.Build.Module{null} ** mod_table.len; | ||
| 428 | for (&mod_table, 0..) |spec, i| { | ||
| 429 | if (spec.wasm) wasm_mods[i] = wasmMod(b, wasm_target, spec.path); | ||
| 430 | } | ||
| 431 | for (&mod_table, 0..) |spec, i| { | ||
| 432 | if (wasm_mods[i]) |wm| { | ||
| 433 | for (spec.imports) |dep| { | ||
| 434 | wm.addImport(dep, wasm_mods[idx.of(dep)] orelse | ||
| 435 | @panic("wasm module imports a module without the wasm flag")); | ||
| 436 | } | ||
| 437 | } | ||
| 438 | } | ||
| 439 | const engine_wasm_mod = wasm_mods[idx.of("engine")].?; | ||
| 515 | if (ghostty_wasm_dep) |dep| { | 440 | if (ghostty_wasm_dep) |dep| { |
| 516 | engine_wasm_mod.addImport("ghostty-vt", dep.module("ghostty-vt")); | 441 | engine_wasm_mod.addImport("ghostty-vt", dep.module("ghostty-vt")); |
| 517 | } | 442 | } |
| 518 | const protocol_wasm_mod = wasmMod(b, wasm_target, "src/protocol.zig"); | ||
| 519 | const replica_wasm_mod = wasmMod(b, wasm_target, "src/replica.zig"); | ||
| 520 | replica_wasm_mod.addImport("engine", engine_wasm_mod); | ||
| 521 | replica_wasm_mod.addImport("protocol", protocol_wasm_mod); | ||
| 522 | const keymap_wasm_mod = wasmMod(b, wasm_target, "src/keymap.zig"); | ||
| 523 | const wasm_core_mod = wasmMod(b, wasm_target, "src/wasm_core.zig"); | 443 | const wasm_core_mod = wasmMod(b, wasm_target, "src/wasm_core.zig"); |
| 524 | wasm_core_mod.addImport("engine", engine_wasm_mod); | 444 | wasm_core_mod.addImport("engine", engine_wasm_mod); |
| 525 | wasm_core_mod.addImport("protocol", protocol_wasm_mod); | 445 | wasm_core_mod.addImport("protocol", wasm_mods[idx.of("protocol")].?); |
| 526 | wasm_core_mod.addImport("replica", replica_wasm_mod); | 446 | wasm_core_mod.addImport("replica", wasm_mods[idx.of("replica")].?); |
| 527 | wasm_core_mod.addImport("keymap", keymap_wasm_mod); | 447 | wasm_core_mod.addImport("keymap", wasm_mods[idx.of("keymap")].?); |
| 528 | const wasm_exe = b.addExecutable(.{ .name = "mux_core", .root_module = wasm_core_mod }); | 448 | const wasm_exe = b.addExecutable(.{ .name = "mux_core", .root_module = wasm_core_mod }); |
| 529 | // A wasm reactor, not a command: no _start, and the exports must | 449 | // A wasm reactor, not a command: no _start, and the exports must |
| 530 | // survive the linker's dead-strip. Deliberately NOT use_llvm/use_lld — | 450 | // survive the linker's dead-strip. Deliberately NOT use_llvm/use_lld — |
| @@ -535,38 +455,14 @@ pub fn build(b: *std.Build) void { | |||
| 535 | b.installArtifact(wasm_exe); | 455 | b.installArtifact(wasm_exe); |
| 536 | 456 | ||
| 537 | // ---- muxweb, the hub binary (M-web Task 7) ---- | 457 | // ---- muxweb, the hub binary (M-web Task 7) ---- |
| 538 | // The page's three assets arrive as anonymous imports so @embedFile | ||
| 539 | // can name them; the wasm one is the artifact itself, which also | ||
| 540 | // sequences the wasm build before the hub's. | ||
| 541 | const wsclient_mod = b.createModule(.{ | ||
| 542 | .root_source_file = b.path("test/wsclient.zig"), | ||
| 543 | .target = target, | ||
| 544 | .optimize = optimize, | ||
| 545 | .link_libc = true, | ||
| 546 | }); | ||
| 547 | wsclient_mod.addImport("engine", engine_mod); | ||
| 548 | wsclient_mod.addImport("replica", replica_mod); | ||
| 549 | wsclient_mod.addImport("protocol", protocol_mod); | ||
| 550 | wsclient_mod.addImport("script", script_mod); | ||
| 551 | const wsclient_exe = b.addExecutable(.{ .name = "wsclient", .root_module = wsclient_mod }); | 458 | const wsclient_exe = b.addExecutable(.{ .name = "wsclient", .root_module = wsclient_mod }); |
| 552 | wsclient_exe.use_llvm = true; | 459 | wsclient_exe.use_llvm = true; |
| 553 | wsclient_exe.use_lld = true; | 460 | wsclient_exe.use_lld = true; |
| 554 | b.installArtifact(wsclient_exe); | 461 | b.installArtifact(wsclient_exe); |
| 555 | 462 | ||
| 556 | const webhub_main_mod = b.createModule(.{ | 463 | // The page's three assets arrive as anonymous imports so @embedFile |
| 557 | .root_source_file = b.path("src/webhub_main.zig"), | 464 | // can name them; the wasm one is the artifact itself, which also |
| 558 | .target = target, | 465 | // sequences the wasm build before the hub's. |
| 559 | .optimize = optimize, | ||
| 560 | .link_libc = true, | ||
| 561 | }); | ||
| 562 | webhub_main_mod.addImport("client", client_mod); | ||
| 563 | webhub_main_mod.addImport("webhub", webhub_mod); | ||
| 564 | webhub_main_mod.addImport("xdg", xdg_mod); | ||
| 565 | // The HOST tile's recipe comes from the same owner mux_main uses. | ||
| 566 | webhub_main_mod.addImport("handoff", handoff_mod); | ||
| 567 | // ...and the sun_path bound its --sock tiles are refused against. | ||
| 568 | webhub_main_mod.addImport("sockpath", sockpath_mod); | ||
| 569 | webhub_main_mod.addImport("build_options", version_opts.createModule()); | ||
| 570 | webhub_main_mod.addAnonymousImport("index.html", .{ .root_source_file = b.path("web/index.html") }); | 466 | webhub_main_mod.addAnonymousImport("index.html", .{ .root_source_file = b.path("web/index.html") }); |
| 571 | webhub_main_mod.addAnonymousImport("mux.js", .{ .root_source_file = b.path("web/mux.js") }); | 467 | webhub_main_mod.addAnonymousImport("mux.js", .{ .root_source_file = b.path("web/mux.js") }); |
| 572 | webhub_main_mod.addAnonymousImport("mux_core.wasm", .{ .root_source_file = wasm_exe.getEmittedBin() }); | 468 | webhub_main_mod.addAnonymousImport("mux_core.wasm", .{ .root_source_file = wasm_exe.getEmittedBin() }); |
| @@ -577,35 +473,15 @@ pub fn build(b: *std.Build) void { | |||
| 577 | b.installArtifact(webhub_exe); | 473 | b.installArtifact(webhub_exe); |
| 578 | 474 | ||
| 579 | const test_step = b.step("test", "Run unit tests"); | 475 | const test_step = b.step("test", "Run unit tests"); |
| 580 | // delta_mod, cmd_mod, shellint_mod and sockpath_mod sit BEFORE server_mod, | 476 | for (test_order) |name| { |
| 581 | // deliberately: their tests are seconds-long and socket-free, while a | 477 | const i = idx.of(name); |
| 582 | // regression in any of them can wedge a server test that waits on a | 478 | const t = b.addTest(.{ .root_module = mods[i] }); |
| 583 | // client forever — and a wedged step prints nothing at all. Failing | ||
| 584 | // first is what makes the catch legible. | ||
| 585 | // | ||
| 586 | // mux_mod and exe_mod are executable roots, but they carry the argument | ||
| 587 | // parsers, and a test that is never built is not a test. exe_mod's | ||
| 588 | // absence here was a live hazard recorded in decisions.md — muxd's | ||
| 589 | // entrypoint could grow tests that silently never ran, exactly as | ||
| 590 | // mux_main.zig's five did before it was added. | ||
| 591 | // | ||
| 592 | // script_mod leads for the same order-is-legibility reason: its tests | ||
| 593 | // are instant and allocation-only, and the escape pins they carry are | ||
| 594 | // the ones both fixtures inherit. | ||
| 595 | for ([_]*std.Build.Module{ script_mod, protocol_mod, engine_mod, pty_mod, delta_mod, cmd_mod, shellint_mod, replica_mod, keymap_mod, webhub_mod, sockpath_mod, muxa_mod, server_mod, client_mod, proxy_mod, mux_mod, quic_mod, quic_server_mod, exe_mod, testtmp_mod, quic_client_mod, predict_mod, rawmode_mod, delaypipe_mod, xdg_mod, spawn_mod, handoff_mod, paint_mod, render_mod, ptyclient_mod, webhub_main_mod, wsclient_mod }) |mod| { | ||
| 596 | const t = b.addTest(.{ .root_module = mod }); | ||
| 597 | t.use_llvm = true; | 479 | t.use_llvm = true; |
| 598 | t.use_lld = true; | 480 | t.use_lld = true; |
| 599 | // The server's tests are where the QUIC listener's in-process | 481 | // quic_tests is also what makes `make test` build the QUIC deps on |
| 600 | // loopback test will live (2c), so this test binary needs the stack | 482 | // a clean checkout — the dependency must reach the test binaries, |
| 601 | // — and wiring it here is also what makes `make test` build the | 483 | // not only muxd (decisions.md, M8). |
| 602 | // deps when they are absent. Without it the dependency reached only | 484 | if (mod_table[i].quic_tests) linkQuic(b, t, quic); |
| 603 | // `muxd`, and a clean checkout running `make test` first would have | ||
| 604 | // found no libraries and no explanation. | ||
| 605 | if (mod == server_mod or mod == quic_mod or mod == quic_server_mod or | ||
| 606 | mod == exe_mod or mod == client_mod or mod == mux_mod or | ||
| 607 | mod == quic_client_mod or mod == webhub_mod or | ||
| 608 | mod == muxa_mod or mod == webhub_main_mod) linkQuic(b, t, quic); | ||
| 609 | test_step.dependOn(&b.addRunArtifact(t).step); | 485 | test_step.dependOn(&b.addRunArtifact(t).step); |
| 610 | } | 486 | } |
| 611 | 487 | ||