cross_test.sh (18310B)
1 #!/usr/bin/env bash 2 # scripts/cross_test.sh — cross-compilation correctness orchestrator: the engine 3 # behind `make test-cross`. For each target in the (expanded) support set it 4 # cross-compiles and cross-links with the host kit, then optionally runs the 5 # linked artifact on that target through the shared exec seam, checking the 6 # result. 7 # 8 # cross_test.sh <selector> [DEPTH] 9 # 10 # selector hosted.sh grammar: all | <os> | linux-<libc> | <token> | a,b,c 11 # DEPTH coarse — smoke cases plus compile+run front/back amalgam 12 # at -O0 and -O1 (execution follows RUN) 13 # smoke (default) — exit/hello smoke cases, build/link + optional run, 14 # exit code (+ stdout) when execution is enabled 15 # full — coarse lane + toy/parse/libc corpora 16 # 17 # env: KIT (default build/kit), KIT_VM (default 1; 0 drops freebsd/windows), 18 # KIT_CROSS_RUN (default 1; 0 disables execution), DEPTH (overridden by the 19 # positional arg). 20 # 21 # Provision-or-error: a requested target whose sysroot/image/VM is missing is a 22 # hard error (all missing reported up front), never a silent skip. Provision with 23 # `make provision TARGET=…` (see doc/plan/PORT.md). 24 25 set -u 26 27 ROOT="$(cd "$(dirname "$0")/.." && pwd)" 28 KIT="${KIT:-$ROOT/build/kit}" 29 HOSTED="$ROOT/scripts/hosted.sh" 30 CASES="$ROOT/test/cross/cases" 31 BUILD_DIR="$ROOT/build/test/cross" 32 SELECTOR="${1:-${TARGET:-all}}" 33 DEPTH="${2:-${DEPTH:-smoke}}" 34 export KIT_VM="${KIT_VM:-1}" 35 KIT_CROSS_RUN="${KIT_CROSS_RUN:-${CROSS_RUN:-${RUN:-1}}}" 36 case "$KIT_CROSS_RUN" in 0|false|FALSE|no|NO|off|OFF) KIT_CROSS_RUN=0 ;; *) KIT_CROSS_RUN=1 ;; esac 37 case "$DEPTH" in 38 coarse|smoke|full) ;; 39 *) echo "cross_test: bad DEPTH '$DEPTH' (want coarse|smoke|full)" >&2; exit 2 ;; 40 esac 41 export KIT_CROSS_RUN 42 43 [ -x "$KIT" ] || { echo "cross_test: kit not found at $KIT (run 'make bin')" >&2; exit 2; } 44 mkdir -p "$BUILD_DIR" 45 RUN_LIST_SMOKE="$BUILD_DIR/.cross-run-list-smoke.$$" 46 RUN_LIST_COARSE="$BUILD_DIR/.cross-run-list-coarse.$$" 47 LIST_PATH="$RUN_LIST_SMOKE" 48 49 # shellcheck source=../test/lib/kit_sh_report.sh 50 . "$ROOT/test/lib/kit_sh_report.sh" 51 52 # exec_target caller contract (linux/macos runners read these; VM/bare ignore). 53 have_podman=0; command -v podman >/dev/null 2>&1 && have_podman=1 54 QEMU_BIN="${QEMU_BIN:-$(command -v qemu-aarch64 2>/dev/null || true)}" 55 QEMU_RV64_BIN="${QEMU_RV64_BIN:-$(command -v qemu-riscv64 2>/dev/null || true)}" 56 have_qemu=0; [ -n "$QEMU_BIN" ] && have_qemu=1 57 case "$(uname -m 2>/dev/null)" in aarch64|arm64) is_aarch64=1 ;; *) is_aarch64=0 ;; esac 58 export have_podman QEMU_BIN QEMU_RV64_BIN have_qemu is_aarch64 59 EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"; export EXEC_TARGET_MOUNT_ROOT 60 # shellcheck source=../test/lib/exec_target.sh 61 . "$ROOT/test/lib/exec_target.sh" 62 63 kit_report_init 64 trap exec_target_teardown_all EXIT 65 66 # ---- token helpers --------------------------------------------------------- 67 tok_os() { printf '%s' "${1%%-*}"; } 68 tok_arch() { printf '%s' "${1##*-}"; } 69 tok_libc() { case "$1" in linux-*) local r="${1#linux-}"; printf '%s' "${r%%-*}" ;; *) printf '' ;; esac; } 70 71 # Per-arch -march/-mabi a freestanding corpus object must be compiled with so 72 # its ELF e_flags match the bare-metal stub + runtime it is linked against 73 # (test/lib/exec_bare.sh). Both RISC-V widths have a float-ABI axis, so make the 74 # contract explicit rather than inheriting a compiler default. rv32's harness 75 # is hard-single (ilp32f); rv64 enables D and uses hard-double (lp64d). Keep in 76 # sync with _bare_emit_rv32/_bare_emit_rv64 and test/toy/run.sh. 77 freestanding_abi_flags() { # token -> echoes extra cc flags ("" for most arches) 78 case "$(tok_arch "$1")" in 79 rv32) printf '%s' "-march=rv32imafc_zicsr_zifencei -mabi=ilp32f" ;; 80 rv64) printf '%s' "-march=rv64imafd_zicsr_zifencei -mabi=lp64d" ;; 81 *) printf '' ;; 82 esac 83 } 84 85 # Link modes to smoke per target. musl exercises BOTH static (libc.a) and 86 # dynamic (libc.so) — the two ways the sysroot is consumed. freebsd is static 87 # (self-contained base). Everything else uses the toolchain default (one mode; 88 # glibc is dynamic-only — static glibc is discouraged/NSS-fragile). 89 link_modes() { 90 case "$1" in 91 linux-musl-*) echo "static dynamic" ;; 92 freebsd-*) echo "static" ;; 93 *) echo "default" ;; 94 esac 95 } 96 # static -> libc.a; dynamic -> kit's default link for a libc sysroot is already 97 # dynamic (.interp + libc.so), and `-dynamic` is not a kit flag, so dynamic and 98 # the single-mode default both pass no link flag. 99 mode_flag() { case "$1" in static) echo -static ;; *) echo ;; esac; } 100 101 # ---- provisioning pre-flight ---------------------------------------------- 102 # Echo a one-line missing-reason for a token, or nothing if ready to build+run. 103 preflight_token() { 104 local t="$1" os tag sr 105 os="$(tok_os "$t")"; tag="$("$HOSTED" tag "$t")"; sr="$("$HOSTED" path "$t" 2>/dev/null)" 106 case "$os" in 107 linux|freebsd|windows|android) 108 if [ -z "$sr" ] || [ ! -d "$sr" ]; then 109 echo "sysroot missing — run 'make provision TARGET=$t'"; return 110 fi ;; 111 esac 112 if [ "$KIT_CROSS_RUN" -eq 1 ] && ! exec_target_supported "$tag"; then 113 case "$os" in 114 freestanding) echo "qemu-system missing for $tag — install qemu-system-$(printf '%s' "$($HOSTED triple "$t")" | sed 's/-none-elf//')" ;; 115 freebsd|windows) echo "VM not provisioned/reachable — run 'make provision TARGET=$t KIT_VM=1'" ;; 116 macos) echo "macOS exec needs a Darwin host with matching arch" ;; 117 linux) echo "no linux runner — run 'make provision TARGET=$t' (sysroot) + 'make test-images'/'make hosted-glibc-images' (run image)" ;; 118 android) echo "no Android runner — use RUN=0 for build/link-only validation" ;; 119 *) echo "no runner for $tag" ;; 120 esac 121 fi 122 } 123 124 # ---- smoke build/link + run lanes ----------------------------------------- 125 # Per target: the `exit` case (toolchain + link + crt + exit path; the only case 126 # freestanding can run) then `hello` (sysroot headers + libc + stdout, hosted 127 # only) — each built once per link mode (link_modes): musl gets static + dynamic. 128 smoke_build_token() { 129 local t="$1" os name m 130 os="$(tok_os "$t")" 131 if [ "$os" = android ]; then 132 smoke_build_android "$t" 133 return 134 fi 135 if [ "$os" = freestanding ]; then 136 smoke_build_case "$t" exit "" # bare-metal: no link mode 137 return 138 fi 139 for name in exit hello; do 140 for m in $(link_modes "$t"); do 141 smoke_build_case "$t" "$name" "$m" 142 done 143 done 144 } 145 146 smoke_build_android() { 147 local t="$1" tag cdir src obj so label 148 tag="$("$HOSTED" tag "$t")" 149 cdir="$BUILD_DIR/$t"; mkdir -p "$cdir" 150 src="$CASES/native_activity.c" 151 obj="$cdir/native_activity.o" 152 so="$cdir/libkit_native_activity.so" 153 label="native_activity/$t" 154 if ! "$HOSTED" cc "$t" -O1 -fPIC -c "$src" -o "$obj" \ 155 >"$cdir/native_activity.cc.out" 2>"$cdir/native_activity.cc.err"; then 156 kit_fail "$label:cc" "cc failed"; sed 's/^/ | /' "$cdir/native_activity.cc.err" | head; return 157 fi 158 kit_pass "$label:cc" 159 if ! "$HOSTED" cc "$t" -shared "$obj" -landroid -o "$so" \ 160 >"$cdir/native_activity.ld.out" 2>"$cdir/native_activity.ld.err"; then 161 kit_fail "$label:ld" "ld failed"; sed 's/^/ | /' "$cdir/native_activity.ld.err" | head; return 162 fi 163 if [ ! -s "$so" ]; then 164 kit_fail "$label:ld" "missing shared object" 165 return 166 fi 167 : "$tag" 168 kit_pass "$label:ld" 169 } 170 171 coarse_build_token() { 172 local t="$1" os mode opt sfx cdir tag 173 os="$(tok_os "$t")" 174 if [ "$os" = android ]; then 175 printf 'cross: NOTE %s — Android coarse lane uses the native_activity shared-library smoke only\n' "$t" 176 return 177 fi 178 tag="$($HOSTED tag "$t")" 179 cdir="$BUILD_DIR/$t"; mkdir -p "$cdir" 180 for opt in O0 O1; do 181 if [ "$os" = freestanding ]; then 182 local fobj="$cdir/c_frontback_amalgam.${opt}.o" 183 local out="$cdir/c_frontback_amalgam.${opt}.out" 184 local err="$cdir/c_frontback_amalgam.${opt}.err" 185 # shellcheck disable=SC2046 186 if ! "$HOSTED" cc "$t" -$opt $(freestanding_abi_flags "$t") -ffreestanding -c "$CASES/c_frontback_amalgam.c" -o "$fobj" \ 187 >"$cdir/c_frontback_amalgam.$opt.cc.out" 2>"$cdir/c_frontback_amalgam.$opt.cc.err"; then 188 kit_fail "frontback_amalgam/$t:$opt:cc" "cc failed"; sed 's/^/ | /' "$cdir/c_frontback_amalgam.$opt.cc.err" | head; return 189 fi 190 kit_pass "frontback_amalgam/$t:$opt:cc" 191 run_list_add target "frontback_amalgam/$t:$opt" "$tag" "$fobj" "$out" "$err" 0 - 192 else 193 for mode in $(link_modes "$t"); do 194 case "$mode" in static|dynamic) sfx=".$mode" ;; *) sfx="" ;; esac 195 local ext=""; [ "$os" = windows ] && ext=.exe 196 local fobj="$cdir/c_frontback_amalgam_${opt}${sfx}.o" 197 local fexe="$cdir/c_frontback_amalgam_${opt}${sfx}$ext" 198 if ! "$HOSTED" cc "$t" -$opt -c "$CASES/c_frontback_amalgam.c" -o "$fobj" \ 199 >"$cdir/c_frontback_amalgam_${opt}${sfx}.cc.out" 2>"$cdir/c_frontback_amalgam_${opt}${sfx}.cc.err"; then 200 kit_fail "frontback_amalgam/$t:$opt$sfx:cc" "cc failed"; sed 's/^/ | /' "$cdir/c_frontback_amalgam_${opt}${sfx}.cc.err" | head; return 201 fi 202 kit_pass "frontback_amalgam/$t:$opt$sfx:cc" 203 # shellcheck disable=SC2046 204 if ! "$HOSTED" cc "$t" -$opt $(mode_flag "$mode") "$fobj" -o "$fexe" \ 205 >"$cdir/c_frontback_amalgam_${opt}${sfx}.ld.out" 2>"$cdir/c_frontback_amalgam_${opt}${sfx}.ld.err"; then 206 kit_fail "frontback_amalgam/$t:$opt$sfx:ld" "ld failed"; sed 's/^/ | /' "$cdir/c_frontback_amalgam_${opt}${sfx}.ld.err" | head; return 207 fi 208 kit_pass "frontback_amalgam/$t:$opt$sfx:ld" 209 run_list_add target "frontback_amalgam/$t:$opt$sfx" "$tag" "$fexe" \ 210 "$cdir/c_frontback_amalgam_${opt}${sfx}.out" "$cdir/c_frontback_amalgam_${opt}${sfx}.err" 0 - 211 done 212 fi 213 done 214 } 215 216 run_list_add() { # kind label tag exe out err expected want-file 217 printf '%s|%s|%s|%s|%s|%s|%s|%s\n' "$@" >> "$LIST_PATH" 218 } 219 220 # smoke_build_case TOKEN CASENAME MODE — compile and link one case in one link 221 # mode, then queue the linked artifact for phase-2 execution. MODE is 222 # static/dynamic (suffixes the lane) or default/"" (unsuffixed). 223 smoke_build_case() { 224 local t="$1" name="$2" mode="$3" os tag cdir label sfx src exp want_file 225 os="$(tok_os "$t")"; tag="$("$HOSTED" tag "$t")" 226 cdir="$BUILD_DIR/$t"; mkdir -p "$cdir" 227 src="$CASES/$name.c" 228 case "$mode" in static|dynamic) sfx=".$mode" ;; *) sfx="" ;; esac 229 label="$name/$t${sfx}" 230 exp=0; [ -f "$CASES/$name.expected" ] && exp="$(cat "$CASES/$name.expected")" 231 want_file="-"; [ -f "$CASES/$name.stdout" ] && want_file="$CASES/$name.stdout" 232 233 if [ "$os" = freestanding ]; then 234 # Bare-metal: compile to an object, link with the reset stub now, and leave 235 # only the qemu boot for the execution phase. 236 local obj="$cdir/$name.o" elf="$cdir/$name.elf" barch reason smopt 237 barch="$(tok_arch "$t")" 238 # Every freestanding arch (arm32 included, since Phase 2 landed the -O1 239 # known-frame path) exercises the optimizer at -O1. 240 smopt=O1 241 # shellcheck disable=SC2046 242 if ! "$HOSTED" cc "$t" -$smopt $(freestanding_abi_flags "$t") -ffreestanding -c "$src" -o "$obj" \ 243 >"$cdir/$name.cc.out" 2>"$cdir/$name.cc.err"; then 244 kit_fail "$label:cc" "cc failed"; sed 's/^/ | /' "$cdir/$name.cc.err" | head; return 245 fi 246 kit_pass "$label:cc" 247 if ! exec_bare_setup_build "$barch" "$EXEC_BARE_WORK" >"$cdir/$name.bare.out" 2>"$cdir/$name.bare.err"; then 248 kit_fail "$label:ld" "bare setup failed"; sed 's/^/ | /' "$cdir/$name.bare.err" | head; return 249 fi 250 reason="$(exec_bare_link "$barch" "$obj" "$EXEC_BARE_WORK" "$elf" 2>&1)" || { 251 kit_fail "$label:ld" "${reason:-kit ld failed}"; return 252 } 253 kit_pass "$label:ld" 254 run_list_add bare "$label" "$tag" "$elf" "$cdir/$name.out" "$cdir/$name.err" "$exp" "$want_file" 255 else 256 local ext=""; [ "$os" = windows ] && ext=.exe 257 local obj="$cdir/$name$sfx.o" exe="$cdir/$name$sfx$ext" 258 if ! "$HOSTED" cc "$t" -O1 -c "$src" -o "$obj" \ 259 >"$cdir/$name$sfx.cc.out" 2>"$cdir/$name$sfx.cc.err"; then 260 kit_fail "$label:cc" "cc failed"; sed 's/^/ | /' "$cdir/$name$sfx.cc.err" | head; return 261 fi 262 kit_pass "$label:cc" 263 # shellcheck disable=SC2046 264 if ! "$HOSTED" cc "$t" $(mode_flag "$mode") "$obj" -o "$exe" \ 265 >"$cdir/$name$sfx.ld.out" 2>"$cdir/$name$sfx.ld.err"; then 266 kit_fail "$label:ld" "ld failed"; sed 's/^/ | /' "$cdir/$name$sfx.ld.err" | head; return 267 fi 268 kit_pass "$label:ld" 269 run_list_add target "$label" "$tag" "$exe" "$cdir/$name$sfx.out" "$cdir/$name$sfx.err" "$exp" "$want_file" 270 fi 271 } 272 273 cross_run_all() { 274 local list="$1" 275 local kind label tag exe out err exp want_file rc got want barch 276 [ -s "$list" ] || { echo "cross: no linked artifacts to execute"; return; } 277 # Read the list on a private fd (9), not stdin: the runners below spawn 278 # children that read host stdin (qemu-system -nographic muxes the guest UART 279 # onto stdio; VM readiness ssh probes forward stdin), which would otherwise 280 # drain the list mid-loop and truncate execution after the first such case. 281 # Each runner also gets </dev/null so it can never block on or consume stdin. 282 while IFS='|' read -r kind label tag exe out err exp want_file <&9; do 283 case "$kind" in 284 bare) 285 barch="$(_exec_target_arch "$tag")" 286 exec_bare_run_image "$barch" "$exe" "$out" "$err" </dev/null ;; 287 *) 288 exec_target_run "$tag" "$exe" "$out" "$err" </dev/null ;; 289 esac 290 rc="${RUN_RC:-127}"; got="$(cat "$out" 2>/dev/null)" 291 want=""; [ "$want_file" != "-" ] && want="$(cat "$want_file")" 292 if [ "$((rc & 255))" -ne "$((exp & 255))" ]; then 293 kit_fail "$label:run" "expected rc $exp, got $rc" 294 elif [ "$want_file" != "-" ] && [ "$got" != "$want" ]; then 295 kit_fail "$label:run" "stdout mismatch (want '$want' got '$got')" 296 else 297 kit_pass "$label:run" 298 fi 299 done 9< "$list" 300 } 301 302 # ---- full lanes ------------------------------------------------------------ 303 # Run an existing corpus runner as a sub-process; its exit status is one verdict. 304 _full_lane() { 305 local label="$1"; shift 306 printf '\ncross: full lane %s →\n' "$label" 307 if "$@"; then kit_pass "$label"; else kit_fail "$label" "lane failed (see output above)"; fi 308 } 309 310 full_token() { 311 local t="$1" os arch libc vm_arch 312 os="$(tok_os "$t")"; arch="$(tok_arch "$t")"; libc="$(tok_libc "$t")" 313 case "$os" in 314 linux) 315 _full_lane "$t:toy" env KIT="$KIT" KIT_TOY_CROSS_ARCHS="$arch" KIT_TEST_PATHS=X bash "$ROOT/test/toy/run.sh" 316 _full_lane "$t:parse" env KIT="$KIT" KIT_TEST_ARCH="$arch" KIT_TEST_PATHS=E bash "$ROOT/test/parse/run.sh" 317 _full_lane "$t:libc" env KIT="$KIT" KIT_LIBC_ARCHES="$arch" bash "$ROOT/test/libc/$libc/run.sh" 318 ;; 319 macos) 320 local ha; ha="$(uname -m 2>/dev/null)" 321 case "$arch:$ha" in 322 aa64:arm64|aa64:aarch64|x64:x86_64|x64:amd64) 323 # Native arch: run the corpus in-process. 324 _full_lane "$t:toy" env KIT="$KIT" bash "$ROOT/test/toy/run.sh" 325 _full_lane "$t:parse" env KIT="$KIT" bash "$ROOT/test/parse/run.sh" ;; 326 *) 327 # Cross arch (e.g. x86_64 on Apple silicon): cross-compile the toy 328 # corpus and run it via Rosetta 2 through the shared exec_target seam. 329 _full_lane "$t:toy-rosetta" \ 330 env KIT="$KIT" KIT_TOY_MACOS_ARCHES="$arch" bash "$ROOT/test/toy/vm.sh" macos ;; 331 esac 332 ;; 333 freebsd) 334 case "$arch" in 335 x64) vm_arch=amd64 ;; 336 aa64) vm_arch=aarch64 ;; 337 rv64) vm_arch=riscv64 ;; 338 esac 339 _full_lane "$t:toy-vm" \ 340 env KIT="$KIT" KIT_TOY_FREEBSD_ARCHES="$vm_arch" \ 341 bash "$ROOT/test/toy/vm.sh" freebsd 342 ;; 343 windows) 344 case "$arch" in 345 x64) vm_arch=x64 ;; 346 aa64) vm_arch=aarch64 ;; 347 esac 348 _full_lane "$t:toy-vm" \ 349 env KIT="$KIT" KIT_TOY_WINDOWS_ARCHES="$vm_arch" \ 350 bash "$ROOT/test/toy/vm.sh" windows 351 ;; 352 freestanding) 353 case "$arch" in 354 rv32) 355 _full_lane "$t:toy" env KIT="$KIT" KIT_TOY_CROSS_ARCHS=rv32 KIT_TEST_PATHS=X bash "$ROOT/test/toy/run.sh" 356 _full_lane "$t:parse" env KIT="$KIT" KIT_TEST_ARCH=rv32 KIT_TEST_PATHS=E bash "$ROOT/test/parse/run.sh" ;; 357 arm32) 358 _full_lane "$t:toy" env KIT="$KIT" KIT_TOY_CROSS_ARCHS=arm32 KIT_TEST_PATHS=X bash "$ROOT/test/toy/run.sh" 359 _full_lane "$t:parse" env KIT="$KIT" KIT_TEST_ARCH=arm32 KIT_TEST_PATHS=E bash "$ROOT/test/parse/run.sh" ;; 360 *) 361 printf 'cross: NOTE %s — full-corpus bare-metal not yet wired for %s (smoke only); see doc/plan/PORT.md\n' "$t" "$arch" ;; 362 esac 363 ;; 364 esac 365 } 366 367 # ---- drive ----------------------------------------------------------------- 368 TOKENS="$("$HOSTED" expand "$SELECTOR" --mode=cross)" || exit 1 369 [ -n "$TOKENS" ] || { echo "cross_test: selector '$SELECTOR' expanded to nothing" >&2; exit 2; } 370 371 printf 'cross: DEPTH=%s KIT_VM=%s KIT_CROSS_RUN=%s\ncross: targets=%s\n' \ 372 "$DEPTH" "$KIT_VM" "$KIT_CROSS_RUN" "$(echo $TOKENS | tr '\n' ' ')" 373 374 missing=0 375 for t in $TOKENS; do 376 reason="$(preflight_token "$t")" 377 if [ -n "$reason" ]; then 378 printf 'cross: NOT PROVISIONED %-22s %s\n' "$t" "$reason"; missing=1 379 fi 380 done 381 if [ "$missing" -eq 1 ]; then 382 echo "cross: aborting — provision the targets above, or narrow TARGET / set KIT_VM=0." >&2 383 exit 1 384 fi 385 386 printf '\ncross: phase 1 — compile/link smoke cases\n' 387 : > "$RUN_LIST_SMOKE" 388 : > "$RUN_LIST_COARSE" 389 for t in $TOKENS; do 390 LIST_PATH="$RUN_LIST_SMOKE" 391 smoke_build_token "$t" 392 if [ "$DEPTH" != smoke ]; then 393 LIST_PATH="$RUN_LIST_COARSE" 394 coarse_build_token "$t" 395 fi 396 done 397 398 if [ "$KIT_CROSS_RUN" -eq 1 ]; then 399 printf '\ncross: phase 2 — execute smoke cases\n' 400 cross_run_all "$RUN_LIST_SMOKE" 401 if [ "$DEPTH" != smoke ]; then 402 printf '\ncross: phase 3 — execute front/back amalgam lane\n' 403 cross_run_all "$RUN_LIST_COARSE" 404 fi 405 if [ "$DEPTH" = full ]; then 406 printf '\ncross: phase 4 — full execution lanes\n' 407 for t in $TOKENS; do 408 full_token "$t" 409 done 410 fi 411 else 412 printf '\ncross: phase 2 — execution disabled (KIT_CROSS_RUN=0)\n' 413 fi 414 415 KIT_SKIP_IS_FAILURE=0 416 kit_summary test-cross 417 kit_exit