kit

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

commit b6df55cdbea665c89a313a54e17804032f8512c0
parent 532cc5f1ebd1b7bed82472244cd4cb8e0a378a90
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sun, 10 May 2026 20:15:28 -0700

libc: Linux hosted shim + test-libc 9-cell matrix (musl/glibc × x64/aa64/rv64)

Extends test-libc beyond macOS to a Linux cell matrix: 3 arches
× {musl-static, musl-dynamic, glibc-dynamic} = 9 cells, each
cross-compiled + cross-linked on the host and executed inside podman
(alpine for musl, debian for glibc) per arch.

- rt/lib/cfree_hosted/linux.c: ABI-neutral shim bridging
  __cfree_stdin/stdout/stderr/errno_location to glibc+musl's
  stdin/stdout/stderr data globals + __errno_location.
- rt/Makefile: riscv64-linux variant.
- Makefile: hosted-linux-{aarch64,x64,rv64} build the shim per arch
  via cfree-cc; rt-x86_64-linux and rt-riscv64-linux aliases.
- test/libc/{musl,glibc}/Containerfile.{x64,rv64}: arch-specific
  sysroots; extract.sh -a {aarch64|x64|rv64} selects the right
  Containerfile and writes build/{libc}-sysroot[-arch]/. aarch64
  keeps the bare path so test-musl / test-glibc are untouched.
- test/libc/run.sh: refactored into a cell-driven harness. Darwin
  = 1 cell (host-native), Linux = 9 cells (cfree cc -c, cfree ld,
  podman/qemu exec per arch).

Current results from a Darwin/arm64 host:

  darwin                 7/7  PASS
  aarch64-musl-static    7/7  PASS
  aarch64-musl-dynamic   0/7  cfree ld: unhandled reloc 21 vs import
  aarch64-glibc-dynamic  0/7  reloc 21 + missing __dso_handle
  x64-musl-{static,dynamic}, x64-glibc-dynamic
                         0/7  undefined ref _GLOBAL_OFFSET_TABLE_
  rv64-musl-{static,dynamic}
                         0/7  unsupported RISC-V reloc 60 (R_RISCV_RELAX)
  rv64-glibc-dynamic     0/7  undefined ref __global_pointer$

The Linux failures are concrete cfree-ld / codegen gaps surfaced
intentionally (no static-glibc workaround) so the matrix mirrors
real-world deployment shapes rather than masking the gaps.

Diffstat:
MMakefile | 41++++++++++++++++++++++++++++++++++++++++-
Mrt/Makefile | 8++++++++
Art/lib/cfree_hosted/linux.c | 33+++++++++++++++++++++++++++++++++
Atest/libc/glibc/Containerfile.rv64 | 36++++++++++++++++++++++++++++++++++++
Atest/libc/glibc/Containerfile.x64 | 34++++++++++++++++++++++++++++++++++
Mtest/libc/glibc/extract.sh | 69++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Atest/libc/musl/Containerfile.rv64 | 37+++++++++++++++++++++++++++++++++++++
Atest/libc/musl/Containerfile.x64 | 29+++++++++++++++++++++++++++++
Mtest/libc/musl/extract.sh | 62+++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mtest/libc/run.sh | 540+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mtest/test.mk | 41++++++++++++++++++++++++++++++++++-------
11 files changed, 790 insertions(+), 140 deletions(-)

diff --git a/Makefile b/Makefile @@ -27,7 +27,9 @@ DRIVER_DEPS = $(DRIVER_OBJS:.o=.d) LIB_AR = build/libcfree.a BIN = build/cfree -.PHONY: all lib driver bin rt rt-aarch64-linux hosted-macos format clean self self-stage2 +.PHONY: all lib driver bin rt rt-aarch64-linux rt-x86_64-linux rt-riscv64-linux \ + hosted-macos hosted-linux hosted-linux-aarch64 hosted-linux-x64 hosted-linux-rv64 \ + format clean self self-stage2 # Default: compile libcfree.a, the driver objects, and link the cfree # binary. The link step currently fails because most libcfree functions @@ -46,10 +48,18 @@ bin: $(BIN) # variants. Each variant produces rt/build/<variant>/libcfree_rt.a. # The aarch64-linux variant (LDBL128=1) is what static-musl tests need. rt-aarch64-linux: rt/build/aarch64-linux/libcfree_rt.a +rt-x86_64-linux: rt/build/x86_64-linux/libcfree_rt.a +rt-riscv64-linux: rt/build/riscv64-linux/libcfree_rt.a rt/build/aarch64-linux/libcfree_rt.a: $(MAKE) -C rt VARIANT=aarch64-linux OUT=build/aarch64-linux all +rt/build/x86_64-linux/libcfree_rt.a: + $(MAKE) -C rt VARIANT=x86_64-linux OUT=build/x86_64-linux all + +rt/build/riscv64-linux/libcfree_rt.a: + $(MAKE) -C rt VARIANT=riscv64-linux OUT=build/riscv64-linux all + # `rt` alias builds whichever variants are typically wanted on the host. rt: rt-aarch64-linux @@ -77,6 +87,35 @@ $(HOSTED_MACOS_AR): $(HOSTED_MACOS_OBJ) $(BIN) @rm -f $@ $(BIN) ar rcs $@ $(HOSTED_MACOS_OBJ) +# libcfree_hosted_linux: per-arch ELF shim object. Built by cfree-cc for +# the cross-target so test-libc can link host-libc test cases against it +# without needing clang's resource dir or a separate compile pass. +# stdin/stdout/stderr are extern data globals on Linux; cfree-cc doesn't +# yet emit GOT-loads for extern data on ELF (only Mach-O), so callers +# must static-link the resulting exes — the addresses resolve at link +# time when libc.a is in the input set. +HOSTED_LINUX_SRC = rt/lib/cfree_hosted/linux.c +HOSTED_LINUX_AARCH64_OBJ = build/cfree_hosted/linux-aarch64.o +HOSTED_LINUX_X64_OBJ = build/cfree_hosted/linux-x64.o +HOSTED_LINUX_RV64_OBJ = build/cfree_hosted/linux-rv64.o + +hosted-linux: hosted-linux-aarch64 hosted-linux-x64 hosted-linux-rv64 +hosted-linux-aarch64: $(HOSTED_LINUX_AARCH64_OBJ) +hosted-linux-x64: $(HOSTED_LINUX_X64_OBJ) +hosted-linux-rv64: $(HOSTED_LINUX_RV64_OBJ) + +$(HOSTED_LINUX_AARCH64_OBJ): $(HOSTED_LINUX_SRC) $(BIN) + @mkdir -p $(dir $@) + $(BIN) cc -target aarch64-linux -c $< -o $@ + +$(HOSTED_LINUX_X64_OBJ): $(HOSTED_LINUX_SRC) $(BIN) + @mkdir -p $(dir $@) + $(BIN) cc -target x86_64-linux -c $< -o $@ + +$(HOSTED_LINUX_RV64_OBJ): $(HOSTED_LINUX_SRC) $(BIN) + @mkdir -p $(dir $@) + $(BIN) cc -target riscv64-linux -c $< -o $@ + # Replace the archive (`ar rcs` only adds/updates), so removing a .c file # also removes its .o from the archive on the next build. $(LIB_AR): $(LIB_OBJS) diff --git a/rt/Makefile b/rt/Makefile @@ -10,6 +10,7 @@ VARIANTS = \ x86_64-apple-darwin \ aarch64-linux \ aarch64-apple-darwin \ + riscv64-linux \ riscv64-elf \ riscv64-elf-save-restore \ x86_64-pc-windows \ @@ -66,6 +67,13 @@ ifeq ($(VARIANT),aarch64-apple-darwin) INT128 = 1 CORO = aarch64 endif +ifeq ($(VARIANT),riscv64-linux) + TARGET = riscv64-linux-gnu + ABI = lp64 + INT128 = 1 + CORO = riscv64 + ARCH_FLAGS = -mabi=lp64d -march=rv64imafd +endif ifeq ($(VARIANT),riscv64-elf) TARGET = riscv64-unknown-elf ABI = lp64 diff --git a/rt/lib/cfree_hosted/linux.c b/rt/lib/cfree_hosted/linux.c @@ -0,0 +1,33 @@ +/* libcfree_hosted -- Linux shim (glibc + musl) + * + * Bridges the ABI-neutral names declared in rt/include/libc/ to the + * actual symbols glibc / musl export. Compiled by cfree per Linux + * arch into build/cfree_hosted/linux-<arch>.o; programs link it in + * alongside -lc (and crt files) when they include any of the libc/ + * headers that name a platform-divergent symbol. + * + * On Linux both common libcs agree on these names: stdin/stdout/stderr + * are FILE* data globals (TLS-backed in glibc, plain data in musl), + * and errno is reached via __errno_location(). The single source file + * covers both. */ + +/* FILE is opaque in libc/stdio.h. Use the same incomplete-struct form + * here so the function signatures line up; we never dereference. */ +struct FILE; + +/* glibc/musl-exported globals: declared as data, not functions. */ +extern struct FILE *stdin; +extern struct FILE *stdout; +extern struct FILE *stderr; + +struct FILE *__cfree_stdin(void) { return stdin; } +struct FILE *__cfree_stdout(void) { return stdout; } +struct FILE *__cfree_stderr(void) { return stderr; } + +/* glibc and musl both expose `int *__errno_location(void)` returning a + * thread-local int*. Darwin uses `__error()` with the same shape; + * the rt/include/libc/errno.h header routes through the cfree-named + * accessor so platform divergence stays in this file. */ +extern int *__errno_location(void); + +int *__cfree_errno_location(void) { return __errno_location(); } diff --git a/test/libc/glibc/Containerfile.rv64 b/test/libc/glibc/Containerfile.rv64 @@ -0,0 +1,36 @@ +# test/libc/glibc/Containerfile.rv64 — produces a glibc riscv64 +# sysroot tarball on stdout. Pinned to Debian trixie (Debian 13); +# riscv64 was promoted to an official Debian architecture in trixie, +# so it's the first stable-ish release with libc6-dev available +# through the standard apt path. +# +# Companion to ./Containerfile (aarch64). See that file for the +# design notes on what gets staged and why; arch-specific differences: +# - riscv64/debian base, trixie release +# - multi-arch dir riscv64-linux-gnu +# - runtime loader is /lib/ld-linux-riscv64-lp64d.so.1 +FROM docker.io/riscv64/debian:trixie-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + libc6-dev \ + linux-libc-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN set -eux; \ + mkdir -p /sysroot/lib /sysroot/include; \ + cd /usr/lib/riscv64-linux-gnu; \ + cp Scrt1.o crti.o crtn.o libc_nonshared.a /sysroot/lib/; \ + cp -L /lib/riscv64-linux-gnu/libc.so.6 /sysroot/lib/libc.so.6; \ + cp -L /lib/riscv64-linux-gnu/libm.so.6 /sysroot/lib/libm.so.6; \ + cp -L /lib/ld-linux-riscv64-lp64d.so.1 /sysroot/lib/ld-linux-riscv64-lp64d.so.1; \ + cp -r /usr/include/. /sysroot/include/ + +RUN set -eux; \ + { \ + echo "debian trixie-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/Containerfile.x64 b/test/libc/glibc/Containerfile.x64 @@ -0,0 +1,34 @@ +# test/libc/glibc/Containerfile.x64 — produces a glibc x86_64 sysroot +# tarball on stdout. Pinned to Debian bookworm (glibc 2.36). +# +# Companion to ./Containerfile (aarch64). See that file for the +# design notes on what gets staged and why; arch-specific differences: +# - amd64/debian base +# - multi-arch dir x86_64-linux-gnu (/usr/lib + /usr/include + /lib) +# - runtime loader is /lib64/ld-linux-x86-64.so.2 (note /lib64, +# not /lib, unlike aarch64 + riscv64). +FROM docker.io/amd64/debian:bookworm-slim + +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/* + +RUN set -eux; \ + mkdir -p /sysroot/lib /sysroot/lib64 /sysroot/include; \ + cd /usr/lib/x86_64-linux-gnu; \ + cp Scrt1.o crti.o crtn.o libc_nonshared.a /sysroot/lib/; \ + cp -L /lib/x86_64-linux-gnu/libc.so.6 /sysroot/lib/libc.so.6; \ + cp -L /lib/x86_64-linux-gnu/libm.so.6 /sysroot/lib/libm.so.6; \ + cp -L /lib64/ld-linux-x86-64.so.2 /sysroot/lib64/ld-linux-x86-64.so.2; \ + cp -r /usr/include/. /sysroot/include/ + +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 @@ -1,30 +1,69 @@ #!/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. +# test/libc/glibc/extract.sh — build a glibc sysroot container image +# and unpack /sysroot from it into build/glibc-sysroot[-ARCH]/ on the +# host. 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/) +# Usage: +# extract.sh # default aarch64 -> build/glibc-sysroot/ +# extract.sh -a aarch64 # same as default +# extract.sh -a x64 # x86_64 -> build/glibc-sysroot-x64/ +# extract.sh -a rv64 # riscv64 -> build/glibc-sysroot-rv64/ +# +# Arch-specific Containerfiles live next to this script +# (Containerfile, Containerfile.x64, Containerfile.rv64). +# +# The aarch64 sysroot keeps the historical bare `build/glibc-sysroot/` +# path so test-glibc (the cfree-ld aarch64 harness) is unaffected. +# test-libc on Linux uses the arch-suffixed paths uniformly and +# re-targets `build/glibc-sysroot/` for aarch64. +# +# Output layout (per arch): +# build/glibc-sysroot[-ARCH]/ +# lib/ Scrt1.o crti.o crtn.o libc_nonshared.a libc.so.6 ... +# lib64/ (x64 only) ld-linux-x86-64.so.2 +# include/ glibc + linux uapi tree # PROVENANCE set -eu ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" -SYSROOT="$ROOT/build/glibc-sysroot" -TAG="cfree-glibc-sysroot" +ARCH=aarch64 FORCE=0 while [ $# -gt 0 ]; do case "$1" in + -a) ARCH="$2"; shift 2 ;; + --arch=*) ARCH="${1#--arch=}"; shift ;; -f|--force) FORCE=1; shift ;; *) echo "unknown arg: $1" >&2; exit 2 ;; esac done +case "$ARCH" in + aarch64) + SYSROOT="$ROOT/build/glibc-sysroot" + PLATFORM="linux/arm64" + CFILE="Containerfile" + TAG="cfree-glibc-sysroot" + ;; + x64) + SYSROOT="$ROOT/build/glibc-sysroot-x64" + PLATFORM="linux/amd64" + CFILE="Containerfile.x64" + TAG="cfree-glibc-sysroot-x64" + ;; + rv64) + SYSROOT="$ROOT/build/glibc-sysroot-rv64" + PLATFORM="linux/riscv64" + CFILE="Containerfile.rv64" + TAG="cfree-glibc-sysroot-rv64" + ;; + *) + echo "extract.sh: unknown arch '$ARCH' (want aarch64|x64|rv64)" >&2 + exit 2 + ;; +esac + if [ -f "$SYSROOT/PROVENANCE" ] && [ $FORCE -eq 0 ]; then echo "glibc sysroot already present at $SYSROOT (use -f to rebuild)" exit 0 @@ -36,14 +75,14 @@ if ! command -v podman >/dev/null 2>&1; then 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 +echo "Building $TAG (Debian $ARCH + libc6-dev)..." +podman build --platform "$PLATFORM" -f "$CFILE" -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 - +podman run --rm --platform "$PLATFORM" "$TAG" | tar -C "$SYSROOT" -xf - echo "Done. Provenance:" cat "$SYSROOT/PROVENANCE" diff --git a/test/libc/musl/Containerfile.rv64 b/test/libc/musl/Containerfile.rv64 @@ -0,0 +1,37 @@ +# test/libc/musl/Containerfile.rv64 — produces a musl riscv64 sysroot +# tarball on stdout. Uses Alpine edge (riscv64 is in community ports; +# stable Alpine has not yet promoted it to a tier-1 release branch). +# +# Companion to ./Containerfile (aarch64). See that file for the design +# notes on what gets staged and why; arch-specific differences: +# - riscv64/alpine base +# - ld-musl-riscv64.so.1 as the dynamic loader (still aliased to +# libc.so so cfree ld can link against it as a shared input). +# +# Note: alpine:edge floats. We pin via the digest-bearing tag at +# extract time when possible; the PROVENANCE record below records the +# resolved musl version so a regression in the upstream package is +# visible in test failures. +FROM docker.io/riscv64/alpine:edge + +RUN apk add --no-cache musl-dev linux-headers + +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-riscv64.so.1 /sysroot/lib/ \ + && ln -s ld-musl-riscv64.so.1 /sysroot/lib/libc.so \ + && cp -r /usr/include/. /sysroot/include/ + +RUN sh -c 'set -eu; \ + musl_ver=$(apk info -v musl-dev | head -1); \ + alpine_rel=$(cat /etc/alpine-release); \ + echo "alpine $alpine_rel $musl_ver" > /sysroot/PROVENANCE; \ + uname -m >> /sysroot/PROVENANCE' + +ENTRYPOINT ["sh", "-c", "tar -C /sysroot -cf - ."] diff --git a/test/libc/musl/Containerfile.x64 b/test/libc/musl/Containerfile.x64 @@ -0,0 +1,29 @@ +# test/libc/musl/Containerfile.x64 — produces a musl x86_64 sysroot +# tarball on stdout. Pinned to Alpine 3.20.10 + musl 1.2.5. +# +# Companion to ./Containerfile (aarch64). See that file for the design +# notes on what gets staged and why; only the arch-specific bits differ +# here: +# - amd64/alpine base image +# - ld-musl-x86_64.so.1 as the dynamic loader (still aliased to +# libc.so so cfree ld can link against it as a shared input). +FROM docker.io/amd64/alpine:3.20.10 + +RUN apk add --no-cache musl-dev=1.2.5-r3 linux-headers + +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-x86_64.so.1 /sysroot/lib/ \ + && ln -s ld-musl-x86_64.so.1 /sysroot/lib/libc.so \ + && cp -r /usr/include/. /sysroot/include/ + +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 @@ -1,27 +1,67 @@ #!/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. +# test/libc/musl/extract.sh — build a musl sysroot container image and +# unpack /sysroot from it into build/musl-sysroot[-ARCH]/ on the host. +# 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 +# Usage: +# extract.sh # default aarch64 -> build/musl-sysroot/ +# extract.sh -a aarch64 # same as default +# extract.sh -a x64 # x86_64 -> build/musl-sysroot-x64/ +# extract.sh -a rv64 # riscv64 -> build/musl-sysroot-rv64/ +# +# Arch-specific Containerfiles live next to this script +# (Containerfile, Containerfile.x64, Containerfile.rv64). +# +# The aarch64 sysroot keeps the historical bare `build/musl-sysroot/` +# path so test-musl (the cfree-ld dynamic/static aarch64 harness) is +# unaffected. test-libc on Linux uses the arch-suffixed paths uniformly +# and re-targets `build/musl-sysroot/` for aarch64. +# +# Output layout (per arch): +# build/musl-sysroot[-ARCH]/ +# lib/ crt1.o crti.o crtn.o libc.a libssp_nonshared.a ld-musl-* # include/ musl + linux-headers tree # PROVENANCE set -eu ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" -SYSROOT="$ROOT/build/musl-sysroot" -TAG="cfree-musl-sysroot" +ARCH=aarch64 FORCE=0 while [ $# -gt 0 ]; do case "$1" in + -a) ARCH="$2"; shift 2 ;; + --arch=*) ARCH="${1#--arch=}"; shift ;; -f|--force) FORCE=1; shift ;; *) echo "unknown arg: $1" >&2; exit 2 ;; esac done +case "$ARCH" in + aarch64) + SYSROOT="$ROOT/build/musl-sysroot" + PLATFORM="linux/arm64" + CFILE="Containerfile" + TAG="cfree-musl-sysroot" + ;; + x64) + SYSROOT="$ROOT/build/musl-sysroot-x64" + PLATFORM="linux/amd64" + CFILE="Containerfile.x64" + TAG="cfree-musl-sysroot-x64" + ;; + rv64) + SYSROOT="$ROOT/build/musl-sysroot-rv64" + PLATFORM="linux/riscv64" + CFILE="Containerfile.rv64" + TAG="cfree-musl-sysroot-rv64" + ;; + *) + echo "extract.sh: unknown arch '$ARCH' (want aarch64|x64|rv64)" >&2 + exit 2 + ;; +esac + if [ -f "$SYSROOT/PROVENANCE" ] && [ $FORCE -eq 0 ]; then echo "musl sysroot already present at $SYSROOT (use -f to rebuild)" exit 0 @@ -33,14 +73,14 @@ if ! command -v podman >/dev/null 2>&1; then fi cd "$ROOT/test/libc/musl" -echo "Building $TAG (Alpine aarch64 + musl-dev)..." -podman build --platform linux/arm64 -f Containerfile -t "$TAG" . >/dev/null +echo "Building $TAG (Alpine $ARCH + musl-dev)..." +podman build --platform "$PLATFORM" -f "$CFILE" -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 - +podman run --rm --platform "$PLATFORM" "$TAG" | tar -C "$SYSROOT" -xf - echo "Done. Provenance:" cat "$SYSROOT/PROVENANCE" diff --git a/test/libc/run.sh b/test/libc/run.sh @@ -1,26 +1,42 @@ #!/usr/bin/env bash # test/libc/run.sh -- exercise cfree's rt/include/libc headers + the -# per-OS libcfree_hosted shim on the host. Each case is compiled by -# cfree-cc against the libc header set, linked against the platform's -# C library plus the hosted shim, and executed on the host. +# per-OS libcfree_hosted shim. Each case is compiled by cfree-cc against +# the libc header set, linked against the platform's C library plus +# the hosted shim, and executed. # -# This sits parallel to test/libc/{musl,glibc}/run.sh: those run inside -# containers against extracted Linux sysroots; this one targets the -# host directly. macOS is the only host wired today; other hosts skip. +# Cells: +# Darwin host -> one cell (darwin), runs natively on the host. +# Linux -> 9 cells = {x64, aarch64, rv64} +# x {musl-static, musl-dynamic, glibc-dynamic}. +# Each cell cross-compiles + cross-links with cfree on +# the host against the matching sysroot under build/, +# then execs the produced ELF inside podman (or under +# qemu-user when available) in alpine (musl) or +# debian (glibc). +# +# musl gets both static and dynamic because musl is happy in either +# shape and the two link paths exercise different cfree-ld surfaces. +# glibc is dynamic-only — static-glibc is officially discouraged and +# small-program-only, and isn't a real-world deployment shape. # # Each case file may carry: # <name>.expected -- numeric exit code, default 0 # <name>.stdout -- exact-substring match against captured stdout # -# Cases that aren't part of the host-libc surface (raw-syscall, unistd.h) -# are filtered out by name. Run with CFREE_LIBC_KEEP=1 to leave -# intermediates in build/host-libc/<case>/. +# Cases that aren't part of the host-libc surface (raw-syscall, +# unistd.h) are filtered out by name. Run with CFREE_LIBC_KEEP=1 to +# leave intermediates in build/libc/<cell>/<case>/. +# +# Environment knobs: +# CFREE_LIBC_KEEP=1 keep build dir after run +# CFREE_LIBC_CELLS=... comma-separated cell allowlist (default: all +# cells for the detected host). set -u ROOT="$(cd "$(dirname "$0")/../.." && pwd)" CASES_DIR="$ROOT/test/libc/cases" -BUILD_DIR="$ROOT/build/host-libc" +BUILD_ROOT="$ROOT/build/libc" CFREE="$ROOT/build/cfree" color_red() { printf '\033[31m%s\033[0m' "$1"; } @@ -31,153 +47,465 @@ note_skip() { printf ' %s %s -- %s\n' "$(color_yel SKIP)" "$1" "$2"; } note_fail() { printf ' %s %s\n' "$(color_red FAIL)" "$1"; } note_pass() { printf ' %s %s\n' "$(color_grn PASS)" "$1"; } -# Host-platform setup. Each host needs: -# - HOSTED_OBJ -- cfree-hosted shim object for stdin/stdout/stderr/errno -# - LDFLAGS -- extra link flags (e.g. -L $SDK/usr/lib) -# - LDLIBS -- link arguments after the source (e.g. -lSystem) -# - TARGET -- cfree-cc -target triple -HOSTED_OBJ="" -LDFLAGS=() -LDLIBS=() -TARGET="" +if [ ! -x "$CFREE" ]; then + printf 'cfree driver missing at %s -- run `make` first\n' "$CFREE" >&2 + exit 2 +fi + +# Cases shared with test-musl/test-glibc; filter to the subset that +# exercises rt/include/libc surface (we don't ship raw-syscall or +# unistd.h headers, so 01/02 are out). +case_supported() { + case "$1" in + 03_printf_hello|10_*|11_*|12_*|13_*|14_*|15_*) return 0 ;; + *) return 1 ;; + esac +} + +# ----- per-host cell selection ---------------------------------------------- +# +# A cell is identified by a tag like "darwin" or "aarch64-musl-dynamic". +# Each cell is configured by configure_cell into a set of shell globals +# (TRIPLE, SYSROOT, HOSTED_OBJ, CFREE_RT, LINK_VARIANT, EXEC_TAG, ...) +# consumed by run_one_case below. uname_s="$(uname -s 2>/dev/null || echo unknown)" case "$uname_s" in Darwin) - arch_raw="$(uname -m 2>/dev/null || true)" - case "$arch_raw" in - arm64|aarch64) TARGET="aarch64-darwin" ;; - x86_64) TARGET="x86_64-darwin" ;; - *) printf 'unknown Darwin arch: %s\n' "$arch_raw" >&2; exit 2 ;; - esac - if ! command -v xcrun >/dev/null 2>&1; then - printf 'xcrun missing -- Xcode CLT required\n' >&2 - exit 2 - fi - SDK="$(xcrun --show-sdk-path 2>/dev/null || true)" - if [ -z "$SDK" ] || [ ! -d "$SDK" ]; then - printf 'xcrun --show-sdk-path failed\n' >&2 - exit 2 - fi - HOSTED_OBJ="$ROOT/build/cfree_hosted/macos.o" - LDFLAGS=(-L "$SDK/usr/lib") - LDLIBS=(-lSystem) + HOST_CELLS=(darwin) ;; Linux) - note_skip "all" "host-libc shim for Linux not wired yet" - exit 0 + HOST_CELLS=( + x64-musl-static x64-musl-dynamic x64-glibc-dynamic + aarch64-musl-static aarch64-musl-dynamic aarch64-glibc-dynamic + rv64-musl-static rv64-musl-dynamic rv64-glibc-dynamic + ) ;; *) - note_skip "all" "host-libc not supported on $uname_s" + note_skip "all" "test-libc not supported on $uname_s" exit 0 ;; esac -if [ ! -x "$CFREE" ]; then - printf 'cfree driver missing at %s -- run `make` first\n' "$CFREE" >&2 - exit 2 -fi -if [ ! -f "$HOSTED_OBJ" ]; then - printf 'hosted shim missing at %s -- run `make hosted-macos`\n' \ - "$HOSTED_OBJ" >&2 - exit 2 +if [ -n "${CFREE_LIBC_CELLS:-}" ]; then + IFS=',' read -r -a HOST_CELLS <<<"$CFREE_LIBC_CELLS" fi -mkdir -p "$BUILD_DIR" +# ----- runner detection (shared by linux cells) ----------------------------- +# +# Each cell that runs Linux ELFs needs *some* runner: matching-arch +# native exec, qemu-user for the target, or podman with the right +# platform. configure_cell records which is usable, in that priority +# order. -# Cases under test/libc/cases/ are shared with the musl/glibc runners, -# which test surface (raw syscalls via inline asm, <unistd.h>) we don't -# ship in rt/include/libc. Skip those by base-name; only files matching -# our explicit allowlist run. -case_supported() { - case "$1" in - 03_printf_hello|10_*|11_*|12_*|13_*|14_*|15_*) return 0 ;; +arch_raw="$(uname -m 2>/dev/null || true)" +# host_is_linux_arch returns 0 only when the host kernel is Linux AND +# its native arch matches. Darwin/arm64 cannot exec a Linux aarch64 +# ELF, so native exec is gated on both axes. +host_is_linux_arch() { + [ "$uname_s" = "Linux" ] || return 1 + case "$arch_raw" in + aarch64|arm64) [ "$1" = "aarch64" ] ;; + x86_64|amd64) [ "$1" = "x64" ] ;; + riscv64) [ "$1" = "rv64" ] ;; *) return 1 ;; esac } -PASS=0 -FAIL=0 -SKIP=0 -FAIL_NAMES=() +have_podman=0; command -v podman >/dev/null 2>&1 && have_podman=1 -shopt -s nullglob +qemu_for_arch() { + case "$1" in + aarch64) + command -v qemu-aarch64-static 2>/dev/null \ + || command -v qemu-aarch64 2>/dev/null \ + || true + ;; + x64) + command -v qemu-x86_64-static 2>/dev/null \ + || command -v qemu-x86_64 2>/dev/null \ + || true + ;; + rv64) + command -v qemu-riscv64-static 2>/dev/null \ + || command -v qemu-riscv64 2>/dev/null \ + || true + ;; + esac +} + +# Cell config writes into these globals. +CELL="" +LABEL="" +TRIPLE="" +SYSROOT="" +HOSTED_OBJ="" +CFREE_RT="" +LINK_VARIANT="" # macos | musl-static | musl-dynamic | glibc-dynamic +LOADER="" # -dynamic-linker arg, or empty +EXEC_KIND="" # native | qemu | podman | host-darwin +QEMU_BIN="" +PODMAN_PLATFORM="" +PODMAN_IMAGE="" -printf 'Running host-libc cases (%s)...\n' "$TARGET" +# Returns 0 (configured) or 1 (skip + reason in CELL_SKIP_REASON). +CELL_SKIP_REASON="" +configure_cell() { + CELL="$1" + LABEL="$CELL" + CELL_SKIP_REASON="" + TRIPLE=""; SYSROOT=""; HOSTED_OBJ=""; CFREE_RT="" + LINK_VARIANT=""; LOADER=""; EXEC_KIND=""; QEMU_BIN="" + PODMAN_PLATFORM=""; PODMAN_IMAGE="" -for src in "$CASES_DIR"/*.c; do - name="$(basename "$src" .c)" - if ! case_supported "$name"; then - note_skip "$name" "not in host-libc surface (uses unistd/raw syscall)" - SKIP=$((SKIP + 1)) - continue + if [ "$CELL" = "darwin" ]; then + case "$arch_raw" in + arm64|aarch64) TRIPLE="aarch64-darwin" ;; + x86_64) TRIPLE="x86_64-darwin" ;; + *) CELL_SKIP_REASON="unknown Darwin arch: $arch_raw"; return 1 ;; + esac + if ! command -v xcrun >/dev/null 2>&1; then + CELL_SKIP_REASON="xcrun missing (Xcode CLT required)"; return 1 + fi + SDK="$(xcrun --show-sdk-path 2>/dev/null || true)" + if [ -z "$SDK" ] || [ ! -d "$SDK" ]; then + CELL_SKIP_REASON="xcrun --show-sdk-path failed"; return 1 + fi + HOSTED_OBJ="$ROOT/build/cfree_hosted/macos.o" + if [ ! -f "$HOSTED_OBJ" ]; then + CELL_SKIP_REASON="hosted-macos shim missing at $HOSTED_OBJ (run 'make hosted-macos')" + return 1 + fi + LINK_VARIANT="macos" + EXEC_KIND="host-darwin" + DARWIN_LDFLAGS=(-L "$SDK/usr/lib") + DARWIN_LDLIBS=(-lSystem) + return 0 fi - work="$BUILD_DIR/$name" + # Linux cells: <arch>-<libc>-<variant> + local arch libc variant + arch="${CELL%%-*}" + local rest="${CELL#*-}" + libc="${rest%%-*}" + variant="${rest#*-}" + + case "$arch" in + x64) TRIPLE="x86_64-linux" ; PODMAN_PLATFORM="linux/amd64" ;; + aarch64) TRIPLE="aarch64-linux" ; PODMAN_PLATFORM="linux/arm64" ;; + rv64) TRIPLE="riscv64-linux" ; PODMAN_PLATFORM="linux/riscv64" ;; + *) CELL_SKIP_REASON="unknown arch in cell: $arch"; return 1 ;; + esac + + # Sysroot path. aarch64 reuses the bare build/{musl,glibc}-sysroot/ + # tree shared with test-musl/test-glibc; x64/rv64 live under the + # arch-suffixed dirs that test/libc/{musl,glibc}/extract.sh -a writes. + case "$libc:$arch" in + musl:aarch64) SYSROOT="$ROOT/build/musl-sysroot" ;; + musl:x64) SYSROOT="$ROOT/build/musl-sysroot-x64" ;; + musl:rv64) SYSROOT="$ROOT/build/musl-sysroot-rv64" ;; + glibc:aarch64) SYSROOT="$ROOT/build/glibc-sysroot" ;; + glibc:x64) SYSROOT="$ROOT/build/glibc-sysroot-x64" ;; + glibc:rv64) SYSROOT="$ROOT/build/glibc-sysroot-rv64" ;; + *) CELL_SKIP_REASON="unknown libc in cell: $libc"; return 1 ;; + esac + if [ ! -f "$SYSROOT/PROVENANCE" ]; then + CELL_SKIP_REASON="sysroot missing at $SYSROOT (run 'make test-libc' or extract.sh -a $arch)" + return 1 + fi + + # Hosted shim object for this arch (cfree-built ELF .o). + case "$arch" in + x64) HOSTED_OBJ="$ROOT/build/cfree_hosted/linux-x64.o" ;; + aarch64) HOSTED_OBJ="$ROOT/build/cfree_hosted/linux-aarch64.o" ;; + rv64) HOSTED_OBJ="$ROOT/build/cfree_hosted/linux-rv64.o" ;; + esac + if [ ! -f "$HOSTED_OBJ" ]; then + CELL_SKIP_REASON="hosted-linux shim missing at $HOSTED_OBJ (run 'make hosted-linux')" + return 1 + fi + + # libcfree_rt — soft-float / TF / 128-bit-int helpers. cfree-cc may + # emit calls into these from long-double printf paths and similar. + case "$arch" in + x64) CFREE_RT="$ROOT/rt/build/x86_64-linux/libcfree_rt.a" ;; + aarch64) CFREE_RT="$ROOT/rt/build/aarch64-linux/libcfree_rt.a" ;; + rv64) CFREE_RT="$ROOT/rt/build/riscv64-linux/libcfree_rt.a" ;; + esac + if [ ! -f "$CFREE_RT" ]; then + CELL_SKIP_REASON="cfree rt missing at $CFREE_RT (run 'make rt-${TRIPLE%-*}-linux')" + return 1 + fi + + # Link variant + dynamic-linker path for cfree ld. + case "$libc-$variant" in + musl-static) + LINK_VARIANT="musl-static" + ;; + musl-dynamic) + LINK_VARIANT="musl-dynamic" + case "$arch" in + x64) LOADER="/lib/ld-musl-x86_64.so.1" ;; + aarch64) LOADER="/lib/ld-musl-aarch64.so.1" ;; + rv64) LOADER="/lib/ld-musl-riscv64.so.1" ;; + esac + ;; + glibc-dynamic) + LINK_VARIANT="glibc-dynamic" + case "$arch" in + x64) LOADER="/lib64/ld-linux-x86-64.so.2" ;; + aarch64) LOADER="/lib/ld-linux-aarch64.so.1" ;; + rv64) LOADER="/lib/ld-linux-riscv64-lp64d.so.1";; + esac + ;; + *) CELL_SKIP_REASON="unknown libc-variant: $libc-$variant"; return 1 ;; + esac + + # Runner: prefer native exec, then qemu-user, then podman. + if host_is_linux_arch "$arch"; then + EXEC_KIND="native" + else + QEMU_BIN="$(qemu_for_arch "$arch")" + if [ -n "$QEMU_BIN" ]; then + EXEC_KIND="qemu" + elif [ "$have_podman" -eq 1 ]; then + EXEC_KIND="podman" + else + CELL_SKIP_REASON="no runner for $arch (need native, qemu-$arch, or podman)" + return 1 + fi + fi + + # Podman image per (arch, libc). Pinning the arch-specific repo + # (arm64v8/, amd64/, riscv64/) avoids the manifest-lookup detour + # that --platform triggers on hosts whose podman cache is mixed. + case "$libc:$arch" in + musl:aarch64) PODMAN_IMAGE="docker.io/arm64v8/alpine:latest" ;; + musl:x64) PODMAN_IMAGE="docker.io/amd64/alpine:latest" ;; + musl:rv64) PODMAN_IMAGE="docker.io/riscv64/alpine:edge" ;; + glibc:aarch64) PODMAN_IMAGE="docker.io/arm64v8/debian:bookworm-slim" ;; + glibc:x64) PODMAN_IMAGE="docker.io/amd64/debian:bookworm-slim" ;; + glibc:rv64) PODMAN_IMAGE="docker.io/riscv64/debian:trixie-slim" ;; + esac + + return 0 +} + +# ----- link command builder -------------------------------------------------- + +# Produces the cfree-ld command in the LINK_CMD array given the per-case +# .o path + output exe path. Pulls from configure_cell globals. +build_link_cmd() { + local obj="$1" exe="$2" + case "$LINK_VARIANT" in + musl-static) + # crt1.o crti.o obj hosted_obj libc.a libcfree_rt.a crtn.o + LINK_CMD=( + "$CFREE" ld -static -o "$exe" + "$SYSROOT/lib/crt1.o" "$SYSROOT/lib/crti.o" + "$obj" "$HOSTED_OBJ" + "$SYSROOT/lib/libc.a" "$CFREE_RT" + "$SYSROOT/lib/crtn.o" + ) + ;; + musl-dynamic) + # Scrt1.o crti.o obj hosted_obj libc.so libcfree_rt.a crtn.o + LINK_CMD=( + "$CFREE" ld -pie -dynamic-linker "$LOADER" -o "$exe" + "$SYSROOT/lib/Scrt1.o" "$SYSROOT/lib/crti.o" + "$obj" "$HOSTED_OBJ" + "$SYSROOT/lib/libc.so" "$CFREE_RT" + "$SYSROOT/lib/crtn.o" + ) + ;; + glibc-dynamic) + # Scrt1.o crti.o obj hosted_obj libc.so.6 libc_nonshared.a libcfree_rt.a crtn.o + LINK_CMD=( + "$CFREE" ld -pie -dynamic-linker "$LOADER" -o "$exe" + "$SYSROOT/lib/Scrt1.o" "$SYSROOT/lib/crti.o" + "$obj" "$HOSTED_OBJ" + "$SYSROOT/lib/libc.so.6" "$SYSROOT/lib/libc_nonshared.a" "$CFREE_RT" + "$SYSROOT/lib/crtn.o" + ) + ;; + esac +} + +# ----- exec dispatch --------------------------------------------------------- + +# Sets RUN_RC and writes captured stdout/stderr to <out>/<err>. +exec_one() { + local exe="$1" out="$2" err="$3" + case "$EXEC_KIND" in + host-darwin|native) + "$exe" >"$out" 2>"$err" + RUN_RC=$? + ;; + qemu) + "$QEMU_BIN" "$exe" >"$out" 2>"$err" + RUN_RC=$? + ;; + podman) + local dir base + dir="$(cd "$(dirname "$exe")" && pwd)" + base="$(basename "$exe")" + podman run --rm --platform "$PODMAN_PLATFORM" --net=none \ + -v "$dir":/work:Z -w /work \ + "$PODMAN_IMAGE" "./$base" \ + >"$out" 2>"$err" + RUN_RC=$? + ;; + *) + RUN_RC=127 + ;; + esac +} + +# ----- per-case driver ------------------------------------------------------- + +PASS_TOTAL=0 +FAIL_TOTAL=0 +SKIP_TOTAL=0 +FAIL_NAMES=() + +run_one_case() { + local src="$1" + local name="$(basename "$src" .c)" + local label="$LABEL/$name" + local work="$BUILD_ROOT/$LABEL/$name" mkdir -p "$work" - expected=0 + local expected=0 [ -f "$CASES_DIR/${name}.expected" ] && \ - expected="$(tr -d '[:space:]' < "$CASES_DIR/${name}.expected")" + expected="$(tr -d '[:space:]' < "$CASES_DIR/${name}.expected")" - expect_stdout="" + local expect_stdout="" [ -f "$CASES_DIR/${name}.stdout" ] && \ - expect_stdout="$(cat "$CASES_DIR/${name}.stdout")" - - exe="$work/${name}.exe" - if ! "$CFREE" cc -target "$TARGET" \ - -isystem "$ROOT/rt/include/libc" \ - -isystem "$ROOT/rt/include" \ - -e main \ - "${LDFLAGS[@]}" -o "$exe" "$src" "$HOSTED_OBJ" "${LDLIBS[@]}" \ - >"$work/build.out" 2>"$work/build.err"; then - note_fail "$name (build)" - sed 's/^/ | /' "$work/build.err" | head -10 - FAIL=$((FAIL + 1)) - FAIL_NAMES+=("$name (build)") - continue - fi - chmod +x "$exe" 2>/dev/null + expect_stdout="$(cat "$CASES_DIR/${name}.stdout")" - "$exe" >"$work/run.out" 2>"$work/run.err" - rc=$? + # ---- compile to .o ---- + local cc_flags=(cc -target "$TRIPLE" + -isystem "$ROOT/rt/include/libc" + -isystem "$ROOT/rt/include") + if [ "$LINK_VARIANT" = "macos" ]; then + # Darwin: compile + link in one cfree-cc invocation against the + # SDK, matching the original host-libc flow. + local exe="$work/${name}.exe" + if ! "$CFREE" "${cc_flags[@]}" -e main \ + "${DARWIN_LDFLAGS[@]}" -o "$exe" "$src" "$HOSTED_OBJ" \ + "${DARWIN_LDLIBS[@]}" \ + >"$work/build.out" 2>"$work/build.err"; then + note_fail "$label (build)" + sed 's/^/ | /' "$work/build.err" | head -10 + FAIL_TOTAL=$((FAIL_TOTAL+1)) + FAIL_NAMES+=("$label (build)") + return + fi + chmod +x "$exe" 2>/dev/null + else + # Linux: compile-only with cfree cc, then cfree ld with the cell's + # link variant. -fPIC/-fPIE for dynamic variants so cfree-cc emits + # PC-relative addressing usable in PIE output. + case "$LINK_VARIANT" in + *-dynamic) cc_flags+=(-fPIE -fpic) ;; + esac + cc_flags+=(-isystem "$SYSROOT/include") + # glibc multi-arch headers live under include/<triple-ish>/; add + # the matching subdir when the sysroot ships one. + case "$LINK_VARIANT-$TRIPLE" in + glibc-dynamic-aarch64-linux) + cc_flags+=(-isystem "$SYSROOT/include/aarch64-linux-gnu") ;; + glibc-dynamic-x86_64-linux) + cc_flags+=(-isystem "$SYSROOT/include/x86_64-linux-gnu") ;; + glibc-dynamic-riscv64-linux) + cc_flags+=(-isystem "$SYSROOT/include/riscv64-linux-gnu") ;; + esac + + local obj="$work/${name}.o" + if ! "$CFREE" "${cc_flags[@]}" -c "$src" -o "$obj" \ + >"$work/cc.out" 2>"$work/cc.err"; then + note_fail "$label (cc)" + sed 's/^/ cc| /' "$work/cc.err" | head -10 + FAIL_TOTAL=$((FAIL_TOTAL+1)) + FAIL_NAMES+=("$label (cc)") + return + fi + + local exe="$work/${name}.exe" + build_link_cmd "$obj" "$exe" + if ! "${LINK_CMD[@]}" >"$work/ld.out" 2>"$work/ld.err"; then + note_fail "$label (ld)" + sed 's/^/ ld| /' "$work/ld.err" | head -10 + FAIL_TOTAL=$((FAIL_TOTAL+1)) + FAIL_NAMES+=("$label (ld)") + return + fi + chmod +x "$exe" 2>/dev/null + fi - if [ "$rc" -ne "$expected" ]; then - note_fail "$name (rc=$rc, want $expected)" + # ---- exec ---- + exec_one "$work/${name}.exe" "$work/run.out" "$work/run.err" + if [ "$RUN_RC" -ne "$expected" ]; then + note_fail "$label (rc=$RUN_RC, want $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 - FAIL=$((FAIL + 1)) - FAIL_NAMES+=("$name (rc)") - continue + FAIL_TOTAL=$((FAIL_TOTAL+1)) + FAIL_NAMES+=("$label (rc)") + return fi if [ -n "$expect_stdout" ]; then if ! grep -qF -- "$expect_stdout" "$work/run.out"; then - note_fail "$name (stdout mismatch)" + note_fail "$label (stdout mismatch)" printf ' expected substring:\n' printf '%s\n' "$expect_stdout" | sed 's/^/ | /' printf ' got:\n' sed 's/^/ | /' "$work/run.out" | head -10 - FAIL=$((FAIL + 1)) - FAIL_NAMES+=("$name (stdout)") - continue + FAIL_TOTAL=$((FAIL_TOTAL+1)) + FAIL_NAMES+=("$label (stdout)") + return fi fi - note_pass "$name" - PASS=$((PASS + 1)) + note_pass "$label" + PASS_TOTAL=$((PASS_TOTAL+1)) +} + +# ----- top-level loop -------------------------------------------------------- + +shopt -s nullglob + +mkdir -p "$BUILD_ROOT" + +for cell in "${HOST_CELLS[@]}"; do + if ! configure_cell "$cell"; then + note_skip "$cell/all" "$CELL_SKIP_REASON" + SKIP_TOTAL=$((SKIP_TOTAL+1)) + continue + fi + printf 'Running cell %s (%s)...\n' "$cell" "$TRIPLE" + for src in "$CASES_DIR"/*.c; do + name="$(basename "$src" .c)" + if ! case_supported "$name"; then + # Quiet skip — these are out-of-surface by design, not a config + # gap. Reported once at the end as "out-of-surface" rather than + # noisily per cell. + continue + fi + run_one_case "$src" + done done -if [ "$FAIL" -gt 0 ]; then +if [ "$FAIL_TOTAL" -gt 0 ]; then printf '\nFailed:\n' for n in "${FAIL_NAMES[@]}"; do printf ' %s\n' "$n"; done fi -printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" +printf '\nResults: %s pass, %s fail, %s cell-skip\n' \ + "$PASS_TOTAL" "$FAIL_TOTAL" "$SKIP_TOTAL" -# Keep build dir unless asked. if [ -z "${CFREE_LIBC_KEEP:-}" ]; then - rm -rf "$BUILD_DIR" + rm -rf "$BUILD_ROOT" fi -exit "$FAIL" +exit "$FAIL_TOTAL" diff --git a/test/test.mk b/test/test.mk @@ -172,15 +172,31 @@ test-smoke-rv64: # 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 extract.sh -f forces a rebuild). -MUSL_SYSROOT_MARKER = build/musl-sysroot/PROVENANCE -GLIBC_SYSROOT_MARKER = build/glibc-sysroot/PROVENANCE +MUSL_SYSROOT_MARKER = build/musl-sysroot/PROVENANCE +MUSL_SYSROOT_X64_MARKER = build/musl-sysroot-x64/PROVENANCE +MUSL_SYSROOT_RV64_MARKER = build/musl-sysroot-rv64/PROVENANCE +GLIBC_SYSROOT_MARKER = build/glibc-sysroot/PROVENANCE +GLIBC_SYSROOT_X64_MARKER = build/glibc-sysroot-x64/PROVENANCE +GLIBC_SYSROOT_RV64_MARKER = build/glibc-sysroot-rv64/PROVENANCE $(MUSL_SYSROOT_MARKER): test/libc/musl/extract.sh test/libc/musl/Containerfile @bash test/libc/musl/extract.sh +$(MUSL_SYSROOT_X64_MARKER): test/libc/musl/extract.sh test/libc/musl/Containerfile.x64 + @bash test/libc/musl/extract.sh -a x64 + +$(MUSL_SYSROOT_RV64_MARKER): test/libc/musl/extract.sh test/libc/musl/Containerfile.rv64 + @bash test/libc/musl/extract.sh -a rv64 + $(GLIBC_SYSROOT_MARKER): test/libc/glibc/extract.sh test/libc/glibc/Containerfile @bash test/libc/glibc/extract.sh +$(GLIBC_SYSROOT_X64_MARKER): test/libc/glibc/extract.sh test/libc/glibc/Containerfile.x64 + @bash test/libc/glibc/extract.sh -a x64 + +$(GLIBC_SYSROOT_RV64_MARKER): test/libc/glibc/extract.sh test/libc/glibc/Containerfile.rv64 + @bash test/libc/glibc/extract.sh -a rv64 + test-musl: bin rt-aarch64-linux $(MUSL_SYSROOT_MARKER) @bash test/libc/musl/run.sh @@ -188,11 +204,22 @@ test-glibc: bin rt-aarch64-linux $(GLIBC_SYSROOT_MARKER) @bash test/libc/glibc/run.sh # test-libc: end-to-end host-libc tests. Compiles each test/libc/cases/ -# case with cfree-cc against rt/include/libc, links it against the host's -# C library plus the libcfree_hosted shim, and executes on the host. -# macOS is the only host wired today; other hosts skip with a SKIP report. -# Excluded from default `test` until non-Darwin hosts get a shim. -test-libc: bin hosted-macos +# case with cfree-cc against rt/include/libc, links it against the +# target's C library (libc.a — static, since cfree-cc doesn't yet emit +# GOT-loads for extern data on ELF) plus the libcfree_hosted shim, and +# executes on the host or inside a podman container per target. +# +# macOS host: uses the system SDK + hosted-macos shim natively. +# Linux: per (arch, libc) ∈ {x64, aarch64, rv64} × {musl, glibc}, links +# against the matching sysroot + per-arch hosted-linux shim and runs +# the result in alpine (musl) or debian (glibc) under podman. +# Skips arch+libc combos whose sysroot extraction isn't available. +# Excluded from the default `test` target because Linux paths need +# podman + cross sysroots. +test-libc: bin hosted-macos hosted-linux \ + rt-aarch64-linux rt-x86_64-linux rt-riscv64-linux \ + $(MUSL_SYSROOT_MARKER) $(MUSL_SYSROOT_X64_MARKER) $(MUSL_SYSROOT_RV64_MARKER) \ + $(GLIBC_SYSROOT_MARKER) $(GLIBC_SYSROOT_X64_MARKER) $(GLIBC_SYSROOT_RV64_MARKER) @bash test/libc/run.sh # Fail if libcfree.a depends on any external symbol not in the allowlist.