kit

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

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:
MMakefile | 4++++
Adoc/plan/PORT.md | 262+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdriver/cmd/cc.c | 34++++++++++++++++++++++++++++++++++
Mdriver/env/windows.c | 74++++++++++++++++++++++++++++++++++++--------------------------------------
Mdriver/lib/hosted.c | 16++++++++++++----
Mmk/bootstrap.mk | 26++++++--------------------
Amk/port.mk | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmk/test.mk | 62++++++++++++++++----------------------------------------------
Ascripts/cross_test.sh | 217+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/freebsd_bootstrap.sh | 4++--
Mscripts/hosted.sh | 139++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Ascripts/kit_cross.sh | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascripts/kit_cross_verify.sh | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/linux_bootstrap.sh | 36++++++++++++++++++++++++++++--------
Mscripts/llvm_mingw_sysroot.sh | 13++++++++++++-
Ascripts/provision.sh | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascripts/selfhost.sh | 162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/cross/cases/exit.c | 5+++++
Atest/cross/cases/exit.expected | 1+
Atest/lib/exec_bare.sh | 457+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/lib/exec_kernel.sh | 65+++++++++++------------------------------------------------------
Mtest/lib/exec_target.sh | 34++++++++++++++++++++++++++++++++++
Mtest/lib/exec_vm.sh | 5+++++
Mtest/libc/glibc/Containerfile | 50++++++++++++++++++++++++++++++--------------------
Mtest/libc/glibc/Containerfile.rv64 | 38++++++++++++++++++++++++++------------
Mtest/libc/glibc/Containerfile.x64 | 28++++++++++++++++++++++------
Mtest/libc/glibc/run.sh | 22+++++++++++-----------
Mtest/libc/musl/Containerfile | 45++++++++++++++++++++++++++++-----------------
Mtest/libc/musl/Containerfile.rv64 | 37+++++++++++++++++++++++++------------
Mtest/libc/musl/Containerfile.x64 | 37+++++++++++++++++++++++++------------
Mtest/libc/musl/run.sh | 28++++++++++++++--------------
Mtest/parse/run.sh | 17+++++++----------
Mtest/toy/run.sh | 10+++++-----
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