e3f2d1ec
refactor: the one binary's mains are child files of the dispatcher
a73x 2026-08-30 12:35
Commit message
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -192,7 +192,7 @@ const mod_table = [_]ModSpec{ | |||
| 192 | // renders: `quic` for the remote arm and `xdg` for the one | 192 | // renders: `quic` for the remote arm and `xdg` for the one |
| 193 | // key-resolution rule all three binaries obey. Deliberately still no | 193 | // key-resolution rule all three binaries obey. Deliberately still no |
| 194 | // engine and no replica — muxa has nothing to draw. | 194 | // engine and no replica — muxa has nothing to draw. |
| 195 | .{ .name = "agent_main", .path = "src/cli/muxa.zig", .layer = 2, .link_libc = true, .imports = &.{ "protocol", "sockpath", "quic", "xdg", "cliflags" }, .quic_tests = true }, | 195 | .{ .name = "agent", .path = "src/cli/muxa.zig", .layer = 2, .link_libc = true, .imports = &.{ "protocol", "sockpath", "quic", "xdg", "cliflags" }, .quic_tests = true }, |
| 196 | .{ .name = "wsclient", .path = "test/wsclient.zig", .layer = 2, .link_libc = true, .imports = &.{ "engine", "replica", "protocol", "script" } }, | 196 | .{ .name = "wsclient", .path = "test/wsclient.zig", .layer = 2, .link_libc = true, .imports = &.{ "engine", "replica", "protocol", "script" } }, |
| 197 | // ---- layer 3 ---- | 197 | // ---- layer 3 ---- |
| 198 | // Dialling, and what a chord means. The client is the only thing that | 198 | // Dialling, and what a chord means. The client is the only thing that |
| @@ -207,23 +207,16 @@ const mod_table = [_]ModSpec{ | |||
| 207 | // those files is a file-in-multiple-modules compile error. `resolveHost` | 207 | // those files is a file-in-multiple-modules compile error. `resolveHost` |
| 208 | // sits here rather than in either front so the CLI wall and the browser | 208 | // sits here rather than in either front so the CLI wall and the browser |
| 209 | // hub resolve a host line the same way. Nothing here WRITES that file — | 209 | // hub resolve a host line the same way. Nothing here WRITES that file — |
| 210 | // `wall_host.recordHost` and `hub_main` do. | 210 | // `wall_host.recordHost` and `webhub_main` do. |
| 211 | .{ .name = "client", .path = "src/client/client.zig", .layer = 3, .link_libc = true, .imports = &.{ "protocol", "replica", "quic", "xdg", "sockpath" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | 211 | .{ .name = "client", .path = "src/client/client.zig", .layer = 3, .link_libc = true, .imports = &.{ "protocol", "replica", "quic", "xdg", "sockpath" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, |
| 212 | // ---- layer 4 ---- | 212 | // ---- layer 4 ---- |
| 213 | // The daemon entrypoint loads the key and constructs the listener, so it | 213 | // The muxweb hub's HTTP/WebSocket decisions: Origin gate, route table, |
| 214 | // needs `quic` directly; the listener type and the upgrade vocabulary it | 214 | // WS endpoint naming. Assets are injected (the exe root @embedFiles |
| 215 | // reaches through the daemon root's re-exports, being that root's own | 215 | // them), so its tests build no artifacts. It stays a row rather than |
| 216 | // child files now. `mux d endpoint` prints the announce line, whose | 216 | // becoming a child file of the dispatcher that @embedFiles for it: a |
| 217 | // vocabulary is `client.handoff` — the daemon writes the line and the | 217 | // module's root directory is the dirname of its root file, and |
| 218 | // client reads it, so one owner or the two spell it differently; naming | 218 | // `@import("../client/webhub.zig")` from src/cli is "import of file |
| 219 | // `client` for it is what puts this entrypoint above the link rather | 219 | // outside module path" — a compiler rule, not a table choice. |
| 220 | // than beside it. sockpath is the sun_path bound, checked before any | ||
| 221 | // verb acts on the path; and the keygen round-trip test needs a | ||
| 222 | // directory to generate into, which the daemon itself never touches. | ||
| 223 | .{ .name = "daemon_main", .path = "src/cli/main.zig", .layer = 4, .link_libc = true, .imports = &.{ "daemon", "protocol", "proxy", "quic", "xdg", "spawn", "client", "sockpath", "cliflags" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | ||
| 224 | // The muxweb hub's HTTP/WebSocket decisions: Origin gate, route | ||
| 225 | // table, WS endpoint naming. Assets are injected (the exe root | ||
| 226 | // @embedFiles them), so its tests build no artifacts. | ||
| 227 | .{ .name = "webhub", .path = "src/client/webhub.zig", .layer = 4, .imports = &.{ "protocol", "client" }, .quic_tests = true }, | 220 | .{ .name = "webhub", .path = "src/client/webhub.zig", .layer = 4, .imports = &.{ "protocol", "client" }, .quic_tests = true }, |
| 228 | // The CLI wall (`mux wall`): multiattach stripes in one terminal, one of | 221 | // The CLI wall (`mux wall`): multiattach stripes in one terminal, one of |
| 229 | // which can be ZOOMED — promoted to the terminal's size and typed | 222 | // which can be ZOOMED — promoted to the terminal's size and typed |
| @@ -238,23 +231,21 @@ const mod_table = [_]ModSpec{ | |||
| 238 | // spells; the decoder and the key table they also want reach them | 231 | // spells; the decoder and the key table they also want reach them |
| 239 | // through `client`'s seams. | 232 | // through `client`'s seams. |
| 240 | .{ .name = "wall", .path = "src/tui/wallview.zig", .layer = 4, .link_libc = true, .imports = &.{ "protocol", "client", "proxy", "engine", "replica", "spawn" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | 233 | .{ .name = "wall", .path = "src/tui/wallview.zig", .layer = 4, .link_libc = true, .imports = &.{ "protocol", "client", "proxy", "engine", "replica", "spawn" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, |
| 241 | // ---- layer 5 ---- | ||
| 242 | // hosts owns the host grammar and the state file, so argv is parsed by | ||
| 243 | // the SAME rules the CLI's own `mux hosts add` is — one grammar, not | ||
| 244 | // two. Resolution is `client.resolveHost`, shared with the CLI wall; | ||
| 245 | // sockpath stays for the one startup message that names the sun_path | ||
| 246 | // bound. | ||
| 247 | .{ .name = "hub_main", .path = "src/cli/webhub_main.zig", .layer = 5, .link_libc = true, .imports = &.{ "client", "webhub", "xdg", "sockpath", "cliflags" }, .quic_tests = true }, | ||
| 248 | // sockpath is the sun_path bound only; the client binds no socket itself. | ||
| 249 | // protocol is the session-name validator alone (validSessionName): a bad | ||
| 250 | // --session has to be a usage error here, at parse, not bytes some | ||
| 251 | // daemon downstream has to notice and refuse. Layer 5 since the no-arg | ||
| 252 | // wall pulled in wallview (layer 4). | ||
| 253 | .{ .name = "client_main", .path = "src/cli/mux_main.zig", .layer = 5, .link_libc = true, .imports = &.{ "client", "protocol", "xdg", "spawn", "sockpath", "wall", "cliflags" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | ||
| 254 | // ---- layer 6: the one binary ---- | 234 | // ---- layer 6: the one binary ---- |
| 255 | // Four words, one image. Nothing but the mode letter lives here, which | 235 | // Four words, one image — and one row: the daemon's entrypoint, the |
| 256 | // is why it may sit above every other main without owning any of them. | 236 | // client's and the hub's are CHILD FILES of the dispatcher, so a second |
| 257 | .{ .name = "mux", .path = "src/cli/mux.zig", .layer = 6, .link_libc = true, .imports = &.{ "daemon_main", "agent_main", "hub_main", "client_main", "client" } }, | 237 | // module claiming any of them is a file-in-multiple-modules compile |
| 238 | // error. `agent` stays a row because muxa's suite must be able to fail on | ||
| 239 | // its own. The grants are the four mains' union: `webhub` for the hub | ||
| 240 | // surface the entrypoint serves, `daemon` for the Server it constructs | ||
| 241 | // and the `quic_server`/`upgrade` seams it reaches through it, `quic` for the key | ||
| 242 | // it loads before that listener exists, `client` for the announce | ||
| 243 | // vocabulary the daemon writes and the client reads — one owner, or the | ||
| 244 | // two spell it differently — plus the wall the no-arg client opens, | ||
| 245 | // `protocol` for validSessionName (a bad --session is a usage error at | ||
| 246 | // parse, not bytes a daemon downstream must refuse) and `sockpath` for | ||
| 247 | // the sun_path bound every verb checks before acting on a path. | ||
| 248 | .{ .name = "mux", .path = "src/cli/mux.zig", .layer = 6, .link_libc = true, .imports = &.{ "daemon", "client", "wall", "agent", "webhub", "protocol", "proxy", "quic", "xdg", "spawn", "sockpath", "cliflags" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | ||
| 258 | }; | 249 | }; |
| 259 | 250 | ||
| 260 | /// Comptime row lookup. Every hand-written module name in this file goes | 251 | /// Comptime row lookup. Every hand-written module name in this file goes |
| @@ -792,16 +783,15 @@ fn docGate(b: *std.Build, target: std.Build.ResolvedTarget, check_step: *std.Bui | |||
| 792 | /// can wedge a daemon test that waits on a client forever — and a wedged | 783 | /// can wedge a daemon test that waits on a client forever — and a wedged |
| 793 | /// step prints nothing at all. Failing first is what makes the catch | 784 | /// step prints nothing at all. Failing first is what makes the catch |
| 794 | /// legible. script leads for the same reason: instant, allocation-only, | 785 | /// legible. script leads for the same reason: instant, allocation-only, |
| 795 | /// and both fixtures inherit its escape pins. The mains are roots of | 786 | /// and both fixtures inherit its escape pins. `mux` runs last and carries |
| 796 | /// nothing but the one binary, and they carry the argument parsers — a | 787 | /// every argument parser but muxa's, its mains being child files — a test |
| 797 | /// test that is never built is not a test (decisions.md). | 788 | /// that is never built is not a test (decisions.md). |
| 798 | const test_order = [_][]const u8{ | 789 | const test_order = [_][]const u8{ |
| 799 | "script", "protocol", "engine", "pty", "delta", | 790 | "script", "protocol", "engine", "pty", "delta", |
| 800 | "replica", "webhub", "wall", "sockpath", "agent_main", | 791 | "replica", "webhub", "wall", "sockpath", "agent", |
| 801 | "daemon", "client", "proxy", "client_main", "quic", | 792 | "daemon", "client", "proxy", "quic", "testtmp", |
| 802 | "daemon_main", "testtmp", "rawmode", "delaypipe", "xdg", | 793 | "rawmode", "delaypipe", "xdg", "spawn", "render", |
| 803 | "spawn", "render", "ptyclient", "hub_main", "wsclient", | 794 | "ptyclient", "wsclient", "cliflags", "mux", |
| 804 | "cliflags", "mux", | ||
| 805 | }; | 795 | }; |
| 806 | 796 | ||
| 807 | comptime { | 797 | comptime { |
| @@ -899,34 +889,28 @@ pub fn build(b: *std.Build) void { | |||
| 899 | // jurisdiction, explicit. | 889 | // jurisdiction, explicit. |
| 900 | const engine_mod = mods[comptime idxOf("engine")]; | 890 | const engine_mod = mods[comptime idxOf("engine")]; |
| 901 | const mux_mod = mods[comptime idxOf("mux")]; | 891 | const mux_mod = mods[comptime idxOf("mux")]; |
| 902 | const client_main_mod = mods[comptime idxOf("client_main")]; | 892 | const agent_mod = mods[comptime idxOf("agent")]; |
| 903 | const daemon_main_mod = mods[comptime idxOf("daemon_main")]; | ||
| 904 | const agent_main_mod = mods[comptime idxOf("agent_main")]; | ||
| 905 | const rawmode_mod = mods[comptime idxOf("rawmode")]; | 893 | const rawmode_mod = mods[comptime idxOf("rawmode")]; |
| 906 | const delaypipe_mod = mods[comptime idxOf("delaypipe")]; | 894 | const delaypipe_mod = mods[comptime idxOf("delaypipe")]; |
| 907 | const render_mod = mods[comptime idxOf("render")]; | 895 | const render_mod = mods[comptime idxOf("render")]; |
| 908 | const ptyclient_mod = mods[comptime idxOf("ptyclient")]; | 896 | const ptyclient_mod = mods[comptime idxOf("ptyclient")]; |
| 909 | const wsclient_mod = mods[comptime idxOf("wsclient")]; | 897 | const wsclient_mod = mods[comptime idxOf("wsclient")]; |
| 910 | const hub_main_mod = mods[comptime idxOf("hub_main")]; | ||
| 911 | 898 | ||
| 912 | if (ghostty_dep) |dep| { | 899 | if (ghostty_dep) |dep| { |
| 913 | engine_mod.addImport("ghostty-vt", dep.module("ghostty-vt")); | 900 | engine_mod.addImport("ghostty-vt", dep.module("ghostty-vt")); |
| 914 | } | 901 | } |
| 915 | // ONE module object, shared: the four mains link into one binary now, and | 902 | // ONE module object, shared: the two rows link into one binary, and a |
| 916 | // a second instance of the same root file in one compilation is "file | 903 | // second instance of the same root file in one compilation is "file |
| 917 | // exists in modules 'build_options' and 'build_options0'". | 904 | // exists in modules 'build_options' and 'build_options0'". |
| 918 | const build_opts_mod = version_opts.createModule(); | 905 | const build_opts_mod = version_opts.createModule(); |
| 919 | client_main_mod.addImport("build_options", build_opts_mod); | 906 | mux_mod.addImport("build_options", build_opts_mod); |
| 920 | daemon_main_mod.addImport("build_options", build_opts_mod); | 907 | agent_mod.addImport("build_options", build_opts_mod); |
| 921 | hub_main_mod.addImport("build_options", build_opts_mod); | ||
| 922 | agent_main_mod.addImport("build_options", build_opts_mod); | ||
| 923 | // A row with test_imports gets a SEPARATE module for its test twin, and | 908 | // A row with test_imports gets a SEPARATE module for its test twin, and |
| 924 | // build_options is outside the table's jurisdiction — so every such twin | 909 | // build_options is outside the table's jurisdiction — so every such twin |
| 925 | // needs it by hand, or the argument parsers lose the version they print. | 910 | // needs it by hand, or the argument parsers lose the version they print. |
| 926 | test_mods[comptime idxOf("daemon_main")].addImport("build_options", build_opts_mod); | 911 | test_mods[comptime idxOf("mux")].addImport("build_options", build_opts_mod); |
| 927 | test_mods[comptime idxOf("client_main")].addImport("build_options", build_opts_mod); | ||
| 928 | 912 | ||
| 929 | // ONE product binary. The four mains are modules under it, reached by | 913 | // ONE product binary. The four mains are files under it, reached by |
| 930 | // the mode word rather than by four names on PATH — which is also what | 914 | // the mode word rather than by four names on PATH — which is also what |
| 931 | // lets the client's auto-start exec this same image with argv | 915 | // lets the client's auto-start exec this same image with argv |
| 932 | // `mux d run …` (spawn.zig) instead of hunting PATH for a sibling. The | 916 | // `mux d run …` (spawn.zig) instead of hunting PATH for a sibling. The |
| @@ -1029,9 +1013,14 @@ pub fn build(b: *std.Build) void { | |||
| 1029 | // The page's three assets arrive as anonymous imports so @embedFile | 1013 | // The page's three assets arrive as anonymous imports so @embedFile |
| 1030 | // can name them; the wasm one is the artifact itself, which also | 1014 | // can name them; the wasm one is the artifact itself, which also |
| 1031 | // sequences the wasm build before the hub's. | 1015 | // sequences the wasm build before the hub's. |
| 1032 | hub_main_mod.addAnonymousImport("index.html", .{ .root_source_file = b.path("web/index.html") }); | 1016 | // webhub_main.zig is a child file of the dispatcher, so the assets attach |
| 1033 | hub_main_mod.addAnonymousImport("mux.js", .{ .root_source_file = b.path("web/mux.js") }); | 1017 | // to that module and to its test twin both — an @embedFile the twin |
| 1034 | hub_main_mod.addAnonymousImport("mux_core.wasm", .{ .root_source_file = wasm_exe.getEmittedBin() }); | 1018 | // cannot name is a test binary that will not compile. |
| 1019 | for ([_]*std.Build.Module{ mux_mod, test_mods[comptime idxOf("mux")] }) |m| { | ||
| 1020 | m.addAnonymousImport("index.html", .{ .root_source_file = b.path("web/index.html") }); | ||
| 1021 | m.addAnonymousImport("mux.js", .{ .root_source_file = b.path("web/mux.js") }); | ||
| 1022 | m.addAnonymousImport("mux_core.wasm", .{ .root_source_file = wasm_exe.getEmittedBin() }); | ||
| 1023 | } | ||
| 1035 | 1024 | ||
| 1036 | const test_step = b.step("test", "Run unit tests"); | 1025 | const test_step = b.step("test", "Run unit tests"); |
| 1037 | test_step.dependOn(&client_core_wasm_guard.step); | 1026 | test_step.dependOn(&client_core_wasm_guard.step); |
src/cli/mux.zig
| Old | New | ||
|---|---|---|---|
| @@ -10,10 +10,10 @@ | |||
| 10 | //! exec'd with the prompt as argv[1] and nothing else. Its variable is the | 10 | //! exec'd with the prompt as argv[1] and nothing else. Its variable is the |
| 11 | //! word instead, and it is set in exactly one process tree. | 11 | //! word instead, and it is set in exactly one process tree. |
| 12 | const std = @import("std"); | 12 | const std = @import("std"); |
| 13 | const daemon = @import("daemon_main"); | 13 | const daemon = @import("main.zig"); |
| 14 | const agent = @import("agent_main"); | 14 | const agent = @import("agent"); |
| 15 | const hub = @import("hub_main"); | 15 | const hub = @import("webhub_main.zig"); |
| 16 | const client = @import("client_main"); | 16 | const client = @import("mux_main.zig"); |
| 17 | const askpass = @import("client").askpass; | 17 | const askpass = @import("client").askpass; |
| 18 | 18 | ||
| 19 | /// The whole grammar, as a value, so the one decision this file makes can be | 19 | /// The whole grammar, as a value, so the one decision this file makes can be |
| @@ -104,4 +104,11 @@ test "modeOf: `run` is a host, and no daemon verb has a top-level alias" { | |||
| 104 | 104 | ||
| 105 | test { | 105 | test { |
| 106 | std.testing.refAllDeclsRecursive(@This()); | 106 | std.testing.refAllDeclsRecursive(@This()); |
| 107 | // Three of the four mains are child FILES of this root, so their suites | ||
| 108 | // reach the runner through these lines and nothing else; drop one and | ||
| 109 | // that main's argument parser goes untested against a green tree. | ||
| 110 | // `agent` and `webhub` are rows of their own and run as ones. | ||
| 111 | _ = @import("main.zig"); | ||
| 112 | _ = @import("mux_main.zig"); | ||
| 113 | _ = @import("webhub_main.zig"); | ||
| 107 | } | 114 | } |
src/client/webhub.zig
| Old | New | ||
|---|---|---|---|
| @@ -125,7 +125,7 @@ pub const Hub = struct { | |||
| 125 | /// by `deinit` alone, and both run on the thread that built the Hub. | 125 | /// by `deinit` alone, and both run on the thread that built the Hub. |
| 126 | polling: bool = false, | 126 | polling: bool = false, |
| 127 | 127 | ||
| 128 | /// `specs` is borrowed: hub_main resolves the hosts file into an arena | 128 | /// `specs` is borrowed: webhub_main resolves the hosts file into an arena |
| 129 | /// that outlives the process's accept loop. | 129 | /// that outlives the process's accept loop. |
| 130 | pub fn init(alloc: std.mem.Allocator, specs: []const client.HostSpec) !Hub { | 130 | pub fn init(alloc: std.mem.Allocator, specs: []const client.HostSpec) !Hub { |
| 131 | const rows = try alloc.alloc(HubHost, specs.len); | 131 | const rows = try alloc.alloc(HubHost, specs.len); |
| @@ -1266,7 +1266,7 @@ test "hub: two pumps on one tile — the first fd stays tracked, the second's re | |||
| 1266 | 1266 | ||
| 1267 | test "hub: a HOST tile is never the ask" { | 1267 | test "hub: a HOST tile is never the ask" { |
| 1268 | const alloc = std.testing.allocator; | 1268 | const alloc = std.testing.allocator; |
| 1269 | // Resolved the way hub_main resolves the file, because that is the one | 1269 | // Resolved the way webhub_main resolves the file, because that is the one |
| 1270 | // road onto this wall and the permission is set on it. | 1270 | // road onto this wall and the permission is set on it. |
| 1271 | var arena = std.heap.ArenaAllocator.init(alloc); | 1271 | var arena = std.heap.ArenaAllocator.init(alloc); |
| 1272 | defer arena.deinit(); | 1272 | defer arena.deinit(); |