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