commit a687ea88a10a85efa82c0d5caa8aebcded228003
parent ec872f18f7cfdbe0a2e9b5ff84bc5f1db93f4475
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 16 Jun 2026 11:47:18 -0700
Split test-cross build and run phases
Diffstat:
4 files changed, 123 insertions(+), 46 deletions(-)
diff --git a/doc/plan/PORT.md b/doc/plan/PORT.md
@@ -54,14 +54,14 @@ accepted as input aliases by `scripts/hosted.sh`.
```
make test-port # test-cross + test-selfhost
-make test-cross [TARGET=…] [DEPTH=…] [KIT_VM=…]
+make test-cross [TARGET=…] [DEPTH=…] [KIT_VM=…] [RUN=0|1]
make test-selfhost [TARGET=…] [DEPTH=…] [KIT_VM=…]
make kit-cross [TARGET=…] [CROSS_CC=kit|clang] [VERIFY=0|1]
make provision [TARGET=…] [KIT_VM=…]
```
-Defaults: `TARGET ?= all`, `DEPTH ?= smoke`, `KIT_VM ?= 1`, `CROSS_CC ?= kit`,
-`VERIFY ?= 0`.
+Defaults: `TARGET ?= all`, `DEPTH ?= smoke`, `KIT_VM ?= 1`, `RUN ?= 1`,
+`CROSS_CC ?= kit`, `VERIFY ?= 0`.
`test-port` is **not** part of the default `make test` (it is heavy and needs
provisioning). The native 3-stage `bootstrap` + `test-bootstrap-toy` stay in the
@@ -156,7 +156,8 @@ The Makefile reads it via `$(shell …)`; nothing else re-encodes the matrix.
### Two orchestrators
- `scripts/cross_test.sh <selector> [DEPTH]` — expand (mode=cross) → **fail-fast
- provisioning pre-flight** → per-token build/run per DEPTH → aggregate report.
+ provisioning pre-flight** → compile/link all smoke artifacts → optional
+ execution phase (`RUN=0` / `KIT_CROSS_RUN=0` disables it) → aggregate report.
- `scripts/selfhost.sh <selector> [DEPTH]` — expand (mode=selfhost) → pre-flight
→ per-token self-host (dispatch by OS) → on-target corpus → report.
diff --git a/mk/port.mk b/mk/port.mk
@@ -12,11 +12,13 @@
# <token> | a,b,c (see scripts/hosted.sh expand)
# DEPTH smoke (default) | full
# KIT_VM 1 (default; include freebsd/windows) | 0 (drop them)
+# RUN 1 (default; execute linked smoke/full artifacts) | 0 (build/link only)
# CROSS_CC kit (default) | clang — kit-cross toolchain backend
# VERIFY 0 (default) | 1 — kit-cross: run the built kit on-target
#
# Examples:
# make test-cross # smoke across the whole support set
+# make test-cross RUN=0 # compile/link across the set, no execution
# make test-cross TARGET=linux-glibc-x64 DEPTH=full
# make test-cross TARGET=freestanding # all 4 bare-metal arches
# make test-selfhost TARGET=freebsd-aa64
@@ -27,6 +29,7 @@
TARGET ?= all
DEPTH ?= smoke
KIT_VM ?= 1
+RUN ?= 1
CROSS_CC ?= kit
VERIFY ?= 0
@@ -35,7 +38,7 @@ VERIFY ?= 0
test-port: test-cross test-selfhost
test-cross: bin
- @KIT='$(abspath $(BIN))' KIT_VM='$(KIT_VM)' \
+ @KIT='$(abspath $(BIN))' KIT_VM='$(KIT_VM)' KIT_CROSS_RUN='$(RUN)' \
bash scripts/cross_test.sh '$(TARGET)' '$(DEPTH)'
test-selfhost: bin
diff --git a/scripts/cross_test.sh b/scripts/cross_test.sh
@@ -1,18 +1,21 @@
#!/usr/bin/env bash
# scripts/cross_test.sh — cross-compilation correctness orchestrator: the engine
# behind `make test-cross`. For each target in the (expanded) support set it
-# cross-compiles with the host kit and runs the artifact on that target through
-# the shared exec seam, checking the result.
+# cross-compiles and cross-links with the host kit, then optionally runs the
+# linked artifact on that target through the shared exec seam, checking the
+# result.
#
# cross_test.sh <selector> [DEPTH]
#
# selector hosted.sh grammar: all | <os> | linux-<libc> | <token> | a,b,c
-# DEPTH smoke (default) — one hello program, build + run, exit code (+ stdout)
+# DEPTH smoke (default) — one hello program, build/link + optional run,
+# exit code (+ stdout) when execution is enabled
# full — also the toy + parse corpora (+ libc on linux),
# cross-compiled and run on the target
#
# env: KIT (default build/kit), KIT_VM (default 1; 0 drops freebsd/windows),
-# DEPTH (overridden by the positional arg).
+# KIT_CROSS_RUN (default 1; 0 disables execution), DEPTH (overridden by the
+# positional arg).
#
# Provision-or-error: a requested target whose sysroot/image/VM is missing is a
# hard error (all missing reported up front), never a silent skip. Provision with
@@ -28,9 +31,13 @@ BUILD_DIR="$ROOT/build/test/cross"
SELECTOR="${1:-${TARGET:-all}}"
DEPTH="${2:-${DEPTH:-smoke}}"
export KIT_VM="${KIT_VM:-1}"
+KIT_CROSS_RUN="${KIT_CROSS_RUN:-${CROSS_RUN:-${RUN:-1}}}"
+case "$KIT_CROSS_RUN" in 0|false|FALSE|no|NO|off|OFF) KIT_CROSS_RUN=0 ;; *) KIT_CROSS_RUN=1 ;; esac
+export KIT_CROSS_RUN
[ -x "$KIT" ] || { echo "cross_test: kit not found at $KIT (run 'make bin')" >&2; exit 2; }
mkdir -p "$BUILD_DIR"
+RUN_LIST="$BUILD_DIR/.cross-run-list.$$"
# shellcheck source=../test/lib/kit_sh_report.sh
. "$ROOT/test/lib/kit_sh_report.sh"
@@ -81,7 +88,7 @@ preflight_token() {
echo "sysroot missing — run 'make provision TARGET=$t'"; return
fi ;;
esac
- if ! exec_target_supported "$tag"; then
+ if [ "$KIT_CROSS_RUN" -eq 1 ] && ! exec_target_supported "$tag"; then
case "$os" in
freestanding) echo "qemu-system missing for $tag — install qemu-system-$(printf '%s' "$($HOSTED triple "$t")" | sed 's/-none-elf//')" ;;
freebsd|windows) echo "VM not provisioned/reachable — run 'make provision TARGET=$t KIT_VM=1'" ;;
@@ -92,61 +99,98 @@ preflight_token() {
fi
}
-# ---- smoke lane ------------------------------------------------------------
+# ---- smoke build/link + run lanes -----------------------------------------
# Per target: the `exit` case (toolchain + link + crt + exit path; the only case
# freestanding can run) then `hello` (sysroot headers + libc + stdout, hosted
-# only) — each run once per link mode (link_modes): musl gets static + dynamic.
-smoke_token() {
+# only) — each built once per link mode (link_modes): musl gets static + dynamic.
+smoke_build_token() {
local t="$1" os name m
os="$(tok_os "$t")"
if [ "$os" = freestanding ]; then
- smoke_case "$t" exit "" # bare-metal: no link mode
+ smoke_build_case "$t" exit "" # bare-metal: no link mode
return
fi
for name in exit hello; do
for m in $(link_modes "$t"); do
- smoke_case "$t" "$name" "$m"
+ smoke_build_case "$t" "$name" "$m"
done
done
}
-# smoke_case TOKEN CASENAME MODE — build + run one case in one link mode, check
-# exit code (+ stdout if the case ships a <name>.stdout sidecar). MODE is
+run_list_add() { # kind label tag exe out err expected want-file
+ printf '%s|%s|%s|%s|%s|%s|%s|%s\n' "$@" >> "$RUN_LIST"
+}
+
+# smoke_build_case TOKEN CASENAME MODE — compile and link one case in one link
+# mode, then queue the linked artifact for phase-2 execution. MODE is
# static/dynamic (suffixes the lane) or default/"" (unsuffixed).
-smoke_case() {
- local t="$1" name="$2" mode="$3" os tag cdir label sfx src exp want got rc
+smoke_build_case() {
+ local t="$1" name="$2" mode="$3" os tag cdir label sfx src exp want_file
os="$(tok_os "$t")"; tag="$("$HOSTED" tag "$t")"
cdir="$BUILD_DIR/$t"; mkdir -p "$cdir"
src="$CASES/$name.c"
case "$mode" in static|dynamic) sfx=".$mode" ;; *) sfx="" ;; esac
label="$name/$t${sfx}"
exp=0; [ -f "$CASES/$name.expected" ] && exp="$(cat "$CASES/$name.expected")"
- want=""; [ -f "$CASES/$name.stdout" ] && want="$(cat "$CASES/$name.stdout")"
+ want_file="-"; [ -f "$CASES/$name.stdout" ] && want_file="$CASES/$name.stdout"
if [ "$os" = freestanding ]; then
- # Bare-metal: compile to an object; exec_bare links it with the reset stub
- # and the exit value is read back through the per-arch oracle (no stdout).
- local obj="$cdir/$name.o"
+ # Bare-metal: compile to an object, link with the reset stub now, and leave
+ # only the qemu boot for the execution phase.
+ local obj="$cdir/$name.o" elf="$cdir/$name.elf" barch reason
+ barch="$(tok_arch "$t")"
if ! "$HOSTED" cc "$t" -O1 -ffreestanding -c "$src" -o "$obj" \
>"$cdir/$name.cc.out" 2>"$cdir/$name.cc.err"; then
- kit_fail "$label" "cc failed"; sed 's/^/ | /' "$cdir/$name.cc.err" | head; return
+ kit_fail "$label:cc" "cc failed"; sed 's/^/ | /' "$cdir/$name.cc.err" | head; return
fi
- exec_target_run "$tag" "$obj" "$cdir/$name.out" "$cdir/$name.err"
+ kit_pass "$label:cc"
+ if ! exec_bare_setup_build "$barch" "$EXEC_BARE_WORK" >"$cdir/$name.bare.out" 2>"$cdir/$name.bare.err"; then
+ kit_fail "$label:ld" "bare setup failed"; sed 's/^/ | /' "$cdir/$name.bare.err" | head; return
+ fi
+ reason="$(exec_bare_link "$barch" "$obj" "$EXEC_BARE_WORK" "$elf" 2>&1)" || {
+ kit_fail "$label:ld" "${reason:-kit ld failed}"; return
+ }
+ kit_pass "$label:ld"
+ run_list_add bare "$label" "$tag" "$elf" "$cdir/$name.out" "$cdir/$name.err" "$exp" "$want_file"
else
local ext=""; [ "$os" = windows ] && ext=.exe
- local exe="$cdir/$name$sfx$ext"
- # shellcheck disable=SC2046
- if ! "$HOSTED" cc "$t" $(mode_flag "$mode") "$src" -o "$exe" \
+ local obj="$cdir/$name$sfx.o" exe="$cdir/$name$sfx$ext"
+ if ! "$HOSTED" cc "$t" -O1 -c "$src" -o "$obj" \
>"$cdir/$name$sfx.cc.out" 2>"$cdir/$name$sfx.cc.err"; then
- kit_fail "$label" "cc failed"; sed 's/^/ | /' "$cdir/$name$sfx.cc.err" | head; return
+ kit_fail "$label:cc" "cc failed"; sed 's/^/ | /' "$cdir/$name$sfx.cc.err" | head; return
fi
- exec_target_run "$tag" "$exe" "$cdir/$name$sfx.out" "$cdir/$name$sfx.err"
+ kit_pass "$label:cc"
+ # shellcheck disable=SC2046
+ if ! "$HOSTED" cc "$t" $(mode_flag "$mode") "$obj" -o "$exe" \
+ >"$cdir/$name$sfx.ld.out" 2>"$cdir/$name$sfx.ld.err"; then
+ kit_fail "$label:ld" "ld failed"; sed 's/^/ | /' "$cdir/$name$sfx.ld.err" | head; return
+ fi
+ kit_pass "$label:ld"
+ run_list_add target "$label" "$tag" "$exe" "$cdir/$name$sfx.out" "$cdir/$name$sfx.err" "$exp" "$want_file"
fi
+}
- rc="${RUN_RC:-127}"; got="$(cat "$cdir/$name$sfx.out" 2>/dev/null)"
- if [ "$((rc & 255))" -ne "$((exp & 255))" ]; then kit_fail "$label" "expected rc $exp, got $rc"
- elif [ -n "$want" ] && [ "$got" != "$want" ]; then kit_fail "$label" "stdout mismatch (want '$want' got '$got')"
- else kit_pass "$label"; fi
+smoke_run_all() {
+ local kind label tag exe out err exp want_file rc got want barch
+ [ -s "$RUN_LIST" ] || { echo "cross: no linked smoke artifacts to execute"; return; }
+ while IFS='|' read -r kind label tag exe out err exp want_file; do
+ case "$kind" in
+ bare)
+ barch="$(_exec_target_arch "$tag")"
+ exec_bare_run_image "$barch" "$exe" "$out" "$err" ;;
+ *)
+ exec_target_run "$tag" "$exe" "$out" "$err" ;;
+ esac
+ rc="${RUN_RC:-127}"; got="$(cat "$out" 2>/dev/null)"
+ want=""; [ "$want_file" != "-" ] && want="$(cat "$want_file")"
+ if [ "$((rc & 255))" -ne "$((exp & 255))" ]; then
+ kit_fail "$label:run" "expected rc $exp, got $rc"
+ elif [ "$want_file" != "-" ] && [ "$got" != "$want" ]; then
+ kit_fail "$label:run" "stdout mismatch (want '$want' got '$got')"
+ else
+ kit_pass "$label:run"
+ fi
+ done < "$RUN_LIST"
}
# ---- full lanes ------------------------------------------------------------
@@ -202,8 +246,8 @@ full_token() {
TOKENS="$("$HOSTED" expand "$SELECTOR" --mode=cross)" || exit 1
[ -n "$TOKENS" ] || { echo "cross_test: selector '$SELECTOR' expanded to nothing" >&2; exit 2; }
-printf 'cross: DEPTH=%s KIT_VM=%s\ncross: targets=%s\n' \
- "$DEPTH" "$KIT_VM" "$(echo $TOKENS | tr '\n' ' ')"
+printf 'cross: DEPTH=%s KIT_VM=%s KIT_CROSS_RUN=%s\ncross: targets=%s\n' \
+ "$DEPTH" "$KIT_VM" "$KIT_CROSS_RUN" "$(echo $TOKENS | tr '\n' ' ')"
missing=0
for t in $TOKENS; do
@@ -217,11 +261,26 @@ if [ "$missing" -eq 1 ]; then
exit 1
fi
+printf '\ncross: phase 1 — compile/link smoke cases\n'
+: > "$RUN_LIST"
for t in $TOKENS; do
- smoke_token "$t"
- [ "$DEPTH" = full ] && full_token "$t"
+ smoke_build_token "$t"
done
+if [ "$KIT_CROSS_RUN" -eq 1 ]; then
+ printf '\ncross: phase 2 — execute smoke cases\n'
+ smoke_run_all
+ if [ "$DEPTH" = full ]; then
+ printf '\ncross: phase 3 — full execution lanes\n'
+ for t in $TOKENS; do
+ full_token "$t"
+ done
+ fi
+else
+ printf '\ncross: phase 2 — execution disabled (KIT_CROSS_RUN=0)\n'
+ [ "$DEPTH" = full ] && printf 'cross: NOTE DEPTH=full lanes are execution lanes and were skipped\n'
+fi
+
KIT_SKIP_IS_FAILURE=0
kit_summary test-cross
kit_exit
diff --git a/test/lib/exec_bare.sh b/test/lib/exec_bare.sh
@@ -118,13 +118,12 @@ exec_bare_run_image() { # arch elf out err -> sets RUN_RC
RUN_RC="$(_bare_decode "$arch" "$RUN_RC_RAW")"
}
-# ---- corpus runner: setup (build per-arch stub) ----------------------------
-exec_bare_setup() { # arch work [entry]
+# ---- corpus runner: setup/link/run -----------------------------------------
+exec_bare_setup_build() { # arch work [entry] -> build per-arch stub
local arch work entry dir
arch="$(_bare_canon "$1")" || return 1
work="$2"; entry="${3:-${EXEC_BARE_ENTRY:-main}}"
[ -x "$EXEC_BARE_KIT" ] || return 1
- exec_bare_supported "$arch" || return 1
dir="$work/_bare/$arch"; mkdir -p "$dir"
rm -f "$dir/.ok"
case "$arch" in
@@ -140,14 +139,20 @@ exec_bare_setup() { # arch work [entry]
eval "EXEC_BARE_DIR_$arch=\$dir"
}
-# ---- corpus runner: run (link object + boot) -------------------------------
-exec_bare_run() { # arch obj work rcfile -> echoes reason; 0 ran / 2 build-fail
- local arch obj work rcf dir elf lderr rt
+exec_bare_setup() { # arch work [entry]
+ local arch
+ arch="$(_bare_canon "$1")" || return 1
+ exec_bare_supported "$arch" || return 1
+ exec_bare_setup_build "$arch" "$2" "${3:-${EXEC_BARE_ENTRY:-main}}"
+}
+
+exec_bare_link() { # arch obj work elf -> echoes reason; 0 linked / 2 failed
+ local arch obj work elf dir lderr rt
arch="$(_bare_canon "$1")" || { echo "unknown bare arch '$1'"; return 2; }
- obj="$2"; work="$3"; rcf="$4"
+ obj="$2"; work="$3"; elf="$4"
eval "dir=\"\${EXEC_BARE_DIR_$arch:-$work/_bare/$arch}\""
[ -f "$dir/.ok" ] || { echo "bare runner not set up for $arch"; return 2; }
- elf="$work/$(basename "$obj").$arch.elf"; lderr="$elf.ld.err"
+ lderr="$elf.ld.err"
if [ "$arch" = rv32 ]; then
rt="$EXEC_BARE_ROOT/build/rt/riscv32-elf-hardfloat/libkit_rt.a"
if ! "$EXEC_BARE_KIT" ld -T "$dir/link.ld" -e _start \
@@ -160,6 +165,15 @@ exec_bare_run() { # arch obj work rcfile -> echoes reason; 0 ran / 2 build-fai
echo "kit ld ($arch) failed: $(head -n1 "$lderr" 2>/dev/null)"; return 2
fi
fi
+ return 0
+}
+
+exec_bare_run() { # arch obj work rcfile -> echoes reason; 0 ran / 2 build-fail
+ local arch obj work rcf elf
+ arch="$(_bare_canon "$1")" || { echo "unknown bare arch '$1'"; return 2; }
+ obj="$2"; work="$3"; rcf="$4"
+ elf="$work/$(basename "$obj").$arch.elf"
+ exec_bare_link "$arch" "$obj" "$work" "$elf" || return 2
_bare_qemu_run "$arch" "$elf" "$elf.out" "$elf.err"
_bare_decode "$arch" "$RUN_RC_RAW" > "$rcf"
return 0