Files
Martin Vogel ba5724319c fix(ci): accept the documented test-amd64 leg spelling
run.sh --help advertises 'amd64|test-amd64|tsan-amd64', but only 'amd64'
was dispatched, so the documented spelling failed with
"unknown leg 'test-amd64'". Accept both.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-06 02:54:23 +02:00

319 lines
15 KiB
Bash
Executable File

#!/usr/bin/env bash
# Containerized local-CI legs. Before pushing, pair these with native macOS
# tests and the real-Windows VM gate documented below.
#
# Coverage:
# Linux arm64: test (ASan+LeakSan) + build (-O2) [native, fast]
# Linux amd64: test + build [QEMU, slower]
# Linux portable: Alpine musl static build + smoke [portable binary]
# Windows: cross-compile + Wine version check [fast check only; use
# vm/win.sh for mandatory real-Windows verification]
# macOS: run natively (not in Docker)
#
# Full power, always: the test containers run unconstrained — the suite is
# built to be core-count-independent for correctness (timing/scheduling/
# subprocess suites assert invariants — ordering, bounded-return, RUNNING poll
# state — not wall-clock), so it passes at any parallelism and there is no
# reason to throttle it. The CBM_LOCAL_CI_CPUS knob survives only for the
# smoke/soak services, where deliberate resource starvation is the point; it is
# NOT a pre-push gate for the regular suite. ccache persists in named volumes;
# entries are content-verified
# (stale hits impossible), so warm reruns skip unchanged compilation.
# Run this from the WORKTREE you want tested: the containers mount the repo
# this script resides in.
#
# Usage:
# ./test-infrastructure/run.sh # arm64 + portable + Windows cross-compile
# ./test-infrastructure/run.sh all # above + amd64 + Windows Wine smoke
# ./test-infrastructure/run.sh portable # Alpine portable build + smoke only
# ./test-infrastructure/run.sh windows # Windows cross-compile only
# ./test-infrastructure/run.sh soak-linux # arm64 soak: quick + query-leak
# ./test-infrastructure/run.sh soak-windows # native Windows VM, both legs
# ./test-infrastructure/run.sh test # Linux arm64 test only (no perf)
# ./test-infrastructure/run.sh perf # Linux arm64 perf/incremental only
# ./test-infrastructure/run.sh tsan # Linux arm64 ThreadSanitizer race gate
# ./test-infrastructure/run.sh tsan-amd64 # Linux amd64 ThreadSanitizer race gate
# ./test-infrastructure/run.sh build # Linux arm64 build only
# ./test-infrastructure/run.sh lint # clang-format + cppcheck
# ./test-infrastructure/run.sh shell # debug shell
# Runtime: any Docker-compatible daemon. On macOS we use Colima (free OSS):
# brew install colima docker docker-compose docker-buildx
# ln -sf /opt/homebrew/opt/docker-compose/bin/docker-compose ~/.docker/cli-plugins/docker-compose
# ln -sf /opt/homebrew/opt/docker-buildx/bin/docker-buildx ~/.docker/cli-plugins/docker-buildx
# colima start --vm-type vz --vz-rosetta --cpu "$(sysctl -n hw.ncpu)" --memory 32
# vCPUs are NOT a reservation: idle guest cores cost nothing and the macOS
# scheduler shares freely with everything else running. Exposing all cores
# just removes the artificial ceiling a smaller VM would impose on container
# runs — no session seizes anything. Memory is the only semi-reservation,
# hence 32 GB, leaving macOS ample headroom. --vz-rosetta is required for
# fast amd64 legs (QEMU otherwise). Autostart: brew services start colima.
#
# Monitoring a running leg: docker logs -f <container> (or tail the log you
# redirected to). Check in regularly instead of waiting blind — suite results
# stream as they finish, and failures print their FAIL sites immediately.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
# ── Per-run isolation (concurrent agents / multiple worktrees) ───────────────
# Container names are already unique per `compose run --rm`, but the BUILD
# volume was not: two legs running at once wrote the same /src/build, so one
# run's objects and test-logs replaced the other's — after which the parallel
# scheduler dies reading a suite log that another run had removed. Each run now
# gets its own build volume, keyed by a unique run id. ccache and the fixture
# cache stay SHARED on purpose: ccache is concurrency-safe and content-verified,
# and sharing them is what keeps an isolated run fast rather than cold.
#
# CBM_CI_RUN_ID=<id> name/reuse a run (default: pid + epoch, unique)
# CBM_CI_SHARED_BUILD=1 opt back into the single shared `cbm-build` volume
# CBM_CI_KEEP=1 keep this run's build volume even on success
RUN_ID="${CBM_CI_RUN_ID:-$$-$(date +%s)}"
if [ "${CBM_CI_SHARED_BUILD:-0}" = "1" ]; then
CBM_CI_BUILD_VOLUME="cbm-build"
else
CBM_CI_BUILD_VOLUME="cbm-build-${RUN_ID}"
fi
export CBM_CI_BUILD_VOLUME
# Tidy what this run generated. A FAILED run KEEPS its volume — those artifacts
# are the post-mortem — and prints how to inspect and drop it. Successful runs
# leave nothing behind. Shared-volume mode never removes anything.
ci_cleanup() {
local rc=$?
if [ "${CBM_CI_BUILD_VOLUME}" = "cbm-build" ] || [ "${CBM_CI_KEEP:-0}" = "1" ]; then
return $rc
fi
if [ $rc -eq 0 ]; then
docker volume rm -f "${CBM_CI_BUILD_VOLUME}" >/dev/null 2>&1 || true
else
echo "run.sh: kept ${CBM_CI_BUILD_VOLUME} for post-mortem (run failed)" >&2
echo " inspect: docker run --rm -v ${CBM_CI_BUILD_VOLUME}:/b alpine ls -R /b" >&2
echo " drop: docker volume rm ${CBM_CI_BUILD_VOLUME}" >&2
fi
return $rc
}
trap ci_cleanup EXIT
COMPOSE="docker compose -f $ROOT/test-infrastructure/docker-compose.yml"
usage() {
cat <<'EOF'
Usage: test-infrastructure/run.sh [leg]
The Linux (and cross-compile) local-CI ladder, on Colima. Every leg runs the
SAME canonical scripts CI runs (scripts/test.sh, build.sh, smoke-local.sh,
soak-legs.sh) inside pinned containers; this wrapper only picks the container
and platform. A clean-disk preflight (scripts/ci/preflight-docker.sh) runs
first — CBM_SKIP_PREFLIGHT=1 skips it, CBM_PREFLIGHT_ARGS=--deep widens it.
Legs:
full (default) arm64: test + build + TSan + smoke + portable smoke
+ Windows mingw cross-compile check
all full + amd64 legs + Windows cross-compile/Wine check
test|build|smoke|tsan|msan single arm64 legs
amd64|test-amd64|tsan-amd64 amd64 legs (tsan-amd64 needs real amd64 HW)
perf arm64 incremental-perf leg (CBM_SKIP_PERF unset)
shell|shell-alpine interactive debug shell inside the image
portable|portable-test Alpine musl static build legs
smoke-artifact arm64 artifact-flow smoke: package -> extract -> wrapper
glibc-floor ubuntu-22.04 (glibc 2.35): portable smoke + dynamic refusal
soak-linux both CI soak legs (quick + #581 query-leak) via
scripts/soak-legs.sh; CBM_SOAK_MINUTES per leg (default 10)
soak-windows delegates to the real-Windows VM (win.sh soak 10)
Environment:
CBM_SOAK_MINUTES soak-linux duration per leg (default 10)
CBM_LOCAL_CI_CPUS opt-in CPU cap for smoke/soak starvation checks only
(the regular suite always runs full-parallel)
CBM_SKIP_PREFLIGHT=1 / CBM_PREFLIGHT_ARGS=--deep preflight controls
windows|smoke-windows mingw cross-compile + Wine version check
lint containerized cppcheck + clang-format (CI image)
-h, --help This text.
Real-Windows legs (test/guards/smoke-install/soak) live in
test-infrastructure/vm/win.sh — Wine is a compile check only.
EOF
}
case "${1:-}" in -h|--help) usage; exit 0 ;; esac
print_real_windows_gate() {
echo "=== Container/cross-compile legs passed ==="
echo "=== Real-Windows gate remains: vm/win.sh sync, test, guards, smoke-install ==="
}
if [ "${1:-full}" = "soak-windows" ]; then
echo "=== Windows: native daemon soak (real Windows VM, 10 min) ==="
exec "$ROOT/test-infrastructure/vm/win.sh" soak 10
fi
if ! docker info >/dev/null 2>&1; then
echo "ERROR: no Docker daemon reachable." >&2
echo " Start one first — on macOS: colima start --vm-type vz --vz-rosetta --cpu 12 --memory 16" >&2
echo " (current context: $(docker context show 2>/dev/null || echo unknown))" >&2
exit 1
fi
if ! docker compose version >/dev/null 2>&1; then
echo "ERROR: docker compose plugin missing." >&2
echo " brew install docker-compose && ln -sf /opt/homebrew/opt/docker-compose/bin/docker-compose ~/.docker/cli-plugins/docker-compose" >&2
exit 1
fi
# A GitHub runner starts every job on a fresh image with a known-free disk;
# Colima is long-lived and accumulates exited containers and orphaned volumes.
# Sweep that back to the runner's shape and assert free space before any leg,
# so a disk that has quietly filled fails here rather than surfacing as a bogus
# product failure deep inside an install path. CBM_PREFLIGHT_ARGS=--deep also
# reclaims the build cache; CBM_SKIP_PREFLIGHT=1 opts out for a quick re-run.
if [ "${CBM_SKIP_PREFLIGHT:-0}" != "1" ]; then
# shellcheck disable=SC2086 # deliberate word-splitting of opt-in flags
"$ROOT/scripts/ci/preflight-docker.sh" ${CBM_PREFLIGHT_ARGS:-}
fi
case "${1:-full}" in
full)
echo "=== Linux arm64: test + build ==="
$COMPOSE run --rm -e CBM_SKIP_PERF=1 test
$COMPOSE run --rm build
echo "=== Linux arm64: ThreadSanitizer (data-race gate) ==="
$COMPOSE run --rm test-tsan
echo "=== Linux arm64: smoke test ==="
$COMPOSE run --rm smoke
echo "=== Linux portable: Alpine static build + smoke ==="
$COMPOSE run --rm smoke-portable
echo "=== Linux arm64: artifact-flow smoke ==="
$COMPOSE run --rm smoke-artifact
echo "=== Linux glibc-floor (ubuntu-22.04): portable smoke + dynamic refusal ==="
$COMPOSE run --rm smoke-glibc-floor
echo "=== Windows: cross-compile ==="
$COMPOSE run --rm build-windows
print_real_windows_gate
;;
test)
echo "=== Linux arm64: test (ASan + LeakSanitizer, no perf) ==="
$COMPOSE run --rm -e CBM_SKIP_PERF=1 test
;;
perf)
echo "=== Linux arm64: perf/incremental tests ==="
$COMPOSE run --rm test
;;
tsan)
echo "=== Linux arm64: ThreadSanitizer (data-race gate) ==="
$COMPOSE run --rm test-tsan
;;
msan)
# MemorySanitizer: uninitialized-read detection. Needs every linked
# library instrumented, which is what Dockerfile.msan's libc++/zlib
# build provides — hence its own image rather than a flag on the
# normal one. First run builds that image (slow); later runs hit the
# layer cache.
#
# arm64 note: MSan's shadow mapping is unreliable on aarch64 and the
# grammar suites stack-overflow there regardless of toolchain version
# (reproduced on clang 18 and clang 22). The GitHub leg runs x86-64,
# where the mapping is the well-trodden one, so a local arm64 failure
# in that specific shape is a platform artifact — check the CI leg
# before treating it as a code defect.
echo "=== Linux: MemorySanitizer (uninitialized-read gate) ==="
$COMPOSE run --rm test-msan
;;
tsan-amd64)
# NOTE: TSan's shadow memory is incompatible with x86_64-on-ARM
# translation (Rosetta/QEMU), so this FATALs ("unexpected memory
# mapping") on an Apple-Silicon host — it is a real-amd64-hardware /
# GitHub-CI gate, not a local-on-ARM one. ASan amd64 (test-amd64) runs
# fine under Rosetta; only TSan's mapping does not.
echo "=== Linux amd64: ThreadSanitizer (data-race gate; native amd64 only) ==="
$COMPOSE run --rm test-tsan-amd64
;;
build)
echo "=== Linux arm64: production build (-O2 -Werror) ==="
$COMPOSE run --rm build
;;
smoke)
echo "=== Linux arm64: smoke test (build + run all phases) ==="
$COMPOSE run --rm smoke
;;
soak-linux)
# Both CI legs (quick + #581 query-leak). Duration via CBM_SOAK_MINUTES,
# default 10 to match the dry run. Kept out of `full`/`all` for the same
# reason soak-windows is: it is a ~20-minute endurance gate, not part of
# the fast ladder.
echo "=== Linux arm64: soak (quick + query-leak, ${CBM_SOAK_MINUTES:-10}m each) ==="
$COMPOSE run --rm soak
;;
portable)
echo "=== Linux portable: Alpine static build + smoke ==="
$COMPOSE run --rm smoke-portable
;;
smoke-artifact)
echo "=== Linux arm64: artifact-flow smoke (package -> extract -> wrapper) ==="
$COMPOSE run --rm smoke-artifact
;;
glibc-floor)
echo "=== Linux glibc-floor (ubuntu-22.04): portable smoke + dynamic refusal ==="
$COMPOSE run --rm build
$COMPOSE run --rm build-portable
$COMPOSE run --rm smoke-glibc-floor
;;
portable-test)
echo "=== Linux portable: Alpine test (ASan + LeakSan) ==="
$COMPOSE run --rm -e CBM_SKIP_PERF=1 test-portable
;;
windows)
echo "=== Windows: cross-compile + binary version check (Wine) ==="
$COMPOSE run --rm smoke-windows
;;
smoke-windows)
echo "=== Windows: binary version check (cross-compile + Wine) ==="
$COMPOSE run --rm smoke-windows
;;
amd64 | test-amd64)
# `--help` advertises `amd64|test-amd64`, but only `amd64` dispatched:
# the documented spelling died with "unknown leg 'test-amd64'".
echo "=== Linux amd64: test + build ==="
$COMPOSE run --rm -e CBM_SKIP_PERF=1 test-amd64
$COMPOSE run --rm build-amd64
;;
all)
echo "=== Linux arm64: test + build + smoke ==="
$COMPOSE run --rm -e CBM_SKIP_PERF=1 test
$COMPOSE run --rm build
$COMPOSE run --rm smoke
echo "=== Linux arm64: ThreadSanitizer (data-race gate) ==="
$COMPOSE run --rm test-tsan
# amd64 TSan is CI/native-amd64 only (Rosetta can't map TSan shadow);
# run it with `run.sh tsan-amd64` on real amd64. GitHub CI gates it.
echo "=== Linux portable: Alpine static build + smoke ==="
$COMPOSE run --rm smoke-portable
echo "=== Linux arm64: artifact-flow smoke ==="
$COMPOSE run --rm smoke-artifact
echo "=== Linux glibc-floor (ubuntu-22.04): portable smoke + dynamic refusal ==="
$COMPOSE run --rm smoke-glibc-floor
echo "=== Linux amd64: test + build + smoke ==="
$COMPOSE run --rm -e CBM_SKIP_PERF=1 test-amd64
$COMPOSE run --rm build-amd64
$COMPOSE run --rm smoke-amd64
echo "=== Windows: cross-compile + binary version check (Wine) ==="
$COMPOSE run --rm smoke-windows
print_real_windows_gate
;;
lint)
echo "=== Linters (clang-format-20 + cppcheck 2.20.0) ==="
$COMPOSE run --rm lint
;;
shell)
echo "=== Debug shell (Linux arm64) ==="
$COMPOSE run --rm --entrypoint bash test
;;
shell-alpine)
echo "=== Debug shell (Alpine) ==="
$COMPOSE run --rm --entrypoint bash test-portable
;;
*)
echo "run.sh: unknown leg '${1:-}'. Please consult --help." >&2
exit 2
;;
esac