commit c7e3f5fb08e677e1bab9fb3734f4c2b42a396eac
parent af4c29e112fc606ecc1cff13311ce1f141863928
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 10 Jun 2026 14:07:37 -0700
feat(port): rv64 hybrid self-host (clang seed + emulated stage2/3)
rv64-linux has no native host on a macOS/arm64 dev box, and an in-container
clang stage1 would itself run under qemu emulation (slow). Build the stage1
seed on the host with clang at native speed (kit_cross.sh linux-musl-rv64
--cc=clang) and feed it to the 3-stage bootstrap; only stages 2/3 run emulated.
- mk/bootstrap.mk: BOOTSTRAP_SEED — a pre-built stage1. When set, stage1 is
copied from the seed and the host $(BIN) / outer rt are not prerequisites, so
no native compiler is needed in the container. Stages 2/3 stay byte-identical
(seed and stage2 are the same kit source = functionally identical compilers).
- scripts/linux_bootstrap.sh: KIT_LINUX_BOOT_SEED drives the seeded flow (no
clang/lld/binutils in the container — only make + libc dev + perl). Emulated
builds write objects to container-local storage instead of the :Z mount
(virtiofs flakes a new-file create under sustained emulated write load) and
copy the stage kits back to the host build dir.
- scripts/selfhost.sh: rv64 linux targets auto-build the clang seed then
bootstrap stages 2/3 emulated.
Validated: linux-musl-rv64 debug bootstrap is byte-identical (stage2 == stage3,
sha 837ade11…fef20b9).
Doc: doc/plan/PORT.md self-host shapes notes the rv64 hybrid.
Diffstat:
4 files changed, 97 insertions(+), 15 deletions(-)
diff --git a/doc/plan/PORT.md b/doc/plan/PORT.md
@@ -215,6 +215,14 @@ Exit-code oracle per arch (so callers compare `rc == expected` uniformly):
- **macos** → native 3-stage `bootstrap` here, then the corpus through stage3.
- **linux** → native 3-stage in a podman container (emulated for non-host arch),
then the corpus. Generalizes `scripts/linux_bootstrap.sh` to (arch, libc).
+ **rv64 uses a hybrid seed**: an in-container clang stage1 would itself run
+ emulated (slow), so the stage1 seed is cross-built on the host with clang
+ (`kit_cross.sh linux-musl-rv64 --cc=clang`, native speed) and fed to
+ `mk/bootstrap.mk` via `BOOTSTRAP_SEED`; only stages 2/3 run emulated. They are
+ still byte-identical because the seed and stage2 are the same kit source, hence
+ functionally identical compilers. Emulated builds write objects to
+ container-local storage (the `:Z` virtiofs mount flakes new-file creates under
+ sustained emulated write load) and copy the stage kits back.
- **freebsd** → native 3-stage in the VM, then the corpus
(`scripts/freebsd_bootstrap.sh`).
- **windows** → **cross**-build `kit.exe` on the host (the VM has no seed
diff --git a/mk/bootstrap.mk b/mk/bootstrap.mk
@@ -43,6 +43,22 @@ BOOTSTRAP_RT_LIBS = $(addprefix $(RT_BUILD_DIR)/,$(addsuffix /libkit_rt.a,$(RT_D
BOOTSTRAP_MAKEFILES = Makefile mk/env.mk mk/config.mk mk/flags.mk \
mk/lib_srcs.mk mk/driver_srcs.mk mk/rt.mk
+# BOOTSTRAP_SEED: a pre-built stage1 kit. Empty (default) = the normal native
+# flow where stage1 is the host $(BIN). When set (a path), stage1 is copied from
+# the seed and the host $(BIN) / outer rt are NOT prerequisites — this is how a
+# foreign-arch kit cross-compiled on the host (e.g. riscv64 via clang) bootstraps
+# itself: stages 2/3 (and their rt) run the seed under emulation in a container,
+# but the expensive stage1 was built natively on the host. Stages 2/3 are still
+# byte-identical because the seed and stage2 are the same kit source, hence
+# functionally identical compilers. Driven by scripts/linux_bootstrap.sh.
+BOOTSTRAP_SEED ?=
+BOOTSTRAP_STAGE1_SRC = $(BIN)
+BOOTSTRAP_DEPS = $(BIN) $(BOOTSTRAP_RT_LIBS) $(BOOTSTRAP_MAKEFILES)
+ifneq ($(BOOTSTRAP_SEED),)
+BOOTSTRAP_STAGE1_SRC = $(BOOTSTRAP_SEED)
+BOOTSTRAP_DEPS = $(BOOTSTRAP_SEED) $(BOOTSTRAP_MAKEFILES)
+endif
+
ifeq ($(RELEASE),1)
$(BOOTSTRAP_STAMP): HOST_OPTFLAGS = -O1
$(BOOTSTRAP_STAMP): BOOTSTRAP_HOST_MODE_CFLAGS = $(HOST_MODE_CFLAGS)
@@ -65,10 +81,10 @@ _do-bootstrap: $(BOOTSTRAP_STAMP)
# and scripts/windows_cross.sh per target. See doc/plan/PORT.md. `make bootstrap`
# here remains the native (host-arch) 3-stage byte-identical self-reproduction.
-$(BOOTSTRAP_STAMP): $(BIN) $(BOOTSTRAP_RT_LIBS) $(BOOTSTRAP_MAKEFILES)
+$(BOOTSTRAP_STAMP): $(BOOTSTRAP_DEPS)
rm -rf $(BOOTSTRAP_DIR)
@mkdir -p $(BOOTSTRAP_STAGE1_DIR)
- cp $(BIN) $(BOOTSTRAP_STAGE1_BIN)
+ cp $(BOOTSTRAP_STAGE1_SRC) $(BOOTSTRAP_STAGE1_BIN)
@for tool in $(BOOTSTRAP_TOOLS); do ln -sf kit "$(BOOTSTRAP_STAGE1_DIR)/$$tool"; done
$(MAKE) lib bin \
BUILD_DIR='$(abspath $(BOOTSTRAP_STAGE2_DIR))' \
diff --git a/scripts/linux_bootstrap.sh b/scripts/linux_bootstrap.sh
@@ -22,6 +22,13 @@
# KIT_LINUX_BOOT_IMAGE container image (defaults per libc+arch, below)
# KIT_LINUX_BOOT_PLATFORM podman --platform (default derived from arch)
# KIT_LINUX_BOOT_TOY=1 also run the Toy corpus through the stage3 compiler
+# KIT_LINUX_BOOT_SEED repo-relative path to a pre-built stage1 kit (the
+# "hybrid" flow). When set, stage1 is this seed instead
+# of an in-container clang build — so the seed can be
+# cross-compiled on the host at native speed and only
+# stages 2/3 run emulated. Intended for rv64, where an
+# in-container clang stage1 would itself run emulated.
+# Build one with: scripts/kit_cross.sh <target> --cc=clang
#
# The stage tree lands under build/linux-boot/<libc>/<arch>/ on the host
# (gitignored), so artifacts survive the run for inspection / per-object diffing.
@@ -65,8 +72,13 @@ case "$CHAIN" in
esac
TOY="${KIT_LINUX_BOOT_TOY:-0}"
+SEED="${KIT_LINUX_BOOT_SEED:-}"
BUILD_DIR="build/linux-boot/$LIBC/$ARCH"
+if [ -n "$SEED" ]; then
+ [ -x "$ROOT/$SEED" ] || { echo "linux_bootstrap: seed not found/executable: $ROOT/$SEED (build with scripts/kit_cross.sh)" >&2; exit 2; }
+fi
+
# In-container provisioning + build. The package sets give: a seed clang/lld,
# make, the libc dev headers, binutils (ar/ranlib used by the host stage1
# link), the ASan/UBSan runtime + unwinder for the -O0 stage1, and perl (the
@@ -74,30 +86,68 @@ BUILD_DIR="build/linux-boot/$LIBC/$ARCH"
# arena-allocated and never frees -- LeakSanitizer (absent on the macOS
# reference host, which is why this only bites on Linux) would otherwise abort
# every stage1 cc invocation.
-case "$LIBC" in
- musl)
- PROVISION='apk add --no-cache clang lld make musl-dev binutils compiler-rt libgcc perl-utils bash >/dev/null'
- ;;
- glibc)
- PROVISION='export DEBIAN_FRONTEND=noninteractive; apt-get update -qq >/dev/null && apt-get install -y -qq clang lld make libc6-dev binutils perl >/dev/null'
- ;;
-esac
+# When seeded, stage1 is the host-built kit and stages 2/3 are compiled by kit
+# itself (its own cc/ar/ld), so the container needs no clang/lld/binutils — only
+# make, the libc dev headers/crt, and perl (shasum). Otherwise install the full
+# seed-compiler set for the in-container clang stage1.
+if [ -n "$SEED" ]; then
+ case "$LIBC" in
+ musl) PROVISION='apk add --no-cache make musl-dev perl-utils bash >/dev/null' ;;
+ glibc) PROVISION='export DEBIAN_FRONTEND=noninteractive; apt-get update -qq >/dev/null && apt-get install -y -qq make libc6-dev perl >/dev/null' ;;
+ esac
+else
+ case "$LIBC" in
+ musl) PROVISION='apk add --no-cache clang lld make musl-dev binutils compiler-rt libgcc perl-utils bash >/dev/null' ;;
+ glibc) PROVISION='export DEBIAN_FRONTEND=noninteractive; apt-get update -qq >/dev/null && apt-get install -y -qq clang lld make libc6-dev binutils perl >/dev/null' ;;
+ esac
+fi
+
+# Hybrid (seeded) vs native stage1: a seed bypasses clang entirely and feeds
+# mk/bootstrap.mk a pre-built stage1. The seeded flow is emulated (a foreign
+# arch under binfmt), so it builds into container-local storage rather than the
+# :Z mount: virtiofs/9p occasionally fails a new-file create under sustained
+# emulated write load (hundreds of objects rapid-fire), which would derail an
+# otherwise-good build. Source reads from /work stay reliable; only the stage
+# kit binaries are copied back to the host BUILD_DIR for inspection.
+if [ -n "$SEED" ]; then
+ EFF_BUILD_DIR="/root/boot"
+ BUILD_CMD="make $TARGET BUILD_DIR='$EFF_BUILD_DIR' BOOTSTRAP_SEED='/work/$SEED'"
+ SEED_BANNER="echo \"stage1 seed: /work/$SEED\"; /work/$SEED --help >/dev/null 2>&1 && echo 'seed runs' || echo 'seed FAILED to run'"
+ SEEDED=1
+else
+ EFF_BUILD_DIR="$BUILD_DIR"
+ BUILD_CMD="make $TARGET BUILD_DIR='$EFF_BUILD_DIR' CC=clang AR=ar"
+ SEED_BANNER='clang --version | head -1'
+ SEEDED=0
+fi
read -r -d '' REMOTE <<EOF || true
set -eu
$PROVISION
cd /work
+EFF_BUILD_DIR='$EFF_BUILD_DIR'
echo "=== linux_bootstrap: \$(uname -m) / $LIBC / $TARGET ==="
-clang --version | head -1
-make $TARGET BUILD_DIR='$BUILD_DIR' CC=clang AR=ar
+$SEED_BANNER
+$BUILD_CMD
if [ "$TOY" = "1" ]; then
for m in debug release; do
- s3="$BUILD_DIR/\$m/bootstrap/stage3/kit"
+ s3="\$EFF_BUILD_DIR/\$m/bootstrap/stage3/kit"
[ -x "\$s3" ] || continue
echo "=== Toy corpus through \$m stage3 ==="
KIT="\$(pwd)/\$s3" test/toy/run.sh || true
done
fi
+if [ "$SEEDED" = 1 ]; then
+ for m in debug release; do
+ for st in stage2 stage3; do
+ k="\$EFF_BUILD_DIR/\$m/bootstrap/\$st/kit"
+ [ -x "\$k" ] || continue
+ mkdir -p "/work/$BUILD_DIR/\$m/bootstrap/\$st"
+ cp "\$k" "/work/$BUILD_DIR/\$m/bootstrap/\$st/kit"
+ done
+ done
+ echo "=== linux_bootstrap: copied stage kits to $BUILD_DIR ==="
+fi
echo "=== linux_bootstrap: DONE $TARGET ==="
EOF
diff --git a/scripts/selfhost.sh b/scripts/selfhost.sh
@@ -103,9 +103,10 @@ macos_stage3_hello() {
# ---- dispatch --------------------------------------------------------------
selfhost_token() {
- local t="$1" os arch libc toy
+ local t="$1" os arch libc toy seed_env
os="$(tok_os "$t")"; arch="$(tok_arch "$t")"; libc="$(tok_libc "$t")"
toy=0; [ "$DEPTH" = full ] && toy=1
+ seed_env=""
case "$os" in
macos)
if [ "$DEPTH" = full ]; then
@@ -115,7 +116,14 @@ selfhost_token() {
run_step "$t:run" macos_stage3_hello
fi ;;
linux)
- run_step "$t:bootstrap" env KIT_LINUX_BOOT_ARCH="$arch" KIT_LINUX_BOOT_TOY="$toy" \
+ # rv64 has no native host here, and an in-container clang stage1 would
+ # itself run emulated. Build the stage1 seed via clang cross on the host
+ # (native speed), then bootstrap stages 2/3 in the emulated container.
+ if [ "$arch" = rv64 ]; then
+ run_step "$t:seed" bash "$ROOT/scripts/kit_cross.sh" "linux-$libc-rv64" --cc=clang || return 1
+ seed_env="KIT_LINUX_BOOT_SEED=build/kit-cross/clang/linux-$libc-rv64/kit"
+ fi
+ run_step "$t:bootstrap" env KIT_LINUX_BOOT_ARCH="$arch" KIT_LINUX_BOOT_TOY="$toy" $seed_env \
bash "$ROOT/scripts/linux_bootstrap.sh" "$libc" both ;;
freebsd)
run_step "$t:bootstrap" env KIT_FREEBSD_BOOT_TOY="$toy" \