kit

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

hostas_cross.sh (15709B)


      1 #!/usr/bin/env bash
      2 # test/asm/hostas_cross.sh — cross-compile + cross-exec extension of the
      3 # host-assembler lane (test/asm/hostas_toy.sh) to ELF Linux targets.
      4 # (Type C — shared corpus harness; one kit_corpus_run per ELF target.)
      5 #
      6 # Where hostas_toy.sh proves `cc -S` on the *native* target, this proves it
      7 # CROSS: for each ELF target (aarch64/x86_64/riscv64-linux) it emits ONE
      8 # `cc -S`, feeds it to BOTH kit-as and a host assembler (clang), links each
      9 # into a static ELF with `kit ld`, and runs it under podman/qemu — exit must
     10 # match the toy oracle. The assembler is the only variable between the two
     11 # lanes, judged by EXECUTION (kit and clang emit different code, so a
     12 # byte/text match would be meaningless), exactly like hostas_toy.sh.
     13 #
     14 # Per toy case (each target; both O0 and O1):
     15 #     kit cc -S -target <triple>                 -> s.s   (shared by both)
     16 #   A /kit-as: kit as -target | kit ld -static -> run    exit == oracle
     17 #   B /clang-as: clang --target -c | kit ld -static -> run   exit == oracle
     18 #
     19 # Executable shape: a STATIC, non-PIE ELF (`kit ld -static`) linked with the
     20 # freestanding crt test/link/harness/start.c compiled `-Dtest_main=main`, so
     21 # `_start` runs ctors then calls the toy's `main` and exits with its return
     22 # (the oracle) via a raw syscall — no libc/loader needed, so any same-arch
     23 # Linux image runs it. Execution uses the shared test/lib/exec_target.sh helper.
     24 # Lane A (and, under ENFORCE_CLANG=1, lane B) defers exec to one batched
     25 # `podman run` per target via the corpus engine's kit_queue_e; lane B under
     26 # ENFORCE_CLANG=0 runs inline (bounded) so its exec verdict can be XFAIL/XPASS.
     27 #
     28 # Self-probing: each target is SKIPPED (not failed) unless the host has (1) a
     29 # clang cross-compiler for it, (2) a runner (podman/qemu) per exec_target, (3) a
     30 # working `kit cc -S | kit as` round-trip for that arch, and (4) a bounded
     31 # exec smoke that returns the oracle. So the harness runs green on whatever the
     32 # host supports and self-extends as gaps close. All three ELF targets pass BOTH
     33 # lanes end-to-end (936/936 = 312 cases x {O0,O1} x 3 arches, ENFORCE_CLANG).
     34 # Code locations that an encoding-divergent assembler must recompute — switch
     35 # jump-table entries and `&&label` address-takes — are referenced via per-block
     36 # local symbols emitted by codegen (mc_label_symbol): the jump table is
     37 # `.quad .Lcfblk.*` (R_ABS64) and the address-take a standard PC-relative reloc
     38 # against the same symbol (x86-64 leaq/R_PC32, aarch64 adrp+add, riscv64
     39 # auipc+addi/%pcrel). So the references are genuinely relocatable on every arch
     40 # and clang's encoding choices (movabs vs mov-imm32, jmp rel32 vs rel8, RVC
     41 # compression) can't shift a baked offset onto the wrong instruction.
     42 # Execution under qemu-user (x86_64/riscv64 in their podman containers) is the
     43 # sole judge — kit and clang emit different code, so a byte/text match would be
     44 # meaningless. The batched runner caps each case (EXEC_CASE_TIMEOUT) so one
     45 # hanging binary can't wedge the whole container.
     46 #
     47 # Override the matrix with KIT_HOSTAS_CROSS_TARGETS="tag:triple ..." and the
     48 # clang-as gate with KIT_HOSTAS_ENFORCE_CLANG=0 (demote lane B to XFAIL).
     49 # Every lane hook writes only under $KIT_WORK and records via kit_*, so the runner
     50 # is parallel-safe; KIT_HOSTAS_PARALLEL flips dispatch.
     51 
     52 set -u
     53 
     54 ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
     55 export KIT_LIB_DIR="$ROOT/test/lib"
     56 # shellcheck source=../lib/kit_corpus.sh
     57 . "$ROOT/test/lib/kit_corpus.sh"
     58 
     59 KIT="${KIT:-$ROOT/build/kit}"
     60 CASES="$ROOT/test/toy/cases"
     61 BUILD_DIR="$ROOT/build/test/asm/hostas_cross"
     62 START_SRC="$ROOT/test/link/harness/start.c"
     63 ENFORCE_CLANG="${KIT_HOSTAS_ENFORCE_CLANG:-1}"
     64 EXEC_SMOKE_TIMEOUT="${KIT_HOSTAS_EXEC_TIMEOUT:-45}"
     65 PAR="${KIT_HOSTAS_PARALLEL:-1}"
     66 
     67 # Opt axis. KIT_TEST_OPTS carries "O"-prefixed levels (e.g. "O0 O1"); the
     68 # corpus engine expects bare levels (KIT_OPT_LEVELS="0 1"). Strip the prefix.
     69 OPTS="${KIT_TEST_OPTS:-O0 O1}"
     70 OPT_LEVELS=""
     71 for _o in $OPTS; do OPT_LEVELS="$OPT_LEVELS ${_o#O}"; done
     72 
     73 # "tag:triple" — tag is exec_target.sh's <arch>-<os> spelling. All three ELF
     74 # targets are in the gating default (each SKIPs cleanly if its clang cross
     75 # target or container runner is unavailable). Narrow the matrix with
     76 # KIT_HOSTAS_CROSS_TARGETS, e.g. KIT_HOSTAS_CROSS_TARGETS="x64-linux:x86_64-linux-gnu".
     77 TARGETS="${KIT_HOSTAS_CROSS_TARGETS:-aarch64-linux:aarch64-linux-gnu x64-linux:x86_64-linux-gnu rv64-linux:riscv64-linux-gnu}"
     78 
     79 # Filter ($1) preserved -> the engine filters discovery by KIT_TEST_FILTER.
     80 FILTER="${1:-${KIT_TEST_FILTER:-}}"
     81 export KIT_TEST_FILTER="$FILTER"
     82 
     83 # The original exits on a_fail (+ b_efail when enforcing) only — skips (the known
     84 # 141 case) never gated. Keep that: SKIP must not fail the run.
     85 KIT_SKIP_IS_FAILURE=0
     86 
     87 CLANG="${CLANG:-$(command -v clang 2>/dev/null || true)}"
     88 
     89 color_red() { printf '\033[31m%s\033[0m' "$1"; }
     90 color_grn() { printf '\033[32m%s\033[0m' "$1"; }
     91 color_yel() { printf '\033[33m%s\033[0m' "$1"; }
     92 
     93 if [ ! -x "$KIT" ]; then
     94     printf 'hostas-cross: %s kit missing — run "make bin"\n' "$(color_red FATAL)" >&2
     95     exit 1
     96 fi
     97 if [ -z "$CLANG" ] || [ ! -x "$CLANG" ]; then
     98     printf 'hostas-cross: %s no clang (host assembler); skipping\n' "$(color_yel SKIP)"
     99     exit 0
    100 fi
    101 mkdir -p "$BUILD_DIR"
    102 
    103 # ---- exec_target.sh wiring (mirrors test/link/run.sh detection) ------------
    104 have_qemu=0
    105 QEMU_BIN="$(command -v qemu-aarch64-static 2>/dev/null || command -v qemu-aarch64 2>/dev/null || true)"
    106 [ -n "$QEMU_BIN" ] && have_qemu=1
    107 QEMU_RV64_BIN="$(command -v qemu-riscv64-static 2>/dev/null || command -v qemu-riscv64 2>/dev/null || true)"
    108 have_podman=0
    109 command -v podman >/dev/null 2>&1 && have_podman=1
    110 arch_raw="$(uname -m 2>/dev/null || true)"
    111 is_aarch64=0
    112 { [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1
    113 export have_qemu QEMU_BIN QEMU_RV64_BIN have_podman is_aarch64
    114 # Pin arch-explicit images so podman doesn't resolve an ambiguous multi-arch
    115 # tag to the wrong variant (the bare `alpine:latest` in local storage can map
    116 # to a non-host arch). Overridable; the binaries are static/freestanding so any
    117 # same-arch Linux image with /bin/sh works. Absent images fail --pull=never,
    118 # which the per-target exec smoke turns into a clean SKIP.
    119 : "${RUN_AARCH64_IMAGE:=docker.io/arm64v8/alpine:latest}"
    120 : "${RUN_X64_IMAGE:=docker.io/amd64/alpine:latest}"
    121 : "${RUN_RV64_IMAGE:=docker.io/riscv64/alpine:edge}"
    122 export RUN_AARCH64_IMAGE RUN_X64_IMAGE RUN_RV64_IMAGE
    123 EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"
    124 export EXEC_TARGET_MOUNT_ROOT
    125 # shellcheck source=../lib/exec_target.sh
    126 . "$ROOT/test/lib/exec_target.sh"
    127 
    128 # Same TLS-symbolization skip as the sibling lanes — driven by the shared
    129 # sidecar 141_threadlocal_mutate.link.skip via KIT_READ_CASE (skips the WHOLE
    130 # case here too, matching hostas_toy.sh).
    131 hostas_read_case() {
    132     local reason
    133     if reason=$(kit_skip_sidecar "$KIT_SIDECAR_DIR" "$KIT_BASE" "" "link"); then
    134         KIT_SKIP_CASE="$reason (cc -S symbolizer gap)"
    135     fi
    136 }
    137 
    138 oracle() {
    139     local name="$1" exp=0
    140     [ -f "$CASES/$name.expected" ] && exp=$(head -n1 "$CASES/$name.expected")
    141     echo $((exp & 255))
    142 }
    143 
    144 # First real `error:` line from an assembler's stderr (clang prints a harmless
    145 # -Wmissing-sysroot warning first — we never link with clang, so the SDK is
    146 # irrelevant).
    147 err_reason() {
    148     local f="$1" line=""
    149     line=$(grep -m1 -E 'error:|fatal:' "$f" 2>/dev/null | sed 's|.*\(error\|fatal\): *||')
    150     [ -z "$line" ] && line=$(head -1 "$f" 2>/dev/null | sed 's|.*: ||')
    151     printf '%s' "$line"
    152 }
    153 
    154 # Bounded exec: run exe via exec_target under a wall-clock cap so a wedged
    155 # emulator (e.g. a riscv64 qemu-user that never returns) downgrades a target to
    156 # SKIP instead of hanging the whole harness. Sets SMOKE_RC (124 == timed out).
    157 bounded_exec() {
    158     local to="$1" tag="$2" exe="$3" out="$4" err="$5"
    159     local rc0="$exe.rc0"
    160     rm -f "$rc0"
    161     ( exec_target_run "$tag" "$exe" "$out" "$err"; echo "$RUN_RC" >"$rc0" ) &
    162     local pid=$! waited=0
    163     while kill -0 "$pid" 2>/dev/null; do
    164         sleep 1; waited=$((waited+1))
    165         if [ "$waited" -ge "$to" ]; then
    166             kill -9 "$pid" 2>/dev/null; wait "$pid" 2>/dev/null
    167             SMOKE_RC=124; return
    168         fi
    169     done
    170     wait "$pid" 2>/dev/null
    171     SMOKE_RC=124
    172     [ -f "$rc0" ] && SMOKE_RC="$(cat "$rc0")"
    173 }
    174 
    175 # Link a relocatable object + the target crt into a static ELF. Echoes nothing;
    176 # returns nonzero (and leaves stderr in $3) on failure or a non-empty linker
    177 # diagnostic (warnings are treated as failures, as in hostas_toy.sh).
    178 kit_ld_static() {
    179     local obj="$1" out="$2" lderr="$3" crt="$4"
    180     "$KIT" ld -static "$obj" "$crt" -o "$out" 2>"$lderr" || return 1
    181     [ -s "$lderr" ] && return 1
    182     return 0
    183 }
    184 
    185 # ---- shared `cc -S -target` build for both lanes ---------------------------
    186 # One cross .s per item, cached in $KIT_WORK so lanes A and B reuse it. A cc -S
    187 # failure is a lane-A failure only (lane B records nothing for the item).
    188 # Reads the per-target $TGT_TRIPLE (set before each target's kit_corpus_run).
    189 _ccs_build() {
    190     CCS_S="$KIT_WORK/s.s"
    191     [ -f "$KIT_WORK/.ccs.done" ] && { CCS_RC=$(cat "$KIT_WORK/.ccs.rc"); return "$CCS_RC"; }
    192     if "$KIT" cc -S "-O$KIT_OPT" -target "$TGT_TRIPLE" "$KIT_SRC" -o "$CCS_S" 2>"$KIT_WORK/ccs.err"; then
    193         CCS_RC=0
    194     else
    195         CCS_RC=1
    196     fi
    197     echo "$CCS_RC" >"$KIT_WORK/.ccs.rc"; : >"$KIT_WORK/.ccs.done"
    198     return "$CCS_RC"
    199 }
    200 
    201 # ---- lane A: kit-as -> kit ld -static -> run (deferred batched exec) ----
    202 # Reads per-target $TGT_TRIPLE, $TGT_TAG, $TGT_CRT (set before kit_corpus_run).
    203 kit_lane_A() {
    204     if ! _ccs_build; then
    205         kit_fail "$TGT_TAG/$KIT_NAME/kit-as" "cc -S: $(err_reason "$KIT_WORK/ccs.err")"
    206         return
    207     fi
    208     if ! "$KIT" as -target "$TGT_TRIPLE" "$CCS_S" -o "$KIT_WORK/a.o" 2>"$KIT_WORK/a.as.err"; then
    209         kit_fail "$TGT_TAG/$KIT_NAME/kit-as" "$(err_reason "$KIT_WORK/a.as.err")"; return
    210     fi
    211     if ! kit_ld_static "$KIT_WORK/a.o" "$KIT_WORK/a.out" "$KIT_WORK/a.ld.err" "$TGT_CRT"; then
    212         kit_fail "$TGT_TAG/$KIT_NAME/kit-as ld" "$(err_reason "$KIT_WORK/a.ld.err")"; return
    213     fi
    214     kit_queue_e "$TGT_TAG/$KIT_NAME/kit-as" "$KIT_WORK/a.out" \
    215         "$KIT_WORK/a.run.out" "$KIT_WORK/a.run.err" "$KIT_WORK/a.rc" \
    216         "$KIT_EXPECTED" "$TGT_TAG"
    217 }
    218 
    219 # ---- lane B: clang-as -> kit ld -static -> run ---------------------------
    220 # Gated by ENFORCE_CLANG. Build-stage failures are recorded inline (FAIL when
    221 # enforcing, XFAIL otherwise). The exec stage: when enforcing, defer to the
    222 # batched flush via kit_queue_e (pass/fail). When NOT enforcing, run inline
    223 # (bounded) so the exec verdict can be XPASS (pass) / XFAIL (fail).
    224 kit_lane_B() {
    225     if ! _ccs_build; then return; fi
    226     local name="$TGT_TAG/$KIT_NAME/clang-as"
    227     if ! "$CLANG" --target="$TGT_TRIPLE" -c "$CCS_S" -o "$KIT_WORK/b.o" 2>"$KIT_WORK/b.as.err"; then
    228         if [ "$ENFORCE_CLANG" = "1" ]; then kit_fail "$name" "$(err_reason "$KIT_WORK/b.as.err")"
    229         else kit_xfail "$name" "$(err_reason "$KIT_WORK/b.as.err")"; fi
    230         return
    231     fi
    232     if ! kit_ld_static "$KIT_WORK/b.o" "$KIT_WORK/b.out" "$KIT_WORK/b.ld.err" "$TGT_CRT"; then
    233         if [ "$ENFORCE_CLANG" = "1" ]; then kit_fail "$name" "ld: $(err_reason "$KIT_WORK/b.ld.err")"
    234         else kit_xfail "$name"; fi
    235         return
    236     fi
    237     if [ "$ENFORCE_CLANG" = "1" ]; then
    238         kit_queue_e "$name" "$KIT_WORK/b.out" \
    239             "$KIT_WORK/b.run.out" "$KIT_WORK/b.run.err" "$KIT_WORK/b.rc" \
    240             "$KIT_EXPECTED" "$TGT_TAG"
    241         return
    242     fi
    243     # ENFORCE_CLANG=0: inline bounded exec -> XPASS/XFAIL.
    244     bounded_exec "${EXEC_CASE_TIMEOUT:-20}" "$TGT_TAG" "$KIT_WORK/b.out" \
    245         "$KIT_WORK/b.run.out" "$KIT_WORK/b.run.err"
    246     if [ "$SMOKE_RC" = "$((KIT_EXPECTED & 255))" ]; then kit_xpass "$name"
    247     else kit_xfail "$name"; fi
    248 }
    249 
    250 # ---- per-target capability gates (SKIP-TGT, never FAIL) --------------------
    251 # Returns 0 if the target is runnable (and sets TGT_CRT); prints a single
    252 # SKIP-TGT line and returns 1 otherwise. Mirrors the original gate chain.
    253 target_gate() {
    254     local tag="$1" triple="$2" tdir="$3"
    255     if ! "$CLANG" --target="$triple" -c -x c - -o /dev/null </dev/null 2>/dev/null; then
    256         printf '  %s %s — no clang cross target\n' "$(color_yel SKIP-TGT)" "$tag"; return 1
    257     fi
    258     if ! exec_target_supported "$tag"; then
    259         printf '  %s %s — no runner (podman/qemu)\n' "$(color_yel SKIP-TGT)" "$tag"; return 1
    260     fi
    261     TGT_CRT="$tdir/start.o"
    262     if ! "$CLANG" --target="$triple" -O1 -ffreestanding -fno-stack-protector \
    263             -fno-PIC -fno-pie -Dtest_main=main -c "$START_SRC" -o "$TGT_CRT" 2>"$tdir/crt.err"; then
    264         printf '  %s %s — crt build failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$tdir/crt.err")"; return 1
    265     fi
    266 
    267     # Pick a representative non-skip case for the smokes (first case without a
    268     # 141-style .link.skip sidecar).
    269     local smoke="" s n
    270     for s in "$CASES"/*.toy; do
    271         n="$(basename "$s" .toy)"
    272         kit_skip_sidecar "$CASES" "$n" "" "link" >/dev/null && continue
    273         smoke="$n"; break
    274     done
    275     local sd="$tdir/_smoke"; mkdir -p "$sd"
    276     if ! "$KIT" cc -S -O0 -target "$triple" "$CASES/$smoke.toy" -o "$sd/s.s" 2>"$sd/ccs.err"; then
    277         printf '  %s %s — cc -S failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/ccs.err")"; return 1
    278     fi
    279     if ! "$KIT" as -target "$triple" "$sd/s.s" -o "$sd/a.o" 2>"$sd/as.err"; then
    280         printf '  %s %s — cc -S|as gap: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/as.err")"; return 1
    281     fi
    282     if ! kit_ld_static "$sd/a.o" "$sd/a.out" "$sd/ld.err" "$TGT_CRT"; then
    283         printf '  %s %s — kit ld -static failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/ld.err")"; return 1
    284     fi
    285     bounded_exec "$EXEC_SMOKE_TIMEOUT" "$tag" "$sd/a.out" "$sd/run.out" "$sd/run.err"
    286     local sexp; sexp=$(oracle "$smoke")
    287     if [ "$SMOKE_RC" != "$sexp" ]; then
    288         local reason="exit $SMOKE_RC != $sexp"
    289         [ "$SMOKE_RC" = "124" ] && reason="exec timed out (>${EXEC_SMOKE_TIMEOUT}s)"
    290         printf '  %s %s — exec smoke: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$reason"; return 1
    291     fi
    292     return 0
    293 }
    294 
    295 # ---- drive the corpus, one target at a time --------------------------------
    296 printf 'hostas-cross: kit=%s\n' "$KIT"
    297 printf 'hostas-cross: clang=%s opts="%s" enforce_clang=%s podman=%s\n' \
    298     "$CLANG" "$OPT_LEVELS" "$ENFORCE_CLANG" "$have_podman"
    299 printf 'hostas-cross: targets="%s"\n' "$TARGETS"
    300 
    301 tgt_run=0
    302 shopt -s nullglob
    303 for entry in $TARGETS; do
    304     TGT_TAG="${entry%%:*}"; TGT_TRIPLE="${entry##*:}"
    305     tdir="$BUILD_DIR/$TGT_TAG"; rm -rf "$tdir"; mkdir -p "$tdir"
    306     if ! target_gate "$TGT_TAG" "$TGT_TRIPLE" "$tdir"; then continue; fi
    307 
    308     tgt_run=$((tgt_run + 1))
    309     printf '  %s %s (%s) — running corpus\n' "$(color_grn TGT)" "$TGT_TAG" "$TGT_TRIPLE"
    310     # TGT_TAG / TGT_TRIPLE / TGT_CRT are set; lane hooks (incl. parallel
    311     # workers, forked after this point) read them. One kit_corpus_run per target,
    312     # like elf's A/B/C layers — counters accumulate into the shared summary.
    313     export TGT_TAG TGT_TRIPLE TGT_CRT
    314     KIT_LABEL=test-hostas-cross KIT_BUILD_DIR="$tdir" \
    315       KIT_CORPUS_GLOBS="$CASES/*.toy" KIT_CORPUS_EXT=toy KIT_SIDECAR_DIR="$CASES" \
    316       KIT_LANES="A B" KIT_OPT_LEVELS="$OPT_LEVELS" KIT_TUPLES="$TGT_TAG" \
    317       KIT_TARGETS_EXT="" KIT_READ_CASE=hostas_read_case KIT_PARALLELIZABLE="$PAR" \
    318       kit_corpus_run
    319 done
    320 shopt -u nullglob
    321 
    322 printf '\n'
    323 if [ "$tgt_run" -eq 0 ]; then
    324     printf 'hostas-cross: no target ran (all SKIP-TGT) — needs a clang cross target + podman/qemu + a working cc -S|as for some ELF arch.\n'
    325 fi
    326 
    327 kit_summary test-hostas-cross
    328 kit_exit