exec_target.sh (20530B)
1 # test/lib/exec_target.sh — shared per-target exec helper for test harnesses. 2 # 3 # Sourced by test/{link,cg,parse}/run.sh. Provides three execution modes, 4 # each parameterized by a `<arch>-<os>` target tag: 5 # 6 # exec_target_run TAG EXE OUT ERR 7 # Synchronous one-shot. Sets RUN_RC. Used for kernel images and 8 # negative-test cases that need an immediate rc. 9 # 10 # exec_target_queue TAG NAME EXE OUT ERR RC 11 # Append a case to the internal queue. The tag is stored alongside 12 # so flush can group cases by target and run one batched runner 13 # invocation per group. 14 # 15 # exec_target_queue_size 16 # Total queue size across all targets. 17 # 18 # exec_target_flush 19 # Drain the queue. Cases are grouped by tag and each group runs 20 # through one `podman run` (linux targets) or a native loop (macos 21 # targets). On podman hosts this amortizes the ~150 ms per-launch 22 # client round-trip across the whole suite. 23 # 24 # exec_target_supported TAG 25 # Returns 0 if some runner is available for tag on this host. 26 # 27 # Recognized tags: <arch>-<os> where arch is aa64/x64/rv64 (the long form 28 # aarch64 is accepted as an alias for aa64) and os 29 # is linux/macos. linux tags map to podman --platform strings and an 30 # optional user-mode qemu binary. macos tags require a Darwin host whose 31 # native arch matches; Mach-O cannot be loaded by the Linux kernel and 32 # Linux ELF cannot be loaded by Darwin, so cross-OS exec is unsupported 33 # (callers see exec_target_supported return 1 and SKIP). 34 # 35 # Caller contract: 36 # - Sets the following before sourcing or calling: have_qemu (host 37 # qemu-aarch64), have_podman, is_aarch64, QEMU_BIN (path to 38 # qemu-aarch64). Already detected by every harness. 39 # - For batched podman, sets EXEC_TARGET_MOUNT_ROOT to a host 40 # directory that contains every exe / out / err / rc path that 41 # will be queued. The same path is bind-mounted at the same path 42 # inside the container. 43 # - Optional: RUN_AARCH64_IMAGE / RUN_X64_IMAGE / RUN_RV64_IMAGE 44 # override the container image. The defaults are pinned, per-arch, 45 # content-addressed alpine digests (see test/lib/test_images.sh); 46 # provision them once with `make test-images`. Every `podman run` 47 # below uses --pull=never, so the run path never touches the network. 48 49 # Pinned per-arch image references (sets RUN_<ARCH>_IMAGE defaults and provides 50 # kit_test_image_for_arch). Sourced relative to this file's location. 51 . "$(dirname "${BASH_SOURCE[0]}")/test_images.sh" 52 53 # VM execution backend: the `<arch>-freebsd` / `<arch>-windows` runner plus the 54 # boot/teardown lifecycle. The stateless linux/macos runners live in this file; 55 # the stateful VM runner (which an expensive-to-boot VM needs) lives there. 56 . "$(dirname "${BASH_SOURCE[0]}")/exec_vm.sh" 57 58 # Bare-metal backend: the `<arch>-freestanding` runner. Freestanding images have 59 # no OS — they boot under qemu-system and exit via per-arch semihosting / a test 60 # device. exec_bare.sh owns the reset stubs, linker scripts, and exit oracles. 61 . "$(dirname "${BASH_SOURCE[0]}")/exec_bare.sh" 62 63 # Work dir for the bare backend's per-arch stub + linked images. 64 EXEC_BARE_WORK="${EXEC_BARE_WORK:-${EXEC_TARGET_MOUNT_ROOT:-${TMPDIR:-/tmp}/kit-exec-bare}}" 65 66 # Internal queue arrays. Each entry's tag is recorded alongside the 67 # rest so flush can split into per-target batched runs. 68 EXEC_TARGET_TAGS=() 69 EXEC_TARGET_NAMES=() 70 EXEC_TARGET_EXES=() 71 EXEC_TARGET_OUTS=() 72 EXEC_TARGET_ERRS=() 73 EXEC_TARGET_RCS=() 74 75 # ---- tag parsing ----------------------------------------------------------- 76 # 77 # _exec_target_arch TAG → echoes arch portion ("aarch64", "x64", "rv64"). 78 # _exec_target_os TAG → echoes os portion ("linux", "macos"). 79 # 80 # Bare-arch tags ("aarch64", "x64", "rv64") are accepted and mean 81 # "<arch>-linux" — preserves call-site compatibility while the harness 82 # transition to <arch>-<os> tags is in progress. 83 84 _exec_target_arch() { 85 case "$1" in 86 *-*) printf '%s' "${1%%-*}" ;; # first field (handles <arch>-<os>[-<libc>]) 87 *) printf '%s' "$1" ;; 88 esac 89 } 90 91 _exec_target_os() { 92 case "$1" in 93 *-*) printf '%s' "${1#*-}" ;; 94 *) printf 'linux' ;; 95 esac 96 } 97 98 # ---- per-target capability/dispatch knobs ---------------------------------- 99 100 _exec_target_platform() { 101 case "$(_exec_target_arch "$1")" in 102 aa64|aarch64) echo "linux/arm64" ;; 103 x64) echo "linux/amd64" ;; 104 rv64) echo "linux/riscv64" ;; 105 *) echo "" ;; 106 esac 107 } 108 109 # Per-arch pinned image (content-addressed alpine digest, musl libc). The pins 110 # live in test/lib/test_images.sh and are provisioned by `make test-images`; 111 # RUN_<ARCH>_IMAGE overrides them (e.g. for a glibc base). Distinct digests per 112 # arch mean local storage can never confuse one arch's rootfs for another's. 113 _exec_target_image() { 114 local arch; arch="$(_exec_target_arch "$1")" 115 # glibc targets run in a glibc (Debian) image; musl/default in alpine. The 116 # arch-qualified names disambiguate the manifest so podman never confuses one 117 # arch's rootfs for another's (the alpine pins use digests for the same end). 118 if [ "$(_exec_target_os "$1")" = "linux-glibc" ]; then 119 case "$arch" in 120 aa64|aarch64) printf '%s' "${RUN_GLIBC_AARCH64_IMAGE:-docker.io/arm64v8/debian:bookworm-slim}" ;; 121 x64) printf '%s' "${RUN_GLIBC_X64_IMAGE:-docker.io/amd64/debian:bookworm-slim}" ;; 122 rv64) printf '%s' "${RUN_GLIBC_RV64_IMAGE:-docker.io/riscv64/debian:trixie-slim}" ;; 123 *) printf 'debian:bookworm-slim' ;; 124 esac 125 return 126 fi 127 local img; img="$(kit_test_image_for_arch "$arch")" 128 [ -n "$img" ] && printf '%s' "$img" || printf 'alpine:latest' 129 } 130 131 # Memoized: is this arch's pinned image present in local storage? The harnesses 132 # run with --pull=never, so a missing image means the container runner is 133 # unavailable until `make test-images` provisions it. Cached per arch so 134 # exec_target_supported stays a constant cost across hundreds of cases. 135 _exec_target_image_present() { 136 local img var cached 137 img="$(_exec_target_image "$1")" 138 # Memoize per IMAGE (not per arch): a given arch can map to either the 139 # alpine (musl) or the debian (glibc) image, and those presence answers differ. 140 var="_EXEC_TARGET_IMG_$(printf '%s' "$img" | tr -c 'A-Za-z0-9' _)" 141 cached="${!var:-}" 142 if [ -z "$cached" ]; then 143 if podman image exists "$img" 2>/dev/null; then cached=yes; else cached=no; fi 144 printf -v "$var" '%s' "$cached" 145 fi 146 [ "$cached" = yes ] 147 } 148 149 # True when the host can exec this target without container/qemu help. 150 # 151 # linux targets: matching arch on a Linux host (e.g. aarch64-linux on 152 # a Linux/aarch64 host). On Darwin, Linux ELF cannot be loaded by the 153 # kernel even when the arch matches, so this is Linux-host-only. 154 # 155 # macos targets: matching arch on a Darwin host. The Linux kernel 156 # cannot load Mach-O, so this is Darwin-host-only. 157 _exec_target_native() { 158 local arch os host_kernel host_arch 159 arch="$(_exec_target_arch "$1")" 160 os="$(_exec_target_os "$1")" 161 host_kernel="$(uname -s 2>/dev/null)" 162 host_arch="$(uname -m 2>/dev/null)" 163 case "$os" in 164 linux|linux-glibc) 165 [ "$host_kernel" = "Linux" ] || return 1 166 _exec_target_arch_matches_host "$arch" "$host_arch" 167 ;; 168 macos) 169 [ "$host_kernel" = "Darwin" ] || return 1 170 _exec_target_arch_matches_host "$arch" "$host_arch" && return 0 171 # Rosetta 2: an Apple-silicon Darwin host transparently translates 172 # and runs x86_64 Mach-O binaries — executing the file directly is 173 # enough (no `arch -x86_64` wrapper). Treat x64-on-arm64 as native 174 # when Rosetta is installed. 175 if [ "$arch" = "x64" ] && \ 176 { [ "$host_arch" = "arm64" ] || [ "$host_arch" = "aarch64" ]; }; then 177 _exec_target_rosetta_available && return 0 178 fi 179 return 1 180 ;; 181 *) return 1 ;; 182 esac 183 } 184 185 _exec_target_arch_matches_host() { 186 local arch="$1" host_arch="$2" 187 case "$arch" in 188 aa64|aarch64) [ "$host_arch" = "aarch64" ] || [ "$host_arch" = "arm64" ] ;; 189 x64) [ "$host_arch" = "x86_64" ] || [ "$host_arch" = "amd64" ] ;; 190 rv64) [ "$host_arch" = "riscv64" ] ;; 191 *) return 1 ;; 192 esac 193 } 194 195 # True when Rosetta 2 is available to run x86_64 binaries on this host. Probe 196 # once and cache — exec paths call this per binary. 197 _exec_target_rosetta_available() { 198 case "${_EXEC_TARGET_ROSETTA:-}" in 199 yes) return 0 ;; 200 no) return 1 ;; 201 esac 202 if arch -x86_64 /usr/bin/true >/dev/null 2>&1; then 203 _EXEC_TARGET_ROSETTA=yes; return 0 204 fi 205 _EXEC_TARGET_ROSETTA=no; return 1 206 } 207 208 # True when podman can run this linux target without emulation. The podman 209 # machine on Darwin/arm64 already runs linux/arm64, so passing `--platform 210 # linux/arm64` there is redundant — and worse, triggers a registry manifest 211 # lookup (~30 s) on every `podman run` even when the local image matches. 212 _exec_target_podman_native() { 213 case "$(_exec_target_arch "$1")" in 214 aa64|aarch64) [ "${is_aarch64:-0}" -eq 1 ] ;; 215 x64) [ "$(uname -m 2>/dev/null)" = "x86_64" ] || \ 216 [ "$(uname -m 2>/dev/null)" = "amd64" ] ;; 217 rv64) [ "$(uname -m 2>/dev/null)" = "riscv64" ] ;; 218 *) return 1 ;; 219 esac 220 } 221 222 _exec_target_qemu() { 223 case "$(_exec_target_arch "$1")" in 224 aa64|aarch64) [ "${have_qemu:-0}" -eq 1 ] && echo "${QEMU_BIN:-}" ;; 225 x64) # No qemu-user fallback for x64 in current harnesses. 226 echo "" ;; 227 rv64) # qemu-riscv64 user-mode is the easiest way to exec 228 # rv64 ELFs on a non-rv64 host without podman. 229 if [ -n "${QEMU_RV64_BIN:-}" ]; then 230 echo "${QEMU_RV64_BIN}" 231 elif command -v qemu-riscv64 >/dev/null 2>&1; then 232 command -v qemu-riscv64 233 else 234 echo "" 235 fi 236 ;; 237 *) echo "" ;; 238 esac 239 } 240 241 exec_target_supported() { 242 local tag="$1" os 243 os="$(_exec_target_os "$tag")" 244 # VM-backed OSes: qemu + a provisioned/reachable VM (see exec_vm.sh). 245 case "$os" in freebsd|windows) exec_vm_supported "$tag"; return $? ;; esac 246 # Freestanding: qemu-system bare-metal (see exec_bare.sh). 247 case "$os" in freestanding) exec_bare_supported "$(_exec_target_arch "$tag")"; return $? ;; esac 248 # macOS has no podman/qemu fallback — Mach-O exec requires a Darwin 249 # host with matching arch. Cross-OS exec (macOS-on-Linux) is not 250 # supported. 251 if [ "$os" = "macos" ]; then 252 _exec_target_native "$tag" 253 return $? 254 fi 255 _exec_target_native "$tag" && return 0 256 [ -n "$(_exec_target_qemu "$tag")" ] && return 0 257 # podman is only a usable runner once the arch's pinned image is provisioned 258 # locally (run path is --pull=never); otherwise report unsupported so callers 259 # SKIP cleanly instead of failing on a missing image. 260 [ "${have_podman:-0}" -eq 1 ] && _exec_target_image_present "$tag" && return 0 261 return 1 262 } 263 264 # Synchronous run; sets RUN_RC. 265 exec_target_run() { 266 local tag="$1" exe="$2" out="$3" err="$4" 267 local os qemu 268 os="$(_exec_target_os "$tag")" 269 case "$os" in freebsd|windows) exec_vm_run "$tag" "$exe" "$out" "$err"; return ;; esac 270 if [ "$os" = freestanding ]; then 271 # `exe` is the corpus object; exec_bare links it with the per-arch stub 272 # into a bootable image and runs it under qemu-system. 273 local barch="$(_exec_target_arch "$tag")" 274 exec_bare_setup "$barch" "$EXEC_BARE_WORK" >/dev/null 2>&1 275 exec_bare_run "$barch" "$exe" "$EXEC_BARE_WORK" "$EXEC_BARE_WORK/.run.rc" >/dev/null 2>&1 \ 276 && RUN_RC="$(cat "$EXEC_BARE_WORK/.run.rc" 2>/dev/null || echo 127)" \ 277 || RUN_RC=127 278 : > "$out"; : > "$err" 279 return 280 fi 281 if _exec_target_native "$tag"; then 282 "$exe" >"$out" 2>"$err"; RUN_RC=$?; return 283 fi 284 if [ "$os" = "macos" ]; then 285 # Mach-O cannot run via podman/qemu — only Darwin-native. 286 RUN_RC=127; return 287 fi 288 qemu="$(_exec_target_qemu "$tag")" 289 if [ -n "$qemu" ]; then 290 "$qemu" "$exe" >"$out" 2>"$err"; RUN_RC=$?; return 291 fi 292 if [ "${have_podman:-0}" -eq 1 ]; then 293 local dir base platform image platform_flag=() 294 dir="$(cd "$(dirname "$exe")" && pwd)"; base="$(basename "$exe")" 295 platform="$(_exec_target_platform "$tag")" 296 image="$(_exec_target_image "$tag")" 297 # `--platform` triggers a registry manifest lookup (~30 s) even 298 # when the local image already matches. Only pass it when podman 299 # would otherwise have to emulate — i.e. the podman machine's 300 # native arch differs from the target. (On Darwin/arm64 the 301 # podman VM is already linux/arm64, so aarch64 targets skip the 302 # flag even though the host can't load the ELF directly.) 303 if ! _exec_target_podman_native "$tag"; then 304 platform_flag=(--platform "$platform") 305 fi 306 # ${arr[@]+"${arr[@]}"} (not "${arr[@]}") so an empty platform_flag 307 # expands to zero args under `set -u` on bash 3.2 — macOS /bin/sh — 308 # where a bare "${arr[@]}" on an empty array is an unbound-var error. 309 podman run --rm --pull=never ${platform_flag[@]+"${platform_flag[@]}"} --net=none \ 310 -v "$dir":/work:Z -w /work \ 311 "$image" "./$base" \ 312 >"$out" 2>"$err" 313 RUN_RC=$?; return 314 fi 315 RUN_RC=127 316 } 317 318 # Queue an exe to run later. Stored verbatim; flush writes <rc_file> with 319 # the integer exit code, and routes stdout/stderr to <out_file>/<err_file>. 320 exec_target_queue() { 321 EXEC_TARGET_TAGS+=("$1") 322 EXEC_TARGET_NAMES+=("$2") 323 EXEC_TARGET_EXES+=("$3") 324 EXEC_TARGET_OUTS+=("$4") 325 EXEC_TARGET_ERRS+=("$5") 326 EXEC_TARGET_RCS+=("$6") 327 } 328 329 exec_target_queue_size() { echo "${#EXEC_TARGET_EXES[@]}"; } 330 331 # Lifecycle hooks for stateful runners. The stateless runners (native/qemu/ 332 # podman) need neither; for VM tags these boot the VM lazily (idempotent) and 333 # tear down every VM we booted at suite end. A consumer that may run VM tags 334 # should `trap exec_target_teardown_all EXIT` once. No-ops for linux/macos. 335 exec_target_setup() { 336 case "$(_exec_target_os "$1")" in 337 freebsd|windows) exec_vm_setup "$1" ;; 338 freestanding) exec_bare_setup "$(_exec_target_arch "$1")" "$EXEC_BARE_WORK" ;; 339 *) return 0 ;; 340 esac 341 } 342 exec_target_teardown_all() { exec_vm_teardown_all; } 343 344 # Internal: drain every entry whose tag matches $1, using qemu (if 345 # available for that arch), podman batched run, or the no-runner stub. 346 _exec_target_flush_tag() { 347 local tag="$1" os 348 os="$(_exec_target_os "$tag")" 349 local idx=() 350 local i=0 n="${#EXEC_TARGET_EXES[@]}" 351 while [ $i -lt "$n" ]; do 352 [ "${EXEC_TARGET_TAGS[$i]}" = "$tag" ] && idx+=("$i") 353 i=$((i+1)) 354 done 355 [ "${#idx[@]}" -eq 0 ] && return 0 356 357 # VM-backed OSes: one batched VM session (boots lazily, stays warm). See 358 # exec_vm.sh. Must branch before the native/qemu/podman logic, which would 359 # otherwise try to run a FreeBSD/Windows binary under a Linux runner. 360 case "$os" in 361 freebsd|windows) exec_vm_flush_tag "$tag" "${idx[@]}"; return $? ;; 362 freestanding) 363 # Bare-metal: each queued "exe" is a corpus object; link + boot it. 364 local barch bk 365 barch="$(_exec_target_arch "$tag")" 366 exec_bare_setup "$barch" "$EXEC_BARE_WORK" >/dev/null 2>&1 367 for bk in "${idx[@]}"; do 368 exec_bare_run "$barch" "${EXEC_TARGET_EXES[$bk]}" "$EXEC_BARE_WORK" \ 369 "${EXEC_TARGET_RCS[$bk]}" >/dev/null 2>&1 \ 370 || echo 127 > "${EXEC_TARGET_RCS[$bk]}" 371 : > "${EXEC_TARGET_OUTS[$bk]}"; : > "${EXEC_TARGET_ERRS[$bk]}" 372 done 373 return 0 ;; 374 esac 375 376 local k 377 # Native exec (Linux-on-Linux, Darwin-on-Darwin) — same loop. 378 if _exec_target_native "$tag"; then 379 for k in "${idx[@]}"; do 380 "${EXEC_TARGET_EXES[$k]}" \ 381 >"${EXEC_TARGET_OUTS[$k]}" 2>"${EXEC_TARGET_ERRS[$k]}" 382 echo $? >"${EXEC_TARGET_RCS[$k]}" 383 done 384 return 0 385 fi 386 # macOS: no fallback — mark as 127 so callers can SKIP cleanly. 387 if [ "$os" = "macos" ]; then 388 for k in "${idx[@]}"; do 389 : >"${EXEC_TARGET_OUTS[$k]}" 390 : >"${EXEC_TARGET_ERRS[$k]}" 391 echo 127 >"${EXEC_TARGET_RCS[$k]}" 392 done 393 return 0 394 fi 395 396 local qemu; qemu="$(_exec_target_qemu "$tag")" 397 if [ -n "$qemu" ]; then 398 for k in "${idx[@]}"; do 399 "$qemu" "${EXEC_TARGET_EXES[$k]}" \ 400 >"${EXEC_TARGET_OUTS[$k]}" 2>"${EXEC_TARGET_ERRS[$k]}" 401 echo $? >"${EXEC_TARGET_RCS[$k]}" 402 done 403 return 0 404 fi 405 if [ "${have_podman:-0}" -eq 1 ]; then 406 if [ -z "${EXEC_TARGET_MOUNT_ROOT:-}" ]; then 407 echo "exec_target_flush: EXEC_TARGET_MOUNT_ROOT must be set" >&2 408 return 2 409 fi 410 local platform image platform_flag=() case_to 411 # Per-case wall-clock cap inside the batched container. Without it a 412 # single hanging exe (e.g. a miscompiled loop, or qemu-user wedging on 413 # one binary) blocks the whole single-container run, leaving every 414 # later case with no .rc — which the caller reads back as 127 and 415 # reports as a mass failure. With it, a hang is killed (rc 137) and the 416 # loop moves on, so a real hang fails exactly one case. Override with 417 # EXEC_CASE_TIMEOUT (seconds); generous by default for slow TCG. 418 case_to="${EXEC_CASE_TIMEOUT:-20}" 419 platform="$(_exec_target_platform "$tag")" 420 image="$(_exec_target_image "$tag")" 421 if ! _exec_target_podman_native "$tag"; then 422 platform_flag=(--platform "$platform") 423 fi 424 # Manifest is fed via stdin; one tab-separated line per case. 425 # The in-container shell loop runs each exe and writes its rc 426 # to the bind-mounted .rc file, so the host can poll results 427 # after `podman run` returns. stdout/stderr from individual 428 # exes go to their .out/.err files inside the same mount. 429 { 430 for k in "${idx[@]}"; do 431 printf '%s\t%s\t%s\t%s\n' \ 432 "${EXEC_TARGET_EXES[$k]}" \ 433 "${EXEC_TARGET_OUTS[$k]}" \ 434 "${EXEC_TARGET_ERRS[$k]}" \ 435 "${EXEC_TARGET_RCS[$k]}" 436 done 437 } | podman run -i --rm --pull=never ${platform_flag[@]+"${platform_flag[@]}"} --net=none \ 438 -e EXEC_CASE_TIMEOUT="$case_to" \ 439 -v "$EXEC_TARGET_MOUNT_ROOT":"$EXEC_TARGET_MOUNT_ROOT":Z \ 440 "$image" \ 441 /bin/sh -c ' 442 set -u 443 _to="${EXEC_CASE_TIMEOUT:-20}" 444 if command -v timeout >/dev/null 2>&1; then _t="timeout -s KILL $_to"; else _t=""; fi 445 while IFS=" " read -r exe out err rc; do 446 $_t "$exe" >"$out" 2>"$err" 447 echo $? >"$rc" 448 done 449 ' 450 return 0 451 fi 452 # No runner: mark each as 127, matching the prior fallback. 453 for k in "${idx[@]}"; do 454 : >"${EXEC_TARGET_OUTS[$k]}" 455 : >"${EXEC_TARGET_ERRS[$k]}" 456 echo 127 >"${EXEC_TARGET_RCS[$k]}" 457 done 458 } 459 460 # Drain the queue. Reads back via the .rc files written into the 461 # bind-mounted tree; callers iterate their own bookkeeping arrays after 462 # this returns. Each tag present in the queue runs in its own batch. 463 exec_target_flush() { 464 [ "${#EXEC_TARGET_EXES[@]}" -eq 0 ] && return 0 465 466 # Distinct tags in queue order. Bash 3.2 has no associative arrays; 467 # use a small linear scan. 468 local seen=() a present k 469 local i=0 n="${#EXEC_TARGET_TAGS[@]}" 470 while [ $i -lt "$n" ]; do 471 a="${EXEC_TARGET_TAGS[$i]}" 472 present=0 473 for k in "${seen[@]:-}"; do [ "$k" = "$a" ] && present=1 && break; done 474 [ "$present" -eq 0 ] && seen+=("$a") 475 i=$((i+1)) 476 done 477 478 local rc=0 479 for a in "${seen[@]}"; do 480 _exec_target_flush_tag "$a" || rc=$? 481 done 482 483 EXEC_TARGET_TAGS=() 484 EXEC_TARGET_NAMES=() 485 EXEC_TARGET_EXES=() 486 EXEC_TARGET_OUTS=() 487 EXEC_TARGET_ERRS=() 488 EXEC_TARGET_RCS=() 489 return $rc 490 }