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:
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