a73x

e5b27feb

build: the QUIC prefix and the linker follow the target, not the host

a73x   2026-09-03 15:10

Commit message
build: the QUIC prefix and the linker follow the target, not the host

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

Makefile
Old New
@@ -40,8 +40,12 @@ INSTDIR ?= dist/install
40 # laptop, 2026-09-02). Cost, measured the same day, best-of-5: repaint 40 # laptop, 2026-09-02). Cost, measured the same day, best-of-5: repaint
41 # 92ms -> 114ms (musl memcpy on 15MB of paint bytes, paid once per 41 # 92ms -> 114ms (musl memcpy on 15MB of paint bytes, paid once per
42 # reattach); the solo and client legs moved inside their noise. 42 # reattach); the solo and client legs moved inside their noise.
43 #
44 # 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.
46 MUX_TARGET ?= x86_64-linux-musl
43 install: 47 install:
44 $(ZIG) build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSafe -p $(INSTDIR) 48 $(ZIG) build -Dtarget=$(MUX_TARGET) -Doptimize=ReleaseSafe -p $(INSTDIR)
45 install -d $(BINDIR) 49 install -d $(BINDIR)
46 install -m755 $(INSTDIR)/bin/mux $(BINDIR)/ 50 install -m755 $(INSTDIR)/bin/mux $(BINDIR)/
47 rm -f $(BINDIR)/muxd $(BINDIR)/muxa $(BINDIR)/muxweb 51 rm -f $(BINDIR)/muxd $(BINDIR)/muxa $(BINDIR)/muxweb
@@ -62,12 +66,12 @@ install:
62 VERSION := $(shell sed -n 's/^[[:space:]]*const version = "\(.*\)";/\1/p' build.zig | head -1) 66 VERSION := $(shell sed -n 's/^[[:space:]]*const version = "\(.*\)";/\1/p' build.zig | head -1)
63 RELDIR ?= dist 67 RELDIR ?= dist
64 RELBIN = $(RELDIR)/v$(VERSION) 68 RELBIN = $(RELDIR)/v$(VERSION)
65 RELTAR = $(RELDIR)/mux-v$(VERSION)-x86_64-linux-musl.tar.gz 69 RELTAR = $(RELDIR)/mux-v$(VERSION)-$(MUX_TARGET).tar.gz
66 # The version guard below asks the STRIPPED artifact what it is, so a 70 # The version guard below asks the STRIPPED artifact what it is, so a
67 # stale stage directory cannot ship under a bumped number. 71 # stale stage directory cannot ship under a bumped number.
68 release: 72 release:
69 @test -n "$(VERSION)" || { echo "release: no version found in build.zig"; exit 1; } 73 @test -n "$(VERSION)" || { echo "release: no version found in build.zig"; exit 1; }
70 $(ZIG) build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSafe -p $(RELDIR)/stage 74 $(ZIG) build -Dtarget=$(MUX_TARGET) -Doptimize=ReleaseSafe -p $(RELDIR)/stage
71 rm -rf $(RELBIN) $(RELTAR) 75 rm -rf $(RELBIN) $(RELTAR)
72 install -d $(RELBIN) 76 install -d $(RELBIN)
73 install -m755 $(RELDIR)/stage/bin/mux $(RELBIN)/ 77 install -m755 $(RELDIR)/stage/bin/mux $(RELBIN)/
build.zig
Old New
@@ -1,4 +1,5 @@
1 const std = @import("std"); 1 const std = @import("std");
2 const builtin = @import("builtin");
2 3
3 /// The vendored QUIC stack (deps/quic). Built by a script rather than by 4 /// The vendored QUIC stack (deps/quic). Built by a script rather than by
4 /// addCSourceFiles, and that is a deliberate v1: wolfSSL's build generates 5 /// addCSourceFiles, and that is a deliberate v1: wolfSSL's build generates
@@ -17,8 +18,27 @@ fn quicDeps(b: *std.Build, target: std.Build.ResolvedTarget) struct {
17 step: *std.Build.Step, 18 step: *std.Build.Step,
18 dir: []const u8, 19 dir: []const u8,
19 } { 20 } {
20 const musl = target.result.abi == .musl; 21 // One word per prefix, shared with build-deps.sh, `make deps`,
21 const name = if (musl) "musl" else "native"; 22 // `make clean-deps` and wan.sh's musl cross-build: `native` for the
23 // host's own libc, `musl` for the static x86_64 release, and the target
24 // triple for any cross target — so a third OS is one more `case` arm
25 // in the script and nothing here. The word follows the TARGET, never
26 // the host: a cross build that reused the host's prefix would link
27 // x86_64 Linux archives into an aarch64 macOS binary.
28 //
29 // `musl` carries an architecture as well as a libc: the script's
30 // zigcc-musl wrapper spells `-target x86_64-linux-musl` outright, so
31 // that word is only ever the x86_64 static release. Any other musl
32 // target falls into the `<arch>-<os>` form, which the script's `case`
33 // refuses with its usage line rather than quietly building x86_64
34 // archives for an aarch64 binary to fail to link.
35 const t = target.result;
36 const name = if (t.abi == .musl and t.cpu.arch == .x86_64)
37 "musl"
38 else if (t.os.tag == builtin.os.tag and t.cpu.arch == builtin.cpu.arch)
39 "native"
40 else
41 b.fmt("{s}-{s}", .{ @tagName(t.cpu.arch), @tagName(t.os.tag) });
22 const run = b.addSystemCommand(&.{ "deps/quic/build-deps.sh", name }); 42 const run = b.addSystemCommand(&.{ "deps/quic/build-deps.sh", name });
23 run.setName(b.fmt("build QUIC deps ({s})", .{name})); 43 run.setName(b.fmt("build QUIC deps ({s})", .{name}));
24 // Never cached by the build graph: the script's own marker file is the 44 // Never cached by the build graph: the script's own marker file is the
@@ -40,6 +60,15 @@ fn quicDeps(b: *std.Build, target: std.Build.ResolvedTarget) struct {
40 }; 60 };
41 } 61 }
42 62
63 /// Zig 0.15's self-hosted x86_64 ELF linker can't handle the .sframe
64 /// sections gcc >= 16's crt1.o emits, so ELF goes through LLD. LLD does
65 /// not link Mach-O, and Zig's own linker does — so Darwin is the one
66 /// target that must NOT ask for it.
67 fn linkerFor(c: *std.Build.Step.Compile) void {
68 c.use_llvm = true;
69 c.use_lld = !c.rootModuleTarget().os.tag.isDarwin();
70 }
71
43 /// One wasm-side twin of a native module: same source, the wasm32 target, 72 /// One wasm-side twin of a native module: same source, the wasm32 target,
44 /// and ReleaseSmall — never `optimize`, because the artifact is embedded 73 /// and ReleaseSmall — never `optimize`, because the artifact is embedded
45 /// into the one binary and its Debug build is 3.7MB against ReleaseSmall's 345KB. 74 /// into the one binary and its Debug build is 3.7MB against ReleaseSmall's 345KB.
@@ -706,8 +735,7 @@ fn docGate(b: *std.Build, target: std.Build.ResolvedTarget, check_step: *std.Bui
706 .optimize = .Debug, 735 .optimize = .Debug,
707 }); 736 });
708 const exe = b.addExecutable(.{ .name = "docscheck", .root_module = mod }); 737 const exe = b.addExecutable(.{ .name = "docscheck", .root_module = mod });
709 exe.use_llvm = true; 738 linkerFor(exe);
710 exe.use_lld = true;
711 739
712 // The tool is inside its own corpus: a gate its author is exempt from is 740 // The tool is inside its own corpus: a gate its author is exempt from is
713 // an argument, not a rule. 741 // an argument, not a rule.
@@ -890,31 +918,24 @@ pub fn build(b: *std.Build) void {
890 // fixtures below stay separate: they stand in for users, not for the 918 // fixtures below stay separate: they stand in for users, not for the
891 // product. 919 // product.
892 const mux_exe = b.addExecutable(.{ .name = "mux", .root_module = mux_mod }); 920 const mux_exe = b.addExecutable(.{ .name = "mux", .root_module = mux_mod });
893 // Zig 0.15's self-hosted x86_64 linker can't handle the .sframe 921 linkerFor(mux_exe);
894 // sections emitted by gcc >= 16 crt1.o on this system; LLD can.
895 mux_exe.use_llvm = true;
896 mux_exe.use_lld = true;
897 linkQuic(b, mux_exe, quic); 922 linkQuic(b, mux_exe, quic);
898 b.installArtifact(mux_exe); 923 b.installArtifact(mux_exe);
899 924
900 const rawmode_exe = b.addExecutable(.{ .name = "rawmode", .root_module = rawmode_mod }); 925 const rawmode_exe = b.addExecutable(.{ .name = "rawmode", .root_module = rawmode_mod });
901 rawmode_exe.use_llvm = true; 926 linkerFor(rawmode_exe);
902 rawmode_exe.use_lld = true;
903 b.installArtifact(rawmode_exe); 927 b.installArtifact(rawmode_exe);
904 928
905 const delaypipe_exe = b.addExecutable(.{ .name = "delaypipe", .root_module = delaypipe_mod }); 929 const delaypipe_exe = b.addExecutable(.{ .name = "delaypipe", .root_module = delaypipe_mod });
906 delaypipe_exe.use_llvm = true; 930 linkerFor(delaypipe_exe);
907 delaypipe_exe.use_lld = true;
908 b.installArtifact(delaypipe_exe); 931 b.installArtifact(delaypipe_exe);
909 932
910 const render_exe = b.addExecutable(.{ .name = "render", .root_module = render_mod }); 933 const render_exe = b.addExecutable(.{ .name = "render", .root_module = render_mod });
911 render_exe.use_llvm = true; 934 linkerFor(render_exe);
912 render_exe.use_lld = true;
913 b.installArtifact(render_exe); 935 b.installArtifact(render_exe);
914 936
915 const ptyclient_exe = b.addExecutable(.{ .name = "ptyclient", .root_module = ptyclient_mod }); 937 const ptyclient_exe = b.addExecutable(.{ .name = "ptyclient", .root_module = ptyclient_mod });
916 ptyclient_exe.use_llvm = true; 938 linkerFor(ptyclient_exe);
917 ptyclient_exe.use_lld = true;
918 b.installArtifact(ptyclient_exe); 939 b.installArtifact(ptyclient_exe);
919 940
920 // ---- The wasm core (M-web Task 4) ---- 941 // ---- The wasm core (M-web Task 4) ----
@@ -977,8 +998,7 @@ pub fn build(b: *std.Build) void {
977 998
978 // ---- the hub's browser stand-in ---- 999 // ---- the hub's browser stand-in ----
979 const wsclient_exe = b.addExecutable(.{ .name = "wsclient", .root_module = wsclient_mod }); 1000 const wsclient_exe = b.addExecutable(.{ .name = "wsclient", .root_module = wsclient_mod });
980 wsclient_exe.use_llvm = true; 1001 linkerFor(wsclient_exe);
981 wsclient_exe.use_lld = true;
982 b.installArtifact(wsclient_exe); 1002 b.installArtifact(wsclient_exe);
983 1003
984 // The page's three assets arrive as anonymous imports so @embedFile 1004 // The page's three assets arrive as anonymous imports so @embedFile
@@ -998,8 +1018,7 @@ pub fn build(b: *std.Build) void {
998 for (test_order) |name| { 1018 for (test_order) |name| {
999 const i = idx.of(name); 1019 const i = idx.of(name);
1000 const t = b.addTest(.{ .root_module = test_mods[i] }); 1020 const t = b.addTest(.{ .root_module = test_mods[i] });
1001 t.use_llvm = true; 1021 linkerFor(t);
1002 t.use_lld = true;
1003 // quic_tests is also what makes `make test` build the QUIC deps on 1022 // quic_tests is also what makes `make test` build the QUIC deps on
1004 // a clean checkout — the dependency must reach the test binaries, 1023 // a clean checkout — the dependency must reach the test binaries,
1005 // not only the binary (decisions.md, M8). 1024 // not only the binary (decisions.md, M8).
deps/quic/build-deps.sh
Old New
@@ -2,7 +2,7 @@
2 # Build the vendored QUIC stack (ngtcp2 + wolfSSL) into static libraries that 2 # Build the vendored QUIC stack (ngtcp2 + wolfSSL) into static libraries that
3 # build.zig links, using ONLY the repo's pinned Zig as the C toolchain. 3 # build.zig links, using ONLY the repo's pinned Zig as the C toolchain.
4 # 4 #
5 # ./build-deps.sh <target> target: native | musl 5 # ./build-deps.sh <target> target: native | musl | aarch64-macos
6 # 6 #
7 # Grown up from spike/quic/build.sh (M8 Task 1), which proved this is 7 # Grown up from spike/quic/build.sh (M8 Task 1), which proved this is
8 # possible; this version is the one the real build depends on. It is 8 # possible; this version is the one the real build depends on. It is
@@ -24,8 +24,8 @@ WOLFSSL_SHA=2f4ef3d4fd387a9b3191d36a6316d69116c46ff69bb9583b6c82b36d7b8ca114
24 24
25 T="${1:-native}" 25 T="${1:-native}"
26 case "$T" in 26 case "$T" in
27 native | musl) ;; 27 native | musl | aarch64-macos) ;;
28 *) echo "usage: $0 [native|musl]" >&2; exit 2 ;; 28 *) echo "usage: $0 [native|musl|aarch64-macos]" >&2; exit 2 ;;
29 esac 29 esac
30 30
31 SELF="$(cd "$(dirname "$0")" && pwd)" 31 SELF="$(cd "$(dirname "$0")" && pwd)"
@@ -41,6 +41,16 @@ W="$SELF/work"
41 command -v cmake >/dev/null || { echo "deps/quic: cmake is required" >&2; exit 1; } 41 command -v cmake >/dev/null || { echo "deps/quic: cmake is required" >&2; exit 1; }
42 command -v curl >/dev/null || { echo "deps/quic: curl is required" >&2; exit 1; } 42 command -v curl >/dev/null || { echo "deps/quic: curl is required" >&2; exit 1; }
43 43
44 # The two host tools whose spelling differs on a Darwin host, named once so
45 # no later line has to ask again. BELOW the marker check on purpose: the
46 # early exit is the path every build takes and build.zig budgets it at one
47 # fork, so these two must not be forked to find out the libs are already
48 # there.
49 case "$(uname)" in
50 Darwin) sha_check() { shasum -a 256 -c - >/dev/null; }; NJOBS=$(sysctl -n hw.ncpu) ;;
51 *) sha_check() { sha256sum -c - >/dev/null; }; NJOBS=$(nproc) ;;
52 esac
53
44 echo "deps/quic: building the QUIC stack for $T (first run: downloads ~30MB, takes a few minutes)" >&2 54 echo "deps/quic: building the QUIC stack for $T (first run: downloads ~30MB, takes a few minutes)" >&2
45 mkdir -p "$W/src" "$W/bin" 55 mkdir -p "$W/src" "$W/bin"
46 56
@@ -58,6 +68,10 @@ cat > "$W/bin/zigcc-musl" <<EOF
58 #!/bin/sh 68 #!/bin/sh
59 exec $ZIG cc -target x86_64-linux-musl "\$@" 69 exec $ZIG cc -target x86_64-linux-musl "\$@"
60 EOF 70 EOF
71 cat > "$W/bin/zigcc-aarch64-macos" <<EOF
72 #!/bin/sh
73 exec $ZIG cc -target aarch64-macos "\$@"
74 EOF
61 cat > "$W/bin/zigar" <<EOF 75 cat > "$W/bin/zigar" <<EOF
62 #!/bin/sh 76 #!/bin/sh
63 exec $ZIG ar "\$@" 77 exec $ZIG ar "\$@"
@@ -70,7 +84,7 @@ chmod +x "$W"/bin/*
70 84
71 fetch() { # url sha file 85 fetch() { # url sha file
72 [ -f "$W/src/$3" ] || curl -sSL -o "$W/src/$3" "$1" 86 [ -f "$W/src/$3" ] || curl -sSL -o "$W/src/$3" "$1"
73 echo "$2 $W/src/$3" | sha256sum -c - >/dev/null || { 87 echo "$2 $W/src/$3" | sha_check || {
74 echo "deps/quic: checksum mismatch for $3 — refusing to build" >&2 88 echo "deps/quic: checksum mismatch for $3 — refusing to build" >&2
75 rm -f "$W/src/$3" 89 rm -f "$W/src/$3"
76 exit 1 90 exit 1
@@ -85,7 +99,20 @@ fetch "https://github.com/wolfSSL/wolfssl/archive/refs/tags/v$WOLFSSL_VER.tar.gz
85 99
86 CC="$W/bin/zigcc-$T" 100 CC="$W/bin/zigcc-$T"
87 XTRA="" 101 XTRA=""
88 [ "$T" = musl ] && XTRA="-DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=x86_64" 102 WOLF_XTRA=""
103 case "$T" in
104 musl) XTRA="-DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=x86_64" ;;
105 aarch64-macos)
106 # Cross to Darwin: find nothing on the host (ngtcp2 found the host's
107 # own libwolfssl.so before this fence and linked a Linux shared
108 # object into a Mach-O build), and no system CA path — mux is
109 # PSK-only and wolfSSL's CA path wants Security.framework, which
110 # this toolchain has no SDK for. Both measured 2026-09-03; the probe
111 # log is in docs/superpowers/specs/2026-09-03-macos-port-design.md,
112 # "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"
114 WOLF_XTRA="-DWOLFSSL_SYS_CA_CERTS=no" ;;
115 esac
89 116
90 # wolfSSL. Three flags are load-bearing, all found by link failure in the 117 # wolfSSL. Three flags are load-bearing, all found by link failure in the
91 # spike (spike/quic/README.md records the diagnosis): 118 # spike (spike/quic/README.md records the diagnosis):
@@ -99,14 +126,14 @@ XTRA=""
99 # It stays off here permanently; flip it only in 126 # It stays off here permanently; flip it only in
100 # a local throwaway build when decrypting your 127 # a local throwaway build when decrypting your
101 # own capture, and never commit that. 128 # own capture, and never commit that.
102 cmake -S "$W/src/wolfssl-$WOLFSSL_VER" -B "$W/build/wolfssl-$T" $XTRA \ 129 cmake -S "$W/src/wolfssl-$WOLFSSL_VER" -B "$W/build/wolfssl-$T" $XTRA $WOLF_XTRA \
103 -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-fno-sanitize=undefined -O2" \ 130 -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-fno-sanitize=undefined -O2" \
104 -DCMAKE_C_COMPILER="$CC" -DCMAKE_AR="$W/bin/zigar" -DCMAKE_RANLIB="$W/bin/zigranlib" \ 131 -DCMAKE_C_COMPILER="$CC" -DCMAKE_AR="$W/bin/zigar" -DCMAKE_RANLIB="$W/bin/zigranlib" \
105 -DBUILD_SHARED_LIBS=OFF -DWOLFSSL_QUIC=yes -DWOLFSSL_PSK=yes \ 132 -DBUILD_SHARED_LIBS=OFF -DWOLFSSL_QUIC=yes -DWOLFSSL_PSK=yes \
106 -DWOLFSSL_AESECB=yes -DWOLFSSL_AESCTR=yes -DWOLFSSL_KEYLOG_EXPORT=no \ 133 -DWOLFSSL_AESECB=yes -DWOLFSSL_AESCTR=yes -DWOLFSSL_KEYLOG_EXPORT=no \
107 -DWOLFSSL_SESSION_TICKET=yes -DWOLFSSL_EXAMPLES=no -DWOLFSSL_CRYPT_TESTS=no \ 134 -DWOLFSSL_SESSION_TICKET=yes -DWOLFSSL_EXAMPLES=no -DWOLFSSL_CRYPT_TESTS=no \
108 -DWOLFSSL_INSTALL=yes -DCMAKE_INSTALL_PREFIX="$OUT" >/dev/null 135 -DWOLFSSL_INSTALL=yes -DCMAKE_INSTALL_PREFIX="$OUT" >/dev/null
109 cmake --build "$W/build/wolfssl-$T" -j"$(nproc)" >/dev/null 136 cmake --build "$W/build/wolfssl-$T" -j"$NJOBS" >/dev/null
110 cmake --install "$W/build/wolfssl-$T" >/dev/null 137 cmake --install "$W/build/wolfssl-$T" >/dev/null
111 138
112 # ngtcp2 + its wolfSSL crypto backend. ENABLE_LIB_ONLY is what keeps the 139 # ngtcp2 + its wolfSSL crypto backend. ENABLE_LIB_ONLY is what keeps the
@@ -121,7 +148,7 @@ cmake -S "$W/src/ngtcp2-$NGTCP2_VER" -B "$W/build/ngtcp2-$T" $XTRA \
121 -DENABLE_STATIC_LIB=ON -DENABLE_SHARED_LIB=OFF -DENABLE_WOLFSSL=ON \ 148 -DENABLE_STATIC_LIB=ON -DENABLE_SHARED_LIB=OFF -DENABLE_WOLFSSL=ON \
122 -DENABLE_OPENSSL=OFF -DENABLE_GNUTLS=OFF -DENABLE_BORINGSSL=OFF \ 149 -DENABLE_OPENSSL=OFF -DENABLE_GNUTLS=OFF -DENABLE_BORINGSSL=OFF \
123 -DENABLE_LIB_ONLY=ON -DCMAKE_INSTALL_PREFIX="$OUT" >/dev/null 150 -DENABLE_LIB_ONLY=ON -DCMAKE_INSTALL_PREFIX="$OUT" >/dev/null
124 cmake --build "$W/build/ngtcp2-$T" -j"$(nproc)" >/dev/null 151 cmake --build "$W/build/ngtcp2-$T" -j"$NJOBS" >/dev/null
125 cmake --install "$W/build/ngtcp2-$T" >/dev/null 152 cmake --install "$W/build/ngtcp2-$T" >/dev/null
126 153
127 echo "deps/quic: $T ready in $OUT" >&2 154 echo "deps/quic: $T ready in $OUT" >&2