a73x

deps/quic/build-deps.sh

Ref:   Size: 8.9 KiB   History

#!/bin/sh
# Build the vendored QUIC stack (ngtcp2 + wolfSSL) into static libraries that
# build.zig links, using ONLY the repo's pinned Zig as the C toolchain.
#
#   ./build-deps.sh <target>      target: native | musl | aarch64-macos
#
# Grown up from spike/quic/build.sh (M8 Task 1), which proved this is
# possible; this version is the one the real build depends on. It is
# idempotent and cheap to re-run: if the libraries for a target are already
# present it exits immediately, which is what makes it safe for build.zig to
# invoke on every build.
#
# FIRST RUN NEEDS NETWORK. It downloads two source tarballs (~30MB) and takes
# a few minutes; every run after that is a no-op until `make clean-deps`.
# There is no offline fallback and no prebuilt blob in the repo — the
# toolchain question this milestone answered is precisely "can we build it
# ourselves", and shipping a binary would un-answer it.
set -eu

NGTCP2_VER=1.25.0
WOLFSSL_VER=5.9.2-stable
NGTCP2_SHA=1c0843076528a87b65e9a9d455100941f4cb65d44f96c5da6ae56df146043955
WOLFSSL_SHA=2f4ef3d4fd387a9b3191d36a6316d69116c46ff69bb9583b6c82b36d7b8ca114

T="${1:-native}"
case "$T" in
    native | musl | aarch64-macos) ;;
    *) echo "usage: $0 [native|musl|aarch64-macos]" >&2; exit 2 ;;
esac

SELF="$(cd "$(dirname "$0")" && pwd)"
# The Makefile exports the mise-resolved 0.15.2 executable. Keep the
# environment override so this script remains usable on its own.
ZIG="${ZIG:-$(mise which zig 2>/dev/null || true)}"
OUT="$SELF/out/$T"
W="$SELF/work"

# The marker: if this exists the target is built. Checked before anything
# else so build.zig can call this unconditionally.
[ -f "$OUT/lib/libngtcp2_crypto_wolfssl.a" ] && exit 0

[ -x "$ZIG" ] || { echo "deps/quic: no zig at $ZIG (set ZIG=...)" >&2; exit 1; }
command -v cmake >/dev/null || { echo "deps/quic: cmake is required" >&2; exit 1; }
command -v curl >/dev/null || { echo "deps/quic: curl is required" >&2; exit 1; }

# The two host tools whose spelling differs on a Darwin host, named once so
# no later line has to ask again. BELOW the marker check on purpose: the
# early exit is the path every build takes and build.zig budgets it at one
# fork, so these two must not be forked to find out the libs are already
# there.
case "$(uname)" in
    Darwin) sha_check() { shasum -a 256 -c - >/dev/null; }; NJOBS=$(sysctl -n hw.ncpu) ;;
    *)      sha_check() { sha256sum -c - >/dev/null; };    NJOBS=$(nproc) ;;
esac

echo "deps/quic: building the QUIC stack for $T (first run: downloads ~30MB, takes a few minutes)" >&2
mkdir -p "$W/src" "$W/bin"

# x86_64_v3, not native: valgrind 3.25 decodes no AVX-512, and a host
# CPU that has it would otherwise bake EVEX into wolfSSL's memset —
# making the whole local stack un-valgrindable (found by 6c's first run).
# v3 (AVX2) costs nothing measurable at terminal bandwidth, and the musl
# release target below is baseline anyway, so nothing shipped changes.
# Zig-style CPU name (underscores): zig cc rejects clang's x86-64-v3.
# The flag is x86's: on an arm64 host (an Apple Silicon Mac is the one
# that builds) zig cc rejects it, so a native build there is plain zig cc.
case "$(uname -m)" in
    x86_64) NATIVE_FLAGS="-march=x86_64_v3" ;;
    *)      NATIVE_FLAGS="" ;;
esac
cat > "$W/bin/zigcc-native" <<EOF
#!/bin/sh
exec $ZIG cc $NATIVE_FLAGS "\$@"
EOF
cat > "$W/bin/zigcc-musl" <<EOF
#!/bin/sh
exec $ZIG cc -target x86_64-linux-musl "\$@"
EOF
cat > "$W/bin/zigcc-aarch64-macos" <<EOF
#!/bin/sh
exec $ZIG cc -target aarch64-macos "\$@"
EOF
cat > "$W/bin/zigar" <<EOF
#!/bin/sh
exec $ZIG ar "\$@"
EOF
cat > "$W/bin/zigranlib" <<EOF
#!/bin/sh
exec $ZIG ranlib "\$@"
EOF
chmod +x "$W"/bin/*

fetch() { # url sha file
    [ -f "$W/src/$3" ] || curl -sSL -o "$W/src/$3" "$1"
    echo "$2  $W/src/$3" | sha_check || {
        echo "deps/quic: checksum mismatch for $3 — refusing to build" >&2
        rm -f "$W/src/$3"
        exit 1
    }
}
fetch "https://github.com/ngtcp2/ngtcp2/releases/download/v$NGTCP2_VER/ngtcp2-$NGTCP2_VER.tar.gz" \
      "$NGTCP2_SHA" "ngtcp2-$NGTCP2_VER.tar.gz"
fetch "https://github.com/wolfSSL/wolfssl/archive/refs/tags/v$WOLFSSL_VER.tar.gz" \
      "$WOLFSSL_SHA" "wolfssl-$WOLFSSL_VER.tar.gz"
[ -d "$W/src/ngtcp2-$NGTCP2_VER" ] || tar -C "$W/src" -xzf "$W/src/ngtcp2-$NGTCP2_VER.tar.gz"
[ -d "$W/src/wolfssl-$WOLFSSL_VER" ] || tar -C "$W/src" -xzf "$W/src/wolfssl-$WOLFSSL_VER.tar.gz"

CC="$W/bin/zigcc-$T"
XTRA=""
WOLF_XTRA=""
case "$T" in
    musl) XTRA="-DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=x86_64" ;;
    aarch64-macos)
        # Cross to Darwin: find nothing on the host (ngtcp2 found the host's
        # own libwolfssl.so before this fence and linked a Linux shared
        # object into a Mach-O build), and no system CA path — mux is
        # PSK-only and wolfSSL's CA path wants Security.framework, which
        # this toolchain has no SDK for. Both measured 2026-09-03; the probe
        # log is in docs/superpowers/specs/2026-09-03-macos-port-design.md,
        # "Findings the design rests on".
        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"
        WOLF_XTRA="-DWOLFSSL_SYS_CA_CERTS=no" ;;
    native)
        # A native build ON a Mac wants the same two fences for the same two
        # reasons, minus the cross-compile words. ngtcp2's cmake calls
        # find_package(wolfssl), so a brew-installed wolfSSL is what it finds
        # ahead of the one built here -- the same hazard the cross branch
        # above measured, and the reason to name wolfSSL rather than ngtcp2
        # when someone goes looking for the offending package. And the CA
        # path still wants Security.framework linked into a PSK-only binary
        # that never verifies a certificate.
        #
        # Measured on squirtle 2026-09-03: with exactly these flags the QUIC
        # stack built natively in about a minute, so the root-path fence
        # standing without CMAKE_SYSTEM_NAME (which the cross branch needs
        # and a native build has no business setting) is a measurement and
        # not a guess. The log is in
        # docs/superpowers/specs/2026-09-03-macos-port-design.md, "Hardware
        # findings, 2026-09-03".
        if [ "$(uname)" = Darwin ]; then
            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"
            WOLF_XTRA="-DWOLFSSL_SYS_CA_CERTS=no"
        fi ;;
esac

# wolfSSL. Three flags are load-bearing, all found by link failure in the
# spike (spike/quic/README.md records the diagnosis):
#   -fno-sanitize=undefined  zig cc instruments C by default and the ubsan
#                            runtime is absent from a static musl link.
#   WOLFSSL_AESECB           ngtcp2's wolfSSL backend needs AES-ECB for QUIC
#                            header protection; wolfSSL omits it by default.
#   WOLFSSL_KEYLOG_EXPORT=no STANDING BUILD RULE. Built with it on, the
#                            binaries write every handshake's secrets to
#                            ./sslkeylog.log with nothing asking them to.
#                            It stays off here permanently; flip it only in
#                            a local throwaway build when decrypting your
#                            own capture, and never commit that.
cmake -S "$W/src/wolfssl-$WOLFSSL_VER" -B "$W/build/wolfssl-$T" $XTRA $WOLF_XTRA \
    -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-fno-sanitize=undefined -O2" \
    -DCMAKE_C_COMPILER="$CC" -DCMAKE_AR="$W/bin/zigar" -DCMAKE_RANLIB="$W/bin/zigranlib" \
    -DBUILD_SHARED_LIBS=OFF -DWOLFSSL_QUIC=yes -DWOLFSSL_PSK=yes \
    -DWOLFSSL_AESECB=yes -DWOLFSSL_AESCTR=yes -DWOLFSSL_KEYLOG_EXPORT=no \
    -DWOLFSSL_SESSION_TICKET=yes -DWOLFSSL_EXAMPLES=no -DWOLFSSL_CRYPT_TESTS=no \
    -DWOLFSSL_INSTALL=yes -DCMAKE_INSTALL_PREFIX="$OUT" >/dev/null
cmake --build "$W/build/wolfssl-$T" -j"$NJOBS" >/dev/null
cmake --install "$W/build/wolfssl-$T" >/dev/null

# ngtcp2 + its wolfSSL crypto backend. ENABLE_LIB_ONLY is what keeps the
# static link clean: ngtcp2's find_package(Libbrotli*) calls live inside
# `if(NOT ENABLE_LIB_ONLY)`, so a library-only build never goes looking for
# the host's shared brotli. Library sources, not example sources — the
# lesson that outlives cmake if this ever moves to addCSourceFiles.
PKG_CONFIG_PATH="$OUT/lib/pkgconfig" \
cmake -S "$W/src/ngtcp2-$NGTCP2_VER" -B "$W/build/ngtcp2-$T" $XTRA \
    -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-fno-sanitize=undefined -O2" \
    -DCMAKE_C_COMPILER="$CC" -DCMAKE_AR="$W/bin/zigar" -DCMAKE_RANLIB="$W/bin/zigranlib" \
    -DENABLE_STATIC_LIB=ON -DENABLE_SHARED_LIB=OFF -DENABLE_WOLFSSL=ON \
    -DENABLE_OPENSSL=OFF -DENABLE_GNUTLS=OFF -DENABLE_BORINGSSL=OFF \
    -DENABLE_LIB_ONLY=ON -DCMAKE_INSTALL_PREFIX="$OUT" >/dev/null
cmake --build "$W/build/ngtcp2-$T" -j"$NJOBS" >/dev/null
cmake --install "$W/build/ngtcp2-$T" >/dev/null

echo "deps/quic: $T ready in $OUT" >&2