a73x

c8ff232e

build: a Mac builds natively through a shadow SDK, and the dep script follows the host

a73x   2026-09-03 16:52

Commit message
build: a Mac builds natively through a shadow SDK, and the dep script follows the host

zig 0.15.2's Mach-O linker matches the bare arm64 slice only, and Xcode
26.4+ ships a libSystem stub that lists arm64e-macos and no arm64-macos,
so on a Mac every libSystem symbol is undefined and `zig build` cannot
even link its own build runner. deps/mac-sdk.sh builds a shadow SDK --
the real one in every path but usr/lib/libSystem*, where zig's own stub
stands in -- plus an `xcrun` shim answering --show-sdk-path with it, which
is how both zig and ghostty's apple_sdk helper find an SDK. It retires
itself when the real stub lists arm64-macos again.

The Makefile puts the shim dir first on PATH on Darwin only, makes the
build targets depend on `mac-sdk`, and lets MUX_TARGET follow the host so
a Mac installs a Mac binary. Every Linux recipe expands byte-identically
apart from that one prerequisite line.

deps/quic/build-deps.sh defaults ZIG to the repo's pinned toolchain (a Mac
has no other 0.15.2; brew ships 0.16), gates -march=x86_64_v3 on uname -m
since it is an x86 flag zig cc rejects on arm64, and gives a native Darwin
build the same two cmake fences the cross build already had.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SakwJEwD9dXBoRP5kWbemW

.gitignore
Old New
@@ -18,3 +18,6 @@ dist/
18 deps/zig 18 deps/zig
19 deps/quic/out 19 deps/quic/out
20 deps/quic/work 20 deps/quic/work
21
22 # Shadow SDK from deps/mac-sdk.sh, Darwin only.
23 deps/mac-sdk
Makefile
Old New
@@ -2,7 +2,18 @@
2 # default zig is 0.17-dev. Override with ZIG=... if yours lives elsewhere. 2 # default zig is 0.17-dev. Override with ZIG=... if yours lives elsewhere.
3 ZIG ?= $(CURDIR)/deps/zig/zig 3 ZIG ?= $(CURDIR)/deps/zig/zig
4 4
5 .PHONY: build check ci test e2e soak bench agent throughput vm coverage deps clean clean-deps xversion xversion-build install release 5 # On a Mac the pinned zig cannot link against Xcode 26.4+'s SDK (see
6 # deps/mac-sdk.sh). The shim dir goes FIRST on PATH for every recipe here,
7 # and `mac-sdk` builds it; both are no-ops on Linux and on a Mac whose SDK
8 # is linkable. MUX_TARGET follows the host: a Mac installs a Mac binary.
9 UNAME_S := $(shell uname -s)
10 ifeq ($(UNAME_S),Darwin)
11 export PATH := $(CURDIR)/deps/mac-sdk/bin:/opt/homebrew/bin:$(PATH)
12 MUX_TARGET ?= aarch64-macos
13 endif
14 MUX_TARGET ?= x86_64-linux-musl
15
16 .PHONY: build check ci test e2e soak bench agent throughput vm coverage deps clean clean-deps xversion xversion-build install release mac-sdk mac
6 17
7 # The QUIC stack (deps/quic) is built on demand by build.zig, so no target 18 # The QUIC stack (deps/quic) is built on demand by build.zig, so no target
8 # here needs to depend on this one. It exists to make the one-time cost 19 # here needs to depend on this one. It exists to make the one-time cost
@@ -13,7 +24,7 @@ deps:
13 ./deps/quic/build-deps.sh native 24 ./deps/quic/build-deps.sh native
14 ./deps/quic/build-deps.sh musl 25 ./deps/quic/build-deps.sh musl
15 26
16 build: 27 build: mac-sdk
17 $(ZIG) build 28 $(ZIG) build
18 29
19 # Only the ONE user-facing binary: everything else in zig-out/bin 30 # Only the ONE user-facing binary: everything else in zig-out/bin
@@ -43,8 +54,8 @@ INSTDIR ?= dist/install
43 # 54 #
44 # The target the installed and released binaries are built for. Static 55 # The target the installed and released binaries are built for. Static
45 # musl on Linux, for the reason above; another OS names its triple here. 56 # musl on Linux, for the reason above; another OS names its triple here.
46 MUX_TARGET ?= x86_64-linux-musl 57 # The `?=` at the top of this file is what sets it, so a Mac gets its own.
47 install: 58 install: mac-sdk
48 $(ZIG) build -Dtarget=$(MUX_TARGET) -Doptimize=ReleaseSafe -p $(INSTDIR) 59 $(ZIG) build -Dtarget=$(MUX_TARGET) -Doptimize=ReleaseSafe -p $(INSTDIR)
49 install -d $(BINDIR) 60 install -d $(BINDIR)
50 install -m755 $(INSTDIR)/bin/mux $(BINDIR)/ 61 install -m755 $(INSTDIR)/bin/mux $(BINDIR)/
@@ -86,7 +97,7 @@ release:
86 @echo "remote: ssh HOST 'mkdir -p ~/.local/bin && tar xzf - -C ~/.local/bin' < $(RELTAR)" 97 @echo "remote: ssh HOST 'mkdir -p ~/.local/bin && tar xzf - -C ~/.local/bin' < $(RELTAR)"
87 @echo "publish: git collab release publish v$(VERSION) $(RELTAR)" 98 @echo "publish: git collab release publish v$(VERSION) $(RELTAR)"
88 99
89 test: 100 test: mac-sdk
90 $(ZIG) build test 101 $(ZIG) build test
91 102
92 e2e: 103 e2e:
@@ -135,16 +146,23 @@ vm:
135 ./test/vm.sh $(VMDIR)/bin/mux $(VMDIR)/bin/ptyclient 146 ./test/vm.sh $(VMDIR)/bin/mux $(VMDIR)/bin/ptyclient
136 147
137 THRUDIR ?= dist/throughput 148 THRUDIR ?= dist/throughput
138 throughput: 149 throughput: mac-sdk
139 $(ZIG) build -Doptimize=ReleaseSafe -p $(THRUDIR) 150 $(ZIG) build -Doptimize=ReleaseSafe -p $(THRUDIR)
140 ./test/throughput.sh $(THRUDIR)/bin/mux $(THRUDIR)/bin/ptyclient 151 ./test/throughput.sh $(THRUDIR)/bin/mux $(THRUDIR)/bin/ptyclient
141 152
153 mac-sdk:
154 sh ./deps/mac-sdk.sh
155
156 # The `make vm` of the macOS port: user journeys on the Mac, over ssh.
157 mac: build
158 sh ./test/mac.sh
159
142 # `zig build check` grades the tree; bans.sh grades the grader. The folder 160 # `zig build check` grades the tree; bans.sh grades the grader. The folder
143 # rules are the one gate whose failure mode is silence — a needle list 161 # rules are the one gate whose failure mode is silence — a needle list
144 # edited down to nothing, or a folder dropped from a rule's list, leaves a 162 # edited down to nothing, or a folder dropped from a rule's list, leaves a
145 # green tree asserting rules that no longer bite — so one planted needle per 163 # green tree asserting rules that no longer bite — so one planted needle per
146 # rule runs here, after the gate it is checking. Two seconds. 164 # rule runs here, after the gate it is checking. Two seconds.
147 check: 165 check: mac-sdk
148 $(ZIG) build check 166 $(ZIG) build check
149 sh test/bans.sh $(ZIG) 167 sh test/bans.sh $(ZIG)
150 168
deps/mac-sdk.sh
Old New
@@ -0,0 +1,55 @@
1 #!/bin/sh
2 # deps/mac-sdk.sh — a shadow macOS SDK for zig 0.15.2, built on a Darwin host.
3 #
4 # Xcode 26.4 and later ship a libSystem.B.tbd whose `targets:` line lists
5 # arm64e-macos and no arm64-macos. zig 0.15.2's Mach-O linker matches the
6 # bare arm64 slice only, so against that SDK every libSystem symbol is
7 # undefined — for the build runner too, which is why `zig build` cannot
8 # even start (ziglang/zig#31658 on Codeberg; fixed in 0.16, which ghostty's
9 # pin cannot use). zig's own lib/libc/darwin/libSystem.tbd is what a
10 # `-target aarch64-macos` build links against, and it works. This script
11 # builds an SDK that is the real one in every path but usr/lib/libSystem*,
12 # where zig's stub stands in, and an `xcrun` shim that answers
13 # --show-sdk-path with it — zig finds the SDK by running `xcrun` off PATH
14 # (std/zig/system/darwin.zig), and so does ghostty's apple_sdk helper.
15 #
16 # Self-retiring: when the real SDK's stub lists arm64-macos again, no shadow
17 # is built and the shim, if a stale one is on PATH, passes straight through.
18 # Idempotent and cheap: the marker check is the first thing it does.
19 #
20 # ./deps/mac-sdk.sh builds deps/mac-sdk/{sdk,bin/xcrun}
21 set -eu
22 [ "$(uname)" = Darwin ] || exit 0
23 SELF="$(cd "$(dirname "$0")" && pwd)"
24 OUT="$SELF/mac-sdk"
25 ZIG_STUB="$SELF/zig/lib/libc/darwin/libSystem.tbd"
26 REAL="$(/usr/bin/xcrun --sdk macosx --show-sdk-path)"
27 if grep -m1 '^targets:' "$REAL/usr/lib/libSystem.B.tbd" | grep -q 'arm64-macos'; then
28 rm -rf "$OUT" # the SDK is linkable again; leave nothing to shadow it
29 exit 0
30 fi
31 [ -f "$ZIG_STUB" ] || { echo "deps/mac-sdk: no zig stub at $ZIG_STUB (is deps/zig in place?)" >&2; exit 1; }
32 # Marker: the shim exists AND points at this SDK. A new Xcode moves REAL.
33 if [ -x "$OUT/bin/xcrun" ] && [ "$(readlink "$OUT/sdk/usr/include")" = "$REAL/usr/include" ]; then
34 exit 0
35 fi
36 rm -rf "$OUT"
37 mkdir -p "$OUT/sdk/usr/lib" "$OUT/bin"
38 for e in "$REAL"/*; do b=$(basename "$e"); [ "$b" = usr ] || ln -s "$e" "$OUT/sdk/$b"; done
39 for e in "$REAL"/usr/*; do b=$(basename "$e"); [ "$b" = lib ] || ln -s "$e" "$OUT/sdk/usr/$b"; done
40 for e in "$REAL"/usr/lib/*; do
41 b=$(basename "$e")
42 case "$b" in libSystem.tbd | libSystem.B.tbd) ;; *) ln -s "$e" "$OUT/sdk/usr/lib/$b" ;; esac
43 done
44 cp "$ZIG_STUB" "$OUT/sdk/usr/lib/libSystem.tbd"
45 cp "$ZIG_STUB" "$OUT/sdk/usr/lib/libSystem.B.tbd"
46 cat > "$OUT/bin/xcrun" <<EOF
47 #!/bin/sh
48 # Shim from deps/mac-sdk.sh: only --show-sdk-path is answered here.
49 case "\$*" in
50 *--show-sdk-path*) [ -d "$OUT/sdk" ] && { echo "$OUT/sdk"; exit 0; } ;;
51 esac
52 exec /usr/bin/xcrun "\$@"
53 EOF
54 chmod +x "$OUT/bin/xcrun"
55 echo "deps/mac-sdk: shadow SDK over $REAL (zig 0.15.2 cannot link the Xcode 26.4+ stub)" >&2
deps/quic/build-deps.sh
Old New
@@ -29,7 +29,11 @@ case "$T" in
29 esac 29 esac
30 30
31 SELF="$(cd "$(dirname "$0")" && pwd)" 31 SELF="$(cd "$(dirname "$0")" && pwd)"
32 ZIG="${ZIG:-$HOME/Downloads/zig-x86_64-linux-0.15.2/zig}" 32 # The repo's pinned toolchain first: every box that builds mux has it at
33 # deps/zig, and a Mac has no other 0.15.2 (brew ships 0.16). The Downloads
34 # path is the original dev box's spelling, kept so an old rig still runs.
35 ZIG="${ZIG:-$SELF/../zig/zig}"
36 [ -x "$ZIG" ] || ZIG="$HOME/Downloads/zig-x86_64-linux-0.15.2/zig"
33 OUT="$SELF/out/$T" 37 OUT="$SELF/out/$T"
34 W="$SELF/work" 38 W="$SELF/work"
35 39
@@ -60,9 +64,15 @@ mkdir -p "$W/src" "$W/bin"
60 # v3 (AVX2) costs nothing measurable at terminal bandwidth, and the musl 64 # v3 (AVX2) costs nothing measurable at terminal bandwidth, and the musl
61 # release target below is baseline anyway, so nothing shipped changes. 65 # release target below is baseline anyway, so nothing shipped changes.
62 # Zig-style CPU name (underscores): zig cc rejects clang's x86-64-v3. 66 # Zig-style CPU name (underscores): zig cc rejects clang's x86-64-v3.
67 # The flag is x86's: on an arm64 host (an Apple Silicon Mac is the one
68 # that builds) zig cc rejects it, so a native build there is plain zig cc.
69 case "$(uname -m)" in
70 x86_64) NATIVE_FLAGS="-march=x86_64_v3" ;;
71 *) NATIVE_FLAGS="" ;;
72 esac
63 cat > "$W/bin/zigcc-native" <<EOF 73 cat > "$W/bin/zigcc-native" <<EOF
64 #!/bin/sh 74 #!/bin/sh
65 exec $ZIG cc -march=x86_64_v3 "\$@" 75 exec $ZIG cc $NATIVE_FLAGS "\$@"
66 EOF 76 EOF
67 cat > "$W/bin/zigcc-musl" <<EOF 77 cat > "$W/bin/zigcc-musl" <<EOF
68 #!/bin/sh 78 #!/bin/sh
@@ -112,6 +122,16 @@ case "$T" in
112 # "Findings the design rests on". 122 # "Findings the design rests on".
113 XTRA="-DCMAKE_SYSTEM_NAME=Darwin -DCMAKE_SYSTEM_PROCESSOR=arm64 -DCMAKE_FIND_ROOT_PATH=$OUT -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ONLY" 123 XTRA="-DCMAKE_SYSTEM_NAME=Darwin -DCMAKE_SYSTEM_PROCESSOR=arm64 -DCMAKE_FIND_ROOT_PATH=$OUT -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ONLY"
114 WOLF_XTRA="-DWOLFSSL_SYS_CA_CERTS=no" ;; 124 WOLF_XTRA="-DWOLFSSL_SYS_CA_CERTS=no" ;;
125 native)
126 # A native build ON a Mac wants the same two fences for the same
127 # two reasons, minus the cross-compile words: brew carries a
128 # libngtcp2 that ngtcp2's own cmake would otherwise find, and the
129 # CA path still wants Security.framework linked into a PSK-only
130 # binary that never verifies a certificate.
131 if [ "$(uname)" = Darwin ]; then
132 XTRA="-DCMAKE_FIND_ROOT_PATH=$OUT -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ONLY"
133 WOLF_XTRA="-DWOLFSSL_SYS_CA_CERTS=no"
134 fi ;;
115 esac 135 esac
116 136
117 # wolfSSL. Three flags are load-bearing, all found by link failure in the 137 # wolfSSL. Three flags are load-bearing, all found by link failure in the