kit

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

hostas_cross.sh (15726B)


      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 (see hostas_toy.sh): map a KIT_TEST_OPTS override of O-spellings
     68 # onto the engine's bare opt levels; default 0 1.
     69 OPT_LEVELS="0 1"
     70 if [ -n "${KIT_TEST_OPTS:-}" ]; then
     71     OPT_LEVELS=""
     72     for _o in $KIT_TEST_OPTS; do OPT_LEVELS="$OPT_LEVELS ${_o#O}"; done
     73 fi
     74 
     75 # "tag:triple" — tag is exec_target.sh's <arch>-<os> spelling. All three ELF
     76 # targets are in the gating default (each SKIPs cleanly if its clang cross
     77 # target or container runner is unavailable). Narrow the matrix with
     78 # KIT_HOSTAS_CROSS_TARGETS, e.g. KIT_HOSTAS_CROSS_TARGETS="x64-linux:x86_64-linux-gnu".
     79 TARGETS="${KIT_HOSTAS_CROSS_TARGETS:-aarch64-linux:aarch64-linux-gnu x64-linux:x86_64-linux-gnu rv64-linux:riscv64-linux-gnu}"
     80 
     81 # Filter ($1) preserved -> the engine filters discovery by KIT_TEST_FILTER.
     82 FILTER="${1:-${KIT_TEST_FILTER:-}}"
     83 export KIT_TEST_FILTER="$FILTER"
     84 
     85 # The original exits on a_fail (+ b_efail when enforcing) only — skips (the known
     86 # 141 case) never gated. Keep that: SKIP must not fail the run.
     87 KIT_SKIP_IS_FAILURE=0
     88 
     89 CLANG="${CLANG:-$(command -v clang 2>/dev/null || true)}"
     90 
     91 color_red() { printf '\033[31m%s\033[0m' "$1"; }
     92 color_grn() { printf '\033[32m%s\033[0m' "$1"; }
     93 color_yel() { printf '\033[33m%s\033[0m' "$1"; }
     94 
     95 if [ ! -x "$KIT" ]; then
     96     printf 'hostas-cross: %s kit missing — run "make bin"\n' "$(color_red FATAL)" >&2
     97     exit 1
     98 fi
     99 if [ -z "$CLANG" ] || [ ! -x "$CLANG" ]; then
    100     printf 'hostas-cross: %s no clang (host assembler); skipping\n' "$(color_yel SKIP)"
    101     exit 0
    102 fi
    103 mkdir -p "$BUILD_DIR"
    104 
    105 # ---- exec_target.sh wiring (mirrors test/link/run.sh detection) ------------
    106 have_qemu=0
    107 QEMU_BIN="$(command -v qemu-aarch64-static 2>/dev/null || command -v qemu-aarch64 2>/dev/null || true)"
    108 [ -n "$QEMU_BIN" ] && have_qemu=1
    109 QEMU_RV64_BIN="$(command -v qemu-riscv64-static 2>/dev/null || command -v qemu-riscv64 2>/dev/null || true)"
    110 have_podman=0
    111 command -v podman >/dev/null 2>&1 && have_podman=1
    112 arch_raw="$(uname -m 2>/dev/null || true)"
    113 is_aarch64=0
    114 { [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1
    115 export have_qemu QEMU_BIN QEMU_RV64_BIN have_podman is_aarch64
    116 # Pin arch-explicit images so podman doesn't resolve an ambiguous multi-arch
    117 # tag to the wrong variant (the bare `alpine:latest` in local storage can map
    118 # to a non-host arch). Overridable; the binaries are static/freestanding so any
    119 # same-arch Linux image with /bin/sh works. Absent images fail --pull=never,
    120 # which the per-target exec smoke turns into a clean SKIP.
    121 : "${RUN_AARCH64_IMAGE:=docker.io/arm64v8/alpine:latest}"
    122 : "${RUN_X64_IMAGE:=docker.io/amd64/alpine:latest}"
    123 : "${RUN_RV64_IMAGE:=docker.io/riscv64/alpine:edge}"
    124 export RUN_AARCH64_IMAGE RUN_X64_IMAGE RUN_RV64_IMAGE
    125 EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"
    126 export EXEC_TARGET_MOUNT_ROOT
    127 # shellcheck source=../lib/exec_target.sh
    128 . "$ROOT/test/lib/exec_target.sh"
    129 
    130 # Same TLS-symbolization skip as the sibling lanes — driven by the shared
    131 # sidecar 141_threadlocal_mutate.link.skip via KIT_READ_CASE (skips the WHOLE
    132 # case here too, matching hostas_toy.sh).
    133 hostas_read_case() {
    134     local reason
    135     if reason=$(kit_skip_sidecar "$KIT_SIDECAR_DIR" "$KIT_BASE" "" "link"); then
    136         KIT_SKIP_CASE="$reason (cc -S symbolizer gap)"
    137     fi
    138 }
    139 
    140 oracle() {
    141     local name="$1" exp=0
    142     [ -f "$CASES/$name.expected" ] && exp=$(head -n1 "$CASES/$name.expected")
    143     echo $((exp & 255))
    144 }
    145 
    146 # First real `error:` line from an assembler's stderr (clang prints a harmless
    147 # -Wmissing-sysroot warning first — we never link with clang, so the SDK is
    148 # irrelevant).
    149 err_reason() {
    150     local f="$1" line=""
    151     line=$(grep -m1 -E 'error:|fatal:' "$f" 2>/dev/null | sed 's|.*\(error\|fatal\): *||')
    152     [ -z "$line" ] && line=$(head -1 "$f" 2>/dev/null | sed 's|.*: ||')
    153     printf '%s' "$line"
    154 }
    155 
    156 # Bounded exec: run exe via exec_target under a wall-clock cap so a wedged
    157 # emulator (e.g. a riscv64 qemu-user that never returns) downgrades a target to
    158 # SKIP instead of hanging the whole harness. Sets SMOKE_RC (124 == timed out).
    159 bounded_exec() {
    160     local to="$1" tag="$2" exe="$3" out="$4" err="$5"
    161     local rc0="$exe.rc0"
    162     rm -f "$rc0"
    163     ( exec_target_run "$tag" "$exe" "$out" "$err"; echo "$RUN_RC" >"$rc0" ) &
    164     local pid=$! waited=0
    165     while kill -0 "$pid" 2>/dev/null; do
    166         sleep 1; waited=$((waited+1))
    167         if [ "$waited" -ge "$to" ]; then
    168             kill -9 "$pid" 2>/dev/null; wait "$pid" 2>/dev/null
    169             SMOKE_RC=124; return
    170         fi
    171     done
    172     wait "$pid" 2>/dev/null
    173     SMOKE_RC=124
    174     [ -f "$rc0" ] && SMOKE_RC="$(cat "$rc0")"
    175 }
    176 
    177 # Link a relocatable object + the target crt into a static ELF. Echoes nothing;
    178 # returns nonzero (and leaves stderr in $3) on failure or a non-empty linker
    179 # diagnostic (warnings are treated as failures, as in hostas_toy.sh).
    180 kit_ld_static() {
    181     local obj="$1" out="$2" lderr="$3" crt="$4"
    182     "$KIT" ld -static "$obj" "$crt" -o "$out" 2>"$lderr" || return 1
    183     [ -s "$lderr" ] && return 1
    184     return 0
    185 }
    186 
    187 # ---- shared `cc -S -target` build for both lanes ---------------------------
    188 # One cross .s per item, cached in $KIT_WORK so lanes A and B reuse it. A cc -S
    189 # failure is a lane-A failure only (lane B records nothing for the item).
    190 # Reads the per-target $TGT_TRIPLE (set before each target's kit_corpus_run).
    191 _ccs_build() {
    192     CCS_S="$KIT_WORK/s.s"
    193     [ -f "$KIT_WORK/.ccs.done" ] && { CCS_RC=$(cat "$KIT_WORK/.ccs.rc"); return "$CCS_RC"; }
    194     if "$KIT" cc -S "-O$KIT_OPT" -target "$TGT_TRIPLE" "$KIT_SRC" -o "$CCS_S" 2>"$KIT_WORK/ccs.err"; then
    195         CCS_RC=0
    196     else
    197         CCS_RC=1
    198     fi
    199     echo "$CCS_RC" >"$KIT_WORK/.ccs.rc"; : >"$KIT_WORK/.ccs.done"
    200     return "$CCS_RC"
    201 }
    202 
    203 # ---- lane A: kit-as -> kit ld -static -> run (deferred batched exec) ----
    204 # Reads per-target $TGT_TRIPLE, $TGT_TAG, $TGT_CRT (set before kit_corpus_run).
    205 kit_lane_A() {
    206     if ! _ccs_build; then
    207         kit_fail "$TGT_TAG/$KIT_NAME/kit-as" "cc -S: $(err_reason "$KIT_WORK/ccs.err")"
    208         return
    209     fi
    210     if ! "$KIT" as -target "$TGT_TRIPLE" "$CCS_S" -o "$KIT_WORK/a.o" 2>"$KIT_WORK/a.as.err"; then
    211         kit_fail "$TGT_TAG/$KIT_NAME/kit-as" "$(err_reason "$KIT_WORK/a.as.err")"; return
    212     fi
    213     if ! kit_ld_static "$KIT_WORK/a.o" "$KIT_WORK/a.out" "$KIT_WORK/a.ld.err" "$TGT_CRT"; then
    214         kit_fail "$TGT_TAG/$KIT_NAME/kit-as ld" "$(err_reason "$KIT_WORK/a.ld.err")"; return
    215     fi
    216     kit_queue_e "$TGT_TAG/$KIT_NAME/kit-as" "$KIT_WORK/a.out" \
    217         "$KIT_WORK/a.run.out" "$KIT_WORK/a.run.err" "$KIT_WORK/a.rc" \
    218         "$KIT_EXPECTED" "$TGT_TAG"
    219 }
    220 
    221 # ---- lane B: clang-as -> kit ld -static -> run ---------------------------
    222 # Gated by ENFORCE_CLANG. Build-stage failures are recorded inline (FAIL when
    223 # enforcing, XFAIL otherwise). The exec stage: when enforcing, defer to the
    224 # batched flush via kit_queue_e (pass/fail). When NOT enforcing, run inline
    225 # (bounded) so the exec verdict can be XPASS (pass) / XFAIL (fail).
    226 kit_lane_B() {
    227     if ! _ccs_build; then return; fi
    228     local name="$TGT_TAG/$KIT_NAME/clang-as"
    229     if ! "$CLANG" --target="$TGT_TRIPLE" -c "$CCS_S" -o "$KIT_WORK/b.o" 2>"$KIT_WORK/b.as.err"; then
    230         if [ "$ENFORCE_CLANG" = "1" ]; then kit_fail "$name" "$(err_reason "$KIT_WORK/b.as.err")"
    231         else kit_xfail "$name" "$(err_reason "$KIT_WORK/b.as.err")"; fi
    232         return
    233     fi
    234     if ! kit_ld_static "$KIT_WORK/b.o" "$KIT_WORK/b.out" "$KIT_WORK/b.ld.err" "$TGT_CRT"; then
    235         if [ "$ENFORCE_CLANG" = "1" ]; then kit_fail "$name" "ld: $(err_reason "$KIT_WORK/b.ld.err")"
    236         else kit_xfail "$name"; fi
    237         return
    238     fi
    239     if [ "$ENFORCE_CLANG" = "1" ]; then
    240         kit_queue_e "$name" "$KIT_WORK/b.out" \
    241             "$KIT_WORK/b.run.out" "$KIT_WORK/b.run.err" "$KIT_WORK/b.rc" \
    242             "$KIT_EXPECTED" "$TGT_TAG"
    243         return
    244     fi
    245     # ENFORCE_CLANG=0: inline bounded exec -> XPASS/XFAIL.
    246     bounded_exec "${EXEC_CASE_TIMEOUT:-20}" "$TGT_TAG" "$KIT_WORK/b.out" \
    247         "$KIT_WORK/b.run.out" "$KIT_WORK/b.run.err"
    248     if [ "$SMOKE_RC" = "$((KIT_EXPECTED & 255))" ]; then kit_xpass "$name"
    249     else kit_xfail "$name"; fi
    250 }
    251 
    252 # ---- per-target capability gates (SKIP-TGT, never FAIL) --------------------
    253 # Returns 0 if the target is runnable (and sets TGT_CRT); prints a single
    254 # SKIP-TGT line and returns 1 otherwise. Mirrors the original gate chain.
    255 target_gate() {
    256     local tag="$1" triple="$2" tdir="$3"
    257     if ! "$CLANG" --target="$triple" -c -x c - -o /dev/null </dev/null 2>/dev/null; then
    258         printf '  %s %s — no clang cross target\n' "$(color_yel SKIP-TGT)" "$tag"; return 1
    259     fi
    260     if ! exec_target_supported "$tag"; then
    261         printf '  %s %s — no runner (podman/qemu)\n' "$(color_yel SKIP-TGT)" "$tag"; return 1
    262     fi
    263     TGT_CRT="$tdir/start.o"
    264     if ! "$CLANG" --target="$triple" -O1 -ffreestanding -fno-stack-protector \
    265             -fno-PIC -fno-pie -Dtest_main=main -c "$START_SRC" -o "$TGT_CRT" 2>"$tdir/crt.err"; then
    266         printf '  %s %s — crt build failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$tdir/crt.err")"; return 1
    267     fi
    268 
    269     # Pick a representative non-skip case for the smokes (first case without a
    270     # 141-style .link.skip sidecar).
    271     local smoke="" s n
    272     for s in "$CASES"/*.toy; do
    273         n="$(basename "$s" .toy)"
    274         kit_skip_sidecar "$CASES" "$n" "" "link" >/dev/null && continue
    275         smoke="$n"; break
    276     done
    277     local sd="$tdir/_smoke"; mkdir -p "$sd"
    278     if ! "$KIT" cc -S -O0 -target "$triple" "$CASES/$smoke.toy" -o "$sd/s.s" 2>"$sd/ccs.err"; then
    279         printf '  %s %s — cc -S failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/ccs.err")"; return 1
    280     fi
    281     if ! "$KIT" as -target "$triple" "$sd/s.s" -o "$sd/a.o" 2>"$sd/as.err"; then
    282         printf '  %s %s — cc -S|as gap: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/as.err")"; return 1
    283     fi
    284     if ! kit_ld_static "$sd/a.o" "$sd/a.out" "$sd/ld.err" "$TGT_CRT"; then
    285         printf '  %s %s — kit ld -static failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/ld.err")"; return 1
    286     fi
    287     bounded_exec "$EXEC_SMOKE_TIMEOUT" "$tag" "$sd/a.out" "$sd/run.out" "$sd/run.err"
    288     local sexp; sexp=$(oracle "$smoke")
    289     if [ "$SMOKE_RC" != "$sexp" ]; then
    290         local reason="exit $SMOKE_RC != $sexp"
    291         [ "$SMOKE_RC" = "124" ] && reason="exec timed out (>${EXEC_SMOKE_TIMEOUT}s)"
    292         printf '  %s %s — exec smoke: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$reason"; return 1
    293     fi
    294     return 0
    295 }
    296 
    297 # ---- drive the corpus, one target at a time --------------------------------
    298 printf 'hostas-cross: kit=%s\n' "$KIT"
    299 printf 'hostas-cross: clang=%s opts="%s" enforce_clang=%s podman=%s\n' \
    300     "$CLANG" "$OPT_LEVELS" "$ENFORCE_CLANG" "$have_podman"
    301 printf 'hostas-cross: targets="%s"\n' "$TARGETS"
    302 
    303 tgt_run=0
    304 shopt -s nullglob
    305 for entry in $TARGETS; do
    306     TGT_TAG="${entry%%:*}"; TGT_TRIPLE="${entry##*:}"
    307     tdir="$BUILD_DIR/$TGT_TAG"; rm -rf "$tdir"; mkdir -p "$tdir"
    308     if ! target_gate "$TGT_TAG" "$TGT_TRIPLE" "$tdir"; then continue; fi
    309 
    310     tgt_run=$((tgt_run + 1))
    311     printf '  %s %s (%s) — running corpus\n' "$(color_grn TGT)" "$TGT_TAG" "$TGT_TRIPLE"
    312     # TGT_TAG / TGT_TRIPLE / TGT_CRT are set; lane hooks (incl. parallel
    313     # workers, forked after this point) read them. One kit_corpus_run per target,
    314     # like elf's A/B/C layers — counters accumulate into the shared summary.
    315     export TGT_TAG TGT_TRIPLE TGT_CRT
    316     KIT_LABEL=test-hostas-cross KIT_BUILD_DIR="$tdir" \
    317       KIT_CORPUS_GLOBS="$CASES/*.toy" KIT_CORPUS_EXT=toy KIT_SIDECAR_DIR="$CASES" \
    318       KIT_LANES="A B" KIT_OPT_LEVELS="$OPT_LEVELS" KIT_TUPLES="$TGT_TAG" \
    319       KIT_TARGETS_EXT="" KIT_READ_CASE=hostas_read_case KIT_PARALLELIZABLE="$PAR" \
    320       kit_corpus_run
    321 done
    322 shopt -u nullglob
    323 
    324 printf '\n'
    325 if [ "$tgt_run" -eq 0 ]; then
    326     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'
    327 fi
    328 
    329 kit_summary test-hostas-cross
    330 kit_exit