exec_target.sh (17450B)
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 # Internal queue arrays. Each entry's tag is recorded alongside the 59 # rest so flush can split into per-target batched runs. 60 EXEC_TARGET_TAGS=() 61 EXEC_TARGET_NAMES=() 62 EXEC_TARGET_EXES=() 63 EXEC_TARGET_OUTS=() 64 EXEC_TARGET_ERRS=() 65 EXEC_TARGET_RCS=() 66 67 # ---- tag parsing ----------------------------------------------------------- 68 # 69 # _exec_target_arch TAG → echoes arch portion ("aarch64", "x64", "rv64"). 70 # _exec_target_os TAG → echoes os portion ("linux", "macos"). 71 # 72 # Bare-arch tags ("aarch64", "x64", "rv64") are accepted and mean 73 # "<arch>-linux" — preserves call-site compatibility while the harness 74 # transition to <arch>-<os> tags is in progress. 75 76 _exec_target_arch() { 77 case "$1" in 78 *-*) printf '%s' "${1%%-*}" ;; # first field (handles <arch>-<os>[-<libc>]) 79 *) printf '%s' "$1" ;; 80 esac 81 } 82 83 _exec_target_os() { 84 case "$1" in 85 *-*) printf '%s' "${1#*-}" ;; 86 *) printf 'linux' ;; 87 esac 88 } 89 90 # ---- per-target capability/dispatch knobs ---------------------------------- 91 92 _exec_target_platform() { 93 case "$(_exec_target_arch "$1")" in 94 aa64|aarch64) echo "linux/arm64" ;; 95 x64) echo "linux/amd64" ;; 96 rv64) echo "linux/riscv64" ;; 97 *) echo "" ;; 98 esac 99 } 100 101 # Per-arch pinned image (content-addressed alpine digest, musl libc). The pins 102 # live in test/lib/test_images.sh and are provisioned by `make test-images`; 103 # RUN_<ARCH>_IMAGE overrides them (e.g. for a glibc base). Distinct digests per 104 # arch mean local storage can never confuse one arch's rootfs for another's. 105 _exec_target_image() { 106 local arch; arch="$(_exec_target_arch "$1")" 107 # glibc targets run in a glibc (Debian) image; musl/default in alpine. The 108 # arch-qualified names disambiguate the manifest so podman never confuses one 109 # arch's rootfs for another's (the alpine pins use digests for the same end). 110 if [ "$(_exec_target_os "$1")" = "linux-glibc" ]; then 111 case "$arch" in 112 aa64|aarch64) printf '%s' "${RUN_GLIBC_AARCH64_IMAGE:-docker.io/arm64v8/debian:bookworm-slim}" ;; 113 x64) printf '%s' "${RUN_GLIBC_X64_IMAGE:-docker.io/amd64/debian:bookworm-slim}" ;; 114 rv64) printf '%s' "${RUN_GLIBC_RV64_IMAGE:-docker.io/riscv64/debian:trixie-slim}" ;; 115 *) printf 'debian:bookworm-slim' ;; 116 esac 117 return 118 fi 119 local img; img="$(kit_test_image_for_arch "$arch")" 120 [ -n "$img" ] && printf '%s' "$img" || printf 'alpine:latest' 121 } 122 123 # Memoized: is this arch's pinned image present in local storage? The harnesses 124 # run with --pull=never, so a missing image means the container runner is 125 # unavailable until `make test-images` provisions it. Cached per arch so 126 # exec_target_supported stays a constant cost across hundreds of cases. 127 _exec_target_image_present() { 128 local img var cached 129 img="$(_exec_target_image "$1")" 130 # Memoize per IMAGE (not per arch): a given arch can map to either the 131 # alpine (musl) or the debian (glibc) image, and those presence answers differ. 132 var="_EXEC_TARGET_IMG_$(printf '%s' "$img" | tr -c 'A-Za-z0-9' _)" 133 cached="${!var:-}" 134 if [ -z "$cached" ]; then 135 if podman image exists "$img" 2>/dev/null; then cached=yes; else cached=no; fi 136 printf -v "$var" '%s' "$cached" 137 fi 138 [ "$cached" = yes ] 139 } 140 141 # True when the host can exec this target without container/qemu help. 142 # 143 # linux targets: matching arch on a Linux host (e.g. aarch64-linux on 144 # a Linux/aarch64 host). On Darwin, Linux ELF cannot be loaded by the 145 # kernel even when the arch matches, so this is Linux-host-only. 146 # 147 # macos targets: matching arch on a Darwin host. The Linux kernel 148 # cannot load Mach-O, so this is Darwin-host-only. 149 _exec_target_native() { 150 local arch os host_kernel host_arch 151 arch="$(_exec_target_arch "$1")" 152 os="$(_exec_target_os "$1")" 153 host_kernel="$(uname -s 2>/dev/null)" 154 host_arch="$(uname -m 2>/dev/null)" 155 case "$os" in 156 linux|linux-glibc) 157 [ "$host_kernel" = "Linux" ] || return 1 158 _exec_target_arch_matches_host "$arch" "$host_arch" 159 ;; 160 macos) 161 [ "$host_kernel" = "Darwin" ] || return 1 162 _exec_target_arch_matches_host "$arch" "$host_arch" 163 ;; 164 *) return 1 ;; 165 esac 166 } 167 168 _exec_target_arch_matches_host() { 169 local arch="$1" host_arch="$2" 170 case "$arch" in 171 aa64|aarch64) [ "$host_arch" = "aarch64" ] || [ "$host_arch" = "arm64" ] ;; 172 x64) [ "$host_arch" = "x86_64" ] || [ "$host_arch" = "amd64" ] ;; 173 rv64) [ "$host_arch" = "riscv64" ] ;; 174 *) return 1 ;; 175 esac 176 } 177 178 # True when podman can run this linux target without emulation. The podman 179 # machine on Darwin/arm64 already runs linux/arm64, so passing `--platform 180 # linux/arm64` there is redundant — and worse, triggers a registry manifest 181 # lookup (~30 s) on every `podman run` even when the local image matches. 182 _exec_target_podman_native() { 183 case "$(_exec_target_arch "$1")" in 184 aa64|aarch64) [ "${is_aarch64:-0}" -eq 1 ] ;; 185 x64) [ "$(uname -m 2>/dev/null)" = "x86_64" ] || \ 186 [ "$(uname -m 2>/dev/null)" = "amd64" ] ;; 187 rv64) [ "$(uname -m 2>/dev/null)" = "riscv64" ] ;; 188 *) return 1 ;; 189 esac 190 } 191 192 _exec_target_qemu() { 193 case "$(_exec_target_arch "$1")" in 194 aa64|aarch64) [ "${have_qemu:-0}" -eq 1 ] && echo "${QEMU_BIN:-}" ;; 195 x64) # No qemu-user fallback for x64 in current harnesses. 196 echo "" ;; 197 rv64) # qemu-riscv64 user-mode is the easiest way to exec 198 # rv64 ELFs on a non-rv64 host without podman. 199 if [ -n "${QEMU_RV64_BIN:-}" ]; then 200 echo "${QEMU_RV64_BIN}" 201 elif command -v qemu-riscv64 >/dev/null 2>&1; then 202 command -v qemu-riscv64 203 else 204 echo "" 205 fi 206 ;; 207 *) echo "" ;; 208 esac 209 } 210 211 exec_target_supported() { 212 local tag="$1" os 213 os="$(_exec_target_os "$tag")" 214 # VM-backed OSes: qemu + a provisioned/reachable VM (see exec_vm.sh). 215 case "$os" in freebsd|windows) exec_vm_supported "$tag"; return $? ;; esac 216 # macOS has no podman/qemu fallback — Mach-O exec requires a Darwin 217 # host with matching arch. Cross-OS exec (macOS-on-Linux) is not 218 # supported. 219 if [ "$os" = "macos" ]; then 220 _exec_target_native "$tag" 221 return $? 222 fi 223 _exec_target_native "$tag" && return 0 224 [ -n "$(_exec_target_qemu "$tag")" ] && return 0 225 # podman is only a usable runner once the arch's pinned image is provisioned 226 # locally (run path is --pull=never); otherwise report unsupported so callers 227 # SKIP cleanly instead of failing on a missing image. 228 [ "${have_podman:-0}" -eq 1 ] && _exec_target_image_present "$tag" && return 0 229 return 1 230 } 231 232 # Synchronous run; sets RUN_RC. 233 exec_target_run() { 234 local tag="$1" exe="$2" out="$3" err="$4" 235 local os qemu 236 os="$(_exec_target_os "$tag")" 237 case "$os" in freebsd|windows) exec_vm_run "$tag" "$exe" "$out" "$err"; return ;; esac 238 if _exec_target_native "$tag"; then 239 "$exe" >"$out" 2>"$err"; RUN_RC=$?; return 240 fi 241 if [ "$os" = "macos" ]; then 242 # Mach-O cannot run via podman/qemu — only Darwin-native. 243 RUN_RC=127; return 244 fi 245 qemu="$(_exec_target_qemu "$tag")" 246 if [ -n "$qemu" ]; then 247 "$qemu" "$exe" >"$out" 2>"$err"; RUN_RC=$?; return 248 fi 249 if [ "${have_podman:-0}" -eq 1 ]; then 250 local dir base platform image platform_flag=() 251 dir="$(cd "$(dirname "$exe")" && pwd)"; base="$(basename "$exe")" 252 platform="$(_exec_target_platform "$tag")" 253 image="$(_exec_target_image "$tag")" 254 # `--platform` triggers a registry manifest lookup (~30 s) even 255 # when the local image already matches. Only pass it when podman 256 # would otherwise have to emulate — i.e. the podman machine's 257 # native arch differs from the target. (On Darwin/arm64 the 258 # podman VM is already linux/arm64, so aarch64 targets skip the 259 # flag even though the host can't load the ELF directly.) 260 if ! _exec_target_podman_native "$tag"; then 261 platform_flag=(--platform "$platform") 262 fi 263 podman run --rm --pull=never "${platform_flag[@]}" --net=none \ 264 -v "$dir":/work:Z -w /work \ 265 "$image" "./$base" \ 266 >"$out" 2>"$err" 267 RUN_RC=$?; return 268 fi 269 RUN_RC=127 270 } 271 272 # Queue an exe to run later. Stored verbatim; flush writes <rc_file> with 273 # the integer exit code, and routes stdout/stderr to <out_file>/<err_file>. 274 exec_target_queue() { 275 EXEC_TARGET_TAGS+=("$1") 276 EXEC_TARGET_NAMES+=("$2") 277 EXEC_TARGET_EXES+=("$3") 278 EXEC_TARGET_OUTS+=("$4") 279 EXEC_TARGET_ERRS+=("$5") 280 EXEC_TARGET_RCS+=("$6") 281 } 282 283 exec_target_queue_size() { echo "${#EXEC_TARGET_EXES[@]}"; } 284 285 # Lifecycle hooks for stateful runners. The stateless runners (native/qemu/ 286 # podman) need neither; for VM tags these boot the VM lazily (idempotent) and 287 # tear down every VM we booted at suite end. A consumer that may run VM tags 288 # should `trap exec_target_teardown_all EXIT` once. No-ops for linux/macos. 289 exec_target_setup() { 290 case "$(_exec_target_os "$1")" in 291 freebsd|windows) exec_vm_setup "$1" ;; 292 *) return 0 ;; 293 esac 294 } 295 exec_target_teardown_all() { exec_vm_teardown_all; } 296 297 # Internal: drain every entry whose tag matches $1, using qemu (if 298 # available for that arch), podman batched run, or the no-runner stub. 299 _exec_target_flush_tag() { 300 local tag="$1" os 301 os="$(_exec_target_os "$tag")" 302 local idx=() 303 local i=0 n="${#EXEC_TARGET_EXES[@]}" 304 while [ $i -lt "$n" ]; do 305 [ "${EXEC_TARGET_TAGS[$i]}" = "$tag" ] && idx+=("$i") 306 i=$((i+1)) 307 done 308 [ "${#idx[@]}" -eq 0 ] && return 0 309 310 # VM-backed OSes: one batched VM session (boots lazily, stays warm). See 311 # exec_vm.sh. Must branch before the native/qemu/podman logic, which would 312 # otherwise try to run a FreeBSD/Windows binary under a Linux runner. 313 case "$os" in 314 freebsd|windows) exec_vm_flush_tag "$tag" "${idx[@]}"; return $? ;; 315 esac 316 317 local k 318 # Native exec (Linux-on-Linux, Darwin-on-Darwin) — same loop. 319 if _exec_target_native "$tag"; then 320 for k in "${idx[@]}"; do 321 "${EXEC_TARGET_EXES[$k]}" \ 322 >"${EXEC_TARGET_OUTS[$k]}" 2>"${EXEC_TARGET_ERRS[$k]}" 323 echo $? >"${EXEC_TARGET_RCS[$k]}" 324 done 325 return 0 326 fi 327 # macOS: no fallback — mark as 127 so callers can SKIP cleanly. 328 if [ "$os" = "macos" ]; then 329 for k in "${idx[@]}"; do 330 : >"${EXEC_TARGET_OUTS[$k]}" 331 : >"${EXEC_TARGET_ERRS[$k]}" 332 echo 127 >"${EXEC_TARGET_RCS[$k]}" 333 done 334 return 0 335 fi 336 337 local qemu; qemu="$(_exec_target_qemu "$tag")" 338 if [ -n "$qemu" ]; then 339 for k in "${idx[@]}"; do 340 "$qemu" "${EXEC_TARGET_EXES[$k]}" \ 341 >"${EXEC_TARGET_OUTS[$k]}" 2>"${EXEC_TARGET_ERRS[$k]}" 342 echo $? >"${EXEC_TARGET_RCS[$k]}" 343 done 344 return 0 345 fi 346 if [ "${have_podman:-0}" -eq 1 ]; then 347 if [ -z "${EXEC_TARGET_MOUNT_ROOT:-}" ]; then 348 echo "exec_target_flush: EXEC_TARGET_MOUNT_ROOT must be set" >&2 349 return 2 350 fi 351 local platform image platform_flag=() case_to 352 # Per-case wall-clock cap inside the batched container. Without it a 353 # single hanging exe (e.g. a miscompiled loop, or qemu-user wedging on 354 # one binary) blocks the whole single-container run, leaving every 355 # later case with no .rc — which the caller reads back as 127 and 356 # reports as a mass failure. With it, a hang is killed (rc 137) and the 357 # loop moves on, so a real hang fails exactly one case. Override with 358 # EXEC_CASE_TIMEOUT (seconds); generous by default for slow TCG. 359 case_to="${EXEC_CASE_TIMEOUT:-20}" 360 platform="$(_exec_target_platform "$tag")" 361 image="$(_exec_target_image "$tag")" 362 if ! _exec_target_podman_native "$tag"; then 363 platform_flag=(--platform "$platform") 364 fi 365 # Manifest is fed via stdin; one tab-separated line per case. 366 # The in-container shell loop runs each exe and writes its rc 367 # to the bind-mounted .rc file, so the host can poll results 368 # after `podman run` returns. stdout/stderr from individual 369 # exes go to their .out/.err files inside the same mount. 370 { 371 for k in "${idx[@]}"; do 372 printf '%s\t%s\t%s\t%s\n' \ 373 "${EXEC_TARGET_EXES[$k]}" \ 374 "${EXEC_TARGET_OUTS[$k]}" \ 375 "${EXEC_TARGET_ERRS[$k]}" \ 376 "${EXEC_TARGET_RCS[$k]}" 377 done 378 } | podman run -i --rm --pull=never "${platform_flag[@]}" --net=none \ 379 -e EXEC_CASE_TIMEOUT="$case_to" \ 380 -v "$EXEC_TARGET_MOUNT_ROOT":"$EXEC_TARGET_MOUNT_ROOT":Z \ 381 "$image" \ 382 /bin/sh -c ' 383 set -u 384 _to="${EXEC_CASE_TIMEOUT:-20}" 385 if command -v timeout >/dev/null 2>&1; then _t="timeout -s KILL $_to"; else _t=""; fi 386 while IFS=" " read -r exe out err rc; do 387 $_t "$exe" >"$out" 2>"$err" 388 echo $? >"$rc" 389 done 390 ' 391 return 0 392 fi 393 # No runner: mark each as 127, matching the prior fallback. 394 for k in "${idx[@]}"; do 395 : >"${EXEC_TARGET_OUTS[$k]}" 396 : >"${EXEC_TARGET_ERRS[$k]}" 397 echo 127 >"${EXEC_TARGET_RCS[$k]}" 398 done 399 } 400 401 # Drain the queue. Reads back via the .rc files written into the 402 # bind-mounted tree; callers iterate their own bookkeeping arrays after 403 # this returns. Each tag present in the queue runs in its own batch. 404 exec_target_flush() { 405 [ "${#EXEC_TARGET_EXES[@]}" -eq 0 ] && return 0 406 407 # Distinct tags in queue order. Bash 3.2 has no associative arrays; 408 # use a small linear scan. 409 local seen=() a present k 410 local i=0 n="${#EXEC_TARGET_TAGS[@]}" 411 while [ $i -lt "$n" ]; do 412 a="${EXEC_TARGET_TAGS[$i]}" 413 present=0 414 for k in "${seen[@]:-}"; do [ "$k" = "$a" ] && present=1 && break; done 415 [ "$present" -eq 0 ] && seen+=("$a") 416 i=$((i+1)) 417 done 418 419 local rc=0 420 for a in "${seen[@]}"; do 421 _exec_target_flush_tag "$a" || rc=$? 422 done 423 424 EXEC_TARGET_TAGS=() 425 EXEC_TARGET_NAMES=() 426 EXEC_TARGET_EXES=() 427 EXEC_TARGET_OUTS=() 428 EXEC_TARGET_ERRS=() 429 EXEC_TARGET_RCS=() 430 return $rc 431 }