kit

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

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: BUILD.md (the 3-stage self-build mechanism), plan/SYSROOTS.md (sysroot provisioning), WINDOWS.md.

Target identity and compiler capability come from the public registry exposed by kit targets. This document owns a narrower concern: which registered profiles have cross/self-host validation lanes, and how those lanes provision and execute. scripts/hosted.sh is the selector/provisioning authority for that test support set, not a second compiler target registry.

Model: two modes over one matrix

                         portability test support set (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:
  linux-{glibc,musl}-{aa64,x64,rv64}     (6)
  freebsd-{aarch64,amd64,riscv64}        (3)
  windows-{aarch64,x64}                  (2)
  android-aa64                           (1)   ← NativeActivity .so build/link
  macos-{aa64,x64}                       (2)
  freestanding-{aa64,x64,rv64,rv32,arm32} (5)  ← rv32/arm32 live here only

selfhost matrix: the same, minus freestanding and android, and filtered to
macOS host-compatible arch

Freestanding has no OS, so there is nothing to run a compiler on. Android currently has no device/emulator runner in the exec seam. Both participate in cross only; android-* is build/link-only unless RUN=0 is used or an Android runner is added.

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=…] [RUN=0|1]
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, RUN ?= 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 / android / 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

test-cross accepts:

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.

test-selfhost accepts smoke and full; it has no build-only coarse lane because its purpose is to run the newly built compiler on the 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

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

Test-matrix authority — scripts/hosted.sh

The public target registry owns canonical triples, architecture/OS/object identity, and compiler capabilities. scripts/hosted.sh selects the subset with portability lanes and owns their runner/provisioning metadata plus the triple/path/tag adapters. It provides:

The Makefile reads it via $(shell …); nothing else re-encodes the test matrix. Where a token maps to a compiler profile, its triple and capability must agree with kit targets; provisioning remains script-owned local state.

Two orchestrators

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:

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:

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 coarse smoke full
linux-{glibc,musl}-{aa64,x64,rv64} ready ready ready (toy X + parse E + libc)
macos-aarch64 ready ready (native) ready (native toy/parse)
freebsd-{aarch64,amd64,riscv64} ready ready (VM) ready (toy via VM)
windows-{aarch64,x64} ready ready (VM) ready (toy via VM)
freestanding-rv32 ready ready (bare) ready (toy X + parse E, bare)
freestanding-arm32 ready ready (bare, -O1) ready (toy X + parse E, bare)
freestanding-{aa64,x64,rv64} ready 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

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 logs that corpus-depth is not yet wired for the arch (it does not silently claim full coverage).