kit

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

commit 9351e593c35aafabb76580dee163dc5e95ba5c1f
parent dc6ab8fec82ec9d1d6755a131fd9c30f8ca64ea4
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat,  9 May 2026 17:42:00 -0700

test/libc: split harness into shared cases + musl/glibc runners

Move test/musl/{cases,Containerfile,extract.sh,run.sh} under
test/libc/{cases,musl}/ and add a sibling test/libc/glibc/ runner
backed by a Debian bookworm sysroot (libc6-dev=2.36-9+deb12u13).
Both runners share test/libc/cases/.

The glibc runner is dynamic-link only: it hands libc.so.6 directly
to cfree ld (the on-disk libc.so is a GROUP linker script we don't
parse), passes -dynamic-linker /lib/ld-linux-aarch64.so.1 to
override cfree's musl default, and pulls libc_nonshared.a alongside
for atexit / __stack_chk_fail_local. Static-glibc isn't a real-world
deployment shape, so the sysroot omits libc.a + the static
-lpthread/-ldl/-lrt stubs.

Both runners now consume cfree's freestanding headers from
rt/include/ via -isystem; glibc's stdio.h/unistd.h reach for
<stddef.h> which neither libc ships. The musl runner's prior
-nostdinc-only setup worked by coincidence (musl headers don't
transitively need stddef.h) — the new ordering makes that explicit.

The glibc runner pins the runtime image to docker.io/arm64v8/debian
:bookworm-slim instead of the multi-arch debian:bookworm-slim, which
both avoids the cached-amd64-manifest trap on arm64 hosts and skips
the registry round-trip that --platform would force on every run.

`make test-glibc` currently surfaces SIGSEGV during ld-linux's reloc
processing for all three cases — real cfree dynamic-link gaps the
musl harness doesn't exercise.

Diffstat:
Mdoc/DWARF.md | 2+-
Mdoc/DYNLD.md | 4+++-
Mdoc/linker-status.md | 8+++++---
Rtest/musl/cases/01_syscall_write.c -> test/libc/cases/01_syscall_write.c | 0
Rtest/musl/cases/01_syscall_write.stdout -> test/libc/cases/01_syscall_write.stdout | 0
Atest/libc/cases/02_errno_touch.c | 14++++++++++++++
Atest/libc/cases/03_printf_hello.c | 7+++++++
Atest/libc/cases/03_printf_hello.stdout | 1+
Atest/libc/glibc/Containerfile | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/libc/glibc/extract.sh | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Atest/libc/glibc/run.sh | 247+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/libc/musl/Containerfile | 40++++++++++++++++++++++++++++++++++++++++
Atest/libc/musl/extract.sh | 46++++++++++++++++++++++++++++++++++++++++++++++
Atest/libc/musl/run.sh | 239+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtest/musl/Containerfile | 40----------------------------------------
Dtest/musl/cases/02_errno_touch.c | 12------------
Dtest/musl/cases/03_printf_hello.c | 7-------
Dtest/musl/cases/03_printf_hello.stdout | 1-
Dtest/musl/extract.sh | 46----------------------------------------------
Dtest/musl/run.sh | 230-------------------------------------------------------------------------------
Mtest/test.mk | 40++++++++++++++++++++++++++--------------
21 files changed, 746 insertions(+), 355 deletions(-)

diff --git a/doc/DWARF.md b/doc/DWARF.md @@ -527,7 +527,7 @@ sanity for the wire format itself: - `readelf --debug-dump=info,line,abbrev,aranges` — reference rendering; hand-diff once per phase. -Both run under `test/musl/` style: optional, gated by tool +Both run under `test/libc/` style: optional, gated by tool availability (`command -v llvm-dwarfdump`), skipped otherwise. They are **not** the oracle for any case; the W path is. They exist to catch wire-format errors that our own consumer would also miss. diff --git a/doc/DYNLD.md b/doc/DYNLD.md @@ -41,7 +41,9 @@ machinery as `link_exe`, with: - DT_R(UN)PATH from `opts->r(un)paths`. - Exports promoted into `.dynsym` from `opts->exports`. -Add a harness case under `test/musl/` (or a new `test/link/dyn/`): +Add a harness case under `test/libc/cases/` (shared by the +`test/libc/musl/` and `test/libc/glibc/` runners) or a new +`test/link/dyn/`: build `libfoo.so` from a single `.c`, link an exe against it, run. ### Phase 8 — TLS GD/IE/LD, IRELATIVE (deferred) diff --git a/doc/linker-status.md b/doc/linker-status.md @@ -29,9 +29,11 @@ live in `test/link/` — they are not duplicated in `test/elf/`. classic ET_EXEC) and **dynamic** (libc.so + Scrt1.o, ET_DYN PIE with PT_INTERP / PT_DYNAMIC / PLT / .got.plt and BIND_NOW resolution against the runtime loader). Both variants run the result under -qemu/podman. Sysroot is produced by `test/musl/Containerfile` -(Alpine 3.20 + musl 1.2.5-r3). Excluded from the default `make test` -because it needs podman. +qemu/podman. Sysroots are produced by `test/libc/musl/Containerfile` +(Alpine 3.20 + musl 1.2.5-r3) and `test/libc/glibc/Containerfile` +(Debian bookworm + glibc 2.36); cases are shared under +`test/libc/cases/`. Excluded from the default `make test` because +they need podman. --- diff --git a/test/musl/cases/01_syscall_write.c b/test/libc/cases/01_syscall_write.c diff --git a/test/musl/cases/01_syscall_write.stdout b/test/libc/cases/01_syscall_write.stdout diff --git a/test/libc/cases/02_errno_touch.c b/test/libc/cases/02_errno_touch.c @@ -0,0 +1,14 @@ +/* Tier 2: touch errno. We deliberately call a function that fails + * (close(-1) → EBADF) and read errno after. errno's lowering varies + * by libc — in musl it's `__thread int errno;` (exercises + * R_AARCH64_TLSLE_* and PT_TLS emission); in glibc it's + * `(*__errno_location())` (a per-thread function call, no TLS reloc + * on the caller side). Either way the test asserts the value. */ + +#include <errno.h> +#include <unistd.h> + +int main(void) { + (void)close(-1); + return errno == EBADF ? 0 : 1; +} diff --git a/test/libc/cases/03_printf_hello.c b/test/libc/cases/03_printf_hello.c @@ -0,0 +1,7 @@ +/* Tier 3: full stdio. printf pulls in FILE buffering, locale, + * vfprintf, write_iov, errno, lock primitives, the works. If this + * runs, cfree ld is meaningfully usable for static-libc programs. */ + +#include <stdio.h> + +int main(void) { return printf("hello, libc\n") < 0 ? 1 : 0; } diff --git a/test/libc/cases/03_printf_hello.stdout b/test/libc/cases/03_printf_hello.stdout @@ -0,0 +1 @@ +hello, libc diff --git a/test/libc/glibc/Containerfile b/test/libc/glibc/Containerfile @@ -0,0 +1,68 @@ +# test/libc/glibc/Containerfile — produces a glibc aarch64 sysroot tarball +# on stdout. Pinned to Debian bookworm (glibc 2.36). +# +# Usage (driven by test/libc/glibc/extract.sh): +# podman build --platform linux/arm64 -f Containerfile -t cfree-glibc-sysroot . +# podman run --rm cfree-glibc-sysroot > sysroot.tar +# +# The image's ENTRYPOINT writes a tar of /sysroot to stdout. The extract +# script unpacks it into build/glibc-sysroot/ on the host. +# +# Notes about glibc on Debian (vs. the musl/Alpine variant): +# - libc is split across libc.so.6 (the dynamic SO) and +# libc_nonshared.a (a few non-shared callbacks pulled in by every +# dynamic exe — atexit, __stack_chk_fail_local, etc.). On disk, +# /usr/lib/aarch64-linux-gnu/libc.so is a *linker script* that +# GROUPs them. cfree ld doesn't parse linker scripts, so we copy +# libc.so.6 directly and hand it to the linker by path. +# - The runtime loader is /lib/ld-linux-aarch64.so.1, distinct from +# libc (unlike musl, where ld-musl-aarch64.so.1 *is* libc). That +# means the dynamic-link variant of the harness must pass +# -dynamic-linker explicitly; cfree ld's default is musl. +# - Debian uses multi-arch paths: Scrt1.o + crti/crtn live under +# /usr/lib/aarch64-linux-gnu, the runtime SOs under +# /lib/aarch64-linux-gnu, and headers under both /usr/include and +# /usr/include/aarch64-linux-gnu. We flatten everything into +# /sysroot/{lib,include} with the multi-arch include kept as a +# subdir so -isystem can pick it up. +FROM docker.io/arm64v8/debian:bookworm-slim + +# libc6-dev: Scrt1.o, crti.o, crtn.o, libc_nonshared.a + glibc headers +# under /usr/include and /usr/include/aarch64-linux-gnu. We +# deliberately do NOT stage libc.a or the static -lpthread/-ldl/-lrt +# archives: the harness only exercises dynamic-link (static-glibc is +# officially discouraged and isn't a real-world deployment shape). +# linux-libc-dev: kernel uapi (linux/*, asm/*, asm-generic/*) under +# /usr/include — used by syscall numbers etc. Pulled in transitively +# by libc6-dev, listed explicitly for clarity. +# Note: we deliberately do NOT pull libgcc / compiler-rt — soft-float +# / TF / 128-bit-int helpers come from our own rt/ build. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + libc6-dev=2.36-9+deb12u13 \ + linux-libc-dev \ + && rm -rf /var/lib/apt/lists/* + +# Stage the artifacts the linker needs into one tree so the host extract +# is a single tar pipe. Multi-arch dirs get flattened to /sysroot/lib/. +# Symlinks (notably ld-linux-aarch64.so.1) are dereferenced so the +# extracted tree is self-contained. +RUN set -eux; \ + mkdir -p /sysroot/lib /sysroot/include; \ + cd /usr/lib/aarch64-linux-gnu; \ + cp Scrt1.o crti.o crtn.o libc_nonshared.a /sysroot/lib/; \ + cp -L /lib/aarch64-linux-gnu/libc.so.6 /sysroot/lib/libc.so.6; \ + cp -L /lib/aarch64-linux-gnu/libm.so.6 /sysroot/lib/libm.so.6; \ + cp -L /lib/ld-linux-aarch64.so.1 /sysroot/lib/ld-linux-aarch64.so.1; \ + # Common include tree + multi-arch (sys/cdefs.h, bits/*, gnu/stubs-lp64.h) + cp -r /usr/include/. /sysroot/include/ + +# Pin the build for cache reuse and reproducibility audits. +RUN set -eux; \ + { \ + echo "debian bookworm-slim glibc $(dpkg-query -W -f='${Version}' libc6-dev)"; \ + echo "linux-libc-dev $(dpkg-query -W -f='${Version}' linux-libc-dev)"; \ + uname -m; \ + } > /sysroot/PROVENANCE + +ENTRYPOINT ["sh", "-c", "tar -C /sysroot -cf - ."] diff --git a/test/libc/glibc/extract.sh b/test/libc/glibc/extract.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# test/libc/glibc/extract.sh — build the glibc sysroot image +# (test/libc/glibc/Containerfile) and unpack /sysroot from it into +# build/glibc-sysroot/. Cached after the first successful run; pass `-f` +# to force a rebuild. +# +# Output layout: +# build/glibc-sysroot/ +# lib/ crt1.o Scrt1.o crti.o crtn.o libc.a libc_nonshared.a +# libc.so.6 libm.so.6 ld-linux-aarch64.so.1 ... +# include/ glibc + linux uapi tree (multi-arch dir kept as +# include/aarch64-linux-gnu/) +# PROVENANCE +set -eu + +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +SYSROOT="$ROOT/build/glibc-sysroot" +TAG="cfree-glibc-sysroot" +FORCE=0 + +while [ $# -gt 0 ]; do + case "$1" in + -f|--force) FORCE=1; shift ;; + *) echo "unknown arg: $1" >&2; exit 2 ;; + esac +done + +if [ -f "$SYSROOT/PROVENANCE" ] && [ $FORCE -eq 0 ]; then + echo "glibc sysroot already present at $SYSROOT (use -f to rebuild)" + exit 0 +fi + +if ! command -v podman >/dev/null 2>&1; then + echo "extract.sh: podman is required" >&2 + exit 1 +fi + +cd "$ROOT/test/libc/glibc" +echo "Building $TAG (Debian bookworm aarch64 + libc6-dev)..." +podman build --platform linux/arm64 -f Containerfile -t "$TAG" . >/dev/null + +rm -rf "$SYSROOT" +mkdir -p "$SYSROOT" + +echo "Extracting sysroot to $SYSROOT..." +podman run --rm --platform linux/arm64 "$TAG" | tar -C "$SYSROOT" -xf - + +echo "Done. Provenance:" +cat "$SYSROOT/PROVENANCE" diff --git a/test/libc/glibc/run.sh b/test/libc/glibc/run.sh @@ -0,0 +1,247 @@ +#!/usr/bin/env bash +# test/libc/glibc/run.sh — drive cfree ld against a real glibc sysroot on +# aarch64-linux. Dynamic-link only — static-linked glibc is officially +# discouraged (libc.a relies on dlopen-loaded NSS modules, has its own +# entire reloc surface area, and isn't a real-world deployment shape), +# so we don't carry the variant. Each case in test/libc/cases/*.c is +# exercised once: +# +# dynamic — PIE object + libc.so.6, with explicit dynamic linker +# cfree ld -pie \ +# -dynamic-linker /lib/ld-linux-aarch64.so.1 \ +# -o case.exe \ +# $SYSROOT/lib/Scrt1.o $SYSROOT/lib/crti.o \ +# case.o \ +# $SYSROOT/lib/libc.so.6 $SYSROOT/lib/libc_nonshared.a $CFREE_RT \ +# $SYSROOT/lib/crtn.o +# +# Unlike musl, where ld-musl-aarch64.so.1 is the same file as libc, +# glibc's loader is a separate ELF — cfree ld's default interp is musl, +# so we override via -dynamic-linker. libc.so.6 carries +# SONAME=libc.so.6 so DT_NEEDED is correct without a linker-script +# intermediary (the on-disk libc.so is a GROUP script that cfree ld +# doesn't parse — we hand the SO directly). libc_nonshared.a +# contributes the handful of non-shared callbacks every glibc dyn-exe +# pulls in — atexit, __stack_chk_fail_local, __libc_csu_init/fini on +# older glibc, etc. — and must follow libc.so.6 in the demand chain. +# +# Each case file may carry an `expected` companion (default 0) and an +# optional `expected_stdout` file checked with substring match. +# +# Designed to fail fast and clearly: the *first* failure surface (compile +# / link / run / output) is the gap to fix next. Run with +# CFREE_GLIBC_KEEP=1 to leave intermediates in build/glibc/<case>/. +set -u + +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +CASES_DIR="$ROOT/test/libc/cases" +BUILD_DIR="$ROOT/build/glibc" +SYSROOT="$ROOT/build/glibc-sysroot" +CFREE="$ROOT/build/cfree" +CFREE_RT="$ROOT/rt/build/aarch64-linux/libcfree_rt.a" + +if [ ! -d "$SYSROOT" ]; then + echo "glibc sysroot missing — run test/libc/glibc/extract.sh first" >&2 + exit 2 +fi +if [ ! -x "$CFREE" ]; then + echo "cfree driver missing at $CFREE — run 'make' first" >&2 + exit 2 +fi +if [ ! -f "$CFREE_RT" ]; then + echo "cfree rt missing at $CFREE_RT — run 'make rt-aarch64-linux'" >&2 + exit 2 +fi + +mkdir -p "$BUILD_DIR" + +color_red() { printf '\033[31m%s\033[0m' "$1"; } +color_grn() { printf '\033[32m%s\033[0m' "$1"; } +color_yel() { printf '\033[33m%s\033[0m' "$1"; } + +PASS=0; FAIL=0; FAIL_NAMES=() + +# Pick a runner. Native arm64 hosts can run aarch64 ELFs directly under +# podman without binfmt; otherwise we want qemu-aarch64-static. +arch_raw="$(uname -m 2>/dev/null || true)" +is_aarch64=0 +{ [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1 + +QEMU_BIN="$(command -v qemu-aarch64-static 2>/dev/null || command -v qemu-aarch64 2>/dev/null || true)" +have_qemu=0; [ -n "$QEMU_BIN" ] && have_qemu=1 +have_podman=0; command -v podman >/dev/null 2>&1 && have_podman=1 + +# clang must understand --target=aarch64-linux-gnu. Every system path +# is overridden via --sysroot / -isystem so the host's headers / +# libraries are not consulted. +if ! clang --target=aarch64-linux-gnu -c -x c - -o /dev/null < /dev/null 2>/dev/null; then + echo "clang does not accept --target=aarch64-linux-gnu" >&2 + exit 2 +fi + +# Dynamic-variant exes need /lib/ld-linux-aarch64.so.1 + libc.so.6 to +# load. qemu-user resolves them relative to QEMU_LD_PREFIX or -L; the +# podman fallback uses a debian:bookworm image which ships them at the +# expected paths. +QEMU_LD_PREFIX_OVERRIDE="$SYSROOT" + +run_aarch64() { + local exe="$1" out="$2" err="$3" + if [ $have_qemu -eq 1 ]; then + # Point qemu-user at our extracted sysroot so the loader + # search ("/lib/ld-linux-aarch64.so.1") resolves to the + # SYSROOT copy rather than the (possibly-absent) host one. + QEMU_LD_PREFIX="$QEMU_LD_PREFIX_OVERRIDE" \ + "$QEMU_BIN" "$exe" >"$out" 2>"$err" + RUN_RC=$?; return + fi + if [ $have_podman -eq 1 ]; then + local dir base + dir="$(cd "$(dirname "$exe")" && pwd)"; base="$(basename "$exe")" + # Pin the image name to the arm64-specific repo + # (docker.io/arm64v8/...) instead of the multi-arch + # debian:bookworm-slim. Two reasons: + # 1. Avoids the cached-amd64-manifest trap that + # debian:bookworm-slim hits on arm64 hosts where an + # amd64 pull happened earlier — podman silently uses + # the wrong arch and the dyn-exe fails to load. + # 2. Avoids passing --platform, which forces podman to + # hit the registry on every run to verify the + # manifest matches. Pinning the repo + relying on the + # local cache keeps subsequent runs offline + fast. + # arm64v8/debian:bookworm-slim ships the matching glibc + # loader, so the dynamic variant resolves PT_INTERP without + # extra mounts. + podman run --rm --net=none \ + -v "$dir":/work:Z -w /work \ + docker.io/arm64v8/debian:bookworm-slim "./$base" \ + >"$out" 2>"$err" + RUN_RC=$?; return + fi + RUN_RC=127 +} + +run_case() { + local src="$1" + local name="$(basename "$src" .c)" + local work="$BUILD_DIR/$name" + local label="$name" + mkdir -p "$work" + + local expected=0 + [ -f "$CASES_DIR/${name}.expected" ] && \ + expected="$(cat "$CASES_DIR/${name}.expected" | tr -d '[:space:]')" + + local expect_stdout="" + if [ -f "$CASES_DIR/${name}.stdout" ]; then + expect_stdout="$(cat "$CASES_DIR/${name}.stdout")" + fi + + # ---- compile ---- + # Three -isystem layers, in order of precedence: + # rt/include/ — cfree's own freestanding + # headers (stddef.h, + # stdarg.h, stdint.h, ...). + # Compiler-defined; libc + # does not ship these. + # glibc's unistd.h / + # stdio.h #include <stddef.h> + # for size_t, so the + # freestanding set must be + # reachable. + # sysroot/include/ — glibc + linux-libc-dev + # headers (top-level uapi). + # sysroot/include/aarch64-linux-gnu — glibc multi-arch (bits/*, + # gnu/stubs-lp64.h, ...); + # <features.h> reaches in. + # -nostdinc strips the host's default include path so clang's + # resource dir is not consulted. + local cc_flags=(--target=aarch64-linux-gnu --sysroot="$SYSROOT" + -nostdinc + -isystem "$ROOT/rt/include" + -isystem "$SYSROOT/include" + -isystem "$SYSROOT/include/aarch64-linux-gnu" + -fPIE -fpic -O0) + + local obj="$work/${name}.o" + if ! clang "${cc_flags[@]}" -c "$src" -o "$obj" 2>"$work/cc.err"; then + FAIL=$((FAIL+1)) + FAIL_NAMES+=("$label (compile)") + printf ' %s %s\n' "$(color_red FAIL)" "$label (compile)" + sed 's/^/ cc| /' "$work/cc.err" + return + fi + + # ---- link ---- + # PIE start file, libc.so.6 as the *shared* input (cfree ld + # doesn't read the libc.so linker script, so we hand the actual + # SO directly), with -dynamic-linker overriding the musl default. + # Expects cfree ld to: + # - accept ET_DYN ELF objects as input, + # - emit PT_INTERP "/lib/ld-linux-aarch64.so.1", + # - emit PT_DYNAMIC with DT_NEEDED libc.so.6, + # - emit a .dynsym/.dynstr/.gnu.hash + .rela.plt/.got.plt + # so the loader can bind imported symbols at runtime. + # libc_nonshared.a still links statically; libcfree_rt.a stays — + # soft-float TF helpers are static-bound from our side. + # crti/crtn are unchanged. + local exe="$work/${name}.exe" + local link_cmd=("$CFREE" "ld" -pie + -dynamic-linker /lib/ld-linux-aarch64.so.1 + -o "$exe" + "$SYSROOT/lib/Scrt1.o" "$SYSROOT/lib/crti.o" + "$obj" + "$SYSROOT/lib/libc.so.6" "$SYSROOT/lib/libc_nonshared.a" + "$CFREE_RT" + "$SYSROOT/lib/crtn.o") + + if ! "${link_cmd[@]}" >"$work/link.out" 2>"$work/link.err"; then + FAIL=$((FAIL+1)) + FAIL_NAMES+=("$label (link)") + printf ' %s %s\n' "$(color_red FAIL)" "$label (link)" + sed 's/^/ ld| /' "$work/link.err" | head -10 + return + fi + + # ---- run ---- + run_aarch64 "$exe" "$work/run.out" "$work/run.err" + if [ "$RUN_RC" -ne "$expected" ]; then + FAIL=$((FAIL+1)) + FAIL_NAMES+=("$label (run rc=$RUN_RC, want $expected)") + printf ' %s %s (rc=%s, want %s)\n' "$(color_red FAIL)" "$label" \ + "$RUN_RC" "$expected" + [ -s "$work/run.err" ] && sed 's/^/ err| /' "$work/run.err" | head -5 + [ -s "$work/run.out" ] && sed 's/^/ out| /' "$work/run.out" | head -5 + return + fi + + if [ -n "$expect_stdout" ]; then + if ! grep -qF -- "$expect_stdout" "$work/run.out"; then + FAIL=$((FAIL+1)) + FAIL_NAMES+=("$label (stdout)") + printf ' %s %s (stdout mismatch)\n' "$(color_red FAIL)" "$label" + printf ' expected substring: %s\n' "$expect_stdout" + sed 's/^/ got| /' "$work/run.out" | head -5 + return + fi + fi + + PASS=$((PASS+1)) + printf ' %s %s\n' "$(color_grn PASS)" "$label" +} + +shopt -s nullglob + +printf 'Running glibc dynamic-link cases...\n' +for src in "$CASES_DIR"/*.c; do + run_case "$src" +done + +printf '\nResults: %s pass, %s fail\n' "$PASS" "$FAIL" + +if [ ${#FAIL_NAMES[@]} -gt 0 ]; then + printf '\nFailed:\n' + for n in "${FAIL_NAMES[@]}"; do printf ' %s\n' "$n"; done + exit 1 +fi +exit 0 diff --git a/test/libc/musl/Containerfile b/test/libc/musl/Containerfile @@ -0,0 +1,40 @@ +# test/libc/musl/Containerfile — produces a static musl aarch64 sysroot +# tarball on stdout. Pinned to Alpine 3.20.10 + musl 1.2.5. +# +# Usage (driven by test/libc/musl/extract.sh): +# podman build --platform linux/arm64 -f Containerfile -t cfree-musl-sysroot . +# podman run --rm cfree-musl-sysroot > sysroot.tar +# +# The image's ENTRYPOINT writes a tar of /sysroot to stdout. The extract +# script unpacks it into build/musl-sysroot/ on the host. +FROM docker.io/arm64v8/alpine:3.20.10 + +# musl-dev: Scrt1.o, crt1.o, crti.o, crtn.o, libc.a + headers under /usr/include. +# Plus the dynamic linker / libc.so used by the dynamic-link variant of the +# harness (in musl, /lib/ld-musl-aarch64.so.1 *is* libc — same file). +# linux-headers: kernel uapi (linux/*, asm/*, asm-generic/*) — used by syscall +# definitions in the musl headers. +# Note: we deliberately do NOT pull clang's compiler-rt or libgcc — the +# soft-float / TF / 128-bit-int helpers (__extenddftf2 etc.) come from +# our own rt/ build (rt/build/aarch64-linux/libcfree_rt.a). +RUN apk add --no-cache musl-dev=1.2.5-r3 linux-headers + +# Stage the artifacts the linker needs into one tree so the host extract +# is a single tar pipe. +RUN mkdir -p /sysroot/lib /sysroot/include \ + && cp /usr/lib/crt1.o /sysroot/lib/ \ + && cp /usr/lib/Scrt1.o /sysroot/lib/ \ + && cp /usr/lib/rcrt1.o /sysroot/lib/ \ + && cp /usr/lib/crti.o /sysroot/lib/ \ + && cp /usr/lib/crtn.o /sysroot/lib/ \ + && cp /usr/lib/libc.a /sysroot/lib/ \ + && cp /usr/lib/libssp_nonshared.a /sysroot/lib/ \ + && cp /lib/ld-musl-aarch64.so.1 /sysroot/lib/ \ + && ln -s ld-musl-aarch64.so.1 /sysroot/lib/libc.so \ + && cp -r /usr/include/. /sysroot/include/ + +# Pin the build for cache reuse and reproducibility audits. +RUN echo "alpine 3.20.10 musl 1.2.5-r3" > /sysroot/PROVENANCE \ + && uname -m >> /sysroot/PROVENANCE + +ENTRYPOINT ["sh", "-c", "tar -C /sysroot -cf - ."] diff --git a/test/libc/musl/extract.sh b/test/libc/musl/extract.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# test/libc/musl/extract.sh — build the musl sysroot image (test/libc/musl/Containerfile) +# and unpack /sysroot from it into build/musl-sysroot/. Cached after the first +# successful run; pass `-f` to force a rebuild. +# +# Output layout: +# build/musl-sysroot/ +# lib/ crt1.o crti.o crtn.o libc.a libssp_nonshared.a +# include/ musl + linux-headers tree +# PROVENANCE +set -eu + +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +SYSROOT="$ROOT/build/musl-sysroot" +TAG="cfree-musl-sysroot" +FORCE=0 + +while [ $# -gt 0 ]; do + case "$1" in + -f|--force) FORCE=1; shift ;; + *) echo "unknown arg: $1" >&2; exit 2 ;; + esac +done + +if [ -f "$SYSROOT/PROVENANCE" ] && [ $FORCE -eq 0 ]; then + echo "musl sysroot already present at $SYSROOT (use -f to rebuild)" + exit 0 +fi + +if ! command -v podman >/dev/null 2>&1; then + echo "extract.sh: podman is required" >&2 + exit 1 +fi + +cd "$ROOT/test/libc/musl" +echo "Building $TAG (Alpine aarch64 + musl-dev)..." +podman build --platform linux/arm64 -f Containerfile -t "$TAG" . >/dev/null + +rm -rf "$SYSROOT" +mkdir -p "$SYSROOT" + +echo "Extracting sysroot to $SYSROOT..." +podman run --rm --platform linux/arm64 "$TAG" | tar -C "$SYSROOT" -xf - + +echo "Done. Provenance:" +cat "$SYSROOT/PROVENANCE" diff --git a/test/libc/musl/run.sh b/test/libc/musl/run.sh @@ -0,0 +1,239 @@ +#!/usr/bin/env bash +# test/libc/musl/run.sh — drive cfree ld against a real musl sysroot on +# aarch64-linux. Each case in test/libc/cases/*.c is exercised in two +# variants: +# +# static — non-PIC object + libc.a, classic static-exe link +# cfree ld -static -o case.exe \ +# $SYSROOT/lib/crt1.o $SYSROOT/lib/crti.o \ +# case.o \ +# $SYSROOT/lib/libc.a $CFREE_RT \ +# $SYSROOT/lib/crtn.o +# +# dynamic — PIE object + libc.so, expects PT_INTERP /lib/ld-musl-aarch64.so.1 +# cfree ld -pie -o case.exe \ +# $SYSROOT/lib/Scrt1.o $SYSROOT/lib/crti.o \ +# case.o \ +# $SYSROOT/lib/libc.so $CFREE_RT \ +# $SYSROOT/lib/crtn.o +# (musl ships ld-musl-aarch64.so.1 *as* libc — same file. The +# harness intentionally has no -dynamic-linker flag yet because +# cfree ld currently doesn't accept one; this is one of the gaps +# we expect the dynamic variant to surface.) +# +# Each case file may carry an `expected` companion (default 0) and an +# optional `expected_stdout` file checked with substring match. +# +# Designed to fail fast and clearly: the *first* failure surface (compile +# / link / run / output) is the gap to fix next. Run with +# CFREE_MUSL_KEEP=1 to leave intermediates in build/musl/<case>/. +set -u + +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +CASES_DIR="$ROOT/test/libc/cases" +BUILD_DIR="$ROOT/build/musl" +SYSROOT="$ROOT/build/musl-sysroot" +CFREE="$ROOT/build/cfree" +CFREE_RT="$ROOT/rt/build/aarch64-linux/libcfree_rt.a" + +if [ ! -d "$SYSROOT" ]; then + echo "musl sysroot missing — run test/libc/musl/extract.sh first" >&2 + exit 2 +fi +if [ ! -x "$CFREE" ]; then + echo "cfree driver missing at $CFREE — run 'make' first" >&2 + exit 2 +fi +if [ ! -f "$CFREE_RT" ]; then + echo "cfree rt missing at $CFREE_RT — run 'make rt-aarch64-linux'" >&2 + exit 2 +fi + +mkdir -p "$BUILD_DIR" + +color_red() { printf '\033[31m%s\033[0m' "$1"; } +color_grn() { printf '\033[32m%s\033[0m' "$1"; } +color_yel() { printf '\033[33m%s\033[0m' "$1"; } + +# Per-variant counters so the dynamic-link surface is visible in its own +# right rather than being averaged into one total. +PASS_static=0; FAIL_static=0; FAIL_NAMES_static=() +PASS_dynamic=0; FAIL_dynamic=0; FAIL_NAMES_dynamic=() + +# Pick a runner. Native arm64 hosts can run aarch64 ELFs directly under +# podman without binfmt; otherwise we want qemu-aarch64-static. +arch_raw="$(uname -m 2>/dev/null || true)" +is_aarch64=0 +{ [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1 + +QEMU_BIN="$(command -v qemu-aarch64-static 2>/dev/null || command -v qemu-aarch64 2>/dev/null || true)" +have_qemu=0; [ -n "$QEMU_BIN" ] && have_qemu=1 +have_podman=0; command -v podman >/dev/null 2>&1 && have_podman=1 + +# clang must understand --target=aarch64-linux-musl. Recent clang ships +# linux-musl as a target alias of linux-gnu for our purposes (we override +# every system path via --sysroot). +if ! clang --target=aarch64-linux-musl -c -x c - -o /dev/null < /dev/null 2>/dev/null; then + echo "clang does not accept --target=aarch64-linux-musl" >&2 + exit 2 +fi + +run_aarch64() { + local exe="$1" out="$2" err="$3" + if [ $have_qemu -eq 1 ]; then + "$QEMU_BIN" "$exe" >"$out" 2>"$err"; RUN_RC=$?; return + fi + if [ $have_podman -eq 1 ]; then + local dir base platform_flag=() + dir="$(cd "$(dirname "$exe")" && pwd)"; base="$(basename "$exe")" + [ $is_aarch64 -eq 0 ] && platform_flag=(--platform linux/arm64) + podman run --rm "${platform_flag[@]}" --net=none \ + -v "$dir":/work:Z -w /work alpine:latest "./$base" >"$out" 2>"$err" + RUN_RC=$?; return + fi + RUN_RC=127 +} + +# run_case <variant> <src> +# variant ∈ {static, dynamic} +run_case() { + local variant="$1" src="$2" + local name="$(basename "$src" .c)" + local work="$BUILD_DIR/$name/$variant" + local label="$name [$variant]" + mkdir -p "$work" + + local expected=0 + [ -f "$CASES_DIR/${name}.expected" ] && \ + expected="$(cat "$CASES_DIR/${name}.expected" | tr -d '[:space:]')" + + local expect_stdout="" + if [ -f "$CASES_DIR/${name}.stdout" ]; then + expect_stdout="$(cat "$CASES_DIR/${name}.stdout")" + fi + + # ---- compile ---- + # Two -isystem layers, in order of precedence: + # rt/include/ — cfree's own freestanding headers + # (stddef.h, stdarg.h, stdint.h, ...). + # sysroot/include/ — musl + linux-headers tree. + # -nostdinc strips the host's default include path so clang's + # resource dir is not consulted. + local cc_flags=(--target=aarch64-linux-musl --sysroot="$SYSROOT" + -nostdinc + -isystem "$ROOT/rt/include" + -isystem "$SYSROOT/include" + -O0) + case "$variant" in + static) cc_flags+=(-fno-PIC -fno-pie) ;; + dynamic) cc_flags+=(-fPIE -fpic) ;; + esac + + local obj="$work/${name}.o" + if ! clang "${cc_flags[@]}" -c "$src" -o "$obj" 2>"$work/cc.err"; then + eval "FAIL_${variant}=\$((FAIL_${variant}+1))" + eval "FAIL_NAMES_${variant}+=(\"\$label (compile)\")" + printf ' %s %s\n' "$(color_red FAIL)" "$label (compile)" + sed 's/^/ cc| /' "$work/cc.err" + return + fi + + # ---- link ---- + local exe="$work/${name}.exe" + local link_cmd + case "$variant" in + static) + # Link order mirrors a typical static-musl invocation: + # crt1.o crti.o obj libc.a libcfree_rt.a crtn.o + # libcfree_rt provides the TF / soft-float builtins + # (__addtf3, __extenddftf2 etc.) that musl's libc.a calls + # from printf's long-double formatting. Archive ingestion + # iterates demand-load to a fixed point so one trailing + # libcfree_rt.a is enough. + link_cmd=("$CFREE" "ld" -static -o "$exe" + "$SYSROOT/lib/crt1.o" "$SYSROOT/lib/crti.o" + "$obj" + "$SYSROOT/lib/libc.a" "$CFREE_RT" + "$SYSROOT/lib/crtn.o") + ;; + dynamic) + # Dynamic-exe link: PIE start file, libc.so as a *shared* + # input (not an archive), expects cfree ld to: + # - accept ET_DYN ELF objects as input, + # - emit PT_INTERP "/lib/ld-musl-aarch64.so.1", + # - emit PT_DYNAMIC with DT_NEEDED libc.so, + # - emit a .dynsym/.dynstr/.gnu.hash + .rela.plt/.got.plt + # so the loader can bind imported symbols at runtime. + # libcfree_rt.a stays — soft-float TF helpers are still + # static-bound from our side. crti/crtn are unchanged. + link_cmd=("$CFREE" "ld" -pie -o "$exe" + "$SYSROOT/lib/Scrt1.o" "$SYSROOT/lib/crti.o" + "$obj" + "$SYSROOT/lib/libc.so" "$CFREE_RT" + "$SYSROOT/lib/crtn.o") + ;; + esac + + if ! "${link_cmd[@]}" >"$work/link.out" 2>"$work/link.err"; then + eval "FAIL_${variant}=\$((FAIL_${variant}+1))" + eval "FAIL_NAMES_${variant}+=(\"\$label (link)\")" + printf ' %s %s\n' "$(color_red FAIL)" "$label (link)" + sed 's/^/ ld| /' "$work/link.err" | head -10 + return + fi + + # ---- run ---- + run_aarch64 "$exe" "$work/run.out" "$work/run.err" + if [ "$RUN_RC" -ne "$expected" ]; then + eval "FAIL_${variant}=\$((FAIL_${variant}+1))" + eval "FAIL_NAMES_${variant}+=(\"\$label (run rc=\$RUN_RC, want \$expected)\")" + printf ' %s %s (rc=%s, want %s)\n' "$(color_red FAIL)" "$label" \ + "$RUN_RC" "$expected" + [ -s "$work/run.err" ] && sed 's/^/ err| /' "$work/run.err" | head -5 + [ -s "$work/run.out" ] && sed 's/^/ out| /' "$work/run.out" | head -5 + return + fi + + if [ -n "$expect_stdout" ]; then + if ! grep -qF -- "$expect_stdout" "$work/run.out"; then + eval "FAIL_${variant}=\$((FAIL_${variant}+1))" + eval "FAIL_NAMES_${variant}+=(\"\$label (stdout)\")" + printf ' %s %s (stdout mismatch)\n' "$(color_red FAIL)" "$label" + printf ' expected substring: %s\n' "$expect_stdout" + sed 's/^/ got| /' "$work/run.out" | head -5 + return + fi + fi + + eval "PASS_${variant}=\$((PASS_${variant}+1))" + printf ' %s %s\n' "$(color_grn PASS)" "$label" +} + +shopt -s nullglob + +printf 'Running musl static-link cases...\n' +for src in "$CASES_DIR"/*.c; do + run_case static "$src" +done + +printf '\nRunning musl dynamic-link cases...\n' +for src in "$CASES_DIR"/*.c; do + run_case dynamic "$src" +done + +printf '\nResults:\n' +printf ' static : %s pass, %s fail\n' "$PASS_static" "$FAIL_static" +printf ' dynamic: %s pass, %s fail\n' "$PASS_dynamic" "$FAIL_dynamic" + +if [ ${#FAIL_NAMES_static[@]} -gt 0 ]; then + printf '\nFailed (static):\n' + for n in "${FAIL_NAMES_static[@]}"; do printf ' %s\n' "$n"; done +fi +if [ ${#FAIL_NAMES_dynamic[@]} -gt 0 ]; then + printf '\nFailed (dynamic):\n' + for n in "${FAIL_NAMES_dynamic[@]}"; do printf ' %s\n' "$n"; done +fi + +total_fail=$((FAIL_static + FAIL_dynamic)) +if [ $total_fail -gt 0 ]; then exit 1; fi +exit 0 diff --git a/test/musl/Containerfile b/test/musl/Containerfile @@ -1,40 +0,0 @@ -# test/musl/Containerfile — produces a static musl aarch64 sysroot -# tarball on stdout. Pinned to Alpine 3.20.10 + musl 1.2.5. -# -# Usage (driven by test/musl/extract.sh): -# podman build --platform linux/arm64 -f Containerfile -t cfree-musl-sysroot . -# podman run --rm cfree-musl-sysroot > sysroot.tar -# -# The image's ENTRYPOINT writes a tar of /sysroot to stdout. The extract -# script unpacks it into build/musl-sysroot/ on the host. -FROM docker.io/arm64v8/alpine:3.20.10 - -# musl-dev: Scrt1.o, crt1.o, crti.o, crtn.o, libc.a + headers under /usr/include. -# Plus the dynamic linker / libc.so used by the dynamic-link variant of the -# harness (in musl, /lib/ld-musl-aarch64.so.1 *is* libc — same file). -# linux-headers: kernel uapi (linux/*, asm/*, asm-generic/*) — used by syscall -# definitions in the musl headers. -# Note: we deliberately do NOT pull clang's compiler-rt or libgcc — the -# soft-float / TF / 128-bit-int helpers (__extenddftf2 etc.) come from -# our own rt/ build (rt/build/aarch64-linux/libcfree_rt.a). -RUN apk add --no-cache musl-dev=1.2.5-r3 linux-headers - -# Stage the artifacts the linker needs into one tree so the host extract -# is a single tar pipe. -RUN mkdir -p /sysroot/lib /sysroot/include \ - && cp /usr/lib/crt1.o /sysroot/lib/ \ - && cp /usr/lib/Scrt1.o /sysroot/lib/ \ - && cp /usr/lib/rcrt1.o /sysroot/lib/ \ - && cp /usr/lib/crti.o /sysroot/lib/ \ - && cp /usr/lib/crtn.o /sysroot/lib/ \ - && cp /usr/lib/libc.a /sysroot/lib/ \ - && cp /usr/lib/libssp_nonshared.a /sysroot/lib/ \ - && cp /lib/ld-musl-aarch64.so.1 /sysroot/lib/ \ - && ln -s ld-musl-aarch64.so.1 /sysroot/lib/libc.so \ - && cp -r /usr/include/. /sysroot/include/ - -# Pin the build for cache reuse and reproducibility audits. -RUN echo "alpine 3.20.10 musl 1.2.5-r3" > /sysroot/PROVENANCE \ - && uname -m >> /sysroot/PROVENANCE - -ENTRYPOINT ["sh", "-c", "tar -C /sysroot -cf - ."] diff --git a/test/musl/cases/02_errno_touch.c b/test/musl/cases/02_errno_touch.c @@ -1,12 +0,0 @@ -/* Tier 2: touch errno. errno is `__thread` in musl, so this exercises - * TLS local-exec relocs (R_AARCH64_TLSLE_*) and PT_TLS emission. We - * deliberately call a function that fails (close(-1) → EBADF) and read - * errno after. */ - -#include <errno.h> -#include <unistd.h> - -int main(void) { - (void)close(-1); - return errno == EBADF ? 0 : 1; -} diff --git a/test/musl/cases/03_printf_hello.c b/test/musl/cases/03_printf_hello.c @@ -1,7 +0,0 @@ -/* Tier 3: full stdio. printf pulls in FILE buffering, locale, - * vfprintf, write_iov, errno, lock primitives, the works. If this - * runs, cfree ld is meaningfully usable for static musl programs. */ - -#include <stdio.h> - -int main(void) { return printf("hello, musl\n") < 0 ? 1 : 0; } diff --git a/test/musl/cases/03_printf_hello.stdout b/test/musl/cases/03_printf_hello.stdout @@ -1 +0,0 @@ -hello, musl diff --git a/test/musl/extract.sh b/test/musl/extract.sh @@ -1,46 +0,0 @@ -#!/usr/bin/env bash -# test/musl/extract.sh — build the musl sysroot image (test/musl/Containerfile) -# and unpack /sysroot from it into build/musl-sysroot/. Cached after the first -# successful run; pass `-f` to force a rebuild. -# -# Output layout: -# build/musl-sysroot/ -# lib/ crt1.o crti.o crtn.o libc.a libssp_nonshared.a -# include/ musl + linux-headers tree -# PROVENANCE -set -eu - -ROOT="$(cd "$(dirname "$0")/../.." && pwd)" -SYSROOT="$ROOT/build/musl-sysroot" -TAG="cfree-musl-sysroot" -FORCE=0 - -while [ $# -gt 0 ]; do - case "$1" in - -f|--force) FORCE=1; shift ;; - *) echo "unknown arg: $1" >&2; exit 2 ;; - esac -done - -if [ -f "$SYSROOT/PROVENANCE" ] && [ $FORCE -eq 0 ]; then - echo "musl sysroot already present at $SYSROOT (use -f to rebuild)" - exit 0 -fi - -if ! command -v podman >/dev/null 2>&1; then - echo "extract.sh: podman is required" >&2 - exit 1 -fi - -cd "$ROOT/test/musl" -echo "Building $TAG (Alpine aarch64 + musl-dev)..." -podman build --platform linux/arm64 -f Containerfile -t "$TAG" . >/dev/null - -rm -rf "$SYSROOT" -mkdir -p "$SYSROOT" - -echo "Extracting sysroot to $SYSROOT..." -podman run --rm --platform linux/arm64 "$TAG" | tar -C "$SYSROOT" -xf - - -echo "Done. Provenance:" -cat "$SYSROOT/PROVENANCE" diff --git a/test/musl/run.sh b/test/musl/run.sh @@ -1,230 +0,0 @@ -#!/usr/bin/env bash -# test/musl/run.sh — drive cfree ld against a real musl sysroot on -# aarch64-linux. Each case in test/musl/cases/*.c is exercised in two -# variants: -# -# static — non-PIC object + libc.a, classic static-exe link -# cfree ld -static -o case.exe \ -# $SYSROOT/lib/crt1.o $SYSROOT/lib/crti.o \ -# case.o \ -# $SYSROOT/lib/libc.a $CFREE_RT \ -# $SYSROOT/lib/crtn.o -# -# dynamic — PIE object + libc.so, expects PT_INTERP /lib/ld-musl-aarch64.so.1 -# cfree ld -pie -o case.exe \ -# $SYSROOT/lib/Scrt1.o $SYSROOT/lib/crti.o \ -# case.o \ -# $SYSROOT/lib/libc.so $CFREE_RT \ -# $SYSROOT/lib/crtn.o -# (musl ships ld-musl-aarch64.so.1 *as* libc — same file. The -# harness intentionally has no -dynamic-linker flag yet because -# cfree ld currently doesn't accept one; this is one of the gaps -# we expect the dynamic variant to surface.) -# -# Each case file may carry an `expected` companion (default 0) and an -# optional `expected_stdout` file checked with substring match. -# -# Designed to fail fast and clearly: the *first* failure surface (compile -# / link / run / output) is the gap to fix next. Run with -# CFREE_MUSL_KEEP=1 to leave intermediates in build/musl/<case>/. -set -u - -ROOT="$(cd "$(dirname "$0")/../.." && pwd)" -TEST_DIR="$ROOT/test/musl" -BUILD_DIR="$ROOT/build/musl" -SYSROOT="$ROOT/build/musl-sysroot" -CFREE="$ROOT/build/cfree" -CFREE_RT="$ROOT/rt/build/aarch64-linux/libcfree_rt.a" - -if [ ! -d "$SYSROOT" ]; then - echo "musl sysroot missing — run test/musl/extract.sh first" >&2 - exit 2 -fi -if [ ! -x "$CFREE" ]; then - echo "cfree driver missing at $CFREE — run 'make' first" >&2 - exit 2 -fi -if [ ! -f "$CFREE_RT" ]; then - echo "cfree rt missing at $CFREE_RT — run 'make rt-aarch64-linux'" >&2 - exit 2 -fi - -mkdir -p "$BUILD_DIR" - -color_red() { printf '\033[31m%s\033[0m' "$1"; } -color_grn() { printf '\033[32m%s\033[0m' "$1"; } -color_yel() { printf '\033[33m%s\033[0m' "$1"; } - -# Per-variant counters so the dynamic-link surface is visible in its own -# right rather than being averaged into one total. -PASS_static=0; FAIL_static=0; FAIL_NAMES_static=() -PASS_dynamic=0; FAIL_dynamic=0; FAIL_NAMES_dynamic=() - -# Pick a runner. Native arm64 hosts can run aarch64 ELFs directly under -# podman without binfmt; otherwise we want qemu-aarch64-static. -arch_raw="$(uname -m 2>/dev/null || true)" -is_aarch64=0 -{ [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1 - -QEMU_BIN="$(command -v qemu-aarch64-static 2>/dev/null || command -v qemu-aarch64 2>/dev/null || true)" -have_qemu=0; [ -n "$QEMU_BIN" ] && have_qemu=1 -have_podman=0; command -v podman >/dev/null 2>&1 && have_podman=1 - -# clang must understand --target=aarch64-linux-musl. Recent clang ships -# linux-musl as a target alias of linux-gnu for our purposes (we override -# every system path via --sysroot). -if ! clang --target=aarch64-linux-musl -c -x c - -o /dev/null < /dev/null 2>/dev/null; then - echo "clang does not accept --target=aarch64-linux-musl" >&2 - exit 2 -fi - -run_aarch64() { - local exe="$1" out="$2" err="$3" - if [ $have_qemu -eq 1 ]; then - "$QEMU_BIN" "$exe" >"$out" 2>"$err"; RUN_RC=$?; return - fi - if [ $have_podman -eq 1 ]; then - local dir base platform_flag=() - dir="$(cd "$(dirname "$exe")" && pwd)"; base="$(basename "$exe")" - [ $is_aarch64 -eq 0 ] && platform_flag=(--platform linux/arm64) - podman run --rm "${platform_flag[@]}" --net=none \ - -v "$dir":/work:Z -w /work alpine:latest "./$base" >"$out" 2>"$err" - RUN_RC=$?; return - fi - RUN_RC=127 -} - -# run_case <variant> <src> -# variant ∈ {static, dynamic} -run_case() { - local variant="$1" src="$2" - local name="$(basename "$src" .c)" - local work="$BUILD_DIR/$name/$variant" - local label="$name [$variant]" - mkdir -p "$work" - - local expected=0 - [ -f "$TEST_DIR/cases/${name}.expected" ] && \ - expected="$(cat "$TEST_DIR/cases/${name}.expected" | tr -d '[:space:]')" - - local expect_stdout="" - if [ -f "$TEST_DIR/cases/${name}.stdout" ]; then - expect_stdout="$(cat "$TEST_DIR/cases/${name}.stdout")" - fi - - # ---- compile ---- - local cc_flags=(--target=aarch64-linux-musl --sysroot="$SYSROOT" - -nostdinc -isystem "$SYSROOT/include" -O0) - case "$variant" in - static) cc_flags+=(-fno-PIC -fno-pie) ;; - dynamic) cc_flags+=(-fPIE -fpic) ;; - esac - - local obj="$work/${name}.o" - if ! clang "${cc_flags[@]}" -c "$src" -o "$obj" 2>"$work/cc.err"; then - eval "FAIL_${variant}=\$((FAIL_${variant}+1))" - eval "FAIL_NAMES_${variant}+=(\"\$label (compile)\")" - printf ' %s %s\n' "$(color_red FAIL)" "$label (compile)" - sed 's/^/ cc| /' "$work/cc.err" - return - fi - - # ---- link ---- - local exe="$work/${name}.exe" - local link_cmd - case "$variant" in - static) - # Link order mirrors a typical static-musl invocation: - # crt1.o crti.o obj libc.a libcfree_rt.a crtn.o - # libcfree_rt provides the TF / soft-float builtins - # (__addtf3, __extenddftf2 etc.) that musl's libc.a calls - # from printf's long-double formatting. Archive ingestion - # iterates demand-load to a fixed point so one trailing - # libcfree_rt.a is enough. - link_cmd=("$CFREE" "ld" -static -o "$exe" - "$SYSROOT/lib/crt1.o" "$SYSROOT/lib/crti.o" - "$obj" - "$SYSROOT/lib/libc.a" "$CFREE_RT" - "$SYSROOT/lib/crtn.o") - ;; - dynamic) - # Dynamic-exe link: PIE start file, libc.so as a *shared* - # input (not an archive), expects cfree ld to: - # - accept ET_DYN ELF objects as input, - # - emit PT_INTERP "/lib/ld-musl-aarch64.so.1", - # - emit PT_DYNAMIC with DT_NEEDED libc.so, - # - emit a .dynsym/.dynstr/.gnu.hash + .rela.plt/.got.plt - # so the loader can bind imported symbols at runtime. - # libcfree_rt.a stays — soft-float TF helpers are still - # static-bound from our side. crti/crtn are unchanged. - link_cmd=("$CFREE" "ld" -pie -o "$exe" - "$SYSROOT/lib/Scrt1.o" "$SYSROOT/lib/crti.o" - "$obj" - "$SYSROOT/lib/libc.so" "$CFREE_RT" - "$SYSROOT/lib/crtn.o") - ;; - esac - - if ! "${link_cmd[@]}" >"$work/link.out" 2>"$work/link.err"; then - eval "FAIL_${variant}=\$((FAIL_${variant}+1))" - eval "FAIL_NAMES_${variant}+=(\"\$label (link)\")" - printf ' %s %s\n' "$(color_red FAIL)" "$label (link)" - sed 's/^/ ld| /' "$work/link.err" | head -10 - return - fi - - # ---- run ---- - run_aarch64 "$exe" "$work/run.out" "$work/run.err" - if [ "$RUN_RC" -ne "$expected" ]; then - eval "FAIL_${variant}=\$((FAIL_${variant}+1))" - eval "FAIL_NAMES_${variant}+=(\"\$label (run rc=\$RUN_RC, want \$expected)\")" - printf ' %s %s (rc=%s, want %s)\n' "$(color_red FAIL)" "$label" \ - "$RUN_RC" "$expected" - [ -s "$work/run.err" ] && sed 's/^/ err| /' "$work/run.err" | head -5 - [ -s "$work/run.out" ] && sed 's/^/ out| /' "$work/run.out" | head -5 - return - fi - - if [ -n "$expect_stdout" ]; then - if ! grep -qF -- "$expect_stdout" "$work/run.out"; then - eval "FAIL_${variant}=\$((FAIL_${variant}+1))" - eval "FAIL_NAMES_${variant}+=(\"\$label (stdout)\")" - printf ' %s %s (stdout mismatch)\n' "$(color_red FAIL)" "$label" - printf ' expected substring: %s\n' "$expect_stdout" - sed 's/^/ got| /' "$work/run.out" | head -5 - return - fi - fi - - eval "PASS_${variant}=\$((PASS_${variant}+1))" - printf ' %s %s\n' "$(color_grn PASS)" "$label" -} - -shopt -s nullglob - -printf 'Running musl static-link cases...\n' -for src in "$TEST_DIR/cases"/*.c; do - run_case static "$src" -done - -printf '\nRunning musl dynamic-link cases...\n' -for src in "$TEST_DIR/cases"/*.c; do - run_case dynamic "$src" -done - -printf '\nResults:\n' -printf ' static : %s pass, %s fail\n' "$PASS_static" "$FAIL_static" -printf ' dynamic: %s pass, %s fail\n' "$PASS_dynamic" "$FAIL_dynamic" - -if [ ${#FAIL_NAMES_static[@]} -gt 0 ]; then - printf '\nFailed (static):\n' - for n in "${FAIL_NAMES_static[@]}"; do printf ' %s\n' "$n"; done -fi -if [ ${#FAIL_NAMES_dynamic[@]} -gt 0 ]; then - printf '\nFailed (dynamic):\n' - for n in "${FAIL_NAMES_dynamic[@]}"; do printf ' %s\n' "$n"; done -fi - -total_fail=$((FAIL_static + FAIL_dynamic)) -if [ $total_fail -gt 0 ]; then exit 1; fi -exit 0 diff --git a/test/test.mk b/test/test.mk @@ -24,7 +24,7 @@ # against the public cfree.h surface; reuses cfree-roundtrip, # link-exe-runner, and jit-runner. -.PHONY: test test-lex test-pp test-pp-err test-elf test-ar test-ar-driver test-link test-cg test-dwarf test-debug test-parse test-parse-err test-musl test-lib-deps +.PHONY: test test-lex test-pp test-pp-err test-elf test-ar test-ar-driver test-link test-cg test-dwarf test-debug test-parse test-parse-err test-musl test-glibc test-lib-deps test: test-lex test-pp test-pp-err test-elf test-ar test-ar-driver test-link test-cg test-dwarf test-debug test-parse test-parse-err test-lib-deps @@ -130,24 +130,36 @@ test-parse: lib $(PARSE_RUNNER) $(ROUNDTRIP_BIN) $(LINK_EXE_RUNNER) $(JIT_RUNNER test-parse-err: lib $(PARSE_RUNNER) sh test/parse/run_errors.sh -# test-musl: end-to-end static + dynamic musl link/run on aarch64. -# Pulls a pinned musl sysroot (test/musl/extract.sh — podman against -# Alpine 3.20), builds rt/build/aarch64-linux/libcfree_rt.a for the -# soft-float / TF builtins, and runs `cfree ld` against the real musl -# libc.a (static variant) and libc.so (dynamic variant — see -# doc/DYNLD.md). Excluded from the default `test` target because it -# needs podman and ~30s on first run; opt-in via `make test-musl`. +# test-musl / test-glibc: end-to-end static + dynamic libc link/run on +# aarch64. Each variant pulls its own pinned sysroot (podman, ~30s on +# first run) and shares the same case files under test/libc/cases/: # -# The sysroot is treated as a real prerequisite via its PROVENANCE +# test-musl — Alpine 3.20 + musl 1.2.5 (test/libc/musl/) +# test-glibc — Debian bookworm + glibc 2.36 (test/libc/glibc/) +# +# Both build rt/build/aarch64-linux/libcfree_rt.a for soft-float / TF +# builtins, and run `cfree ld` against the real libc.a (static) and +# libc.so / libc.so.6 (dynamic — see doc/DYNLD.md). Excluded from the +# default `test` target because they need podman; opt-in via +# `make test-musl` / `make test-glibc`. +# +# Each sysroot is treated as a real prerequisite via its PROVENANCE # marker so subsequent runs skip extraction and re-extract only when -# the file is removed (or test/musl/extract.sh -f forces a rebuild). -MUSL_SYSROOT_MARKER = build/musl-sysroot/PROVENANCE +# the file is removed (or extract.sh -f forces a rebuild). +MUSL_SYSROOT_MARKER = build/musl-sysroot/PROVENANCE +GLIBC_SYSROOT_MARKER = build/glibc-sysroot/PROVENANCE -$(MUSL_SYSROOT_MARKER): test/musl/extract.sh test/musl/Containerfile - @bash test/musl/extract.sh +$(MUSL_SYSROOT_MARKER): test/libc/musl/extract.sh test/libc/musl/Containerfile + @bash test/libc/musl/extract.sh + +$(GLIBC_SYSROOT_MARKER): test/libc/glibc/extract.sh test/libc/glibc/Containerfile + @bash test/libc/glibc/extract.sh test-musl: bin rt-aarch64-linux $(MUSL_SYSROOT_MARKER) - @bash test/musl/run.sh + @bash test/libc/musl/run.sh + +test-glibc: bin rt-aarch64-linux $(GLIBC_SYSROOT_MARKER) + @bash test/libc/glibc/run.sh # Fail if libcfree.a depends on any external symbol not in the allowlist. # Drift in either direction (new dep, or stale entry) is a failure.