Linux and WSL builder

This recipe follows the current distro workflow on an x86_64 Debian/Ubuntu Linux builder, including Ubuntu in WSL2. The imported Alpine Rose runtime is the output, not the recommended build environment. Commands run in Bash inside Linux unless explicitly labeled Windows PowerShell.

1. Prepare a builder

On Windows, use a separate Ubuntu WSL2 distribution for development, keeping the managed 404 and operator 404-cli distributions separate. If needed, install Ubuntu from Windows PowerShell using wsl --install -d Ubuntu-24.04, then open it. Keep the checkout in the Linux filesystem, such as $HOME/404-builder, rather than under /mnt/c.

Install Rust/rustup through Rust’s installation instructions, Node.js 20 with npm, and a working Docker engine. On WSL, enable Docker Desktop’s WSL integration for the builder; on native Linux, use Docker Engine. docker info must succeed as your build user. Do not run the entire Rust build as root merely to access Docker.

sudo apt-get update
sudo apt-get install -y \
  git curl ca-certificates clang llvm libclang-dev musl-tools \
  pkg-config cmake ninja-build perl make g++ xz-utils \
  iproute2 libbpf-dev libelf-dev linux-libc-dev
uname -m
node --version
npm --version
rustup --version
docker info

Expect x86_64 and Node 20.x for this source/CI recipe. linux-libc-dev supplies /usr/include/linux/bpf.h; the BPF Makefile also checks bpf_helpers.h, clang, llvm-strip, and tc. A Debian package named after WSL’s Microsoft kernel may not exist, so installing linux-headers-$(uname -r) is not a universal WSL prerequisite.

2. Pin one source checkout

git clone https://github.com/un-nf/404.git "$HOME/404-builder"
cd "$HOME/404-builder"
SOURCE_REF=10ee191ec3f7132ac51b359988d3326f9cd2aab2
git checkout "$SOURCE_REF"
rustup target add x86_64-unknown-linux-musl
npm ci --prefix src/STATIC_proxy/build

Use a fresh destination or an existing deliberate checkout. The pinned reference is the source inspected for this recipe; choose an explicit release tag when reproducing that release and inspect its corresponding workflow. build.rs invokes the JS bundler into Cargo’s output directory during compilation and refuses to proceed without Node/npm or a fresh bundle.

3. Install the matching Zig compiler

CI uses Zig 0.14.1. Download its x86_64 Linux archive using the official release metadata, compare its SHA-256, and unpack into this checkout’s toolchain directory:

mkdir -p .toolchains
curl -fsSL https://ziglang.org/download/index.json -o .toolchains/zig-index.json
node --input-type=module - <<'JS' > .toolchains/zig-release.txt
import { readFileSync } from 'node:fs';
const release = JSON.parse(readFileSync('.toolchains/zig-index.json', 'utf8'))['0.14.1']['x86_64-linux'];
if (!release?.tarball || !release?.shasum) throw new Error('Missing Zig release metadata');
console.log(release.tarball);
console.log(release.shasum);
JS
mapfile -t zig_release < .toolchains/zig-release.txt
curl -fsSL "${zig_release[0]}" -o .toolchains/zig.tar.xz
printf '%s  %s\n' "${zig_release[1]}" .toolchains/zig.tar.xz | sha256sum -c -
mkdir -p .toolchains/zig
tar -xJf .toolchains/zig.tar.xz -C .toolchains/zig --strip-components=1
export ZIG_BIN="$PWD/.toolchains/zig/zig"
"$ZIG_BIN" version

The checksum detects a download mismatch with the fetched official metadata; it is not an independent signing-key trust check. Keep the version aligned with the selected workflow.

4. Wire musl C, C++, and the linker runtime

bash ./scripts/setup-musl-toolchain.sh \
  --wrapper-dir "$PWD/.toolchains/musl/bin" \
  --zig-bin "$ZIG_BIN" --target x86_64-linux-musl
export PATH="$PWD/.toolchains/musl/bin:$PATH"
export LIBCLANG_PATH="$(bash ./scripts/resolve-libclang-path.sh)"
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="$(
  bash ./scripts/resolve-zig-cxx-runtime.sh \
    --zig-bin "$ZIG_BIN" --target x86_64-linux-musl
)"
export CC_x86_64_unknown_linux_musl=musl-gcc
export CXX_x86_64_unknown_linux_musl=musl-g++
export BORING_BSSL_RUST_CPPLIB_x86_64_unknown_linux_musl=c++

The wrapper normalizes Rust-style target arguments before calling Zig. The runtime resolver compiles a small C++ probe, locates Zig’s libc++.a, libc++abi.a, and libunwind.a, and emits the Rust linker search paths/group flags. This addresses the current native TLS dependency’s BoringSSL C++ sources; a C-only musl-gcc setup is insufficient. Shared libclang is needed for bindgen as well as the clang executable.

5. Compile and export

cargo build --release --locked \
  --manifest-path src/STATIC_proxy/Cargo.toml \
  --bin static_proxy --target x86_64-unknown-linux-musl
make -C src/ebpf clean all
./distro/build.sh \
  --static-binary "$PWD/src/STATIC_proxy/target/x86_64-unknown-linux-musl/release/static_proxy" \
  --ttl-object "$PWD/src/ebpf/ttl_editor.o" \
  --version v0.0.0-local \
  --output "$PWD/dist/404-distro.tar.gz" \
  --image-tag 404-distro-build:local
tar -tzf dist/404-distro.tar.gz | head -100

Check for opt/404/static, ttl_editor.o, 404-init.sh, distro-version, and etc/wsl.conf. docker export creates the flat rootfs; docker save is the wrong format. A local tarball is not an official signed release. Signing/publishing production artifacts belongs to the release workflow, not this local builder.

6. Test a disposable WSL runtime

Copy your tarball to a known Windows path, then use Windows PowerShell:

$DistroName = "404-dev"
wsl --import $DistroName "$env:LOCALAPPDATA\404\wsl\$DistroName" "C:\path\404-distro.tar.gz" --version 2
wsl -d $DistroName -- sh -lc 'cat /opt/404/distro-version; ls -l /opt/404/static /opt/404/ttl_editor.o'

The tarball does not provision Windows runtime config, profiles, token, or trust. Stage those using the Windows operator guide, consistently substitute 404-dev, write /opt/404/win-user, and explicitly run /opt/404/404-init.sh. The runtime uses Alpine sh; repository helpers use Bash in the builder environment. Check eBPF map/filter/wire state separately from proxy readiness.

Build failures to distinguish

FailureCheck
bindgen cannot find libclanglibclang-dev and the directory returned by resolve-libclang-path.sh.
BoringSSL C++ compile/link errorsBoth wrappers on PATH, the target-specific CXX/BORING variables, and runtime resolver flags.
eBPF prerequisites missingclang, LLVM strip, tc, libbpf and Linux UAPI headers.
Docker cannot connectBuilder’s daemon/integration and user permissions; separate from Cargo.
Imported distro opens a shellExpected: no automatic boot command; stage config and invoke the launcher.

This recipe is traced to CI and its helper scripts. The documentation build does not constitute an end-to-end Rust/Docker build or a Windows runtime test; validate those in your chosen builder before publishing an artifact.