kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

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 }