commit af4c29e112fc606ecc1cff13311ce1f141863928
parent 0fa2b1d65c38f0213f6f5a2df8d73ee2e3d4553c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 10 Jun 2026 13:32:01 -0700
feat(port): harmonized cross/selfhost test surface + make kit-cross
Redesign the scattered cross-compile / self-host test targets into one
harmonized surface over a single support set (design: doc/plan/PORT.md).
Test surface (scripts/hosted.sh is the source of truth + selector grammar):
- make test-cross / test-selfhost / test-port / provision, parametrized by
TARGET=<selector> DEPTH=smoke|full KIT_VM=0|1; provision is the only
network/VM step and an unprovisioned requested target is a hard error,
not a skip. Thin make layer in mk/port.mk; engines scripts/cross_test.sh
+ scripts/selfhost.sh + scripts/provision.sh.
- Exec seam gains a 3rd backend behind test/lib/exec_target.sh:
test/lib/exec_bare.sh (freestanding x4 bare-metal via qemu-system),
consolidating the old rv32_bare/kernel/freestanding stubs; fixes a latent
aa64 reset-stub SP-misalignment hang.
- FHS sysroots: driver/lib/hosted.c resolves a standard usr/include[/triple],
usr/lib, lib, lib64 layout, and the glibc/musl Containerfiles stage a
complete toolchain sysroot (crt + libc + gcc runtime) so a plain
'clang --sysroot' cross-link works against them, not just kit.
- Retire test-hosted*/test-toy-*-vm/bootstrap-linux* etc.; the new engines
orchestrate the existing test/{toy,parse,libc} runners.
make kit-cross (the general form of windows_cross.sh): cross-build kit itself
to run on any hosted target, via CROSS_CC=kit (dogfood, default) or
CROSS_CC=clang (independent clang + lld; llvm-mingw compiler-rt for windows).
VERIFY=1 runs the built kit on-target through the exec seam
(scripts/kit_cross_verify.sh). Engine scripts/kit_cross.sh. Validated: all 12
hosted targets x {kit,clang} build a runnable kit.
Source changes the clang backend (a differential check) required:
- driver/cmd/cc.c: kit cc auto-appends .exe to an extensionless -o for a
windows target, matching gcc/clang-mingw (removes a cp workaround).
- driver/env/windows.c: define _CRT_RAND_S before ALL includes (x86_64-mingw
headers pull in <stdlib.h> transitively before a define placed just above
it), so rand_s is declared. Sources are plain-clang-clean (VEH not __try,
no _environ extern, __thread).
- scripts/kit_cross.sh: GC flag is linker-specific (GNU/lld + kit ld take
--gc-sections; Apple ld / clang-on-darwin takes -dead_strip).
- scripts/llvm_mingw_sysroot.sh: stage compiler-rt builtins into the windows
sysroot so a standard clang cross-link finds them.
Diffstat:
33 files changed, 1986 insertions(+), 308 deletions(-)
diff --git a/Makefile b/Makefile
@@ -197,3 +197,7 @@ include mk/maint.mk
-include $(DRIVER_DEPS)
include mk/test.mk
+# Optional: the portability test-orchestration targets (test-cross/test-selfhost/
+# provision). -include so a tracked-files-only ship (e.g. the freebsd/linux
+# self-host bootstrap) that omits an as-yet-uncommitted port.mk still builds kit.
+-include mk/port.mk
diff --git a/doc/plan/PORT.md b/doc/plan/PORT.md
@@ -0,0 +1,262 @@
+# Portability test surface: `cross` + `selfhost` over one support set
+
+This doc is the spec for kit's portability test surface — the harmonized
+top-level make targets and the scripts beneath them that answer two questions:
+
+1. **cross** — can kit, running on the dev host, cross-compile a *correct*
+ executable for every target in its support set?
+2. **selfhost** — can kit be built to *run on* each target, and then compile +
+ run a program there?
+
+It supersedes the old scattered targets (`test-hosted*`, `test-libc*`,
+`test-freebsd*`, `test-coff-windows-*`, `test-toy-*-vm`, `test-link-x64`,
+`test-parse-rv64-wide`, `bootstrap-linux*`, `bootstrap-freebsd`, the standalone
+`windows_cross.sh`, and the freestanding smokes). See "Migration" at the end.
+
+Related: [BOOTSTRAP.md](BOOTSTRAP.md) (the 3-stage self-build mechanism),
+[SYSROOTS.md](SYSROOTS.md) (sysroot provisioning), [windows.md](windows.md).
+
+## Model: two modes over one matrix
+
+```
+ support set (one canonical list in scripts/hosted.sh)
+ ┌──────────────────────────────────────────────────────┐
+ mode cross │ host kit builds for T → artifact runs correctly on T │
+ mode selfhost│ build a kit that runs on T → it builds + runs a program │
+ │ on T │
+ └──────────────────────────────────────────────────────┘
+ substrate (native / qemu-user / podman / VM / qemu-system) = IMPLICIT,
+ resolved per target by the exec seam; never named in a target.
+```
+
+The support set, one token grammar everywhere: **`<os>[-<libc>]-<arch>`**.
+
+```
+cross matrix (16):
+ linux-{glibc,musl}-{aa64,x64,rv64} (6)
+ freebsd-{aarch64,amd64,riscv64} (3)
+ windows-{aarch64,x64} (2)
+ macos-aarch64 (1)
+ freestanding-{aa64,x64,rv64,rv32} (4) ← rv32 lives here only
+
+selfhost matrix (12): the same, minus freestanding
+```
+
+Freestanding has **no OS**, so there is nothing to run a compiler *on*: it
+participates in `cross` only. `selfhost` of a `freestanding-*` target is a hard
+error.
+
+Arch tokens are the short forms **`aa64` / `x64` / `rv64`** (plus `rv32`,
+freestanding-only). The long forms (`aarch64`/`amd64`/`x86_64`/`riscv64`) are
+accepted as input aliases by `scripts/hosted.sh`.
+
+## Top-level make targets
+
+```
+make test-port # test-cross + test-selfhost
+make test-cross [TARGET=…] [DEPTH=…] [KIT_VM=…]
+make test-selfhost [TARGET=…] [DEPTH=…] [KIT_VM=…]
+make kit-cross [TARGET=…] [CROSS_CC=kit|clang] [VERIFY=0|1]
+make provision [TARGET=…] [KIT_VM=…]
+```
+
+Defaults: `TARGET ?= all`, `DEPTH ?= smoke`, `KIT_VM ?= 1`, `CROSS_CC ?= kit`,
+`VERIFY ?= 0`.
+
+`test-port` is **not** part of the default `make test` (it is heavy and needs
+provisioning). The native 3-stage `bootstrap` + `test-bootstrap-toy` stay in the
+default suite as the fast self-reproduction check.
+
+### `TARGET=` selector grammar
+
+Resolved centrally by `scripts/hosted.sh expand <selector> [--mode=cross|selfhost]`:
+
+| Selector | Expands to |
+|---|---|
+| `all` | every support-set token (mode- and `KIT_VM`-filtered) |
+| `linux` / `freebsd` / `windows` / `macos` / `freestanding` | all configs for that OS |
+| `linux-musl` / `linux-glibc` | that libc, all 3 arches |
+| `linux-glibc-x64`, `freebsd-aarch64`, `freestanding-rv32`, … | one exact config |
+| `a,b,c` | comma list of any of the above |
+
+`--mode=selfhost` drops `freestanding-*` from the expansion. `KIT_VM=0` drops
+`freebsd-*` and `windows-*`.
+
+### `DEPTH`
+
+Uniform across both modes:
+
+- `smoke` (default) — two cases, building up platform coverage:
+ 1. `exit` — an `#include`-free exit-code program (`test/cross/cases/exit.c`,
+ returns 42): toolchain + link + crt + the exit-code path; on freestanding
+ the bare-metal exit oracle. Runs on **every** config (the only case
+ freestanding can run — no libc there).
+ 2. `hello` — the libc hello-world (`hello.c`, prints + returns 0): the
+ **sysroot headers + libc + stdout**. Hosted configs only.
+
+ Each case runs once per **link mode** (`link_modes`): **musl exercises both
+ `static` (libc.a) and `dynamic` (libc.so)** — the two ways the sysroot is
+ consumed; lanes are suffixed `:static`/`:dynamic`. FreeBSD is static; glibc is
+ dynamic-only (static glibc is discouraged / NSS-fragile); macOS/Windows use
+ their single default shape. So a musl target yields four smoke lanes
+ (exit/hello × static/dynamic), a glibc target two, freestanding one.
+- `full` — the two smoke cases + the toy and parse corpora (cross-compiled and
+ run on the target) + the libc cases (linux only). Orchestrates the existing
+ mature `test/{toy,parse,libc}` runners with the right arch/tag; it does not
+ reimplement them.
+
+`smoke` is the default to honor "prefer targeted runs over mass runs"
+(CLAUDE.md). `DEPTH=full` is the deliberate heavy run, usually with a scoped
+`TARGET`.
+
+### `kit-cross` — cross-build the compiler itself
+
+`test-cross` asks "can kit cross-compile a *program* for a target?";
+`kit-cross` asks "can kit (or clang) cross-compile *kit itself* to run on a
+target?". It is the general form of the old `windows_cross.sh`: pick any hosted
+token and a backend, and it produces a runnable `kit` binary.
+
+```
+make kit-cross TARGET=windows-x64 # kit.exe for windows-x64, via kit
+make kit-cross TARGET=linux CROSS_CC=clang VERIFY=1
+```
+
+- `CROSS_CC=kit` (default) dogfoods kit as the cross-compiler; `CROSS_CC=clang`
+ uses an independent clang + lld (and llvm-mingw's compiler-rt for Windows)
+ toolchain — a useful differential check, since clang's strictness surfaces
+ source issues kit's frontend tolerates.
+- `VERIFY=1` runs the freshly built `kit` *on the target* through the same exec
+ seam the tests use (`kit_cross_verify.sh`: bare `kit` prints its banner and
+ exits 0 — exercises load + dynamic-linker + libc init + main).
+- `freestanding-*` is excluded (kit needs an OS to run on): the selector uses
+ `expand --mode=selfhost`.
+- Output: `build/kit-cross/<backend>/<target>/kit[.exe]`. The per-target engine
+ is `scripts/kit_cross.sh`; the makefile loop is in `mk/port.mk`.
+
+The two backends differ only in toolchain wiring, not source: the Windows
+sources are plain-clang-clean (no `__try`, no `_environ` extern, `__thread`
+rather than `__declspec(thread)`), and `kit cc` auto-appends `.exe` to an
+extension-less `-o` for a Windows target exactly as gcc/clang-mingw do, so both
+backends write `kit.exe` directly. The GC flag is the one linker-specific knob:
+GNU/lld and kit ld take `--gc-sections`, Apple ld (clang on darwin) takes
+`-dead_strip`.
+
+## Architecture
+
+### Source of truth — `scripts/hosted.sh`
+
+Already owns the support set and the `triple`/`path`/`tag` resolvers. Extended
+with:
+
+- `hosted.sh list [selector]` — emit tokens (one per line).
+- `hosted.sh expand <selector> [--mode=…]` — the selector grammar above.
+
+The Makefile reads it via `$(shell …)`; nothing else re-encodes the matrix.
+
+### Two orchestrators
+
+- `scripts/cross_test.sh <selector> [DEPTH]` — expand (mode=cross) → **fail-fast
+ provisioning pre-flight** → per-token build/run per DEPTH → aggregate report.
+- `scripts/selfhost.sh <selector> [DEPTH]` — expand (mode=selfhost) → pre-flight
+ → per-token self-host (dispatch by OS) → on-target corpus → report.
+
+Both share `test/lib/kit_sh_report.sh` for the verdict/summary layer.
+
+### Provision-or-error
+
+Provisioning (network + VM prepare) is **separate** from running, and a requested
+target whose sysroot / image / VM is missing is a **hard error**, never a silent
+skip. The orchestrators do an aggregated pre-flight: they collect *all* missing
+provisioning for the expanded TARGET set, print each with the exact
+`make provision TARGET=…` (or host-tool install) to fix, and exit non-zero
+before running anything.
+
+`make provision [TARGET=…]` wraps `hosted.sh prepare`, `make test-images`, the
+glibc run images, the Windows UCRT sysroots, and VM prepare — scoped by TARGET
+and KIT_VM. rt archives are local build artifacts (not network): the runners
+build them on demand via idempotent `make rt-…`.
+
+### One exec front door, three backends
+
+`test/lib/exec_target.sh` is the single tag-dispatched front door. A tag is
+`<arch>-<os>[-<libc>]`. It routes to one of three backends:
+
+```
+exec_target.sh
+├── stateless os ∈ {linux, macos} → native / qemu-user / podman
+├── exec_vm.sh os ∈ {freebsd, windows} → boot VM, run
+└── exec_bare.sh os == freestanding → qemu-system bare-metal (NEW)
+```
+
+`exec_bare.sh` is the consolidated owner of all per-arch bare-metal scaffolding
+(reset stub + linker script + exit-code oracle + `qemu-system` invocation),
+generalizing the old `exec_rv32_bare.sh` to four arches and absorbing the stubs
+that were inlined in `freestanding_system.sh`. It exposes two contracts:
+
+- `exec_bare_run_image <arch> <kernel.elf> <out> <err> <rc>` — run a ready
+ bootable image (what `test/link`'s `kernel_image` cases need; `exec_kernel.sh`
+ is now a thin shim over this).
+- `exec_bare_setup <arch> <work>` + `exec_bare_run <arch> <obj> <work> <rc>` —
+ link a corpus `.o` (entry `main`, returns the exit code) with the per-arch stub
+ + rt into a bootable image, then run it (what toy/parse need).
+
+Exit-code oracle per arch (so callers compare `rc == expected` uniformly):
+
+| arch | mechanism | decode |
+|---|---|---|
+| aa64 | ARM semihosting `hlt #0xf000` + `ADP_Stopped_ApplicationExit` | qemu rc = guest code |
+| rv64/rv32 | SiFive test finisher MMIO at `0x100000` | stub writes `0x3333\|(code<<16)` (or `0x5555` for 0); qemu rc = code |
+| x64 | `isa-debug-exit` (iobase `0x501`) | qemu rc = `(code<<1)\|1` → code = `(rc-1)>>1` |
+
+## Self-host shapes (the substrate asymmetry)
+
+`selfhost.sh` dispatches by OS, because "build a kit that runs on T" differs:
+
+- **macos** → native 3-stage `bootstrap` here, then the corpus through stage3.
+- **linux** → native 3-stage in a podman container (emulated for non-host arch),
+ then the corpus. Generalizes `scripts/linux_bootstrap.sh` to (arch, libc).
+- **freebsd** → native 3-stage in the VM, then the corpus
+ (`scripts/freebsd_bootstrap.sh`).
+- **windows** → **cross**-build `kit.exe` on the host (the VM has no seed
+ compiler), then run it on the VM to cc + run a program
+ (`scripts/windows_cross.sh`).
+
+Every selfhost leaf ends with the same contract: *a kit running on the target
+compiled and ran a program there.*
+
+## Status & phased backlog
+
+`cross`:
+
+| Config | smoke | full |
+|---|---|---|
+| linux-{glibc,musl}-{aa64,x64,rv64} | ready | ready (toy X + parse E + libc) |
+| macos-aarch64 | ready (native) | ready (native toy/parse) |
+| freebsd-{aarch64,amd64,riscv64} | ready (VM) | ready (toy via VM) |
+| windows-{aarch64,x64} | ready (VM) | ready (toy via VM) |
+| freestanding-rv32 | ready (bare) | ready (toy X + parse E, bare) |
+| freestanding-{aa64,x64,rv64} | ready (bare smoke) | **deferred** |
+
+`selfhost`: macos (native), linux musl/glibc aarch64 (container), linux x64/rv64
+(container under emulation), freebsd aarch64/amd64 (VM), windows aa64/x64 (cross
++ VM).
+
+### Backlog: freestanding `DEPTH=full` for aa64 / x64 / rv64
+
+Running the toy/parse corpora bare-metal on these three arches is new capability
+beyond the smoke payload. The smoke stubs in `exec_bare.sh` are minimal; the
+corpus exercises TLS, soft-float, large frames, i128, etc., which need hardened
+per-arch stubs (the rv32 path already has this — its reset stub seeds a static
+TLS image and enables the FPU). Work items:
+
+1. Harden the aa64/rv64 `exec_bare` stubs (TLS image seed + thread-pointer setup,
+ matching the rv32 stub and the toy X-lane `start.c`).
+2. Build the x64 long-mode corpus stub (the rv32/aa64 reset path + a full
+ exit-code oracle via `isa-debug-exit`).
+3. Wire a bare lane into `test/{toy,parse}/run.sh` for aa64/x64/rv64 (the rv32
+ "X / V" path generalized), gated on `qemu-system-<arch>`.
+4. Flip the table rows above from **deferred** to **ready** and drop this section.
+
+Until then, `test-cross TARGET=freestanding-{aa64,x64,rv64} DEPTH=full` runs the
+smoke payload and `log`s that corpus-depth is not yet wired for the arch (it does
+not silently claim full coverage).
diff --git a/driver/cmd/cc.c b/driver/cmd/cc.c
@@ -1302,6 +1302,40 @@ static int cc_parse(int argc, char** argv, CcOptions* o) {
o->output_path = driver_default_exe_name(o->target);
}
}
+ /* Windows PE/COFF executables conventionally carry a `.exe` suffix;
+ * GCC/clang on a mingw target append it to an -o output that has no
+ * extension. Mirror that so `kit cc -o foo` for a windows target
+ * produces foo.exe, matching the rest of the toolchain. Only for
+ * executable links (the default name already carries .exe; shared
+ * libraries use the .dll convention; non-link actions are skipped). */
+ if (o->output_path && o->target.os == KIT_OS_WINDOWS && !o->shared &&
+ !o->syntax_only && !o->compile_only && !o->preprocess_only &&
+ !dep_only) {
+ const char* base = o->output_path;
+ for (const char* q = o->output_path; *q; q++)
+ if (*q == '/' || *q == '\\') base = q + 1;
+ int has_ext = 0;
+ for (const char* q = base; *q; q++)
+ if (*q == '.') {
+ has_ext = 1;
+ break;
+ }
+ if (*base && !has_ext) {
+ size_t len = strlen(o->output_path);
+ size_t nsize = len + 5; /* ".exe" + NUL */
+ char* buf = driver_alloc(o->env, nsize);
+ if (!buf) {
+ driver_errf(CC_TOOL, "out of memory");
+ return 1;
+ }
+ memcpy(buf, o->output_path, len);
+ memcpy(buf + len, ".exe", 4);
+ buf[len + 4] = '\0';
+ o->owned_output_path = buf;
+ o->owned_output_path_size = nsize;
+ o->output_path = o->owned_output_path;
+ }
+ }
}
}
cc_enable_hosted_for_sysroot(o);
diff --git a/driver/env/windows.c b/driver/env/windows.c
@@ -14,7 +14,7 @@
* write alias is RW, the runtime alias is RX after a protect flip.
* Single-mapping fallback uses VirtualAlloc.
* - dbg_os: CreateThread for the worker, event objects, vectored
- * exception handling for SEGV/ILL/BP/etc, __try/__except guarded_copy,
+ * exception handling for SEGV/ILL/BP/etc, VEH-based guarded_copy,
* setjmp/longjmp for call_with_catch / thread_abort, SuspendThread +
* GetThreadContext for the interrupt path (the interrupt's on_fault
* runs on the *caller* thread; natural faults run on the worker
@@ -36,18 +36,27 @@
#define _WIN32_WINNT \
0x0601 /* Windows 7+: SRWLock, AddVectoredExceptionHandler */
#endif
+/* rand_s (the OS CSPRNG, used by driver_random_bytes) is declared in
+ * <stdlib.h> only when _CRT_RAND_S is defined first. It must be set before any
+ * include: several headers below transitively pull in <stdlib.h>, and on some
+ * arches (x86_64 mingw) that happens before stdlib.h's own include, so a
+ * define placed right above #include <stdlib.h> arrives too late. */
+#ifndef _CRT_RAND_S
+#define _CRT_RAND_S
+#endif
/* windows.h must precede psapi.h/process.h: the mingw SDK headers use
- * windows.h's WINBOOL/DWORD/LPVOID etc. and do not self-include it. Kept in
- * its own include group so a formatter's alphabetic sort can't reorder it
- * after psapi.h (which breaks the mingw cross-build). */
+ * windows.h's WINBOOL/DWORD/LPVOID etc. and do not self-include it. It sits in
+ * its OWN include group (blank line below) so a formatter's per-group alphabetic
+ * sort cannot reorder it after psapi.h — which would leave EnumProcesses et al.
+ * as bare `WINAPI` decls that the mingw cross-build's frontend then rejects. */
+#include <windows.h>
+
#include <io.h>
#include <process.h>
#include <psapi.h>
+#include <setjmp.h>
#include <stdint.h>
#include <stdio.h>
-#include <windows.h>
-#define _CRT_RAND_S /* enable rand_s (OS CSPRNG) */
-#include <setjmp.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
@@ -55,7 +64,9 @@
#include "env_internal.h"
-extern char** _environ;
+/* _environ comes from <stdlib.h> (mingw defines it as a macro over
+ * __p__environ()); driver_environ() below uses it directly — no manual extern,
+ * which would clash with the header's dllimport declaration under a strict cc. */
/* Win32 dbg interrupt code: a synthetic signo handed up to on_fault. The
* value just needs to be distinct from real exception codes; we pick a
@@ -1564,13 +1575,12 @@ static DWORD g_dbg_worker_tid;
static int g_dbg_worker_tid_valid;
static PVOID g_veh_cookie;
-/* Guarded-copy landing for the VEH path. Both __try/__except and a VEH
- * fallback set g_guard_armed; if the VEH fires while it's set the
- * exception is "translated" into a longjmp via the VEH's ability to
- * rewrite the resume context. We use __try/__except for the primary
- * path and treat the VEH armed-check as a backstop. */
-static __declspec(thread) int g_guard_armed;
-static __declspec(thread) jmp_buf g_guard_buf;
+/* Guarded-copy landing for the VEH path. dbg_guarded_copy_win sets
+ * g_guard_armed around the copy; if the VEH fires while it's set the exception
+ * is "translated" into a longjmp via the VEH's ability to rewrite the resume
+ * context. This is the single mechanism for every compiler (no SEH __try). */
+static __thread int g_guard_armed;
+static __thread jmp_buf g_guard_buf;
static int win_dbg_caller_is_worker(void) {
return g_dbg_worker_tid_valid && GetCurrentThreadId() == g_dbg_worker_tid;
@@ -1696,9 +1706,9 @@ static KitStatus dbg_event_reset_win(void* user, void* ev) {
/* --- vectored exception handler --- */
-/* Recovery thunk: when guarded_copy's __try/__except is unavailable (or
- * for natural faults raised inside a guarded region) we hand control back
- * to the longjmp target by rewriting the resume context. */
+/* Recovery thunk: for a fault inside a guarded_copy (g_guard_armed) or any
+ * other natural fault raised in a guarded region, hand control back to the
+ * longjmp target by rewriting the resume context. */
static LONG WINAPI dbg_veh(EXCEPTION_POINTERS* ep) {
DWORD code = ep->ExceptionRecord->ExceptionCode;
KitUnwindFrame frame;
@@ -1815,42 +1825,30 @@ static void dbg_flush_icache_win(void* user, void* runtime_addr, size_t n) {
FlushInstructionCache(GetCurrentProcess(), runtime_addr, n);
}
-/* --- guarded copy via SEH __try/__except --- */
+/* --- guarded copy via the vectored exception handler --- */
static KitStatus dbg_guarded_copy_win(void* user, void* dst, const void* src,
size_t n) {
(void)user;
- /* MinGW-w64 supports __try/__except natively for the SEH model on x86_64
- * and arm64 (GCC: -fseh-exceptions; clang: built-in). The VEH backstop
- * above catches it if the toolchain ever falls back to setjmp-based EH. */
+ /* Catch an access fault during the copy without SEH __try/__except: the
+ * vectored handler dbg_veh (installed by dbg_signals_install_win) sees the
+ * thread-local g_guard_armed and longjmps back through g_guard_buf on a
+ * fault. This is one path for every compiler — kit's own frontend, gcc, and
+ * clang alike — so it needs no -fseh-exceptions / -fms-extensions and no
+ * __try language extension. */
if (setjmp(g_guard_buf) != 0) {
g_guard_armed = 0;
return KIT_ERR;
}
g_guard_armed = 1;
-#if defined(__kit__)
- /* kit's C frontend implements no SEH (__try/__except). Rely on the VEH
- * backstop (dbg_veh) installed by dbg_signals_install_win: it sees the
- * thread-local g_guard_armed and longjmps back through g_guard_buf on an
- * access fault, which is the exact fallback the comment above anticipates. */
memcpy(dst, src, n);
g_guard_armed = 0;
return KIT_OK;
-#else
- __try {
- memcpy(dst, src, n);
- g_guard_armed = 0;
- return KIT_OK;
- } __except (EXCEPTION_EXECUTE_HANDLER) {
- g_guard_armed = 0;
- return KIT_ERR;
- }
-#endif
}
/* --- call_with_catch / thread_abort (longjmp-based) --- */
-static __declspec(thread) jmp_buf g_dbg_abort_buf;
+static __thread jmp_buf g_dbg_abort_buf;
static int dbg_call_with_catch_win(void* user, void (*fn)(void*), void* arg) {
(void)user;
diff --git a/driver/lib/hosted.c b/driver/lib/hosted.c
@@ -558,11 +558,11 @@ static int hosted_resolve_windows_mingw(const DriverHostedRequest* req,
static const char* hosted_linux_inc_triple_sub(KitArchKind arch) {
switch (arch) {
case KIT_ARCH_ARM_64:
- return "include/aarch64-linux-gnu";
+ return "usr/include/aarch64-linux-gnu";
case KIT_ARCH_X86_64:
- return "include/x86_64-linux-gnu";
+ return "usr/include/x86_64-linux-gnu";
case KIT_ARCH_RV64:
- return "include/riscv64-linux-gnu";
+ return "usr/include/riscv64-linux-gnu";
default:
return NULL;
}
@@ -587,11 +587,19 @@ static int hosted_sysroot_layout_darwin(DriverHostedDirs* dirs,
static int hosted_sysroot_layout_linux(DriverHostedDirs* dirs,
KitTargetSpec target,
const char* sysroot) {
+ /* Standard FHS sysroot (matching darwin/freebsd/windows): libc + kernel
+ * headers under usr/include (+ the per-arch multiarch subdir that glibc uses
+ * for its bits headers), libraries + crt + the gcc runtime under usr/lib, and
+ * lib/lib64 for the dynamic loader. The same tree is consumed unchanged by a
+ * standard `clang --sysroot` cross-link — see the libc Containerfiles. */
const char* triple = hosted_linux_inc_triple_sub(target.arch);
- if (driver_hosted_dirs_add_inc_join(dirs, sysroot, "include") != 0) return 1;
+ if (driver_hosted_dirs_add_inc_join(dirs, sysroot, "usr/include") != 0)
+ return 1;
if (triple && driver_hosted_dirs_add_inc_join(dirs, sysroot, triple) != 0)
return 1;
+ if (driver_hosted_dirs_add_lib_join(dirs, sysroot, "usr/lib") != 0) return 1;
if (driver_hosted_dirs_add_lib_join(dirs, sysroot, "lib") != 0) return 1;
+ if (driver_hosted_dirs_add_lib_join(dirs, sysroot, "lib64") != 0) return 1;
return 0;
}
diff --git a/mk/bootstrap.mk b/mk/bootstrap.mk
@@ -7,9 +7,7 @@
# the normal Makefile using kit's busybox-style cc/ar/ld symlinks. Stages 2
# and 3 must be bitwise identical.
-.PHONY: bootstrap bootstrap-debug bootstrap-release _do-bootstrap \
- bootstrap-linux bootstrap-linux-musl bootstrap-linux-glibc \
- bootstrap-freebsd
+.PHONY: bootstrap bootstrap-debug bootstrap-release _do-bootstrap
BOOTSTRAP_DIR = $(BUILD_DIR)/bootstrap
BOOTSTRAP_STAGE1_DIR = $(BOOTSTRAP_DIR)/stage1
@@ -61,23 +59,11 @@ bootstrap-release:
_do-bootstrap: $(BOOTSTRAP_STAMP)
-# Drive the aarch64-linux self-build from a non-Linux dev host: `make bootstrap`
-# run natively inside a Linux container (scripts/linux_bootstrap.sh). On a
-# native Linux host, just run `make bootstrap` directly -- it already selects
-# the aarch64-linux toolchain off the host uname.
-bootstrap-linux: bootstrap-linux-musl
-
-bootstrap-linux-musl:
- scripts/linux_bootstrap.sh musl both
-
-bootstrap-linux-glibc:
- scripts/linux_bootstrap.sh glibc both
-
-# Drive the aarch64-freebsd self-build via the running FreeBSD aarch64 VM
-# (scripts/freebsd_vm.sh must be able to SSH to port 2223). The VM must be
-# running; scripts/freebsd_bootstrap.sh starts it if needed via KIT_TOY_VM_KEEP_UP.
-bootstrap-freebsd:
- scripts/freebsd_bootstrap.sh aarch64
+# Cross-OS self-host (kit built to run on linux/freebsd/windows, then compiling +
+# running a program there) is now driven by `make test-selfhost TARGET=…`
+# (engine: scripts/selfhost.sh), which calls scripts/{linux,freebsd}_bootstrap.sh
+# and scripts/windows_cross.sh per target. See doc/plan/PORT.md. `make bootstrap`
+# here remains the native (host-arch) 3-stage byte-identical self-reproduction.
$(BOOTSTRAP_STAMP): $(BIN) $(BOOTSTRAP_RT_LIBS) $(BOOTSTRAP_MAKEFILES)
rm -rf $(BOOTSTRAP_DIR)
diff --git a/mk/port.mk b/mk/port.mk
@@ -0,0 +1,59 @@
+# mk/port.mk — portability test surface (doc/plan/PORT.md).
+#
+# Two modes over the canonical support set defined in scripts/hosted.sh:
+# test-cross can kit cross-compile a correct executable for a target?
+# test-selfhost can kit be built to run on a target, then compile + run there?
+# test-port both
+# kit-cross build a runnable kit binary for a target (clang or kit)
+# provision the network + VM setup (separate from running)
+#
+# Parametrized uniformly:
+# TARGET support-set selector (default all): all | <os> | linux-<libc> |
+# <token> | a,b,c (see scripts/hosted.sh expand)
+# DEPTH smoke (default) | full
+# KIT_VM 1 (default; include freebsd/windows) | 0 (drop them)
+# CROSS_CC kit (default) | clang — kit-cross toolchain backend
+# VERIFY 0 (default) | 1 — kit-cross: run the built kit on-target
+#
+# Examples:
+# make test-cross # smoke across the whole support set
+# make test-cross TARGET=linux-glibc-x64 DEPTH=full
+# make test-cross TARGET=freestanding # all 4 bare-metal arches
+# make test-selfhost TARGET=freebsd-aa64
+# make kit-cross TARGET=windows-x64 # build kit.exe for windows-x64 (kit)
+# make kit-cross TARGET=linux CROSS_CC=clang VERIFY=1
+# make provision TARGET=linux # just the linux sysroots + images
+
+TARGET ?= all
+DEPTH ?= smoke
+KIT_VM ?= 1
+CROSS_CC ?= kit
+VERIFY ?= 0
+
+.PHONY: test-port test-cross test-selfhost kit-cross provision
+
+test-port: test-cross test-selfhost
+
+test-cross: bin
+ @KIT='$(abspath $(BIN))' KIT_VM='$(KIT_VM)' \
+ bash scripts/cross_test.sh '$(TARGET)' '$(DEPTH)'
+
+test-selfhost: bin
+ @KIT='$(abspath $(BIN))' KIT_VM='$(KIT_VM)' \
+ bash scripts/selfhost.sh '$(TARGET)' '$(DEPTH)'
+
+# kit-cross: cross-build a runnable kit for each in-scope target (clang or kit
+# backend), excluding freestanding (kit needs an OS to run on — same set as
+# selfhost). VERIFY=1 additionally runs the built kit on-target via the exec
+# seam. The per-target work lives in scripts/kit_cross.sh.
+kit-cross: bin
+ @set -e; vflag=''; [ '$(VERIFY)' = 1 ] && vflag='--verify'; \
+ for t in $$(bash scripts/hosted.sh expand '$(TARGET)' --mode=selfhost); do \
+ KIT='$(abspath $(BIN))' bash scripts/kit_cross.sh "$$t" --cc='$(CROSS_CC)' $$vflag; \
+ done
+
+# provision: sysroots / run-images / VMs for the in-scope targets. Idempotent;
+# this is the only step that touches the network. The provisioner calls back into
+# `make` for the image/sysroot targets it needs (test-images, etc.).
+provision:
+ @KIT_VM='$(KIT_VM)' MAKE='$(MAKE)' bash scripts/provision.sh '$(TARGET)'
diff --git a/mk/test.mk b/mk/test.mk
@@ -182,9 +182,7 @@ DEFAULT_TEST_TARGETS = \
bootstrap \
test-bootstrap-toy
-.PHONY: test $(TEST_TARGETS) windows-ucrt-sysroots \
- test-toy-vm test-toy-freebsd-vm test-toy-windows-vm \
- test-hosted test-hosted-vm hosted-glibc-images
+.PHONY: test $(TEST_TARGETS) windows-ucrt-sysroots hosted-glibc-images
test: $(DEFAULT_TEST_TARGETS)
@@ -506,20 +504,10 @@ test-native-direct-target: $(NATIVE_DIRECT_TARGET_TEST_BIN)
test-toy: bin
@KIT=$(abspath $(BIN)) test/toy/run.sh
-# Opt-in: run the Toy corpus as real hosted programs inside the FreeBSD and
-# Windows VMs (test/toy/vm.sh), asserting each case's .expected exit code on the
-# genuine OS — the hosted counterpart to run.sh's freestanding-Linux X lane.
-# Not in the default set (needs provisioned VMs + cross sysroots; amd64/riscv64
-# FreeBSD run under slow TCG). The FreeBSD script lazily builds its base.txz
-# sysroots and self-skips arches whose VM/sysroot is absent; the Windows target
-# carries the same rt + UCRT-sysroot deps as test-coff-windows-vm.
-test-toy-freebsd-vm: bin
- @KIT=$(abspath $(BIN)) bash test/toy/vm.sh freebsd
-
-test-toy-windows-vm: bin rt-x86_64-pc-windows rt-aarch64-windows windows-ucrt-sysroots
- @KIT=$(abspath $(BIN)) bash test/toy/vm.sh windows
-
-test-toy-vm: test-toy-freebsd-vm test-toy-windows-vm
+# The Toy corpus on the FreeBSD/Windows VMs (test/toy/vm.sh) is now driven by
+# make test-cross TARGET=freebsd|windows DEPTH=full
+# which calls test/toy/vm.sh through the harmonized portability surface. See
+# doc/plan/PORT.md.
# test-bootstrap-toy: run the Toy corpus through the bootstrapped (self-built)
# stage3 kit instead of the host-built binary, so the self-hosted compiler is
@@ -920,7 +908,7 @@ test-asm-rv32: lib $(ASM_RUNNER)
# test-toy-rv32: the rv32 cross lane (path X) of the Toy corpus, scoped to the
# rv32 arch only. test/toy/run.sh's cross_one_rv32 compiles each case with
# `kit cc -target riscv32-none-elf` and runs the freestanding ELF bare-metal
-# under qemu-system-riscv32 via test/lib/exec_rv32_bare.sh (the qemu exit code
+# under qemu-system-riscv32 via test/lib/exec_bare.sh (the qemu exit code
# is the exit-code oracle). Self-skips per case when the rv32 toolchain
# (clang riscv32 + qemu-system-riscv32) is absent. The corpus is green; this
# lane is opt-in (not in DEFAULT_TEST_TARGETS) because it needs that toolchain,
@@ -933,7 +921,7 @@ test-toy-rv32: bin rt-riscv32-elf-hardfloat
# test-parse-rv32: the C-parser corpus run for riscv32-none-elf, exec lane (E)
# only. parse-runner --emit -> kit ld + start crt -> qemu-system-riscv32
-# (test/parse/run.sh's rv32 freestanding E path via exec_rv32_bare.sh). Models
+# (test/parse/run.sh's rv32 freestanding E path via exec_bare.sh). Models
# test-parse-rv64-wide; opt-in (needs the rv32 toolchain/qemu), so excluded
# from DEFAULT_TEST_TARGETS, matching test-parse-rv64-wide. The corpus is green;
# the only skips are intentionally-unsupported cases (__int128, binary128 long
@@ -1051,13 +1039,11 @@ test-smoke-rv64:
test-smoke-rv32:
bash test/smoke/rv32.sh
-# test-smoke-freestanding-system: whole-toolchain bare-metal smoke for
-# aarch64/x86_64/riscv64/riscv32 under qemu-system. Each lane compiles with kit,
-# assembles its reset stub with kit, links with kit ld -T, then boots the image
-# under the matching qemu-system binary. rv32 may need compiler-rt-style helpers
-# for 64-bit operations, so build that freestanding runtime variant with kit too.
-test-smoke-freestanding-system: bin rt-riscv32-elf-hardfloat
- bash test/smoke/freestanding_system.sh
+# The whole-toolchain bare-metal smoke (compile + assemble reset stub + link +
+# boot under qemu-system, for aarch64/x86_64/riscv64/riscv32) is now driven by
+# make test-cross TARGET=freestanding
+# whose exec_bare backend (test/lib/exec_bare.sh) owns the per-arch reset stubs,
+# linker scripts, and exit oracles. See doc/plan/PORT.md.
# test-parse-rv64-wide: end-to-end coverage of the rv64 128-bit scalar types
# — __int128 (i128_*) and IEEE-754 binary128 long double (ldbl128_*) — built
@@ -1175,20 +1161,10 @@ test-libc-musl-rv64:
test-libc-glibc-rv64:
@$(MAKE) test-libc-glibc KIT_LIBC_ARCHES=rv64
-# Hosted test suite (test/hosted/run.sh): build each C case for every
-# (target, link-mode) config in the support set via scripts/hosted.sh and run it
-# through the shared exec seam, checking exit code + stdout. Opt-in (not in the
-# default set). The default config set is Linux (musl static+dynamic + glibc,
-# all 3 arches) + macOS; it provisions the linux sysroots + rt + the per-arch
-# run images (alpine for musl, debian for glibc) here. test-hosted-vm adds the
-# FreeBSD + Windows VM configs.
-HOSTED_LINUX_DEPS = \
- $(MUSL_SYSROOT_MARKER) $(MUSL_SYSROOT_X64_MARKER) $(MUSL_SYSROOT_RV64_MARKER) \
- $(GLIBC_SYSROOT_MARKER) $(GLIBC_SYSROOT_X64_MARKER) $(GLIBC_SYSROOT_RV64_MARKER) \
- rt-aarch64-linux rt-x86_64-linux rt-riscv64-linux
-
-# Per-arch glibc (Debian) run images for the linux-glibc configs. musl uses the
-# pinned alpine images from `make test-images`. Best-effort pull (idempotent).
+# The hosted cross-compile-and-run suite is now `make test-cross` (engine:
+# scripts/cross_test.sh, source of truth: scripts/hosted.sh). See doc/plan/PORT.md.
+# hosted-glibc-images is kept: scripts/provision.sh pulls the per-arch Debian run
+# images for the linux-glibc run lanes through it (alpine/musl uses test-images).
HOSTED_GLIBC_IMAGES = \
docker.io/arm64v8/debian:bookworm-slim \
docker.io/amd64/debian:bookworm-slim \
@@ -1200,12 +1176,6 @@ hosted-glibc-images:
echo "warn: could not pull $$img (linux-glibc run lanes will SKIP)"; \
done
-test-hosted: bin $(HOSTED_LINUX_DEPS) test-images hosted-glibc-images
- @KIT=$(abspath $(BIN)) bash test/hosted/run.sh
-
-test-hosted-vm: bin $(HOSTED_LINUX_DEPS) test-images hosted-glibc-images rt-x86_64-pc-windows rt-aarch64-windows windows-ucrt-sysroots
- @KIT=$(abspath $(BIN)) KIT_HOSTED_VM=1 bash test/hosted/run.sh
-
# Fail if libkit.a depends on any external symbol not in the allowlist, or
# if a relocatable link exposes non-public global definitions.
# External dependency drift in either direction (new dep, or stale entry) is a
diff --git a/scripts/cross_test.sh b/scripts/cross_test.sh
@@ -0,0 +1,217 @@
+#!/usr/bin/env bash
+# scripts/cross_test.sh — cross-compilation correctness orchestrator: the engine
+# behind `make test-cross`. For each target in the (expanded) support set it
+# cross-compiles with the host kit and runs the artifact on that target through
+# the shared exec seam, checking the result.
+#
+# cross_test.sh <selector> [DEPTH]
+#
+# selector hosted.sh grammar: all | <os> | linux-<libc> | <token> | a,b,c
+# DEPTH smoke (default) — one hello program, build + run, exit code (+ stdout)
+# full — also the toy + parse corpora (+ libc on linux),
+# cross-compiled and run on the target
+#
+# env: KIT (default build/kit), KIT_VM (default 1; 0 drops freebsd/windows),
+# DEPTH (overridden by the positional arg).
+#
+# Provision-or-error: a requested target whose sysroot/image/VM is missing is a
+# hard error (all missing reported up front), never a silent skip. Provision with
+# `make provision TARGET=…` (see doc/plan/PORT.md).
+
+set -u
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+KIT="${KIT:-$ROOT/build/kit}"
+HOSTED="$ROOT/scripts/hosted.sh"
+CASES="$ROOT/test/cross/cases"
+BUILD_DIR="$ROOT/build/test/cross"
+SELECTOR="${1:-${TARGET:-all}}"
+DEPTH="${2:-${DEPTH:-smoke}}"
+export KIT_VM="${KIT_VM:-1}"
+
+[ -x "$KIT" ] || { echo "cross_test: kit not found at $KIT (run 'make bin')" >&2; exit 2; }
+mkdir -p "$BUILD_DIR"
+
+# shellcheck source=../test/lib/kit_sh_report.sh
+. "$ROOT/test/lib/kit_sh_report.sh"
+
+# exec_target caller contract (linux/macos runners read these; VM/bare ignore).
+have_podman=0; command -v podman >/dev/null 2>&1 && have_podman=1
+QEMU_BIN="${QEMU_BIN:-$(command -v qemu-aarch64 2>/dev/null || true)}"
+QEMU_RV64_BIN="${QEMU_RV64_BIN:-$(command -v qemu-riscv64 2>/dev/null || true)}"
+have_qemu=0; [ -n "$QEMU_BIN" ] && have_qemu=1
+case "$(uname -m 2>/dev/null)" in aarch64|arm64) is_aarch64=1 ;; *) is_aarch64=0 ;; esac
+export have_podman QEMU_BIN QEMU_RV64_BIN have_qemu is_aarch64
+EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"; export EXEC_TARGET_MOUNT_ROOT
+# shellcheck source=../test/lib/exec_target.sh
+. "$ROOT/test/lib/exec_target.sh"
+
+kit_report_init
+trap exec_target_teardown_all EXIT
+
+# ---- token helpers ---------------------------------------------------------
+tok_os() { printf '%s' "${1%%-*}"; }
+tok_arch() { printf '%s' "${1##*-}"; }
+tok_libc() { case "$1" in linux-*) local r="${1#linux-}"; printf '%s' "${r%%-*}" ;; *) printf '' ;; esac; }
+
+# Link modes to smoke per target. musl exercises BOTH static (libc.a) and
+# dynamic (libc.so) — the two ways the sysroot is consumed. freebsd is static
+# (self-contained base). Everything else uses the toolchain default (one mode;
+# glibc is dynamic-only — static glibc is discouraged/NSS-fragile).
+link_modes() {
+ case "$1" in
+ linux-musl-*) echo "static dynamic" ;;
+ freebsd-*) echo "static" ;;
+ *) echo "default" ;;
+ esac
+}
+# static -> libc.a; dynamic -> kit's default link for a libc sysroot is already
+# dynamic (.interp + libc.so), and `-dynamic` is not a kit flag, so dynamic and
+# the single-mode default both pass no link flag.
+mode_flag() { case "$1" in static) echo -static ;; *) echo ;; esac; }
+
+# ---- provisioning pre-flight ----------------------------------------------
+# Echo a one-line missing-reason for a token, or nothing if ready to build+run.
+preflight_token() {
+ local t="$1" os tag sr
+ os="$(tok_os "$t")"; tag="$("$HOSTED" tag "$t")"; sr="$("$HOSTED" path "$t" 2>/dev/null)"
+ case "$os" in
+ linux|freebsd|windows)
+ if [ -z "$sr" ] || [ ! -d "$sr" ]; then
+ echo "sysroot missing — run 'make provision TARGET=$t'"; return
+ fi ;;
+ esac
+ if ! exec_target_supported "$tag"; then
+ case "$os" in
+ freestanding) echo "qemu-system missing for $tag — install qemu-system-$(printf '%s' "$($HOSTED triple "$t")" | sed 's/-none-elf//')" ;;
+ freebsd|windows) echo "VM not provisioned/reachable — run 'make provision TARGET=$t KIT_VM=1'" ;;
+ macos) echo "macOS exec needs a Darwin host with matching arch" ;;
+ linux) echo "no linux runner — run 'make provision TARGET=$t' (sysroot) + 'make test-images'/'make hosted-glibc-images' (run image)" ;;
+ *) echo "no runner for $tag" ;;
+ esac
+ fi
+}
+
+# ---- smoke lane ------------------------------------------------------------
+# Per target: the `exit` case (toolchain + link + crt + exit path; the only case
+# freestanding can run) then `hello` (sysroot headers + libc + stdout, hosted
+# only) — each run once per link mode (link_modes): musl gets static + dynamic.
+smoke_token() {
+ local t="$1" os name m
+ os="$(tok_os "$t")"
+ if [ "$os" = freestanding ]; then
+ smoke_case "$t" exit "" # bare-metal: no link mode
+ return
+ fi
+ for name in exit hello; do
+ for m in $(link_modes "$t"); do
+ smoke_case "$t" "$name" "$m"
+ done
+ done
+}
+
+# smoke_case TOKEN CASENAME MODE — build + run one case in one link mode, check
+# exit code (+ stdout if the case ships a <name>.stdout sidecar). MODE is
+# static/dynamic (suffixes the lane) or default/"" (unsuffixed).
+smoke_case() {
+ local t="$1" name="$2" mode="$3" os tag cdir label sfx src exp want got rc
+ os="$(tok_os "$t")"; tag="$("$HOSTED" tag "$t")"
+ cdir="$BUILD_DIR/$t"; mkdir -p "$cdir"
+ src="$CASES/$name.c"
+ case "$mode" in static|dynamic) sfx=".$mode" ;; *) sfx="" ;; esac
+ label="$name/$t${sfx}"
+ exp=0; [ -f "$CASES/$name.expected" ] && exp="$(cat "$CASES/$name.expected")"
+ want=""; [ -f "$CASES/$name.stdout" ] && want="$(cat "$CASES/$name.stdout")"
+
+ if [ "$os" = freestanding ]; then
+ # Bare-metal: compile to an object; exec_bare links it with the reset stub
+ # and the exit value is read back through the per-arch oracle (no stdout).
+ local obj="$cdir/$name.o"
+ if ! "$HOSTED" cc "$t" -O1 -ffreestanding -c "$src" -o "$obj" \
+ >"$cdir/$name.cc.out" 2>"$cdir/$name.cc.err"; then
+ kit_fail "$label" "cc failed"; sed 's/^/ | /' "$cdir/$name.cc.err" | head; return
+ fi
+ exec_target_run "$tag" "$obj" "$cdir/$name.out" "$cdir/$name.err"
+ else
+ local ext=""; [ "$os" = windows ] && ext=.exe
+ local exe="$cdir/$name$sfx$ext"
+ # shellcheck disable=SC2046
+ if ! "$HOSTED" cc "$t" $(mode_flag "$mode") "$src" -o "$exe" \
+ >"$cdir/$name$sfx.cc.out" 2>"$cdir/$name$sfx.cc.err"; then
+ kit_fail "$label" "cc failed"; sed 's/^/ | /' "$cdir/$name$sfx.cc.err" | head; return
+ fi
+ exec_target_run "$tag" "$exe" "$cdir/$name$sfx.out" "$cdir/$name$sfx.err"
+ fi
+
+ rc="${RUN_RC:-127}"; got="$(cat "$cdir/$name$sfx.out" 2>/dev/null)"
+ if [ "$((rc & 255))" -ne "$((exp & 255))" ]; then kit_fail "$label" "expected rc $exp, got $rc"
+ elif [ -n "$want" ] && [ "$got" != "$want" ]; then kit_fail "$label" "stdout mismatch (want '$want' got '$got')"
+ else kit_pass "$label"; fi
+}
+
+# ---- full lanes ------------------------------------------------------------
+# Run an existing corpus runner as a sub-process; its exit status is one verdict.
+_full_lane() {
+ local label="$1"; shift
+ printf '\ncross: full lane %s →\n' "$label"
+ if "$@"; then kit_pass "$label"; else kit_fail "$label" "lane failed (see output above)"; fi
+}
+
+full_token() {
+ local t="$1" os arch libc
+ os="$(tok_os "$t")"; arch="$(tok_arch "$t")"; libc="$(tok_libc "$t")"
+ case "$os" in
+ linux)
+ _full_lane "$t:toy" env KIT="$KIT" KIT_TOY_CROSS_ARCHS="$arch" KIT_TEST_PATHS=X bash "$ROOT/test/toy/run.sh"
+ _full_lane "$t:parse" env KIT="$KIT" KIT_TEST_ARCH="$arch" KIT_TEST_PATHS=E bash "$ROOT/test/parse/run.sh"
+ _full_lane "$t:libc" env KIT="$KIT" KIT_LIBC_ARCHES="$arch" bash "$ROOT/test/libc/$libc/run.sh"
+ ;;
+ macos)
+ _full_lane "$t:toy" env KIT="$KIT" bash "$ROOT/test/toy/run.sh"
+ _full_lane "$t:parse" env KIT="$KIT" bash "$ROOT/test/parse/run.sh"
+ ;;
+ freebsd)
+ _full_lane "$t:toy-vm" env KIT="$KIT" bash "$ROOT/test/toy/vm.sh" freebsd
+ ;;
+ windows)
+ _full_lane "$t:toy-vm" env KIT="$KIT" bash "$ROOT/test/toy/vm.sh" windows
+ ;;
+ freestanding)
+ case "$arch" in
+ rv32)
+ _full_lane "$t:toy" env KIT="$KIT" KIT_TOY_CROSS_ARCHS=rv32 KIT_TEST_PATHS=X bash "$ROOT/test/toy/run.sh"
+ _full_lane "$t:parse" env KIT="$KIT" KIT_TEST_ARCH=rv32 KIT_TEST_PATHS=E bash "$ROOT/test/parse/run.sh" ;;
+ *)
+ printf 'cross: NOTE %s — full-corpus bare-metal not yet wired for %s (smoke only); see doc/plan/PORT.md\n' "$t" "$arch" ;;
+ esac
+ ;;
+ esac
+}
+
+# ---- drive -----------------------------------------------------------------
+TOKENS="$("$HOSTED" expand "$SELECTOR" --mode=cross)" || exit 1
+[ -n "$TOKENS" ] || { echo "cross_test: selector '$SELECTOR' expanded to nothing" >&2; exit 2; }
+
+printf 'cross: DEPTH=%s KIT_VM=%s\ncross: targets=%s\n' \
+ "$DEPTH" "$KIT_VM" "$(echo $TOKENS | tr '\n' ' ')"
+
+missing=0
+for t in $TOKENS; do
+ reason="$(preflight_token "$t")"
+ if [ -n "$reason" ]; then
+ printf 'cross: NOT PROVISIONED %-22s %s\n' "$t" "$reason"; missing=1
+ fi
+done
+if [ "$missing" -eq 1 ]; then
+ echo "cross: aborting — provision the targets above, or narrow TARGET / set KIT_VM=0." >&2
+ exit 1
+fi
+
+for t in $TOKENS; do
+ smoke_token "$t"
+ [ "$DEPTH" = full ] && full_token "$t"
+done
+
+KIT_SKIP_IS_FAILURE=0
+kit_summary test-cross
+kit_exit
diff --git a/scripts/freebsd_bootstrap.sh b/scripts/freebsd_bootstrap.sh
@@ -26,8 +26,8 @@ ARCH="${1:-aarch64}"
CHAIN="${2:-both}"
case "$ARCH" in
- aarch64|arm64) ARCH=aarch64 ;;
- amd64|x64) ARCH=amd64 ;;
+ aarch64|arm64|aa64) ARCH=aarch64 ;;
+ amd64|x64|x86_64) ARCH=amd64 ;;
*) echo "freebsd_bootstrap: unsupported arch '$ARCH'" >&2; exit 2 ;;
esac
diff --git a/scripts/hosted.sh b/scripts/hosted.sh
@@ -7,19 +7,31 @@
# Linux -> test/libc/{glibc,musl}/extract.sh (podman container extract)
# macOS -> host SDK (native; no cross sysroot)
#
-# Targets: <os>[-<libc>]-<arch>
+# Targets: <os>[-<libc>]-<arch> (canonical arch tokens: aa64/x64/rv64[/rv32])
# linux-glibc-{aa64,x64,rv64} linux-musl-{aa64,x64,rv64}
-# freebsd-{amd64,aarch64,riscv64} windows-{x64,aarch64} macos-aarch64
+# freebsd-{aa64,x64,rv64} windows-{aa64,x64} macos-aa64
+# freestanding-{aa64,x64,rv64,rv32} (no OS — cross only, bare-metal exec)
+# Long arch spellings (aarch64/amd64/x86_64/riscv64) are accepted as input
+# aliases; canon_token normalizes them to the short forms above.
#
# Commands:
# doctor support set + what is provisioned here
+# list [selector] print expanded canonical tokens, one per line
+# expand <selector> [--mode=cross|selfhost]
+# like list, but mode-filtered (selfhost drops
+# freestanding) and KIT_VM-filtered
+# modes <target> print which modes apply ("cross" / "cross selfhost")
# prepare <target>|all provision the sysroot for a target
-# path <target> print the sysroot dir ("" for macOS native)
+# path <target> print the sysroot dir ("" for macOS/freestanding)
# triple <target> print the kit -target triple
# tag <target> print the test/lib/exec_target.sh exec tag
# cc <target> [kit-cc-args] compile+link: kit cc -target T --sysroot S [...]
#
-# env: KIT (default build/kit), plus the per-OS provisioners' own env.
+# Selector grammar (list/expand): all | <os> | linux-<libc> | <full-token> |
+# comma-list of any of those. <os> is linux/freebsd/windows/macos/freestanding.
+#
+# env: KIT (default build/kit), KIT_VM (default 1; 0 drops freebsd/windows from
+# expand), plus the per-OS provisioners' own env.
set -u
@@ -29,9 +41,10 @@ KIT="${KIT:-$ROOT/build/kit}"
SUPPORT_SET="
linux-glibc-aa64 linux-glibc-x64 linux-glibc-rv64
linux-musl-aa64 linux-musl-x64 linux-musl-rv64
-freebsd-amd64 freebsd-aarch64 freebsd-riscv64
-windows-x64 windows-aarch64
-macos-aarch64
+freebsd-aa64 freebsd-x64 freebsd-rv64
+windows-aa64 windows-x64
+macos-aa64
+freestanding-aa64 freestanding-x64 freestanding-rv64 freestanding-rv32
"
die() { printf 'hosted: %s\n' "$*" >&2; exit 1; }
@@ -70,6 +83,14 @@ parse_target() {
aarch64|arm64|aa64) T_ARCH=aarch64 ;;
*) die "bad macos arch '$rest' in '$t' (arm64 only)" ;;
esac ;;
+ freestanding)
+ case "$rest" in
+ aarch64|arm64|aa64) T_ARCH=aarch64 ;;
+ x64|x86_64|amd64) T_ARCH=x64 ;;
+ rv64|riscv64) T_ARCH=rv64 ;;
+ rv32|riscv32) T_ARCH=rv32 ;;
+ *) die "bad freestanding arch '$rest' in '$t'" ;;
+ esac ;;
*) die "unknown os in target '$t'" ;;
esac
}
@@ -96,6 +117,13 @@ triple_of() {
aarch64) echo aarch64-windows ;;
esac ;;
macos) echo aarch64-apple-darwin ;;
+ freestanding)
+ case "$T_ARCH" in
+ aarch64) echo aarch64-none-elf ;;
+ x64) echo x86_64-none-elf ;;
+ rv64) echo riscv64-none-elf ;;
+ rv32) echo riscv32-none-elf ;;
+ esac ;;
esac
}
@@ -110,9 +138,31 @@ tag_of() {
macos) echo "$ea-macos" ;;
freebsd) echo "$T_ARCH-freebsd" ;;
windows) echo "$T_ARCH-windows" ;;
+ freestanding) echo "$ea-freestanding" ;;
+ esac
+}
+
+# Canonical token (<os>[-<libc>]-<short-arch>) for any input/alias spelling.
+canon_token() {
+ parse_target "$1"
+ local ea
+ case "$T_ARCH" in
+ aarch64) ea=aa64 ;; x64|amd64) ea=x64 ;; rv64|riscv64) ea=rv64 ;;
+ rv32) ea=rv32 ;; *) ea="$T_ARCH" ;;
+ esac
+ case "$T_OS" in
+ linux) printf 'linux-%s-%s' "$T_LIBC" "$ea" ;;
+ *) printf '%s-%s' "$T_OS" "$ea" ;;
esac
}
+# Which modes apply to a target. Freestanding has no hosted environment, so a
+# compiler cannot run on it: cross only.
+modes_of() {
+ parse_target "$1"
+ case "$T_OS" in freestanding) echo cross ;; *) echo "cross selfhost" ;; esac
+}
+
_linux_sysroot_dir() { # libc arch
local libc="$1" arch="$2" suf=""
case "$arch" in x64) suf=-x64 ;; rv64) suf=-rv64 ;; esac
@@ -126,6 +176,7 @@ path_of() {
freebsd) "$ROOT/scripts/freebsd_sysroot.sh" path "$T_ARCH" 2>/dev/null ;;
windows) "$ROOT/scripts/llvm_mingw_sysroot.sh" path "$T_ARCH" 2>/dev/null ;;
macos) echo "" ;;
+ freestanding) echo "" ;;
esac
}
@@ -139,6 +190,9 @@ prepare_one() {
freebsd) "$ROOT/scripts/freebsd_sysroot.sh" "$T_ARCH" ;;
windows) "$ROOT/scripts/llvm_mingw_sysroot.sh" prepare "$T_ARCH" ;;
macos) printf 'macos: native host SDK, nothing to prepare\n' ;;
+ freestanding)
+ printf 'freestanding: no sysroot to provision; run-time prereq is qemu-system-%s\n' \
+ "$(triple_of "$1" | sed 's/-none-elf//')" ;;
esac
}
@@ -172,25 +226,78 @@ doctor() {
local t sr ok
for t in $SUPPORT_SET; do
sr="$(path_of "$t")"
- if [ "$(printf '%s' "$t" | cut -d- -f1)" = macos ]; then ok='native'
- elif [ -n "$sr" ] && [ -d "$sr" ]; then ok='yes'
- else ok='no'; fi
- printf ' %-20s triple=%-22s tag=%-12s sysroot=%s\n' \
+ case "$t" in
+ macos-*) ok='native' ;;
+ freestanding-*) ok='bare (qemu-system)' ;;
+ *) if [ -n "$sr" ] && [ -d "$sr" ]; then ok='yes'; else ok='no'; fi ;;
+ esac
+ printf ' %-22s triple=%-22s tag=%-16s sysroot=%s\n' \
"$t" "$(triple_of "$t")" "$(tag_of "$t")" "$ok"
done
}
+# ---- selector expansion ----------------------------------------------------
+# _match_one SELECTOR-ITEM -> space-separated canonical tokens (no filters).
+_match_one() {
+ local it="$1" t
+ case "$it" in
+ linux|freebsd|windows|macos|freestanding)
+ for t in $SUPPORT_SET; do case "$t" in "$it"-*) printf '%s ' "$t" ;; esac; done
+ return ;;
+ linux-musl|linux-glibc)
+ for t in $SUPPORT_SET; do case "$t" in "$it"-*) printf '%s ' "$t" ;; esac; done
+ return ;;
+ esac
+ local c x; c="$(canon_token "$it")" || die "bad target '$it'"
+ for x in $SUPPORT_SET; do
+ [ "$x" = "$c" ] && { printf '%s ' "$c"; return; }
+ done
+ die "unknown target '$it' (not in support set; see '$0 list')"
+}
+
+# _expand_raw SELECTOR -> deduped canonical tokens (commas -> spaces; no
+# filters). Returns non-zero (so callers can `|| die`) on an unknown token —
+# _match_one's die exits only its own subshell, so we check each item's status.
+_expand_raw() {
+ local sel item m raw="" t out=""
+ sel="$(printf '%s' "${1:-all}" | tr , ' ')"
+ for item in $sel; do
+ if [ "$item" = all ]; then raw="$raw $SUPPORT_SET"; continue; fi
+ m="$(_match_one "$item")" || return 1
+ raw="$raw $m"
+ done
+ for t in $raw; do case " $out " in *" $t "*) ;; *) out="$out $t" ;; esac; done
+ printf '%s' "${out# }"
+}
+
+# cmd_expand SELECTOR [--mode=cross|selfhost] -> filtered tokens, one per line.
+# selfhost mode drops freestanding; KIT_VM=0 drops freebsd/windows.
+cmd_expand() {
+ local sel="" mode="" a t toks
+ for a in "$@"; do case "$a" in --mode=*) mode="${a#--mode=}" ;; *) sel="$a" ;; esac; done
+ toks="$(_expand_raw "$sel")" || exit 1
+ for t in $toks; do
+ case "$t" in freestanding-*) [ "$mode" = selfhost ] && continue ;; esac
+ case "$t" in freebsd-*|windows-*) [ "${KIT_VM:-1}" = 0 ] && continue ;; esac
+ printf '%s\n' "$t"
+ done
+}
+
cmd="${1:-}"
case "$cmd" in
doctor) doctor ;;
+ list) shift; toks="$(_expand_raw "${1:-all}")" || exit 1; for t in $toks; do printf '%s\n' "$t"; done ;;
+ expand) shift; [ $# -ge 1 ] || die "usage: $0 expand <selector> [--mode=cross|selfhost]"; cmd_expand "$@" ;;
+ modes) [ $# -eq 2 ] || die "usage: $0 modes <target>"; modes_of "$2" ;;
prepare)
- [ $# -ge 2 ] || die "usage: $0 prepare <target>|all"
- if [ "$2" = all ]; then for t in $SUPPORT_SET; do prepare_one "$t"; done
- else prepare_one "$2"; fi ;;
+ [ $# -ge 2 ] || die "usage: $0 prepare <selector>|all"
+ shift
+ toks="$(_expand_raw "$1")" || exit 1
+ for t in $toks; do prepare_one "$t"; done ;;
path) [ $# -eq 2 ] || die "usage: $0 path <target>"; path_of "$2" ;;
triple) [ $# -eq 2 ] || die "usage: $0 triple <target>"; triple_of "$2" ;;
tag) [ $# -eq 2 ] || die "usage: $0 tag <target>"; tag_of "$2" ;;
cc) [ $# -ge 2 ] || die "usage: $0 cc <target> [kit-cc-args]"; shift; cc_target "$@" ;;
- -h|--help|help|"") sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//' ;;
- *) die "unknown command '$cmd' (try: doctor prepare path triple tag cc)" ;;
+ -h|--help|help|"") sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//' ;;
+ *) die "unknown command '$cmd' (try: doctor list expand modes prepare path triple tag cc)" ;;
esac
diff --git a/scripts/kit_cross.sh b/scripts/kit_cross.sh
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+# scripts/kit_cross.sh — build a runnable `kit` binary for a hosted support-set
+# target, using either clang (independent toolchain, clang + lld end to end) or
+# kit itself (dogfood) as the cross-compiler. The general form of the old
+# windows_cross.sh; see doc/plan/PORT.md.
+#
+# kit_cross.sh <target> [--cc=clang|kit] [--verify]
+#
+# target a hosted token: linux-{glibc,musl}-{aa64,x64,rv64} | freebsd-{aa64,
+# x64,rv64} | windows-{aa64,x64} | macos-aa64. (freestanding is excluded
+# — kit needs an OS to run on.)
+# --cc toolchain backend (default: kit).
+# --verify after building, run the cross-built kit on the target via the exec
+# seam (kit --version) to confirm it executes there.
+#
+# Output: build/kit-cross/<toolchain>/<target>/kit[.exe]
+#
+# env: KIT (default build/kit), KIT_CROSS_OPT (codegen opt, default -O0).
+
+set -eu
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+cd "$ROOT"
+HOSTED="$ROOT/scripts/hosted.sh"
+KIT="${KIT:-$ROOT/build/kit}"
+
+die() { printf 'kit_cross: %s\n' "$*" >&2; exit 1; }
+
+TARGET=""; TC="kit"; VERIFY=0
+for a in "$@"; do
+ case "$a" in
+ --cc=*) TC="${a#--cc=}" ;;
+ --verify) VERIFY=1 ;;
+ -*) die "unknown flag '$a'" ;;
+ *) TARGET="$a" ;;
+ esac
+done
+[ -n "$TARGET" ] || die "usage: kit_cross.sh <target> [--cc=clang|kit] [--verify]"
+case "$TC" in clang|kit) ;; *) die "bad --cc '$TC' (want clang|kit)" ;; esac
+
+# ---- resolve target --------------------------------------------------------
+os="${TARGET%%-*}"
+case "$os" in freestanding) die "$TARGET is freestanding — kit needs an OS to run on" ;; esac
+triple_kit="$("$HOSTED" triple "$TARGET")" || die "bad target '$TARGET'"
+sysroot="$("$HOSTED" path "$TARGET" 2>/dev/null || true)"
+# macos is the degenerate native case: hosted.sh returns no sysroot, so the
+# "sysroot" is the host SDK (both backends consume it: clang via -isysroot, kit
+# via --sysroot). kit's internal OS tag for macOS is `darwin`, not `macos`.
+hos="$os"
+if [ "$os" = macos ]; then
+ hos=darwin
+ [ -n "$sysroot" ] || sysroot="$(xcrun --show-sdk-path 2>/dev/null || true)"
+fi
+arch_tok="${TARGET##*-}"
+case "$arch_tok" in
+ aa64|aarch64) harch=aarch64; carch=aarch64 ;;
+ x64|x86_64|amd64) harch=x86_64; carch=x86_64 ;;
+ rv64|riscv64) harch=rv64; carch=riscv64 ;;
+ *) die "bad arch in '$TARGET'" ;;
+esac
+
+# clang target triple (differs from kit's for windows: mingw gnu).
+clang_triple() {
+ case "$os" in
+ linux) echo "$triple_kit" ;; # carch-linux-{gnu,musl}
+ freebsd) echo "${carch}-unknown-freebsd${KIT_FREEBSD_CLANG_VER:-14}" ;;
+ windows) echo "${carch}-w64-windows-gnu" ;;
+ macos) echo "" ;; # native
+ esac
+}
+
+BUILD_DIR="build/kit-cross/$TC/$TARGET"
+TCDIR="$ROOT/$BUILD_DIR/toolchain"
+mkdir -p "$TCDIR"
+
+# ---- toolchain wrappers ----------------------------------------------------
+if [ "$TC" = kit ]; then
+ [ -x "$KIT" ] || die "kit not found at $KIT (run 'make bin')"
+ cat > "$TCDIR/cc" <<EOF
+#!/bin/sh
+exec "$KIT" cc -target $triple_kit ${sysroot:+--sysroot "$sysroot"} "\$@"
+EOF
+ chmod +x "$TCDIR/cc"
+ for t in ar ranlib as ld; do ln -sf "$KIT" "$TCDIR/$t"; done
+ CC_W="$TCDIR/cc"; AR_W="$TCDIR/ar"
+else
+ command -v clang >/dev/null 2>&1 || die "clang not found"
+ ct="$(clang_triple)"
+ if [ "$os" = macos ]; then
+ sdk="$(xcrun --show-sdk-path 2>/dev/null || true)"
+ cat > "$TCDIR/cc" <<EOF
+#!/bin/sh
+exec clang ${sdk:+-isysroot "$sdk"} "\$@"
+EOF
+ else
+ command -v ld.lld >/dev/null 2>&1 || command -v lld >/dev/null 2>&1 || die "lld not found (needed for clang cross-link)"
+ [ -n "$sysroot" ] && [ -d "$sysroot" ] || die "sysroot missing for $TARGET — run 'make provision TARGET=$TARGET'"
+ extra=""
+ if [ "$os" = windows ]; then
+ # llvm-mingw's runtime is compiler-rt + libunwind (not libgcc), so select
+ # them explicitly and compose a resource dir = host clang's headers + the
+ # sysroot's staged compiler-rt builtins (lib/windows). These are toolchain
+ # runtime-selection flags, not source workarounds (the windows sources are
+ # plain-clang-clean).
+ [ -d "$sysroot/lib/windows" ] || \
+ die "windows sysroot lacks compiler-rt builtins (lib/windows) — re-run 'make windows-ucrt-sysroots'"
+ RD="$TCDIR/clang-rt"; mkdir -p "$RD/lib"
+ ln -sfn "$(clang -print-resource-dir)/include" "$RD/include"
+ ln -sfn "$sysroot/lib/windows" "$RD/lib/windows"
+ extra="-rtlib=compiler-rt -unwindlib=libunwind -resource-dir $RD"
+ fi
+ cat > "$TCDIR/cc" <<EOF
+#!/bin/sh
+exec clang --target=$ct --sysroot "$sysroot" -fuse-ld=lld $extra -Wno-unused-command-line-argument "\$@"
+EOF
+ fi
+ chmod +x "$TCDIR/cc"
+ CC_W="$TCDIR/cc"
+ AR_W="$(command -v llvm-ar 2>/dev/null || command -v ar)"
+fi
+
+# ---- build -----------------------------------------------------------------
+# kit cc emits no -MMD deps, so wipe the object tree (keep the toolchain) for a
+# correct full rebuild; harmless for the clang backend too.
+find "$BUILD_DIR" -mindepth 1 -maxdepth 1 ! -name toolchain -exec rm -rf {} + 2>/dev/null || true
+JOBS="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)"
+
+printf 'kit_cross: building kit for %s via %s (triple=%s)\n' "$TARGET" "$TC" \
+ "$([ "$TC" = clang ] && clang_triple || echo "$triple_kit")"
+
+# RELEASE=1 enables -ffunction-sections/-fdata-sections; dead-section GC then
+# drops the dead sections target headers' unused static inlines pull in. -O0
+# codegen + asserts on (HOST_MODE_CPPFLAGS=) keeps the cross-build fast but
+# checked. The sysroot rides the cc wrapper, so env.mk's host -isysroot is
+# cleared. The GC flag is linker-specific: GNU/lld (and kit ld) take
+# --gc-sections; Apple ld (clang on darwin) takes -dead_strip.
+GC_LDFLAG="-Wl,--gc-sections"
+if [ "$TC" = clang ] && [ "$os" = macos ]; then GC_LDFLAG="-Wl,-dead_strip"; fi
+make bin -j"$JOBS" \
+ BUILD_DIR="$BUILD_DIR" \
+ HOST_OS="$hos" HOST_ARCH="$harch" \
+ RELEASE=1 HOST_OPTFLAGS="${KIT_CROSS_OPT:--O0}" HOST_MODE_CPPFLAGS= \
+ HOST_MODE_LDFLAGS="$GC_LDFLAG" \
+ HOST_SYSROOT_CFLAGS= HOST_SYSROOT_LDFLAGS= \
+ CC="$CC_W" AR="$AR_W"
+
+OUT="$BUILD_DIR/kit"
+if [ "$os" = windows ]; then
+ # Both clang and kit auto-append .exe to a -o output that has no extension for
+ # a windows target, so the linker writes kit.exe directly (no copy needed).
+ OUT="$BUILD_DIR/kit.exe"
+fi
+printf 'kit_cross: built %s\n' "$OUT"
+file "$OUT" 2>/dev/null || true
+
+# ---- verify (optional): run the cross-built kit on the target --------------
+if [ "$VERIFY" = 1 ]; then
+ printf 'kit_cross: verifying %s runs on %s\n' "$OUT" "$TARGET"
+ bash "$ROOT/scripts/kit_cross_verify.sh" "$TARGET" "$OUT"
+fi
diff --git a/scripts/kit_cross_verify.sh b/scripts/kit_cross_verify.sh
@@ -0,0 +1,80 @@
+#!/usr/bin/env bash
+# scripts/kit_cross_verify.sh — run a cross-built `kit` binary on its target via
+# the shared exec seam (test/lib/exec_target.sh) and confirm it executes there.
+# Called by `kit_cross.sh --verify`; usable standalone.
+#
+# kit_cross_verify.sh <target> <kit-binary>
+#
+# target a hosted token (linux-…/freebsd-…/windows-…/macos-aa64).
+# kit-binary path to the cross-built kit[.exe] (kit_cross.sh's output).
+#
+# The probe runs the binary with no arguments: bare `kit` prints its top-level
+# help banner and exits 0, which exercises load + dynamic-linker + libc init +
+# main on the target. Pass = exit 0 and the banner appears on stdout.
+#
+# Per the port-infra contract, an unprovisioned/unreachable runner is an ERROR
+# (not a skip): run `make provision TARGET=<target> [KIT_VM=1]` first.
+
+# set -u only (no -e): exec_target.sh's runners return non-zero in normal flow
+# and report status via the RUN_RC global, exactly as scripts/cross_test.sh
+# drives them. Errors here are handled explicitly via die() / RUN_RC checks.
+set -u
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+cd "$ROOT"
+HOSTED="$ROOT/scripts/hosted.sh"
+
+die() { printf 'kit_cross_verify: %s\n' "$*" >&2; exit 1; }
+
+TARGET="${1:-}"; BIN="${2:-}"
+[ -n "$TARGET" ] && [ -n "$BIN" ] || die "usage: kit_cross_verify.sh <target> <kit-binary>"
+[ -f "$BIN" ] || die "kit binary not found: $BIN"
+
+os="${TARGET%%-*}"
+case "$os" in freestanding) die "$TARGET is freestanding — kit needs an OS to run on" ;; esac
+tag="$("$HOSTED" tag "$TARGET")" || die "bad target '$TARGET'"
+
+# ---- exec_target caller contract (mirrors scripts/cross_test.sh) -----------
+have_podman=0; command -v podman >/dev/null 2>&1 && have_podman=1
+QEMU_BIN="${QEMU_BIN:-$(command -v qemu-aarch64 2>/dev/null || true)}"
+QEMU_RV64_BIN="${QEMU_RV64_BIN:-$(command -v qemu-riscv64 2>/dev/null || true)}"
+have_qemu=0; [ -n "$QEMU_BIN" ] && have_qemu=1
+case "$(uname -m 2>/dev/null)" in aarch64|arm64) is_aarch64=1 ;; *) is_aarch64=0 ;; esac
+export have_podman QEMU_BIN QEMU_RV64_BIN have_qemu is_aarch64
+
+# Stage the binary in its own scratch dir: the podman runner mounts the exe's
+# directory as /work, so keep it free of the multi-MB build tree around it.
+WORK="$ROOT/build/kit-cross/.verify/$TARGET"
+rm -rf "$WORK"; mkdir -p "$WORK"
+runbase="kit"; case "$os" in windows) runbase="kit.exe" ;; esac
+cp "$BIN" "$WORK/$runbase" || die "failed to stage $BIN"; chmod +x "$WORK/$runbase"
+EXEC_TARGET_MOUNT_ROOT="$WORK"; export EXEC_TARGET_MOUNT_ROOT
+
+# shellcheck source=../test/lib/exec_target.sh
+. "$ROOT/test/lib/exec_target.sh"
+trap exec_target_teardown_all EXIT
+
+exec_target_supported "$tag" || {
+ case "$os" in
+ freebsd|windows) die "VM not provisioned/reachable for $tag — run 'make provision TARGET=$TARGET KIT_VM=1'" ;;
+ macos) die "macOS targets only verify on a Darwin host" ;;
+ *) die "no runner for $tag here — install podman or qemu-user, or run 'make provision TARGET=$TARGET'" ;;
+ esac
+}
+
+exec_target_setup "$tag" || true
+
+printf 'kit_cross_verify: running %s on %s (tag=%s)\n' "$runbase" "$TARGET" "$tag"
+RUN_RC=127
+exec_target_run "$tag" "$WORK/$runbase" "$WORK/out.txt" "$WORK/err.txt"
+
+banner='freestanding C compiler toolchain'
+if [ "${RUN_RC:-127}" -eq 0 ] && grep -q "$banner" "$WORK/out.txt" 2>/dev/null; then
+ printf 'kit_cross_verify: PASS — %s ran on %s (rc=0, banner ok)\n' "$runbase" "$TARGET"
+ exit 0
+fi
+
+printf 'kit_cross_verify: FAIL — %s on %s (rc=%s)\n' "$runbase" "$TARGET" "${RUN_RC:-127}" >&2
+printf '--- stdout ---\n' >&2; tail -5 "$WORK/out.txt" 2>/dev/null >&2 || true
+printf '--- stderr ---\n' >&2; tail -5 "$WORK/err.txt" 2>/dev/null >&2 || true
+exit 1
diff --git a/scripts/linux_bootstrap.sh b/scripts/linux_bootstrap.sh
@@ -16,23 +16,43 @@
# chain both (default) | debug (-O0) | release (-O1)
#
# Env overrides:
-# KIT_LINUX_BOOT_IMAGE container image (defaults per libc, below)
-# KIT_LINUX_BOOT_PLATFORM podman --platform (default linux/arm64)
+# KIT_LINUX_BOOT_ARCH aa64 (default) | x64 | rv64 — selects --platform +
+# the default base image; non-host arches run the whole
+# native 3-stage build under qemu emulation (binfmt).
+# KIT_LINUX_BOOT_IMAGE container image (defaults per libc+arch, below)
+# KIT_LINUX_BOOT_PLATFORM podman --platform (default derived from arch)
# KIT_LINUX_BOOT_TOY=1 also run the Toy corpus through the stage3 compiler
#
-# The stage tree lands under build/linux-boot/<libc>/ on the host (gitignored),
-# so artifacts survive the run for inspection / per-object diffing.
+# The stage tree lands under build/linux-boot/<libc>/<arch>/ on the host
+# (gitignored), so artifacts survive the run for inspection / per-object diffing.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
LIBC="${1:-musl}"
CHAIN="${2:-both}"
-PLATFORM="${KIT_LINUX_BOOT_PLATFORM:-linux/arm64}"
+
+case "${KIT_LINUX_BOOT_ARCH:-aarch64}" in
+ aa64|aarch64|arm64) ARCH=aarch64; DEF_PLATFORM=linux/arm64 ;;
+ x64|x86_64|amd64) ARCH=x86_64; DEF_PLATFORM=linux/amd64 ;;
+ rv64|riscv64) ARCH=riscv64; DEF_PLATFORM=linux/riscv64 ;;
+ *) echo "linux_bootstrap: unknown arch '$KIT_LINUX_BOOT_ARCH' (want aa64|x64|rv64)" >&2; exit 2 ;;
+esac
+PLATFORM="${KIT_LINUX_BOOT_PLATFORM:-$DEF_PLATFORM}"
case "$LIBC" in
- musl) DEF_IMAGE="docker.io/library/alpine:3.23" ;;
- glibc) DEF_IMAGE="docker.io/arm64v8/debian:bookworm-slim" ;;
+ musl)
+ case "$ARCH" in
+ aarch64) DEF_IMAGE="docker.io/library/alpine:3.23" ;;
+ x86_64) DEF_IMAGE="docker.io/library/alpine:3.23" ;;
+ riscv64) DEF_IMAGE="docker.io/riscv64/alpine:edge" ;;
+ esac ;;
+ glibc)
+ case "$ARCH" in
+ aarch64) DEF_IMAGE="docker.io/arm64v8/debian:bookworm-slim" ;;
+ x86_64) DEF_IMAGE="docker.io/amd64/debian:bookworm-slim" ;;
+ riscv64) DEF_IMAGE="docker.io/riscv64/debian:trixie-slim" ;;
+ esac ;;
*) echo "linux_bootstrap: unknown libc '$LIBC' (want musl|glibc)" >&2; exit 2 ;;
esac
IMAGE="${KIT_LINUX_BOOT_IMAGE:-$DEF_IMAGE}"
@@ -45,7 +65,7 @@ case "$CHAIN" in
esac
TOY="${KIT_LINUX_BOOT_TOY:-0}"
-BUILD_DIR="build/linux-boot/$LIBC"
+BUILD_DIR="build/linux-boot/$LIBC/$ARCH"
# In-container provisioning + build. The package sets give: a seed clang/lld,
# make, the libc dev headers, binutils (ar/ranlib used by the host stage1
diff --git a/scripts/llvm_mingw_sysroot.sh b/scripts/llvm_mingw_sysroot.sh
@@ -173,7 +173,7 @@ archive_top() {
}
extract_arch() {
- local arch triple pkg kind path top tmp out
+ local arch triple pkg kind path top tmp out rt bi
arch="$(canon_arch "$1")"
[ "$arch" != all ] || die "extract_arch requires a concrete arch"
triple="$(target_triple "$arch")"
@@ -216,6 +216,17 @@ extract_arch() {
mv "$tmp/$top/$triple" "$out.tmp"
rm -rf "$out"
mv "$out.tmp" "$out"
+ # Stage the clang compiler-rt builtins (the compiler runtime) into the sysroot
+ # under lib/windows/ so a standard clang cross-link works against it — not only
+ # llvm-mingw's own clang, which finds them in its resource dir. kit links its
+ # own rt and ignores these. Version-agnostic (matches lib/clang/<N>/lib/windows).
+ rt="$(case "$arch" in x64) echo x86_64 ;; aarch64) echo aarch64 ;; esac)"
+ bi="$(tar -tf "$path" | grep -E "/lib/clang/[0-9.]+/lib/windows/libclang_rt\.builtins-$rt\.a$" | head -1 || true)"
+ if [ -n "$bi" ]; then
+ tar -xf "$path" -C "$tmp" "$bi"
+ mkdir -p "$out/lib/windows"
+ cp "$tmp/$bi" "$out/lib/windows/"
+ fi
rm -rf "$tmp"
{
printf 'release=%s\n' "$RELEASE"
diff --git a/scripts/provision.sh b/scripts/provision.sh
@@ -0,0 +1,67 @@
+#!/usr/bin/env bash
+# scripts/provision.sh <selector> — provision the sysroots / run-images / VMs
+# for the in-scope support-set targets. This is the network + VM setup that
+# `make test-cross` / `make test-selfhost` deliberately do NOT do (they error
+# rather than silently skip when something is missing). See doc/plan/PORT.md.
+#
+# env: KIT_VM (default 1; 0 skips freebsd/windows VM setup), MAKE (default make).
+
+set -u
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+HOSTED="$ROOT/scripts/hosted.sh"
+SELECTOR="${1:-${TARGET:-all}}"
+MAKE="${MAKE:-make}"
+export KIT_VM="${KIT_VM:-1}"
+
+# Provision everything the selector names (mode-agnostic: sysroots are useful for
+# both cross and selfhost).
+TOKENS="$("$HOSTED" expand "$SELECTOR")" || exit 1
+[ -n "$TOKENS" ] || { echo "provision: selector '$SELECTOR' expanded to nothing" >&2; exit 2; }
+
+printf 'provision: targets=%s\n' "$(echo $TOKENS | tr '\n' ' ')"
+
+# 1. Sysroots (hosted.sh routes per OS: linux extract / freebsd base.txz /
+# windows UCRT; macos + freestanding are no-ops).
+"$HOSTED" prepare "$SELECTOR" || exit 1
+
+# 2. Run images + VMs, driven by which OSes are in scope.
+need_musl=0 need_glibc=0 fb_arches="" need_win=0
+for t in $TOKENS; do
+ case "$t" in
+ linux-musl-*) need_musl=1 ;;
+ linux-glibc-*) need_glibc=1 ;;
+ freebsd-*) fb_arches="$fb_arches ${t##*-}" ;;
+ windows-*) need_win=1 ;;
+ esac
+done
+
+if [ "$need_musl" = 1 ]; then
+ printf 'provision: musl run images (make test-images)\n'
+ "$MAKE" -C "$ROOT" test-images || exit 1
+fi
+if [ "$need_glibc" = 1 ]; then
+ printf 'provision: glibc run images (make hosted-glibc-images)\n'
+ "$MAKE" -C "$ROOT" hosted-glibc-images || exit 1
+fi
+
+if [ "$KIT_VM" = 1 ] && [ -n "$fb_arches" ]; then
+ for a in $fb_arches; do
+ case "$a" in aa64) va=aarch64 ;; x64) va=amd64 ;; rv64) va=riscv64 ;; *) va="$a" ;; esac
+ printf 'provision: freebsd %s VM (scripts/freebsd_vm.sh prepare %s)\n' "$va" "$va"
+ bash "$ROOT/scripts/freebsd_vm.sh" prepare "$va" || \
+ echo "provision: warn — freebsd $va VM prepare failed (see scripts/freebsd_vm.sh)"
+ done
+fi
+
+if [ "$KIT_VM" = 1 ] && [ "$need_win" = 1 ]; then
+ printf 'provision: windows UCRT sysroots (make windows-ucrt-sysroots)\n'
+ "$MAKE" -C "$ROOT" windows-ucrt-sysroots || exit 1
+ cat >&2 <<'EOF'
+provision: NOTE — the Windows VM needs a user-supplied Windows 11 ARM64 ISO and a
+one-time provision; see doc/plan/windows.md / scripts/windows_vm.sh. Once
+provisioned, `scripts/windows_vm.sh boot` makes the windows-* targets runnable.
+EOF
+fi
+
+printf 'provision: done\n'
diff --git a/scripts/selfhost.sh b/scripts/selfhost.sh
@@ -0,0 +1,162 @@
+#!/usr/bin/env bash
+# scripts/selfhost.sh — self-host orchestrator: the engine behind
+# `make test-selfhost`. For each target it builds a kit that RUNS on that target,
+# then uses that kit to compile + run a program there. The shape differs by OS
+# (see doc/plan/PORT.md):
+#
+# macos native 3-stage `make bootstrap`, then a program through stage3
+# linux native 3-stage in a podman container (emulated for a non-host arch)
+# freebsd native 3-stage in the VM
+# windows CROSS-build kit.exe on the host (the VM has no seed compiler);
+# on-VM self-compile/run is a tracked follow-up (doc/plan/PORT.md)
+#
+# selfhost.sh <selector> [DEPTH]
+#
+# selector hosted.sh grammar (mode=selfhost; freestanding is cross-only and
+# rejected). DEPTH: smoke (default) | full (toy corpus through the
+# on-target kit).
+#
+# env: KIT, KIT_VM (default 1), DEPTH, KIT_SELFHOST_DRYRUN=1 (print the plan).
+# Provision-or-error: missing podman / VM / sysroot is a hard error, reported up
+# front (provision with `make provision TARGET=…`).
+
+set -u
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+KIT="${KIT:-$ROOT/build/kit}"
+HOSTED="$ROOT/scripts/hosted.sh"
+CASES="$ROOT/test/cross/cases"
+BUILD_DIR="$ROOT/build/test/selfhost"
+SELECTOR="${1:-${TARGET:-all}}"
+DEPTH="${2:-${DEPTH:-smoke}}"
+export KIT_VM="${KIT_VM:-1}"
+DRYRUN="${KIT_SELFHOST_DRYRUN:-0}"
+
+mkdir -p "$BUILD_DIR"
+# shellcheck source=../test/lib/kit_sh_report.sh
+. "$ROOT/test/lib/kit_sh_report.sh"
+# exec_vm_supported (provisioning pre-flight) + VM teardown live in the exec seam.
+have_podman=0; command -v podman >/dev/null 2>&1 && have_podman=1
+export have_podman
+# shellcheck source=../test/lib/exec_target.sh
+. "$ROOT/test/lib/exec_target.sh"
+kit_report_init
+trap exec_target_teardown_all EXIT
+
+tok_os() { printf '%s' "${1%%-*}"; }
+tok_arch() { printf '%s' "${1##*-}"; }
+tok_libc() { case "$1" in linux-*) local r="${1#linux-}"; printf '%s' "${r%%-*}" ;; *) printf '' ;; esac; }
+
+_host_arch_is() { # canonical arch token -> true if it matches this host
+ local want="$1" m; m="$(uname -m 2>/dev/null)"
+ case "$want" in
+ aa64) [ "$m" = arm64 ] || [ "$m" = aarch64 ] ;;
+ x64) [ "$m" = x86_64 ] || [ "$m" = amd64 ] ;;
+ rv64) [ "$m" = riscv64 ] ;;
+ *) return 1 ;;
+ esac
+}
+
+# ---- provisioning pre-flight ----------------------------------------------
+preflight_token() {
+ local t="$1" os arch sr
+ os="$(tok_os "$t")"; arch="$(tok_arch "$t")"
+ case "$os" in
+ macos)
+ { [ "$(uname -s)" = Darwin ] && _host_arch_is "$arch"; } || \
+ echo "selfhost macos needs a Darwin host with matching arch" ;;
+ linux)
+ [ "$have_podman" = 1 ] || echo "podman required for linux self-host — install podman" ;;
+ freebsd)
+ case "$arch" in
+ aa64|x64) ;;
+ *) echo "freebsd self-host: only aa64/x64 have a native compiler in the base VM"; return ;;
+ esac
+ exec_vm_supported "$("$HOSTED" tag "$t")" || \
+ echo "freebsd VM not provisioned/reachable — run 'make provision TARGET=$t KIT_VM=1'" ;;
+ windows)
+ sr="$("$HOSTED" path "$t" 2>/dev/null)"
+ { [ -n "$sr" ] && [ -d "$sr" ]; } || \
+ echo "windows UCRT sysroot missing — run 'make provision TARGET=$t'" ;;
+ esac
+}
+
+# ---- step runner -----------------------------------------------------------
+run_step() { # label cmd...
+ local label="$1"; shift
+ if [ "$DRYRUN" = 1 ]; then printf 'selfhost: PLAN %-28s %s\n' "$label" "$*"; return 0; fi
+ printf '\nselfhost: step %s →\n' "$label"
+ if "$@"; then kit_pass "$label"; return 0; else kit_fail "$label" "step failed (see output above)"; return 1; fi
+}
+
+# macOS: after the native bootstrap, prove the self-built stage3 compiles + runs
+# a program (the "compile and run on the target" half of self-host).
+macos_stage3_hello() {
+ local s3="$ROOT/build/debug/bootstrap/stage3/kit" d="$BUILD_DIR/macos"
+ [ -x "$s3" ] || { echo "stage3 kit missing at $s3"; return 1; }
+ mkdir -p "$d"
+ # -lc engages kit's hosted profile (SDK/libc discovery), like the bootstrap.
+ "$s3" cc -lc "$CASES/hello.c" -o "$d/hello" 2>"$d/cc.err" || { sed 's/^/ | /' "$d/cc.err"; return 1; }
+ "$d/hello" >"$d/out" 2>"$d/err" || return 1
+ [ "$(cat "$d/out")" = "$(cat "$CASES/hello.stdout")" ]
+}
+
+# ---- dispatch --------------------------------------------------------------
+selfhost_token() {
+ local t="$1" os arch libc toy
+ os="$(tok_os "$t")"; arch="$(tok_arch "$t")"; libc="$(tok_libc "$t")"
+ toy=0; [ "$DEPTH" = full ] && toy=1
+ case "$os" in
+ macos)
+ if [ "$DEPTH" = full ]; then
+ run_step "$t:bootstrap+toy" make -C "$ROOT" test-bootstrap-toy
+ else
+ run_step "$t:bootstrap" make -C "$ROOT" bootstrap-debug && \
+ run_step "$t:run" macos_stage3_hello
+ fi ;;
+ linux)
+ run_step "$t:bootstrap" env KIT_LINUX_BOOT_ARCH="$arch" KIT_LINUX_BOOT_TOY="$toy" \
+ bash "$ROOT/scripts/linux_bootstrap.sh" "$libc" both ;;
+ freebsd)
+ run_step "$t:bootstrap" env KIT_FREEBSD_BOOT_TOY="$toy" \
+ bash "$ROOT/scripts/freebsd_bootstrap.sh" "$arch" both ;;
+ windows)
+ run_step "$t:cross-build" bash "$ROOT/scripts/windows_cross.sh" "$arch"
+ printf 'selfhost: NOTE %s — on-VM self-compile/run is a tracked follow-up (kit.exe cross-built here); see doc/plan/PORT.md\n' "$t" ;;
+ esac
+}
+
+# ---- drive -----------------------------------------------------------------
+# freestanding is cross-only. If the user EXPLICITLY named it, that's an error
+# with a clear message; for a wildcard like `all` it is silently dropped by the
+# mode filter below.
+for it in $(printf '%s' "$SELECTOR" | tr , ' '); do
+ case "$it" in freestanding|freestanding-*)
+ echo "selfhost: '$it' has no hosted environment — freestanding is cross-only (use test-cross)" >&2
+ exit 2 ;;
+ esac
+done
+
+TOKENS="$("$HOSTED" expand "$SELECTOR" --mode=selfhost)" || exit 1
+[ -n "$TOKENS" ] || { echo "selfhost: selector '$SELECTOR' expanded to nothing" >&2; exit 2; }
+
+printf 'selfhost: DEPTH=%s KIT_VM=%s\nselfhost: targets=%s\n' \
+ "$DEPTH" "$KIT_VM" "$(echo $TOKENS | tr '\n' ' ')"
+
+if [ "$DRYRUN" != 1 ]; then
+ missing=0
+ for t in $TOKENS; do
+ reason="$(preflight_token "$t")"
+ if [ -n "$reason" ]; then printf 'selfhost: NOT PROVISIONED %-22s %s\n' "$t" "$reason"; missing=1; fi
+ done
+ if [ "$missing" = 1 ]; then
+ echo "selfhost: aborting — provision the targets above, or narrow TARGET / set KIT_VM=0." >&2
+ exit 1
+ fi
+fi
+
+for t in $TOKENS; do selfhost_token "$t"; done
+
+KIT_SKIP_IS_FAILURE=0
+kit_summary test-selfhost
+kit_exit
diff --git a/test/cross/cases/exit.c b/test/cross/cases/exit.c
@@ -0,0 +1,5 @@
+/* Exit-code smoke: no #include, no libc calls — just a return value. Exercises
+ * the toolchain (compile + link + crt) and the platform's exit-code path end to
+ * end, and on freestanding the bare-metal exit oracle. Distinct from hello.c,
+ * which additionally exercises the sysroot headers + libc (stdout). */
+int main(void) { return 42; }
diff --git a/test/cross/cases/exit.expected b/test/cross/cases/exit.expected
@@ -0,0 +1 @@
+42
diff --git a/test/lib/exec_bare.sh b/test/lib/exec_bare.sh
@@ -0,0 +1,457 @@
+# test/lib/exec_bare.sh — bare-metal (freestanding) execution backend for
+# test/lib/exec_target.sh, and the single owner of kit's per-arch boot
+# scaffolding (reset stub + linker script + exit-code oracle + qemu-system
+# invocation) for aarch64 / x86_64 / riscv64 / riscv32.
+#
+# This consolidates what used to be three scripts: the rv32 corpus runner
+# (exec_rv32_bare.sh), the aa64/rv64 ready-image runner (exec_kernel.sh, now a
+# thin shim over this file), and the per-arch stubs inlined in
+# test/smoke/freestanding_system.sh.
+#
+# Two contracts:
+#
+# exec_bare_run_image <arch> <kernel.elf> <out> <err> → sets RUN_RC
+# Run a ready bootable image (the image owns its own _start + exit
+# convention). What test/link's kernel_image cases need.
+#
+# exec_bare_setup <arch> <work> [entry]
+# exec_bare_run <arch> <obj> <work> <rcfile> → writes <rcfile>
+# Link a corpus object (entry default `main`, returns the exit code) with
+# the per-arch reset stub + (rv32) runtime into a bootable image, run it,
+# and write the decoded guest exit code. What toy/parse need. exec_bare_run
+# echoes a one-line reason and returns 0 (ran; rc in <rcfile>) or 2
+# (build/link failure; caller decides skip vs fail).
+#
+# exec_bare_supported <arch> true if qemu-system-<arch> is on PATH.
+#
+# Exit-code oracle (so callers compare rc == expected uniformly):
+# aa64 ARM semihosting SYS_EXIT_EXTENDED → qemu rc = guest code
+# rv* SiFive test finisher MMIO @0x100000 → qemu rc = guest code
+# x64 isa-debug-exit @0x501 → qemu rc = (code<<1)|1, decoded here
+#
+# Corpus depth (exec_bare_setup/run) is fully wired for rv32 only; the aa64/x64/
+# rv64 stubs here are smoke-capable (single object, no TLS/rt). Hardening them
+# for the toy/parse corpora is the tracked follow-up in doc/plan/PORT.md.
+
+EXEC_BARE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+EXEC_BARE_KIT="${KIT:-$EXEC_BARE_ROOT/build/kit}"
+EXEC_BARE_TIMEOUT="timeout"
+command -v "$EXEC_BARE_TIMEOUT" >/dev/null 2>&1 || {
+ command -v gtimeout >/dev/null 2>&1 && EXEC_BARE_TIMEOUT="gtimeout"
+}
+EXEC_BARE_TO="${EXEC_BARE_TO:-20}"
+
+# ---- arch helpers ----------------------------------------------------------
+_bare_canon() {
+ case "$1" in
+ aa64|aarch64|arm64) echo aa64 ;;
+ x64|x86_64|amd64) echo x64 ;;
+ rv64|riscv64) echo rv64 ;;
+ rv32|riscv32) echo rv32 ;;
+ *) return 1 ;;
+ esac
+}
+_bare_qemu() {
+ case "$(_bare_canon "$1")" in
+ aa64) echo qemu-system-aarch64 ;;
+ x64) echo qemu-system-x86_64 ;;
+ rv64) echo qemu-system-riscv64 ;;
+ rv32) echo qemu-system-riscv32 ;;
+ esac
+}
+_bare_triple() {
+ case "$(_bare_canon "$1")" in
+ aa64) echo aarch64-none-elf ;;
+ x64) echo x86_64-none-elf ;;
+ rv64) echo riscv64-none-elf ;;
+ rv32) echo riscv32-none-elf ;;
+ esac
+}
+
+exec_bare_supported() {
+ local q; q="$(_bare_qemu "$1")" || return 1
+ [ -n "$q" ] && command -v "$q" >/dev/null 2>&1
+}
+
+# ---- qemu invocation + exit decode -----------------------------------------
+# Sets RUN_RC_RAW to the raw qemu-system exit status.
+_bare_qemu_run() {
+ local arch elf out err q
+ arch="$(_bare_canon "$1")"; elf="$2"; out="$3"; err="$4"
+ q="$(_bare_qemu "$arch")"
+ case "$arch" in
+ aa64)
+ $EXEC_BARE_TIMEOUT "$EXEC_BARE_TO" "$q" -machine virt -cpu cortex-a72 \
+ -kernel "$elf" -nographic \
+ -semihosting-config enable=on,target=native -no-reboot \
+ >"$out" 2>"$err"; RUN_RC_RAW=$? ;;
+ x64)
+ $EXEC_BARE_TIMEOUT "$EXEC_BARE_TO" "$q" -kernel "$elf" \
+ -device isa-debug-exit,iobase=0x501,iosize=0x02 \
+ -display none -serial none -monitor none -no-reboot \
+ >"$out" 2>"$err"; RUN_RC_RAW=$? ;;
+ rv64|rv32)
+ $EXEC_BARE_TIMEOUT "$EXEC_BARE_TO" "$q" -machine virt -bios none \
+ -kernel "$elf" -nographic -no-reboot \
+ >"$out" 2>"$err"; RUN_RC_RAW=$? ;;
+ *) RUN_RC_RAW=127 ;;
+ esac
+}
+
+# Decode a raw qemu exit into the guest exit code. Only x64's isa-debug-exit
+# needs it (qemu maps a guest write of N to (N<<1)|1); for aa64/rv* qemu already
+# returns the guest code, so identity. A non-odd x64 rc (e.g. 124 timeout) is a
+# genuine failure and is passed through unchanged.
+_bare_decode() {
+ local r="$2"
+ case "$(_bare_canon "$1")" in
+ x64) if [ "$r" -ge 1 ] && [ $((r & 1)) -eq 1 ]; then echo $(((r - 1) / 2)); else echo "$r"; fi ;;
+ *) echo "$r" ;;
+ esac
+}
+
+# ---- ready-image runner (exec_kernel contract) -----------------------------
+exec_bare_run_image() { # arch elf out err -> sets RUN_RC
+ local arch; arch="$(_bare_canon "$1")" || { RUN_RC=127; return; }
+ exec_bare_supported "$arch" || { RUN_RC=127; return; }
+ _bare_qemu_run "$arch" "$2" "$3" "$4"
+ RUN_RC="$(_bare_decode "$arch" "$RUN_RC_RAW")"
+}
+
+# ---- corpus runner: setup (build per-arch stub) ----------------------------
+exec_bare_setup() { # arch work [entry]
+ local arch work entry dir
+ arch="$(_bare_canon "$1")" || return 1
+ work="$2"; entry="${3:-${EXEC_BARE_ENTRY:-main}}"
+ [ -x "$EXEC_BARE_KIT" ] || return 1
+ exec_bare_supported "$arch" || return 1
+ dir="$work/_bare/$arch"; mkdir -p "$dir"
+ rm -f "$dir/.ok"
+ case "$arch" in
+ aa64) _bare_emit_aa64 "$dir" || return 1 ;;
+ x64) _bare_emit_x64 "$dir" || return 1 ;;
+ rv64) _bare_emit_rv64 "$dir" || return 1 ;;
+ rv32) _bare_emit_rv32 "$dir" "$entry" || return 1 ;;
+ esac
+ : > "$dir/.ok"
+ # Cache the stub dir per arch so a caller may pass a different work dir to
+ # exec_bare_run for the output image (toy/parse set up once under BUILD_DIR
+ # but link each case under its own KIT_WORK).
+ eval "EXEC_BARE_DIR_$arch=\$dir"
+}
+
+# ---- corpus runner: run (link object + boot) -------------------------------
+exec_bare_run() { # arch obj work rcfile -> echoes reason; 0 ran / 2 build-fail
+ local arch obj work rcf dir elf lderr rt
+ arch="$(_bare_canon "$1")" || { echo "unknown bare arch '$1'"; return 2; }
+ obj="$2"; work="$3"; rcf="$4"
+ eval "dir=\"\${EXEC_BARE_DIR_$arch:-$work/_bare/$arch}\""
+ [ -f "$dir/.ok" ] || { echo "bare runner not set up for $arch"; return 2; }
+ elf="$work/$(basename "$obj").$arch.elf"; lderr="$elf.ld.err"
+ if [ "$arch" = rv32 ]; then
+ rt="$EXEC_BARE_ROOT/build/rt/riscv32-elf-hardfloat/libkit_rt.a"
+ if ! "$EXEC_BARE_KIT" ld -T "$dir/link.ld" -e _start \
+ "$dir/start.o" "$dir/wrap.o" "$obj" "$rt" -o "$elf" 2>"$lderr"; then
+ echo "kit ld (rv32) failed: $(head -n1 "$lderr" 2>/dev/null)"; return 2
+ fi
+ else
+ if ! "$EXEC_BARE_KIT" ld -T "$dir/link.ld" -e _start \
+ "$dir/start.o" "$obj" -o "$elf" 2>"$lderr"; then
+ echo "kit ld ($arch) failed: $(head -n1 "$lderr" 2>/dev/null)"; return 2
+ fi
+ fi
+ _bare_qemu_run "$arch" "$elf" "$elf.out" "$elf.err"
+ _bare_decode "$arch" "$RUN_RC_RAW" > "$rcf"
+ return 0
+}
+
+# ===========================================================================
+# Per-arch stub emitters. Each writes start.S + link.ld under <dir> and
+# assembles start.o with kit-as. The stub sets up the stack (+ FPU on riscv),
+# calls main, and reports main's return through the arch's exit oracle.
+# ===========================================================================
+
+_bare_emit_aa64() {
+ local dir="$1"
+ cat > "$dir/start.S" <<'EOF'
+.section .text.start,"ax",@progbits
+.globl _start
+_start:
+ adrp x0, stack_top
+ add x0, x0, :lo12:stack_top
+ and x0, x0, #0xfffffffffffffff0 // AArch64 faults on a misaligned SP; the
+ mov sp, x0 // linker-script .bss lump may not 16-align
+ bl main // stack_top, so force it here.
+ adrp x1, semihost_args
+ add x1, x1, :lo12:semihost_args
+ str x0, [x1, #8]
+ mov x0, #0x20
+ hlt #0xf000
+.Lhang:
+ b .Lhang
+
+.section .data.semihost,"aw",@progbits
+.balign 8
+semihost_args:
+ .quad 0x20026
+ .quad 0
+
+.section .bss.stack,"aw",@nobits
+.balign 16
+stack_bottom:
+ .zero 65536
+stack_top:
+EOF
+ cat > "$dir/link.ld" <<'EOF'
+ENTRY(_start)
+SECTIONS {
+ . = 0x40080000;
+ .text : { *(.text.start) *(.text*) }
+ .rodata : { *(.rodata*) }
+ .data : { *(.data*) }
+ .bss : { *(.bss*) *(COMMON) }
+ /DISCARD/ : { *(.comment) }
+}
+EOF
+ "$EXEC_BARE_KIT" as -target aarch64-none-elf \
+ -o "$dir/start.o" "$dir/start.S" 2>"$dir/as.err"
+}
+
+_bare_emit_rv64() {
+ local dir="$1"
+ cat > "$dir/start.S" <<'EOF'
+.section .text.start,"ax",@progbits
+.globl _start
+_start:
+ li sp, 0x81000000
+ li t0, 0x2000
+ csrs mstatus, t0
+ call main
+ li t0, 0x100000
+ beqz a0, .Lpass
+ slli a0, a0, 16
+ li t1, 0x3333
+ or a0, a0, t1
+ sw a0, 0(t0)
+.Lhang:
+ j .Lhang
+.Lpass:
+ li t1, 0x5555
+ sw t1, 0(t0)
+ j .Lhang
+EOF
+ cat > "$dir/link.ld" <<'EOF'
+ENTRY(_start)
+SECTIONS {
+ . = 0x80000000;
+ .text : { *(.text.start) *(.text*) }
+ .rodata : { *(.rodata*) }
+ .data : { *(.data*) }
+ .bss : { *(.bss*) *(COMMON) }
+ /DISCARD/ : { *(.riscv.attributes) *(.comment) }
+}
+EOF
+ "$EXEC_BARE_KIT" as -target riscv64-none-elf \
+ -march=rv64imafd_zicsr_zifencei -mabi=lp64 \
+ -o "$dir/start.o" "$dir/start.S" 2>"$dir/as.err"
+}
+
+# rv32 is the corpus-capable arch: the reset stub seeds a static-TLS image and
+# enables the FPU (ilp32f); the C wrapper maps the entry's return onto the
+# SiFive finisher. The runtime archive (i64 + soft-double helpers) is linked in.
+# Ported from the former exec_rv32_bare.sh; see doc/plan/PORT.md.
+_bare_emit_rv32() {
+ local dir="$1" entry="${2:-main}"
+ local march="rv32imafc_zicsr_zifencei" mabi="ilp32f"
+ local rt="$EXEC_BARE_ROOT/build/rt/riscv32-elf-hardfloat/libkit_rt.a"
+ if [ ! -f "$rt" ]; then
+ make -C "$EXEC_BARE_ROOT" rt-riscv32-elf-hardfloat >/dev/null 2>&1 || true
+ fi
+ [ -f "$rt" ] || return 1
+ cat > "$dir/start.S" <<'EOF'
+.section .text.start,"ax",@progbits
+.globl _start
+_start:
+ li sp, 0x80100000
+ li t0, 0x2000
+ csrs mstatus, t0 # mstatus.FS = Initial (enable the FPU for ilp32f)
+
+ // ---- static thread-local storage setup -----------------------------------
+ la t0, __rv32_tls_block # t0 = block base (TCB at +0)
+ addi t1, t0, 16 # t1 = dst = block + 16 (.tdata copy start)
+ la t2, __rv32_tdata_lma # t2 = src = .tdata load image
+ la t3, __rv32_tdata_size # t3 = .tdata byte count (abs symbol: la yields value)
+.Lcopy:
+ beqz t3, .Lcopy_done
+ lbu t4, 0(t2)
+ sb t4, 0(t1)
+ addi t1, t1, 1
+ addi t2, t2, 1
+ addi t3, t3, -1
+ j .Lcopy
+.Lcopy_done:
+ la t3, __rv32_tbss_size
+.Lzero:
+ beqz t3, .Lzero_done
+ sb zero, 0(t1)
+ addi t1, t1, 1
+ addi t3, t3, -1
+ j .Lzero
+.Lzero_done:
+ la t2, __rv32_tdata_lma
+ sub tp, t0, t2 # tp = block - __rv32_tdata_lma
+ // ---------------------------------------------------------------------------
+
+ call _rv32_cmain
+.Lhang: j .Lhang
+
+.section .bss.rv32tls,"aw",@nobits
+.balign 16
+__rv32_tls_block:
+ .zero 4096
+EOF
+ cat > "$dir/wrap.c" <<EOF
+#define FINISHER ((volatile unsigned int*)0x100000)
+extern int ${entry}(void);
+__attribute__((noreturn)) void _rv32_cmain(void) {
+ int code = ${entry}();
+ *FINISHER = code ? (0x3333u | ((unsigned)code << 16)) : 0x5555u;
+ for (;;) {}
+}
+EOF
+ cat > "$dir/link.ld" <<'EOF'
+ENTRY(_start)
+SECTIONS {
+ . = 0x80000000;
+ .text : { *(.text.start) *(.text*) }
+ .rodata : { *(.rodata*) }
+ .data : { *(.data*) }
+ .tdata : { . = ALIGN(16); __rv32_tdata_lma = .; *(.tdata .tdata.*) }
+ .tdata_end : { __rv32_tdata_end = .; }
+ .tbss : { __rv32_tbss_start = .; *(.tbss .tbss.*) }
+ .tbss_end : { __rv32_tbss_end = .; }
+ .bss : { *(.bss*) *(COMMON) }
+ __rv32_tdata_size = __rv32_tdata_end - __rv32_tdata_lma;
+ __rv32_tbss_size = __rv32_tbss_end - __rv32_tbss_start;
+ /DISCARD/ : { *(.riscv.attributes) *(.comment) }
+}
+EOF
+ "$EXEC_BARE_KIT" as -target riscv32-none-elf -march="$march" -mabi="$mabi" \
+ -o "$dir/start.o" "$dir/start.S" 2>"$dir/as.err" || return 1
+ "$EXEC_BARE_KIT" cc -target riscv32-none-elf -march="$march" -mabi="$mabi" \
+ -O1 -ffreestanding -c "$dir/wrap.c" -o "$dir/wrap.o" 2>"$dir/wrap.err" || return 1
+}
+
+# x86_64 long-mode reset: a multiboot header, a 32-bit entry that sets up
+# identity-mapped paging + long mode, then calls main and reports its return via
+# isa-debug-exit (port 0x501). Ported from freestanding_system.sh.
+_bare_emit_x64() {
+ local dir="$1" out="$dir/start.S" i
+ cat > "$out" <<'EOF'
+.section .multiboot,"a",@progbits
+.balign 4
+.long 0x1badb002
+.long 0
+.long 0xe4524ffe
+
+.section .note.Xen,"a",@note
+.balign 4
+.long 4
+.long 4
+.long 18
+.ascii "Xen"
+.byte 0
+.long _start
+
+.section .text.start,"ax",@progbits
+.globl _start
+_start:
+ .byte 0xfa /* cli */
+ .byte 0xbc /* mov $stack_top, %esp */
+ .long stack_top
+ .byte 0xb8 /* mov $pml4, %eax */
+ .long pml4
+ .byte 0x0f, 0x22, 0xd8 /* mov %eax, %cr3 */
+ .byte 0x0f, 0x20, 0xe0 /* mov %cr4, %eax */
+ .byte 0x83, 0xc8, 0x20 /* or $CR4_PAE, %eax */
+ .byte 0x0f, 0x22, 0xe0 /* mov %eax, %cr4 */
+ .byte 0xb9, 0x80, 0x00, 0x00, 0xc0 /* mov $EFER, %ecx */
+ .byte 0x0f, 0x32 /* rdmsr */
+ .byte 0x0d, 0x00, 0x01, 0x00, 0x00 /* or $EFER_LME, %eax */
+ .byte 0x0f, 0x30 /* wrmsr */
+ .byte 0x0f, 0x20, 0xc0 /* mov %cr0, %eax */
+ .byte 0x0d, 0x00, 0x00, 0x00, 0x80 /* or $CR0_PG, %eax */
+ .byte 0x0f, 0x22, 0xc0 /* mov %eax, %cr0 */
+ .byte 0x0f, 0x01, 0x15 /* lgdt gdt_desc */
+ .long gdt_desc
+ .byte 0xea /* ljmp $0x08,$long_entry */
+ .long long_entry
+ .hword 0x08
+
+long_entry:
+ .byte 0x66, 0xb8, 0x10, 0x00 /* mov $0x10, %ax */
+ .byte 0x8e, 0xd8 /* mov %ax, %ds */
+ .byte 0x8e, 0xc0 /* mov %ax, %es */
+ .byte 0x8e, 0xd0 /* mov %ax, %ss */
+ .byte 0x48, 0xbc /* movabs $stack_top, %rsp */
+ .quad stack_top
+ call main
+ .byte 0x66, 0xba, 0x01, 0x05 /* mov $0x501, %dx */
+ .byte 0x66, 0xef /* outw %ax, %dx */
+.Lhang:
+ .byte 0xf4 /* hlt */
+ jmp .Lhang
+
+.section .data.boot,"aw",@progbits
+.balign 8
+gdt:
+ .quad 0
+ .quad 0x00af9a000000ffff
+ .quad 0x00af92000000ffff
+gdt_desc:
+ .hword 23
+ .long gdt
+
+.balign 4096
+pml4:
+ .quad pdpt + 0x3
+EOF
+ for _ in $(seq 1 511); do printf ' .quad 0\n' >> "$out"; done
+ cat >> "$out" <<'EOF'
+.balign 4096
+pdpt:
+ .quad pd + 0x3
+EOF
+ for _ in $(seq 1 511); do printf ' .quad 0\n' >> "$out"; done
+ printf '.balign 4096\npd:\n' >> "$out"
+ i=0
+ while [ "$i" -lt 512 ]; do
+ printf ' .quad 0x%016x\n' $((i * 0x200000 + 0x83)) >> "$out"
+ i=$((i + 1))
+ done
+ cat >> "$out" <<'EOF'
+
+.section .bss.stack,"aw",@nobits
+.balign 16
+stack_bottom:
+ .zero 65536
+stack_top:
+EOF
+ cat > "$dir/link.ld" <<'EOF'
+ENTRY(_start)
+SECTIONS {
+ . = 0x100000;
+ .multiboot : { *(.multiboot) }
+ .note.Xen : { *(.note.Xen) }
+ .text : { *(.text.start) *(.text*) }
+ .rodata : { *(.rodata*) }
+ .data.boot : ALIGN(4096) { *(.data.boot) }
+ .data : { *(.data*) }
+ .bss.stack : ALIGN(16) { *(.bss.stack) }
+ .bss : { *(.bss*) *(COMMON) }
+ /DISCARD/ : { *(.comment) }
+}
+EOF
+ "$EXEC_BARE_KIT" as -target x86_64-none-elf \
+ -o "$dir/start.o" "$out" 2>"$dir/as.err"
+}
diff --git a/test/lib/exec_kernel.sh b/test/lib/exec_kernel.sh
@@ -1,58 +1,15 @@
-# test/lib/exec_kernel.sh — per-arch qemu-system runner for
-# kernel_image cases.
+# test/lib/exec_kernel.sh — thin compatibility shim over exec_bare.sh.
#
-# Distinct from exec_target.sh (which uses qemu-user / podman to run
-# linux-userland ELFs): kernel_image cases are freestanding boot images
-# with their own entry stub, no Linux ABI, exiting via arch-specific
-# semihosting / test-device MMIO. The harness invokes
+# The per-arch bare-metal scaffolding (reset stubs, linker scripts, exit
+# oracles, qemu-system invocations) now lives in test/lib/exec_bare.sh, the
+# single owner. This shim preserves the historical exec_kernel_* API used by
+# test/link/run.sh's kernel_image cases, which build a complete bootable image
+# themselves and want a synchronous run returning RUN_RC.
#
-# exec_kernel_run <arch> <exe> <out> <err>
-#
-# which sets RUN_RC. Returns rc=127 if no qemu-system-* is available
-# for the requested arch.
-#
-# Per-arch exit conventions:
-# aa64: ARM semihosting hlt #0xf000 + ADP_Stopped_ApplicationExit
-# (subcode = host exit code).
-# rv64: SIFIVE_TEST MMIO device at 0x100000; writing 0x5555 = pass
-# (host exit 0), 0x3333 = fail (host nonzero).
+# exec_kernel_supported <arch> true if qemu-system-<arch> is present
+# exec_kernel_run <arch> <exe> <out> <err> run the ready image; sets RUN_RC
-exec_kernel_supported() {
- local arch="$1"
- case "$arch" in
- aa64) command -v qemu-system-aarch64 >/dev/null 2>&1 ;;
- rv64) command -v qemu-system-riscv64 >/dev/null 2>&1 ;;
- *) return 1 ;;
- esac
-}
+. "$(dirname "${BASH_SOURCE[0]}")/exec_bare.sh"
-# Runs synchronously; sets RUN_RC.
-exec_kernel_run() {
- local arch="$1" exe="$2" out="$3" err="$4"
- local bin
- case "$arch" in
- aa64)
- bin="$(command -v qemu-system-aarch64 2>/dev/null || true)"
- if [ -z "$bin" ]; then RUN_RC=127; return; fi
- "$bin" -machine virt -cpu cortex-a72 \
- -kernel "$exe" -nographic \
- -semihosting-config enable=on,target=native \
- -no-reboot >"$out" 2>"$err"
- RUN_RC=$?
- ;;
- rv64)
- bin="$(command -v qemu-system-riscv64 2>/dev/null || true)"
- if [ -z "$bin" ]; then RUN_RC=127; return; fi
- # -bios none: skip OpenSBI; we own 0x80000000 entry. The
- # SIFIVE_TEST MMIO device on -machine virt handles exit
- # via writes to 0x100000 from the kernel.
- "$bin" -machine virt -bios none \
- -kernel "$exe" -nographic \
- -no-reboot >"$out" 2>"$err"
- RUN_RC=$?
- ;;
- *)
- RUN_RC=127
- ;;
- esac
-}
+exec_kernel_supported() { exec_bare_supported "$1"; }
+exec_kernel_run() { exec_bare_run_image "$1" "$2" "$3" "$4"; }
diff --git a/test/lib/exec_target.sh b/test/lib/exec_target.sh
@@ -55,6 +55,14 @@
# the stateful VM runner (which an expensive-to-boot VM needs) lives there.
. "$(dirname "${BASH_SOURCE[0]}")/exec_vm.sh"
+# Bare-metal backend: the `<arch>-freestanding` runner. Freestanding images have
+# no OS — they boot under qemu-system and exit via per-arch semihosting / a test
+# device. exec_bare.sh owns the reset stubs, linker scripts, and exit oracles.
+. "$(dirname "${BASH_SOURCE[0]}")/exec_bare.sh"
+
+# Work dir for the bare backend's per-arch stub + linked images.
+EXEC_BARE_WORK="${EXEC_BARE_WORK:-${EXEC_TARGET_MOUNT_ROOT:-${TMPDIR:-/tmp}/kit-exec-bare}}"
+
# Internal queue arrays. Each entry's tag is recorded alongside the
# rest so flush can split into per-target batched runs.
EXEC_TARGET_TAGS=()
@@ -213,6 +221,8 @@ exec_target_supported() {
os="$(_exec_target_os "$tag")"
# VM-backed OSes: qemu + a provisioned/reachable VM (see exec_vm.sh).
case "$os" in freebsd|windows) exec_vm_supported "$tag"; return $? ;; esac
+ # Freestanding: qemu-system bare-metal (see exec_bare.sh).
+ case "$os" in freestanding) exec_bare_supported "$(_exec_target_arch "$tag")"; return $? ;; esac
# macOS has no podman/qemu fallback — Mach-O exec requires a Darwin
# host with matching arch. Cross-OS exec (macOS-on-Linux) is not
# supported.
@@ -235,6 +245,17 @@ exec_target_run() {
local os qemu
os="$(_exec_target_os "$tag")"
case "$os" in freebsd|windows) exec_vm_run "$tag" "$exe" "$out" "$err"; return ;; esac
+ if [ "$os" = freestanding ]; then
+ # `exe` is the corpus object; exec_bare links it with the per-arch stub
+ # into a bootable image and runs it under qemu-system.
+ local barch="$(_exec_target_arch "$tag")"
+ exec_bare_setup "$barch" "$EXEC_BARE_WORK" >/dev/null 2>&1
+ exec_bare_run "$barch" "$exe" "$EXEC_BARE_WORK" "$EXEC_BARE_WORK/.run.rc" >/dev/null 2>&1 \
+ && RUN_RC="$(cat "$EXEC_BARE_WORK/.run.rc" 2>/dev/null || echo 127)" \
+ || RUN_RC=127
+ : > "$out"; : > "$err"
+ return
+ fi
if _exec_target_native "$tag"; then
"$exe" >"$out" 2>"$err"; RUN_RC=$?; return
fi
@@ -289,6 +310,7 @@ exec_target_queue_size() { echo "${#EXEC_TARGET_EXES[@]}"; }
exec_target_setup() {
case "$(_exec_target_os "$1")" in
freebsd|windows) exec_vm_setup "$1" ;;
+ freestanding) exec_bare_setup "$(_exec_target_arch "$1")" "$EXEC_BARE_WORK" ;;
*) return 0 ;;
esac
}
@@ -312,6 +334,18 @@ _exec_target_flush_tag() {
# otherwise try to run a FreeBSD/Windows binary under a Linux runner.
case "$os" in
freebsd|windows) exec_vm_flush_tag "$tag" "${idx[@]}"; return $? ;;
+ freestanding)
+ # Bare-metal: each queued "exe" is a corpus object; link + boot it.
+ local barch bk
+ barch="$(_exec_target_arch "$tag")"
+ exec_bare_setup "$barch" "$EXEC_BARE_WORK" >/dev/null 2>&1
+ for bk in "${idx[@]}"; do
+ exec_bare_run "$barch" "${EXEC_TARGET_EXES[$bk]}" "$EXEC_BARE_WORK" \
+ "${EXEC_TARGET_RCS[$bk]}" >/dev/null 2>&1 \
+ || echo 127 > "${EXEC_TARGET_RCS[$bk]}"
+ : > "${EXEC_TARGET_OUTS[$bk]}"; : > "${EXEC_TARGET_ERRS[$bk]}"
+ done
+ return 0 ;;
esac
local k
diff --git a/test/lib/exec_vm.sh b/test/lib/exec_vm.sh
@@ -93,6 +93,11 @@ exec_vm_supported() {
if [ "$os" = freebsd ]; then
command -v "$(_exec_vm_fbsd_qemu "$vmarch")" >/dev/null 2>&1 || return 1
[ -f "$EXEC_VM_ROOT/build/freebsd-vm/images/freebsd-$vmarch.provisioned" ] && return 0
+ # A durable golden disk in the cache is bootable via `freebsd_vm.sh run`
+ # even when the working disk / build marker is absent (e.g. after a clean
+ # build). exec_vm_setup restores + boots it lazily.
+ ls "${XDG_CACHE_HOME:-$HOME/.cache}"/kit/freebsd-vm/*/freebsd-"$vmarch"-golden.qcow2 \
+ >/dev/null 2>&1 && return 0
_exec_vm_reachable freebsd "$vmarch" && return 0
return 1
fi
diff --git a/test/libc/glibc/Containerfile b/test/libc/glibc/Containerfile
@@ -11,10 +11,9 @@
# 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. kit ld doesn't parse linker scripts, so we copy
-# libc.so.6 directly and hand it to the linker by path.
+# dynamic exe — atexit, __stack_chk_fail_local, etc.). We write a
+# libc.so linker script that GROUPs them so `clang -lc` resolves
+# correctly; kit ld links libc.so.6 by path and ignores the script.
# - 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
@@ -22,9 +21,9 @@
# - 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.
+# /usr/include/aarch64-linux-gnu (bits/libc-header-start.h etc.).
+# The output tree follows the standard FHS layout under /sysroot/
+# so both `kit` and `clang --sysroot` work against it.
FROM docker.io/arm64v8/debian:bookworm-slim
# libc6-dev: Scrt1.o, crti.o, crtn.o, libc_nonshared.a + glibc headers
@@ -35,27 +34,38 @@ FROM docker.io/arm64v8/debian:bookworm-slim
# 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.
+# gcc: pulled in for its *runtime only* (libgcc + crt{begin,end}*.o), staged
+# below so this is a COMPLETE toolchain sysroot — a standard `clang --sysroot`
+# cross-link works against it, not just kit. (kit still links its own rt/ for the
+# soft-float / TF / 128-bit-int helpers; only gcc's runtime objects are copied.)
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libc6-dev \
linux-libc-dev \
+ gcc \
&& 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.
+# Stage the artifacts into a standard FHS tree under /sysroot/ so the
+# result works with both `kit` and a plain `clang --sysroot` cross-link.
+# usr/include/ — full include tree; the multiarch subdir
+# (aarch64-linux-gnu/bits/libc-header-start.h etc.) is
+# preserved inside it so -isystem picks it up.
+# usr/lib/ — crt objects, libc_nonshared.a, shared SOs, gcc runtime,
+# and libc.so/libm.so linker scripts for -lc/-lm.
+# lib/ — dynamic loader (dereferenced, self-contained).
RUN set -eux; \
- mkdir -p /sysroot/lib /sysroot/include; \
+ mkdir -p /sysroot/usr/include /sysroot/usr/lib /sysroot/lib; \
+ cp -r /usr/include/. /sysroot/usr/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/
+ cp Scrt1.o crti.o crtn.o libc_nonshared.a /sysroot/usr/lib/; \
+ cp -L /lib/aarch64-linux-gnu/libc.so.6 /sysroot/usr/lib/libc.so.6; \
+ cp -L /lib/aarch64-linux-gnu/libm.so.6 /sysroot/usr/lib/libm.so.6; \
+ cp -L /lib/ld-linux-aarch64.so.1 /sysroot/lib/ld-linux-aarch64.so.1; \
+ printf 'GROUP ( libc.so.6 libc_nonshared.a )\n' > /sysroot/usr/lib/libc.so; \
+ printf 'GROUP ( libm.so.6 )\n' > /sysroot/usr/lib/libm.so; \
+ cp -aL /usr/lib/gcc /sysroot/usr/lib/gcc; \
+ cp -L "$(find /usr/lib /lib -name libgcc_s.so.1 | head -1)" /sysroot/usr/lib/libgcc_s.so.1; \
+ ln -sf libgcc_s.so.1 /sysroot/usr/lib/libgcc_s.so
# Pin the build for cache reuse and reproducibility audits.
RUN set -eux; \
diff --git a/test/libc/glibc/Containerfile.rv64 b/test/libc/glibc/Containerfile.rv64
@@ -11,27 +11,41 @@
# - runtime loader is /lib/ld-linux-riscv64-lp64d.so.1
FROM docker.io/riscv64/debian:trixie-slim
+# gcc: runtime only (libgcc + crt{begin,end}*.o), staged below for a complete
+# clang-usable sysroot — see ./Containerfile for the rationale.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libc6-dev \
linux-libc-dev \
+ gcc \
&& rm -rf /var/lib/apt/lists/*
+# Stage the artifacts into a standard FHS tree under /sysroot/ so the
+# result works with both `kit` and a plain `clang --sysroot` cross-link.
+# usr/include/ — full include tree (cp -rL to dereference symlinks; on
+# trixie linux-libc-dev stages uapi asm headers under
+# /usr/lib/linux/uapi/<arch>/asm and symlinks them from
+# /usr/include/<multiarch>/asm/ — plain cp -r leaves
+# those broken in the sysroot, so -L is required here;
+# bookworm ships real files so only rv64/trixie needs it).
+# The multiarch subdir (riscv64-linux-gnu/bits/...) is
+# preserved inside usr/include/ so -isystem picks it up.
+# usr/lib/ — crt objects, libc_nonshared.a, shared SOs, gcc runtime,
+# and libc.so/libm.so linker scripts for -lc/-lm.
+# lib/ — dynamic loader (dereferenced, self-contained).
RUN set -eux; \
- mkdir -p /sysroot/lib /sysroot/include; \
+ mkdir -p /sysroot/usr/include /sysroot/usr/lib /sysroot/lib; \
+ cp -rL /usr/include/. /sysroot/usr/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 Scrt1.o crti.o crtn.o libc_nonshared.a /sysroot/usr/lib/; \
+ cp -L /lib/riscv64-linux-gnu/libc.so.6 /sysroot/usr/lib/libc.so.6; \
+ cp -L /lib/riscv64-linux-gnu/libm.so.6 /sysroot/usr/lib/libm.so.6; \
cp -L /lib/ld-linux-riscv64-lp64d.so.1 /sysroot/lib/ld-linux-riscv64-lp64d.so.1; \
- # On Debian trixie, linux-libc-dev stages the uapi asm headers under
- # /usr/lib/linux/uapi/<arch>/asm and symlinks them from
- # /usr/include/<multiarch>/asm/. A plain `cp -r` preserves the
- # symlinks and they end up broken in the sysroot because
- # /usr/lib/linux is not copied. Use -L to dereference symlinks so
- # the extracted tree is self-contained — bookworm (the aa64
- # variant) ships real files, so this is rv64/trixie-specific.
- cp -rL /usr/include/. /sysroot/include/
+ printf 'GROUP ( libc.so.6 libc_nonshared.a )\n' > /sysroot/usr/lib/libc.so; \
+ printf 'GROUP ( libm.so.6 )\n' > /sysroot/usr/lib/libm.so; \
+ cp -aL /usr/lib/gcc /sysroot/usr/lib/gcc; \
+ cp -L "$(find /usr/lib /lib -name libgcc_s.so.1 | head -1)" /sysroot/usr/lib/libgcc_s.so.1; \
+ ln -sf libgcc_s.so.1 /sysroot/usr/lib/libgcc_s.so
RUN set -eux; \
{ \
diff --git a/test/libc/glibc/Containerfile.x64 b/test/libc/glibc/Containerfile.x64
@@ -9,20 +9,36 @@
# not /lib, unlike aarch64 + riscv64).
FROM docker.io/amd64/debian:bookworm-slim
+# gcc: runtime only (libgcc + crt{begin,end}*.o), staged below for a complete
+# clang-usable sysroot — see ./Containerfile for the rationale.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libc6-dev \
linux-libc-dev \
+ gcc \
&& rm -rf /var/lib/apt/lists/*
+# Stage the artifacts into a standard FHS tree under /sysroot/ so the
+# result works with both `kit` and a plain `clang --sysroot` cross-link.
+# usr/include/ — full include tree; the multiarch subdir
+# (x86_64-linux-gnu/bits/libc-header-start.h etc.) is
+# preserved inside it so -isystem picks it up.
+# usr/lib/ — crt objects, libc_nonshared.a, shared SOs, gcc runtime,
+# and libc.so/libm.so linker scripts for -lc/-lm.
+# lib64/ — dynamic loader (dereferenced, self-contained).
RUN set -eux; \
- mkdir -p /sysroot/lib /sysroot/lib64 /sysroot/include; \
+ mkdir -p /sysroot/usr/include /sysroot/usr/lib /sysroot/lib64; \
+ cp -r /usr/include/. /sysroot/usr/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/
+ cp Scrt1.o crti.o crtn.o libc_nonshared.a /sysroot/usr/lib/; \
+ cp -L /lib/x86_64-linux-gnu/libc.so.6 /sysroot/usr/lib/libc.so.6; \
+ cp -L /lib/x86_64-linux-gnu/libm.so.6 /sysroot/usr/lib/libm.so.6; \
+ cp -L /lib64/ld-linux-x86-64.so.2 /sysroot/lib64/ld-linux-x86-64.so.2; \
+ printf 'GROUP ( libc.so.6 libc_nonshared.a )\n' > /sysroot/usr/lib/libc.so; \
+ printf 'GROUP ( libm.so.6 )\n' > /sysroot/usr/lib/libm.so; \
+ cp -aL /usr/lib/gcc /sysroot/usr/lib/gcc; \
+ cp -L "$(find /usr/lib /lib -name libgcc_s.so.1 | head -1)" /sysroot/usr/lib/libgcc_s.so.1; \
+ ln -sf libgcc_s.so.1 /sysroot/usr/lib/libgcc_s.so
RUN set -eux; \
{ \
diff --git a/test/libc/glibc/run.sh b/test/libc/glibc/run.sh
@@ -11,10 +11,10 @@
# kit ld -pie \
# -dynamic-linker <loader> \
# -o case.exe \
-# $SYSROOT/lib/Scrt1.o $SYSROOT/lib/crti.o \
+# $SYSROOT/usr/lib/Scrt1.o $SYSROOT/usr/lib/crti.o \
# case.o \
-# $SYSROOT/lib/libc.so.6 $SYSROOT/lib/libc_nonshared.a $KIT_RT \
-# $SYSROOT/lib/crtn.o
+# $SYSROOT/usr/lib/libc.so.6 $SYSROOT/usr/lib/libc_nonshared.a $KIT_RT \
+# $SYSROOT/usr/lib/crtn.o
#
# Unlike musl, where ld-musl-<arch>.so.1 is the same file as libc, glibc's
# loader is a separate ELF — kit ld's default interp is musl, so we override
@@ -97,7 +97,7 @@ arch_target() {
}
arch_triple_include() {
- # The per-arch multi-arch include subdir under sysroot/include/.
+ # The per-arch multi-arch include subdir under sysroot/usr/include/.
case "$1" in
aa64) echo "aarch64-linux-gnu" ;;
x64) echo "x86_64-linux-gnu" ;;
@@ -246,9 +246,9 @@ kit_lane_D() {
# ---- compile ----
# Three -isystem layers, in order of precedence:
- # sysroot/include/ — glibc + linux-libc-dev headers
+ # sysroot/usr/include/ — glibc + linux-libc-dev headers
# (top-level uapi).
- # sysroot/include/<triple> — glibc multi-arch (bits/*,
+ # sysroot/usr/include/<triple> — glibc multi-arch (bits/*,
# gnu/stubs-lp64.h, ...);
# <features.h> reaches in.
# rt/include/ — kit's freestanding overlay
@@ -261,8 +261,8 @@ kit_lane_D() {
# accidentally pick up the host's compiler headers.
local cc_flags=(--target="$LC_TARGET" --sysroot="$LC_SYSROOT"
-nostdinc
- -isystem "$LC_SYSROOT/include"
- -isystem "$LC_SYSROOT/include/$LC_TRIPLE_INC"
+ -isystem "$LC_SYSROOT/usr/include"
+ -isystem "$LC_SYSROOT/usr/include/$LC_TRIPLE_INC"
-isystem "$ROOT/rt/include"
-fPIE -fpic -O0)
@@ -288,11 +288,11 @@ kit_lane_D() {
local link_cmd=("$KIT" "ld" -pie
-dynamic-linker "$LC_LOADER"
-o "$exe"
- "$LC_SYSROOT/lib/Scrt1.o" "$LC_SYSROOT/lib/crti.o"
+ "$LC_SYSROOT/usr/lib/Scrt1.o" "$LC_SYSROOT/usr/lib/crti.o"
"$obj"
- "$LC_SYSROOT/lib/libc.so.6" "$LC_SYSROOT/lib/libc_nonshared.a"
+ "$LC_SYSROOT/usr/lib/libc.so.6" "$LC_SYSROOT/usr/lib/libc_nonshared.a"
"$LC_RT"
- "$LC_SYSROOT/lib/crtn.o")
+ "$LC_SYSROOT/usr/lib/crtn.o")
if ! "${link_cmd[@]}" >"$work/link.out" 2>"$work/link.err"; then
kit_fail "$label (link)"
diff --git a/test/libc/musl/Containerfile b/test/libc/musl/Containerfile
@@ -14,24 +14,35 @@ FROM docker.io/arm64v8/alpine:3.20.10
# 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 (build/rt/aarch64-linux/libkit_rt.a).
-RUN apk add --no-cache musl-dev linux-headers
+# gcc: pulled in for its *runtime only* (libgcc + crt{begin,end}*.o), staged
+# below so this is a COMPLETE toolchain sysroot — a standard `clang --sysroot`
+# cross-link works against it, not just kit. (kit still links its own rt/ for the
+# soft-float / TF / 128-bit-int helpers; only gcc's runtime objects are copied
+# into the sysroot, not the compiler.)
+RUN apk add --no-cache musl-dev linux-headers gcc
-# 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/
+# Stage a standard FHS sysroot so both kit and a plain `clang --sysroot`
+# cross-link can consume it:
+# /sysroot/usr/include/ — libc + linux-headers
+# /sysroot/usr/lib/ — crt objects, static libc, ssp, gcc runtime
+# /sysroot/lib/ — dynamic loader (in musl, this IS libc.so)
+# cp -aL dereferences the alpine triple symlink + libgcc_s links so the
+# extracted tree is self-contained. find locates libgcc_s.so.1 (musl: /usr/lib).
+RUN set -eux; \
+ mkdir -p /sysroot/usr/include /sysroot/usr/lib /sysroot/lib; \
+ cp -r /usr/include/. /sysroot/usr/include/; \
+ cp /usr/lib/crt1.o /usr/lib/Scrt1.o /usr/lib/rcrt1.o \
+ /usr/lib/crti.o /usr/lib/crtn.o \
+ /usr/lib/libc.a /usr/lib/libssp_nonshared.a \
+ /sysroot/usr/lib/; \
+ cp /lib/ld-musl-aarch64.so.1 /sysroot/lib/; \
+ ln -s ../../lib/ld-musl-aarch64.so.1 /sysroot/usr/lib/libc.so; \
+ cp -aL /usr/lib/gcc /sysroot/usr/lib/gcc; \
+ cp -L "$(find /usr/lib /lib -name libgcc_s.so.1 | head -1)" /sysroot/usr/lib/libgcc_s.so.1; \
+ ln -sf libgcc_s.so.1 /sysroot/usr/lib/libgcc_s.so; \
+ for d in /sysroot/usr/lib/gcc/*-alpine-linux-musl; do \
+ ln -sfn "$(basename "$d")" "${d%-alpine-linux-musl}-linux-musl"; \
+ done
# Pin the build for cache reuse and reproducibility audits.
RUN echo "alpine 3.20.10 musl 1.2.5-r3" > /sysroot/PROVENANCE \
diff --git a/test/libc/musl/Containerfile.rv64 b/test/libc/musl/Containerfile.rv64
@@ -14,19 +14,32 @@
# visible in test failures.
FROM docker.io/riscv64/alpine:edge
-RUN apk add --no-cache musl-dev linux-headers
+# gcc: runtime only (libgcc + crt{begin,end}*.o), staged below for a complete
+# clang-usable sysroot — see ./Containerfile for the rationale.
+RUN apk add --no-cache musl-dev linux-headers gcc
-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/
+# Stage a standard FHS sysroot so both kit and a plain `clang --sysroot`
+# cross-link can consume it:
+# /sysroot/usr/include/ — libc + linux-headers
+# /sysroot/usr/lib/ — crt objects, static libc, ssp, gcc runtime
+# /sysroot/lib/ — dynamic loader (in musl, this IS libc.so)
+# cp -aL dereferences the alpine triple symlink + libgcc_s links so the
+# extracted tree is self-contained. find locates libgcc_s.so.1 (musl: /usr/lib).
+RUN set -eux; \
+ mkdir -p /sysroot/usr/include /sysroot/usr/lib /sysroot/lib; \
+ cp -r /usr/include/. /sysroot/usr/include/; \
+ cp /usr/lib/crt1.o /usr/lib/Scrt1.o /usr/lib/rcrt1.o \
+ /usr/lib/crti.o /usr/lib/crtn.o \
+ /usr/lib/libc.a /usr/lib/libssp_nonshared.a \
+ /sysroot/usr/lib/; \
+ cp /lib/ld-musl-riscv64.so.1 /sysroot/lib/; \
+ ln -s ../../lib/ld-musl-riscv64.so.1 /sysroot/usr/lib/libc.so; \
+ cp -aL /usr/lib/gcc /sysroot/usr/lib/gcc; \
+ cp -L "$(find /usr/lib /lib -name libgcc_s.so.1 | head -1)" /sysroot/usr/lib/libgcc_s.so.1; \
+ ln -sf libgcc_s.so.1 /sysroot/usr/lib/libgcc_s.so; \
+ for d in /sysroot/usr/lib/gcc/*-alpine-linux-musl; do \
+ ln -sfn "$(basename "$d")" "${d%-alpine-linux-musl}-linux-musl"; \
+ done
RUN sh -c 'set -eu; \
musl_ver=$(apk info -v musl-dev | head -1); \
diff --git a/test/libc/musl/Containerfile.x64 b/test/libc/musl/Containerfile.x64
@@ -9,19 +9,32 @@
# libc.so so kit ld can link against it as a shared input).
FROM docker.io/amd64/alpine:3.20.10
-RUN apk add --no-cache musl-dev linux-headers
+# gcc: runtime only (libgcc + crt{begin,end}*.o), staged below for a complete
+# clang-usable sysroot — see ./Containerfile for the rationale.
+RUN apk add --no-cache musl-dev linux-headers gcc
-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/
+# Stage a standard FHS sysroot so both kit and a plain `clang --sysroot`
+# cross-link can consume it:
+# /sysroot/usr/include/ — libc + linux-headers
+# /sysroot/usr/lib/ — crt objects, static libc, ssp, gcc runtime
+# /sysroot/lib/ — dynamic loader (in musl, this IS libc.so)
+# cp -aL dereferences the alpine triple symlink + libgcc_s links so the
+# extracted tree is self-contained. find locates libgcc_s.so.1 (musl: /usr/lib).
+RUN set -eux; \
+ mkdir -p /sysroot/usr/include /sysroot/usr/lib /sysroot/lib; \
+ cp -r /usr/include/. /sysroot/usr/include/; \
+ cp /usr/lib/crt1.o /usr/lib/Scrt1.o /usr/lib/rcrt1.o \
+ /usr/lib/crti.o /usr/lib/crtn.o \
+ /usr/lib/libc.a /usr/lib/libssp_nonshared.a \
+ /sysroot/usr/lib/; \
+ cp /lib/ld-musl-x86_64.so.1 /sysroot/lib/; \
+ ln -s ../../lib/ld-musl-x86_64.so.1 /sysroot/usr/lib/libc.so; \
+ cp -aL /usr/lib/gcc /sysroot/usr/lib/gcc; \
+ cp -L "$(find /usr/lib /lib -name libgcc_s.so.1 | head -1)" /sysroot/usr/lib/libgcc_s.so.1; \
+ ln -sf libgcc_s.so.1 /sysroot/usr/lib/libgcc_s.so; \
+ for d in /sysroot/usr/lib/gcc/*-alpine-linux-musl; do \
+ ln -sfn "$(basename "$d")" "${d%-alpine-linux-musl}-linux-musl"; \
+ done
RUN echo "alpine 3.20.10 musl 1.2.5-r3" > /sysroot/PROVENANCE \
&& uname -m >> /sysroot/PROVENANCE
diff --git a/test/libc/musl/run.sh b/test/libc/musl/run.sh
@@ -7,17 +7,17 @@
#
# S static — non-PIC object + libc.a, classic static-exe link
# kit ld -static -o case.exe \
-# $SYSROOT/lib/crt1.o $SYSROOT/lib/crti.o \
+# $SYSROOT/usr/lib/crt1.o $SYSROOT/usr/lib/crti.o \
# case.o \
-# $SYSROOT/lib/libc.a $KIT_RT \
-# $SYSROOT/lib/crtn.o
+# $SYSROOT/usr/lib/libc.a $KIT_RT \
+# $SYSROOT/usr/lib/crtn.o
#
# D dynamic — PIE object + libc.so, expects PT_INTERP /lib/ld-musl-<arch>.so.1
# kit ld -pie -o case.exe \
-# $SYSROOT/lib/Scrt1.o $SYSROOT/lib/crti.o \
+# $SYSROOT/usr/lib/Scrt1.o $SYSROOT/usr/lib/crti.o \
# case.o \
-# $SYSROOT/lib/libc.so $KIT_RT \
-# $SYSROOT/lib/crtn.o
+# $SYSROOT/usr/lib/libc.so $KIT_RT \
+# $SYSROOT/usr/lib/crtn.o
# (musl ships ld-musl-<arch>.so.1 *as* libc — same file. The harness
# intentionally has no -dynamic-linker flag yet because kit ld
# currently doesn't accept one; this is one of the gaps we expect the
@@ -209,10 +209,10 @@ libc_run_variant() {
# ---- compile ----
# -nostdinc strips clang's default include path (resource dir +
# /usr/include) so the sysroot's musl + linux-headers tree is the sole
- # source. -isystem $sysroot/include picks it up.
+ # source. -isystem $sysroot/usr/include picks it up.
local cc_flags=(--target="$LC_TARGET" --sysroot="$LC_SYSROOT"
-nostdinc
- -isystem "$LC_SYSROOT/include"
+ -isystem "$LC_SYSROOT/usr/include"
-O0)
case "$variant" in
static) cc_flags+=(-fno-PIC -fno-pie) ;;
@@ -238,10 +238,10 @@ libc_run_variant() {
# long-double formatting. Archive ingestion iterates demand-load
# to a fixed point so one trailing libkit_rt.a is enough.
link_cmd=("$KIT" "ld" -static -o "$exe"
- "$LC_SYSROOT/lib/crt1.o" "$LC_SYSROOT/lib/crti.o"
+ "$LC_SYSROOT/usr/lib/crt1.o" "$LC_SYSROOT/usr/lib/crti.o"
"$obj"
- "$LC_SYSROOT/lib/libc.a" "$LC_RT"
- "$LC_SYSROOT/lib/crtn.o")
+ "$LC_SYSROOT/usr/lib/libc.a" "$LC_RT"
+ "$LC_SYSROOT/usr/lib/crtn.o")
;;
dynamic)
# Dynamic-exe link: PIE start file, libc.so as a *shared* input
@@ -254,10 +254,10 @@ libc_run_variant() {
# libkit_rt.a stays — soft-float TF helpers are still
# static-bound from our side. crti/crtn are unchanged.
link_cmd=("$KIT" "ld" -pie -o "$exe"
- "$LC_SYSROOT/lib/Scrt1.o" "$LC_SYSROOT/lib/crti.o"
+ "$LC_SYSROOT/usr/lib/Scrt1.o" "$LC_SYSROOT/usr/lib/crti.o"
"$obj"
- "$LC_SYSROOT/lib/libc.so" "$LC_RT"
- "$LC_SYSROOT/lib/crtn.o")
+ "$LC_SYSROOT/usr/lib/libc.so" "$LC_RT"
+ "$LC_SYSROOT/usr/lib/crtn.o")
;;
esac
diff --git a/test/parse/run.sh b/test/parse/run.sh
@@ -89,7 +89,7 @@ case "$KIT_TEST_ARCH" in
x64|x86_64|amd64) TEST_ARCH=x64; CLANG_TRIPLE=x86_64-linux-gnu; EXEC_ARCH=x64 ;;
rv64|riscv64) TEST_ARCH=rv64; CLANG_TRIPLE=riscv64-linux-gnu; EXEC_ARCH=rv64 ;;
# rv32 is freestanding: the E lane runs bare-metal under qemu-system-riscv32
- # via exec_rv32_bare.sh, not exec_target's qemu-user path. CLANG_TRIPLE is
+ # via exec_bare (rv32), not exec_target's qemu-user path. CLANG_TRIPLE is
# only for clang probes; the kit target comes from KIT_TEST_ARCH.
rv32|riscv32) TEST_ARCH=rv32; CLANG_TRIPLE=riscv32-unknown-elf; EXEC_ARCH=rv32 ;;
*) printf 'unknown KIT_TEST_ARCH=%s\n' "$KIT_TEST_ARCH" >&2; exit 2 ;;
@@ -100,7 +100,7 @@ case "$TEST_ARCH" in
aa64) RT_AR="$ROOT/build/rt/aarch64-linux/libkit_rt.a" ;;
x64) RT_AR="$ROOT/build/rt/x86_64-linux/libkit_rt.a" ;;
rv64) RT_AR="$ROOT/build/rt/riscv64-linux/libkit_rt.a" ;;
- rv32) RT_AR="$ROOT/build/rt/riscv32-elf-hardfloat/libkit_rt.a" ;; # used by exec_rv32_bare
+ rv32) RT_AR="$ROOT/build/rt/riscv32-elf-hardfloat/libkit_rt.a" ;; # used by exec_bare (rv32)
esac
RT_LINK_ARGS=()
if [ -f "$RT_AR" ]; then
@@ -186,14 +186,11 @@ export have_qemu have_podman is_aarch64 QEMU_BIN EXEC_TARGET_MOUNT_ROOT
# shellcheck source=../lib/exec_target.sh
source "$ROOT/test/lib/exec_target.sh"
-# rv32 is freestanding: the E lane runs bare-metal under qemu-system-riscv32.
+# rv32 is freestanding: the E lane runs bare-metal under qemu-system-riscv32 via
+# the exec_bare backend (sourced by exec_target.sh).
if [ "$TEST_ARCH" = "rv32" ]; then
# The parse corpus's entry is test_main() (path C bridges main->test_main).
- RV32_BARE_ENTRY=test_main
- export RV32_BARE_ENTRY
- # shellcheck source=../lib/exec_rv32_bare.sh
- . "$ROOT/test/lib/exec_rv32_bare.sh"
- rv32_bare_setup "$BUILD_DIR/rv32"
+ if exec_bare_setup rv32 "$BUILD_DIR/rv32" test_main; then RV32_BARE_OK=1; else RV32_BARE_OK=0; fi
fi
# ---- harness binaries ------------------------------------------------------
@@ -361,7 +358,7 @@ kit_lane_R() {
kit_lane_E() {
# rv32: freestanding bare-metal. parse-runner --emit -> kit ld with a startup
# that calls main() and reports its return via a SiFive finisher -> run under
- # qemu-system-riscv32 (test/lib/exec_rv32_bare.sh). The qemu exit equals
+ # qemu-system-riscv32 (test/lib/exec_bare.sh). The qemu exit equals
# main()'s return, so the corpus rc==expected oracle applies. Gaps stay RED.
if [ "$TEST_ARCH" = "rv32" ]; then
local exp_byte rc reason t0 dt run_rc
@@ -372,7 +369,7 @@ kit_lane_E() {
_parse_emit_obj || return
exp_byte=$(( KIT_EXPECTED & 0xff ))
t0=$(kit_now_ms)
- reason="$(rv32_bare_run "$PARSE_OBJ" "$KIT_WORK" "$KIT_WORK/exec.rc")"
+ reason="$(exec_bare_run rv32 "$PARSE_OBJ" "$KIT_WORK" "$KIT_WORK/exec.rc")"
run_rc=$?
dt=$(( $(kit_now_ms) - t0 ))
kit_time E "$dt"
diff --git a/test/toy/run.sh b/test/toy/run.sh
@@ -399,7 +399,7 @@ cross_one_rv32() {
fi
if [ -s "$cc_err" ]; then
kit_fail "$label" "kit cc rv32 wrote stderr"; sed 's/^/ | /' "$cc_err"; return; fi
- reason="$(rv32_bare_run "$obj" "$KIT_WORK" "$KIT_WORK/rv32.rc")"
+ reason="$(exec_bare_run rv32 "$obj" "$KIT_WORK" "$KIT_WORK/rv32.rc")"
if [ $? -eq 2 ]; then kit_skip "$label" "$reason"; return; fi
rc="$(cat "$KIT_WORK/rv32.rc" 2>/dev/null || echo 99)"; exp=$(( KIT_EXPECTED & 255 ))
# Bare-metal has no stderr channel; the finisher maps main()'s return onto the
@@ -663,12 +663,12 @@ if [ "$RUN_X" -eq 1 ]; then
# shellcheck source=../lib/exec_target.sh
. "$ROOT/test/lib/exec_target.sh"
# rv32 is a cross arch in path X, but freestanding (qemu-system bare-metal),
- # so it uses its own exec helper rather than exec_target's qemu-user path.
+ # so it uses the exec_bare backend (sourced by exec_target.sh) rather than
+ # exec_target's qemu-user path: exec_bare links each object against a
+ # bare-metal stub + rt and boots it under qemu-system-riscv32.
case " $TOY_CROSS_ARCHS " in
*" rv32 "*)
- # shellcheck source=../lib/exec_rv32_bare.sh
- . "$ROOT/test/lib/exec_rv32_bare.sh"
- rv32_bare_setup "$BUILD_DIR/rv32" ;;
+ if exec_bare_setup rv32 "$BUILD_DIR/rv32"; then RV32_BARE_OK=1; else RV32_BARE_OK=0; fi ;;
esac
fi