kit

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

commit 60e7c4ca59b86810a9103da2e217a85aa0bd3134
parent 7cb63a5b610c688fbf2705c7ad21ce4cabd1de41
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu,  4 Jun 2026 16:40:32 -0700

test: add FreeBSD hosted link VM smoke

Diffstat:
Mmk/test.mk | 13+++++++++++++
Ascripts/freebsd_sysroot.sh | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/freebsd_vm.sh | 7++++++-
Mtest/lib/check_rv64_env.sh | 11++++++++---
Atest/libc/freebsd/run.sh | 232+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 387 insertions(+), 4 deletions(-)

diff --git a/mk/test.mk b/mk/test.mk @@ -1010,6 +1010,19 @@ test-libc-musl: bin $(LIBC_MUSL_DEPS) test-libc-glibc: bin $(LIBC_GLIBC_DEPS) @KIT_LIBC_ARCHES="$(KIT_LIBC_ARCHES)" bash test/libc/glibc/run.sh +# FreeBSD hosted executable smoke (static + dynamic). Sysroots come from +# scripts/freebsd_sysroot.sh (cached base.txz extracts under ~/.cache/kit). By +# default these validate compile/link + ELF metadata; set KIT_FREEBSD_RUN_VM=1 +# to boot the cached FreeBSD VMs and execute each binary under real FreeBSD. +test-freebsd: bin + @KIT_FREEBSD_LINK=both bash test/libc/freebsd/run.sh + +test-freebsd-static: bin + @KIT_FREEBSD_LINK=static bash test/libc/freebsd/run.sh + +test-freebsd-dynamic: bin + @KIT_FREEBSD_LINK=dynamic bash test/libc/freebsd/run.sh + test-libc-musl-rv64: @$(MAKE) test-libc-musl KIT_LIBC_ARCHES=rv64 diff --git a/scripts/freebsd_sysroot.sh b/scripts/freebsd_sysroot.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# Build FreeBSD cross-compile sysroots from the official base.txz dist sets. +# +# Reproducible and VM-free: downloads (and caches) the base.txz for an arch, +# verifies it against the release MANIFEST, then extracts the curated subset +# kit -- and a clang/lld reference build -- needs. Both the downloaded tarball +# and the extracted sysroot live under the XDG cache so `make clean` does not +# wipe them. +# +# usage: +# scripts/freebsd_sysroot.sh <arch>|all extract (cached unless FORCE=1) +# scripts/freebsd_sysroot.sh path <arch> print the sysroot dir +# +# arches: amd64|x64 | aarch64|arm64 | riscv64|rv64 +# +# env: +# KIT_FREEBSD_RELEASE release tag (default: 15.0-RELEASE) +# KIT_FREEBSD_SYSROOT_CACHE cache root (default: $XDG_CACHE_HOME/kit/freebsd-sysroot/<release>) +# KIT_FREEBSD_DIST_URL mirror base (default: https://download.freebsd.org/releases) +# FORCE=1 re-download / re-extract even if cached + +set -eu + +RELEASE="${KIT_FREEBSD_RELEASE:-15.0-RELEASE}" +XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}" +CACHE="${KIT_FREEBSD_SYSROOT_CACHE:-$XDG_CACHE_HOME/kit/freebsd-sysroot/$RELEASE}" +MIRROR="${KIT_FREEBSD_DIST_URL:-https://download.freebsd.org/releases}" + +die() { printf 'freebsd-sysroot: %s\n' "$*" >&2; exit 1; } + +canon_arch() { + case "${1:-}" in + amd64|x64|x86_64) echo amd64 ;; + aarch64|arm64|aa64) echo aarch64 ;; + riscv64|rv64) echo riscv64 ;; + *) die "unknown arch '${1:-}'" ;; + esac +} + +# FreeBSD dist layout is <machine>/<machine_arch>/<release>/base.txz. +dist_path() { + case "$(canon_arch "$1")" in + amd64) echo amd64/amd64 ;; + aarch64) echo arm64/aarch64 ;; + riscv64) echo riscv/riscv64 ;; + esac +} + +sysroot_dir() { printf '%s/%s\n' "$CACHE" "$(canon_arch "$1")"; } + +# Curated members pulled out of base.txz. Headers + crt objects + the static +# and shared libc, the FreeBSD-15 libsys split, and the compiler runtime +# (libcompiler_rt / libgcc) whose helpers libc references -- e.g. rv64's +# binary128 __multf3. The libgcc.a / libgcc_s.so symlinks and their targets +# (libcompiler_rt.a, lib/libgcc_s.so.1) are listed together so they resolve. +# Missing optional members (some arches lack a given crt variant) are +# tolerated; a missing libc.a is fatal. +SYSROOT_MEMBERS=( + ./usr/include + ./usr/lib/crt1.o ./usr/lib/crti.o ./usr/lib/crtn.o ./usr/lib/Scrt1.o + ./usr/lib/crtbegin.o ./usr/lib/crtbeginT.o ./usr/lib/crtbeginS.o + ./usr/lib/crtend.o ./usr/lib/crtendS.o + ./usr/lib/libc.a ./usr/lib/libsys.a ./usr/lib/libm.a + ./usr/lib/libssp_nonshared.a ./usr/lib/libc_nonshared.a + ./usr/lib/libcompiler_rt.a ./usr/lib/libgcc.a ./usr/lib/libgcc_eh.a + ./usr/lib/libgcc_s.so + ./lib/libc.so.7 ./lib/libsys.so.7 ./lib/libgcc_s.so.1 +) + +fetch_txz() { + local arch="$1" dp url txz want got + dp="$(dist_path "$arch")" + url="$MIRROR/$dp/$RELEASE/base.txz" + txz="$CACHE/dist/$arch-base.txz" + mkdir -p "$CACHE/dist" + if [ "${FORCE:-0}" = 1 ] || [ ! -f "$txz" ]; then + printf 'download %s\n' "$url" >&2 + curl -fL --retry 3 -o "$txz.tmp" "$url" || die "download failed: $url" + mv "$txz.tmp" "$txz" + fi + # Verify against the release MANIFEST (sha256 in column 2). Best effort: if + # the MANIFEST is unreachable we proceed with the cached tarball. + want="$(curl -fsL "$MIRROR/$dp/$RELEASE/MANIFEST" 2>/dev/null \ + | awk '$1=="base.txz"{print $2}')" + if [ -n "$want" ]; then + got="$(shasum -a 256 "$txz" | awk '{print $1}')" + [ "$want" = "$got" ] || die "base.txz sha256 mismatch for $arch ($got != $want); rm $txz and retry" + printf 'verified base.txz sha256 %s\n' "$got" >&2 + fi + printf '%s\n' "$txz" +} + +build_sysroot() { + local arch dst txz + arch="$(canon_arch "$1")" + dst="$(sysroot_dir "$arch")" + if [ "${FORCE:-0}" != 1 ] && [ -f "$dst/usr/lib/libc.a" ]; then + printf 'sysroot cached: %s\n' "$dst" + return 0 + fi + txz="$(fetch_txz "$arch")" + rm -rf "$dst" + mkdir -p "$dst" + printf 'extract sysroot %s -> %s\n' "$arch" "$dst" + # bsdtar/GNU tar both extract named members (and whole dirs); a member absent + # from the archive is a warning + nonzero exit, not a stop -- tolerate it and + # gate on libc.a instead. + tar -xf "$txz" -C "$dst" "${SYSROOT_MEMBERS[@]}" 2>/dev/null || true + [ -f "$dst/usr/lib/libc.a" ] || die "extraction incomplete for $arch (no usr/lib/libc.a)" + printf 'sysroot ready: %s\n' "$dst" +} + +cmd="${1:-}" +case "$cmd" in + ""|-h|--help|help) + sed -n '2,24p' "$0" | sed 's/^# \{0,1\}//' + ;; + path) + [ $# -eq 2 ] || die "usage: $0 path <arch>" + sysroot_dir "$2" + ;; + all) + for a in amd64 aarch64 riscv64; do build_sysroot "$a"; done + ;; + *) + build_sysroot "$cmd" + ;; +esac diff --git a/scripts/freebsd_vm.sh b/scripts/freebsd_vm.sh @@ -20,7 +20,11 @@ DL_ROOT="${KIT_FREEBSD_DOWNLOAD_DIR:-$XDG_CACHE_HOME/kit/freebsd-vm/$RELEASE}" BASE_URL="${KIT_FREEBSD_BASE_URL:-https://download.freebsd.org/releases/VM-IMAGES/$RELEASE}" QEMU_SHARE="${KIT_QEMU_SHARE:-/opt/homebrew/share/qemu}" SSH_USER="${KIT_FREEBSD_SSH_USER:-kit}" -SSH_KEY="${KIT_FREEBSD_SSH_KEY:-$VM_ROOT/ssh/id_ed25519}" +# The SSH key lives in the download cache (alongside the golden disks) so +# `make clean` does not wipe it -- the key is baked into the golden disk's +# authorized_keys, so losing it would lock the cached disks out. (Sysroots are +# built VM-free from base.txz; see scripts/freebsd_sysroot.sh.) +SSH_KEY="${KIT_FREEBSD_SSH_KEY:-$DL_ROOT/ssh/id_ed25519}" usage() { cat <<EOF @@ -628,6 +632,7 @@ doctor() { printf ' MISSING %s\n' "$tool" fi done + printf 'ssh key: %s\n' "$([ -f "$SSH_KEY" ] && echo "$SSH_KEY" || echo "$SSH_KEY (absent)")" for arch in amd64 aarch64 riscv64; do printf ' %-7s cloudinit=%s ssh_port=%s golden=%s\n' \ "$arch" \ diff --git a/test/lib/check_rv64_env.sh b/test/lib/check_rv64_env.sh @@ -63,7 +63,7 @@ _rv64_os_tag() { _rv64_hint_qemu() { case "$(_rv64_os_tag)" in - darwin) echo "brew install qemu" ;; + darwin) echo "use podman, or run qemu-user inside a Linux VM" ;; debian) echo "apt install qemu-user-static" ;; fedora) echo "dnf install qemu-user-static" ;; alpine) echo "apk add qemu-riscv64" ;; @@ -170,8 +170,13 @@ _rv64_probe_qemu() { RV64_QEMU_BIN="$bin" _rv64_ok "qemu-riscv64 user-mode emulator ($bin)" else - _rv64_miss "qemu-riscv64" \ - "not on PATH (install: $(_rv64_hint_qemu))" + if [ "$(_rv64_os_tag)" = "darwin" ]; then + _rv64_miss "qemu-riscv64" \ + "not on PATH; macOS/Homebrew qemu provides system emulators only ($(_rv64_hint_qemu))" + else + _rv64_miss "qemu-riscv64" \ + "not on PATH (install: $(_rv64_hint_qemu))" + fi fi } diff --git a/test/libc/freebsd/run.sh b/test/libc/freebsd/run.sh @@ -0,0 +1,232 @@ +#!/usr/bin/env bash +# FreeBSD hosted-link smoke (static and/or dynamic). +# +# Cross-compiles a tiny libc program with kit for all three supported FreeBSD +# targets and validates the linked ELF. Sysroots come from +# scripts/freebsd_sysroot.sh (cached base.txz extracts under ~/.cache/kit). +# +# KIT_FREEBSD_LINK=static|dynamic|both which lane(s) to build (default: both) +# KIT_FREEBSD_RUN_VM=1 also boot the cached FreeBSD VMs and +# execute each binary under real FreeBSD +# KIT_FREEBSD_ARCHES="amd64 aarch64 riscv64" restrict the arch set +# +# The default lane validates ELF metadata only; the VM lane is the real +# end-to-end check (program prints "freebsd <mode>" and exits 7). + +set -u + +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +KIT="${KIT:-$ROOT/build/kit}" +BUILD_DIR="${KIT_FREEBSD_BUILD_DIR:-$ROOT/build/freebsd-link}" +ARCHES="${KIT_FREEBSD_ARCHES:-amd64 aarch64 riscv64}" +RUN_VM="${KIT_FREEBSD_RUN_VM:-0}" +LINK="${KIT_FREEBSD_LINK:-both}" + +. "$ROOT/test/lib/kit_sh_report.sh" + +mkdir -p "$BUILD_DIR" + +if [ ! -x "$KIT" ]; then + echo "kit driver missing at $KIT -- run 'make bin' first" >&2 + exit 2 +fi + +READELF="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/null || true)" +if [ -z "$READELF" ]; then + echo "llvm-readelf/readelf missing" >&2 + exit 2 +fi + +case "$LINK" in + static) MODES="static" ;; + dynamic) MODES="dynamic" ;; + both) MODES="static dynamic" ;; + *) echo "KIT_FREEBSD_LINK must be static|dynamic|both (got '$LINK')" >&2; exit 2 ;; +esac + +arch_target() { + case "$1" in + amd64) echo "x86_64-freebsd" ;; + aarch64) echo "aarch64-freebsd" ;; + riscv64) echo "riscv64-freebsd" ;; + *) echo "" ;; + esac +} + +arch_sysroot() { + case "$1" in + amd64) [ -n "${KIT_FREEBSD_AMD64_SYSROOT:-}" ] && { echo "$KIT_FREEBSD_AMD64_SYSROOT"; return; } ;; + aarch64) [ -n "${KIT_FREEBSD_AARCH64_SYSROOT:-}" ] && { echo "$KIT_FREEBSD_AARCH64_SYSROOT"; return; } ;; + riscv64) [ -n "${KIT_FREEBSD_RISCV64_SYSROOT:-}" ] && { echo "$KIT_FREEBSD_RISCV64_SYSROOT"; return; } ;; + esac + if [ -x "$ROOT/scripts/freebsd_sysroot.sh" ]; then + "$ROOT/scripts/freebsd_sysroot.sh" path "$1" + else + echo "$ROOT/build/freebsd-sysroot/$1" + fi +} + +exe_path() { echo "$BUILD_DIR/$2-$1"; } # <arch> <mode> + +# compile_arch <arch> <mode> +compile_arch() { + local arch="$1" mode="$2" target sysroot exe log meta + target="$(arch_target "$arch")" + sysroot="$(arch_sysroot "$arch")" + exe="$(exe_path "$arch" "$mode")" + log="$BUILD_DIR/cc-$mode-$arch.log" + meta="$BUILD_DIR/readelf-$mode-$arch.txt" + rm -f "$exe" + + if [ -z "$target" ]; then + kit_skip_na "$arch/freebsd-$mode" + return 1 + fi + if [ ! -d "$sysroot/usr/include" ]; then + kit_skip "$arch/freebsd-$mode" "missing sysroot $sysroot (run scripts/freebsd_sysroot.sh $arch)" + return 1 + fi + if [ "$mode" = static ] && [ ! -f "$sysroot/usr/lib/libc.a" ]; then + kit_skip "$arch/freebsd-$mode" "missing $sysroot/usr/lib/libc.a" + return 1 + fi + if [ "$mode" = dynamic ] && [ ! -f "$sysroot/lib/libc.so.7" ]; then + kit_skip "$arch/freebsd-$mode" "missing $sysroot/lib/libc.so.7" + return 1 + fi + + local flags=(-target "$target" --sysroot "$sysroot") + [ "$mode" = static ] && flags+=(-static) + + if ! printf '#include <stdio.h>\nint main(void){puts("freebsd %s");return 7;}\n' "$mode" | + "$KIT" cc "${flags[@]}" -x c - -o "$exe" >"$log" 2>&1; then + kit_fail "$arch/freebsd-$mode link" + sed 's/^/ cc| /' "$log" | head -20 + return 1 + fi + + if ! "$READELF" -h -l -d -r --dyn-symbols "$exe" >"$meta" 2>&1; then + kit_fail "$arch/freebsd-$mode readelf" + return 1 + fi + + if ! grep -qi 'OS/ABI:.*FreeBSD' "$meta"; then + kit_fail "$arch/freebsd-$mode metadata (OS/ABI not FreeBSD)" + sed 's/^/ elf| /' "$meta" | head -20 + return 1 + fi + if grep -Eq 'R_[A-Z0-9_]+_NONE' "$meta"; then + kit_fail "$arch/freebsd-$mode metadata (R_*_NONE reloc)" + sed 's/^/ elf| /' "$meta" | head -40 + return 1 + fi + + if [ "$mode" = static ]; then + if grep -q 'Requesting program interpreter' "$meta"; then + kit_fail "$arch/freebsd-static metadata (unexpected PT_INTERP)" + return 1 + fi + else + if ! grep -q "Requesting program interpreter: /libexec/ld-elf.so.1" "$meta" || + ! grep -q "Shared library: \\[libc.so.7\\]" "$meta" || + ! grep -q "Shared library: \\[libsys.so.7\\]" "$meta" || + ! grep -q "[[:space:]]environ$" "$meta" || + ! grep -q "[[:space:]]__progname$" "$meta" || + ! grep -q "JUMP_SLOT.*puts" "$meta"; then + kit_fail "$arch/freebsd-dynamic metadata" + sed 's/^/ elf| /' "$meta" | head -80 + return 1 + fi + fi + + kit_pass "$arch/freebsd-$mode metadata" + return 0 +} + +shutdown_vm() { + local arch="$1" pid_file="$BUILD_DIR/vm-$arch.pid" pid + "$ROOT/scripts/freebsd_vm.sh" ssh "$arch" 'sync' >"$BUILD_DIR/shutdown-$arch.log" 2>&1 || true + [ -f "$pid_file" ] || return 0 + pid="$(cat "$pid_file")" + for _ in $(seq 1 5); do kill -0 "$pid" 2>/dev/null || return 0; sleep 1; done + kill "$pid" 2>/dev/null || true; sleep 1 + kill -0 "$pid" 2>/dev/null && kill -9 "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true +} + +# Boot an arch's VM once, run every built mode's binary in it, then shut down. +run_vm_arch() { + local arch="$1" port pid_file started_vm=0 qemu mode exe out + port="$(arch_port "$arch")" + pid_file="$BUILD_DIR/vm-$arch.pid" + qemu="qemu-system-$(case "$arch" in amd64) echo x86_64 ;; *) echo "$arch" ;; esac)" + + if ! command -v "$qemu" >/dev/null 2>&1; then + kit_skip "$arch/freebsd vm" "$qemu missing"; return + fi + if [ ! -f "$ROOT/build/freebsd-vm/images/freebsd-$arch.provisioned" ]; then + kit_skip "$arch/freebsd vm" "FreeBSD VM is not provisioned (scripts/freebsd_vm.sh prepare $arch)"; return + fi + + if "$ROOT/scripts/freebsd_vm.sh" ssh "$arch" 'true' >"$BUILD_DIR/reuse-$arch.log" 2>&1; then + : # reuse a guest the caller already has running + else + "$ROOT/scripts/freebsd_vm.sh" run "$arch" >"$BUILD_DIR/vm-$arch.log" 2>&1 & + echo "$!" >"$pid_file"; started_vm=1; sleep 2 + if ! kill -0 "$(cat "$pid_file")" 2>/dev/null; then + kit_fail "$arch/freebsd vm start" + sed 's/^/ qemu| /' "$BUILD_DIR/vm-$arch.log" | head -20; return + fi + fi + if ! "$ROOT/scripts/freebsd_vm.sh" wait-ssh "$arch" >"$BUILD_DIR/wait-$arch.log" 2>&1; then + kit_fail "$arch/freebsd vm wait" + sed 's/^/ wait| /' "$BUILD_DIR/wait-$arch.log" | head -20 + [ "$started_vm" = 1 ] && shutdown_vm "$arch"; return + fi + + for mode in $MODES; do + exe="$(exe_path "$arch" "$mode")" + [ -x "$exe" ] || continue + if ! "$ROOT/scripts/freebsd_vm.sh" ssh "$arch" 'cat > /tmp/kit-fb' <"$exe" \ + >"$BUILD_DIR/copy-$mode-$arch.log" 2>&1; then + kit_fail "$arch/freebsd-$mode vm copy"; continue + fi + out="$BUILD_DIR/run-$mode-$arch.log" + "$ROOT/scripts/freebsd_vm.sh" ssh "$arch" \ + 'chmod +x /tmp/kit-fb; /tmp/kit-fb; printf "rc=%s\n" "$?"' >"$out" 2>&1 || true + if grep -q "^freebsd $mode\$" "$out" && grep -q '^rc=7$' "$out"; then + kit_pass "$arch/freebsd-$mode vm" + else + kit_fail "$arch/freebsd-$mode vm output" + sed 's/^/ run| /' "$out" | head -20 + fi + done + + [ "$started_vm" = 1 ] && shutdown_vm "$arch" +} + +arch_port() { + case "$1" in + amd64) echo "${KIT_FREEBSD_AMD64_SSH_PORT:-2222}" ;; + aarch64) echo "${KIT_FREEBSD_AARCH64_SSH_PORT:-2223}" ;; + riscv64) echo "${KIT_FREEBSD_RISCV64_SSH_PORT:-2224}" ;; + *) echo "" ;; + esac +} + +printf 'test-freebsd link=%s arches=%s run_vm=%s\n' "$LINK" "$ARCHES" "$RUN_VM" + +for arch in $ARCHES; do + for mode in $MODES; do + compile_arch "$arch" "$mode" + done +done + +if [ "$RUN_VM" = "1" ]; then + for arch in $ARCHES; do + run_vm_arch "$arch" + done +fi + +kit_summary test-freebsd +kit_exit