dd868aeb
docs: record the platform layer and the macOS measurements
a73x 2026-09-03 15:10
Commit message
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -8128,3 +8128,128 @@ status` says the stripe is 11 rows so the arithmetic is the layout's, not | |||
| 8128 | the comment's. Graded by mutation: with the row translation capped at one | 8128 | the comment's. Graded by mutation: with the row translation capped at one |
| 8129 | row every older leg passes and only the stacked leg fails. The suite's | 8129 | row every older leg passes and only the stacked leg fails. The suite's |
| 8130 | scenario pin goes from 111 to 112. | 8130 | scenario pin goes from 111 to 112. |
| 8131 | |||
| 8132 | ## 2026-09-03 — the platform layer (macOS port, step 2) | ||
| 8133 | |||
| 8134 | Every OS-specific spelling under `src/` now lives in `src/os/`, one row per | ||
| 8135 | side. `server_os` holds what only the daemon does — the pty fork, the | ||
| 8136 | detached daemon fork, peer credentials, an anonymous fd for the upgrade | ||
| 8137 | manifest, `exitNow`, `closeFrom`, the pty's mode and foreground pgid, the | ||
| 8138 | window-size ioctl and the stale-image verdict — and `client_os` holds the | ||
| 8139 | few the wall and askpass do: pid, euid, peer credentials, the parent walk, | ||
| 8140 | window size and a pty pair. `spawn` moved in beside them. Each root is the | ||
| 8141 | CONTRACT — a doc comment per operation says what it guarantees and which | ||
| 8142 | failure it prevents — and `impl` switches on `builtin.os.tag`, so a build | ||
| 8143 | for an OS with no arm is a `@compileError` at that switch rather than a | ||
| 8144 | link error or a runtime surprise. The client row is deliberately SEPARATE | ||
| 8145 | from the server's rather than one `os` row: the client never links a fork | ||
| 8146 | or a pty, and an app that links the engine and a client must not either. | ||
| 8147 | |||
| 8148 | **The gate is folder rule 7.** No production line under `src/`, | ||
| 8149 | `src/engine/`, `src/client/`, `src/tui/`, `src/server/` or `src/cli/` may | ||
| 8150 | spell `std.os.linux`, `/proc`, `memfd`, `close_range`, `exit_group`, | ||
| 8151 | `so.peercred`, `so_peercred`, `iocsptlck` or `iocgptn`. `src/os/` is absent | ||
| 8152 | from that folder list on purpose: its children may spell anything, and its | ||
| 8153 | roots have no reason to. Comments count, as they do for rule 4, because a | ||
| 8154 | comment naming a Linux mechanism is one that goes stale the day a second | ||
| 8155 | arm exists. Four needles are spelled to catch a name in both the form Zig | ||
| 8156 | writes it and the form C and our own prose do — `so.peercred` and | ||
| 8157 | `so_peercred` for `std.posix.SO.PEERCRED` and `SO_PEERCRED`, where a bare | ||
| 8158 | `peercred` would have banned `client_os.peerCred`, the very operation | ||
| 8159 | callers are supposed to reach for; `iocsptlck` and `iocgptn` drop the | ||
| 8160 | leading T so they catch `std.posix.T.IOCGPTN` as well as `TIOCGPTN`. | ||
| 8161 | |||
| 8162 | **Measured before the design, on a Linux host.** zig 0.15.2 cross-compiles | ||
| 8163 | a libc program using `posix_openpt`, `kqueue`, `libproc` and `dyld` to | ||
| 8164 | aarch64-macos with no SDK, and links Mach-O with its own linker — LLD | ||
| 8165 | refuses ("using LLD to link macho files is unsupported") — so `linkerFor` | ||
| 8166 | asks for `use_lld` on every target except Darwin. ngtcp2 1.25.0 and | ||
| 8167 | wolfSSL 5.9.2 cross-build with `WOLFSSL_SYS_CA_CERTS=no` (mux is PSK-only | ||
| 8168 | and the system-CA path wants Security.framework) and a | ||
| 8169 | `CMAKE_FIND_ROOT_PATH` fence at the target prefix, without which ngtcp2 | ||
| 8170 | finds the host's `libwolfssl.so`. `std.posix.socket` and `std.posix.accept` | ||
| 8171 | already emulate `SOCK_CLOEXEC`, `SOCK_NONBLOCK` and `accept4` on Darwin | ||
| 8172 | with a trailing `fcntl`, so no socket site needed a row at all. ghostty-vt | ||
| 8173 | links three C++ libraries whose build.zig files each call ghostty's | ||
| 8174 | `apple_sdk.addPaths`, which resolves the HOST libc on a Linux host and | ||
| 8175 | fails the C++ compile on glibc headers — upstream code build.zig cannot | ||
| 8176 | reach, the cross-compile blocker that leaves the build-host decision open, | ||
| 8177 | and the reason this step stops at the platform layer and writes no | ||
| 8178 | `_macos.zig` arm. | ||
| 8179 | |||
| 8180 | **One amendment to the spec, made while writing the row.** The spec's | ||
| 8181 | `openPty` plus `becomeSession` are one `forkPty` instead: `forkpty(3)` | ||
| 8182 | exists on both OSes, returning pid 0 in the child exactly as `fork` does, | ||
| 8183 | so the child code stays one branch. Zig ships no `<util.h>` for Darwin, | ||
| 8184 | which is a missing HEADER and not a missing symbol — that arm declares it | ||
| 8185 | with one `extern "c" fn forkpty` line rather than reimplementing the pty | ||
| 8186 | open as `posix_openpt` + `grantpt` + `unlockpt` + `ptsname`. | ||
| 8187 | |||
| 8188 | **The QUIC prefix and the linker follow the TARGET, never the host.** | ||
| 8189 | `quicDeps` names one word per prefix — `native` for the host's own libc, | ||
| 8190 | `musl` for the static release, and `<arch>-<os>` for any other cross target | ||
| 8191 | — shared with `build-deps.sh`, `make deps`, `make clean-deps` and wan.sh's | ||
| 8192 | musl cross-build, so a third OS is one more `case` arm in the script and | ||
| 8193 | nothing in build.zig. A cross build that reused the host's prefix would | ||
| 8194 | link x86_64 Linux archives into an aarch64 macOS binary. | ||
| 8195 | |||
| 8196 | **Two behaviours changed shape on Linux without changing outcome.** The | ||
| 8197 | stale-image verdict is an inode compare against what the image's path held | ||
| 8198 | at boot, not the kernel's ` (deleted)` suffix: `noteBootImage` stamps the | ||
| 8199 | path and its inode once, and every later ask re-stats that path, so a | ||
| 8200 | rename-over reads stale and a file that lands there afterwards can never | ||
| 8201 | promote a stale daemon back to current. The two failures are kept apart. A | ||
| 8202 | path this OS will not name at all records NOTHING and answers false — | ||
| 8203 | unknown is not stale, and a wall must not dress a healthy box in a warning | ||
| 8204 | over a refused readlink. A path that IS named but holds nothing records a | ||
| 8205 | BORN-STALE state, the path with no ident, and answers stale for the life of | ||
| 8206 | the process, because that daemon is already executing an image no path | ||
| 8207 | holds. Second, `xdg.reapDeadPid` asks `kill(pid, 0)` rather than | ||
| 8208 | `access(/proc/PID)`: EPERM is alive-but-not-ours and keeps the entry, and | ||
| 8209 | ESRCH is the one answer that means the pid is gone. | ||
| 8210 | |||
| 8211 | **`sockpath.runtimeDir` owns the default socket directory.** One switch — | ||
| 8212 | `$XDG_RUNTIME_DIR` on Linux, and still no fallback, because a guess cannot | ||
| 8213 | make two binaries agree on one daemon — so the daemon, the client and the | ||
| 8214 | askpass listener agree by construction and another OS spells its own | ||
| 8215 | default in one place. | ||
| 8216 | |||
| 8217 | **The shell harness asks the OS through named helpers.** `test/e2e_lib.sh` | ||
| 8218 | holds one name per question a pin asks about a pid, an fd table, a bound | ||
| 8219 | UDP port or a file's mode: `pid_alive`, `pid_exe`, `pid_comm`, `pid_args`, | ||
| 8220 | `pid_children`, `pid_fd_count`, `pid_fd_targets`, `pid_holds_unix_sock`, | ||
| 8221 | `pid_rss_kb`, `udp_local_bound`, `udp_table`, `file_mode`, `file_size`, | ||
| 8222 | `sha256_of`. The SPELLING of the question now lives in one place per OS and | ||
| 8223 | the question itself stays in the group file, so a second OS adds a | ||
| 8224 | `case "$_os"` arm here and changes no group file. `oracle_selftest` runs | ||
| 8225 | once before the first group and is the helpers' own pin: a helper that | ||
| 8226 | quietly stopped answering — a missing binary, a `/proc` a sandbox will not | ||
| 8227 | show — would not fail a pin loudly, it would make every pin that reads it | ||
| 8228 | agree with anything, and the suite would go green having tested nothing. | ||
| 8229 | Its subject is off-origin on every dimension the helpers could accidentally | ||
| 8230 | hold constant: a child that is not pid 1, more open fds than the three any | ||
| 8231 | fixture would hold, an argv worth losing, and a child of its own. The | ||
| 8232 | socket arm is asked in both directions, of a listener that holds the path | ||
| 8233 | and of a shell that does not, because a `pid_holds_unix_sock` answering yes | ||
| 8234 | to everything would pass the one pin that reads it just as happily as a | ||
| 8235 | correct one. | ||
| 8236 | |||
| 8237 | **How the wire claim was graded, and what the cross-version gate could not | ||
| 8238 | say.** `make ci` is green. `test/xversion.sh` is NOT, and was not green | ||
| 8239 | before this branch either: its preflight demands a `muxd` in the old | ||
| 8240 | prefix and it drives that side as `muxd run`, so the branch base — one | ||
| 8241 | `mux` binary since v0.0.1-16 — cannot serve as its old side at all, and | ||
| 8242 | its pins still assert a PRE-M18 old side ("the daemon created session zz — | ||
| 8243 | this is not a pre-M18 daemon", "it decoded frames an old client has no arm | ||
| 8244 | for"). Against the released v0.0.1-15 tarball it answers 5 passed, 7 | ||
| 8245 | failed. So the claim was graded DIFFERENTIALLY instead: the same gate, the | ||
| 8246 | same v0.0.1-15 old side, run once with this branch's binary as the new | ||
| 8247 | side and once with the branch base's, gives the identical verdict pin for | ||
| 8248 | pin — 5 passed, 7 failed, the same seven messages. The four real | ||
| 8249 | compatibility pins are among the passes in both runs: a new client driving | ||
| 8250 | an old daemon's default session over a socket and over QUIC, an old client | ||
| 8251 | driving the new daemon's, and an old `muxa`'s empty `status_req`. The | ||
| 8252 | refactor moved no byte on the wire. The gate itself needs its old side | ||
| 8253 | re-pinned to a version that still exists; that is not this branch's work. | ||
| 8254 | |||
| 8255 | Spec: `docs/superpowers/specs/2026-09-03-macos-port-design.md`. | ||