kit

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

commit 2c5b3f5a1c54ba790d00a2d17dea3685e8206e01
parent 2fd574b3dbb533dc05f32f8aaa52812579e193c8
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 17 Jun 2026 13:42:07 -0700

selfdist: hermetic test/dist suite (62 cases: install/flip/rollback/prune/verify-key/tamper/bootstrap/networked) + scripts/release.sh matrix driver; exempt update from --version banner shortcut

Diffstat:
Mdriver/main.c | 8++++++--
Mmk/test.mk | 10++++++++++
Ascripts/release.sh | 324+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/dist/run.sh | 34++++++++++++++++++++--------------
4 files changed, 360 insertions(+), 16 deletions(-)

diff --git a/driver/main.c b/driver/main.c @@ -243,8 +243,12 @@ static int dispatch(const char* name, int argc, char** argv, * own argument parser runs, so it works for both the symlink form * (argv[0] == "cc ... --version") and the subcommand form * ("kit cc ... --version", which also routes through dispatch). The scan - * stops at "--", so a flag forwarded to a JITed program / guest is safe. */ - if (driver_argv_wants_version(argc, argv)) { + * stops at "--", so a flag forwarded to a JITed program / guest is safe. + * `update` is exempt: there `--version <ver>` is a value-bearing flag + * (flip to an installed version), not a banner query, so the uniform + * shortcut would hijack it. */ + if (!driver_streq(tool->name, "update") && + driver_argv_wants_version(argc, argv)) { driver_print_version(tool->name); return 0; } diff --git a/mk/test.mk b/mk/test.mk @@ -75,6 +75,7 @@ TEST_TARGETS = \ test-driver-objdump \ test-driver-pkg \ test-dist \ + test-selfdist \ test-driver-strings \ test-driver-tools \ test-driver-wasm \ @@ -325,6 +326,15 @@ test-driver-objdump: bin test-driver-pkg: bin @KIT=$(abspath $(BIN)) sh test/pkg/run.sh +# test-selfdist: hermetic self-distribution suite — drives `kit update` over +# packages built with `kit pkg create` (the same operation `make dist` runs), +# entirely under a sandbox $KIT_HOME so it touches no real install. Covers +# offline install/flip/rollback/prune, explicit-key + wrong-key + tamper +# rejection, stock-minisign bootstrap verification, and the networked + +# monotonic channel-index path over file:// (self-skips if curl is absent). +test-selfdist: bin + @KIT=$(abspath $(BIN)) sh test/dist/run.sh + # test-extlink: prove kit-emitted objects link with the *system* linkers # (LLVM lld ELF/COFF/Mach-O personalities + Apple ld), not just kit's own # `ld`. Lanes self-skip when a linker / SDK is absent, so it is safe in the diff --git a/scripts/release.sh b/scripts/release.sh @@ -0,0 +1,324 @@ +#!/usr/bin/env bash +# scripts/release.sh — the multi-target kit release driver. +# +# This is the matrix counterpart to `make dist`: where `make dist` stages and +# signs the toolchain for the NATIVE host triple only (and falls back to the +# in-tree NON-RELEASE test key), release.sh does the same staging + packaging +# for EVERY hosted (runnable) triple in scripts/hosted.sh's support set, signs +# each artifact with the REAL release key, and finally emits + signs one +# `kit-release 1` channel index covering all of them. See doc/plan/SELFDIST.md +# ("Release artifacts", "Channel index format", and the scripts/release.sh work +# item) and mk/dist.mk for the per-target staging/packaging contract this +# mirrors. +# +# For each hosted target it: +# 1. cross-builds the kit binary for the target (scripts/kit_cross.sh) +# 2. builds the per-target rt archive(s) (make rt-<variant>, target codegen) +# 3. stages the same self-contained tree `make dist` stages +# 4. packages a signed fat .kpkg, a signed portable .tar.gz, and a detached +# .tar.gz.minisig (the stock-minisign bootstrap path) +# then assembles one signed channel index over the whole matrix. +# +# Freestanding targets have no OS to run on, so they produce NO kit release +# artifact (SELFDIST.md: "the release set is the hosted triples only"); they are +# excluded by design. +# +# Prerequisites +# ------------- +# * A REAL signing key in $KIT_SIGN_KEY (see below). Unlike `make dist`, this +# script never falls back to the in-tree test key — a release is signed with +# the real release secret or not at all. +# * Cross-build sysroots for every foreign-libc target, provisioned once with +# `make provision TARGET=<selector>` (doc/plan/SYSROOTS.md). kit_cross.sh +# errors (does not skip) when a requested target's sysroot is missing. +# * For the VM-validated targets (freebsd/windows) the usual VM/exec infra — +# drop them with KIT_VM=0 (below) on a VM-less CI runner. +# +# Environment knobs +# ----------------- +# KIT_SIGN_KEY (required) path to the real release minisign secret key. +# KIT_RELEASE_URL_BASE (optional) one or more space-separated mirror base URLs +# for the channel index `url` lines. Each artifact's URL +# is "<base>/<filename>". Defaults to a GitHub-Releases- +# style base: https://github.com/<org>/kit/releases/ +# download/v<VERSION>. Repeat (space-separated) for an +# ordered mirror list. +# KIT_RELEASE_TARGETS (optional) hosted.sh selector for the target set +# (default: all). Freestanding tokens are always dropped. +# KIT_VM (optional, default 1) 0 drops the VM-only targets +# (freebsd/windows) for a VM-less CI runner, same meaning +# as in scripts/hosted.sh. +# KIT_CHANNEL (optional, default "stable") channel name for the index +# and the index file basename (<channel>.index). +# KIT (optional) path to a prebuilt native kit to use as the +# packaging tool (default: the build/release/kit produced +# by `make RELEASE=1 bin`). + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" +HOSTED="$ROOT/scripts/hosted.sh" + +die() { printf 'release: %s\n' "$*" >&2; exit 1; } +log() { printf 'release: %s\n' "$*" >&2; } + +# ---- preconditions --------------------------------------------------------- +# A real signing key is mandatory; releasing with the test key is never valid. +[ -n "${KIT_SIGN_KEY:-}" ] || die \ + "KIT_SIGN_KEY is unset — set it to the real release minisign secret key. + (Unlike 'make dist', release.sh does NOT fall back to the in-tree test key.)" +[ -f "$KIT_SIGN_KEY" ] || die "KIT_SIGN_KEY=$KIT_SIGN_KEY is not a file" + +[ -f "$ROOT/VERSION" ] || die "no VERSION file at repo root" +VERSION="$(tr -d ' \t\r\n' < "$ROOT/VERSION")" +[ -n "$VERSION" ] || die "VERSION file is empty" + +CHANNEL="${KIT_CHANNEL:-stable}" + +# Default mirror base: GitHub-Releases-style download URL for this version. Can +# be overridden / extended (space-separated) via KIT_RELEASE_URL_BASE. +DEFAULT_URL_BASE="https://github.com/kit/kit/releases/download/v$VERSION" +URL_BASES="${KIT_RELEASE_URL_BASE:-$DEFAULT_URL_BASE}" + +OUT_DIR="$ROOT/build/release-dist" +INDEX_FILE="$OUT_DIR/$CHANNEL.index" + +# ---- the native packaging tool --------------------------------------------- +# The packaging steps (kit pkg create / sign) run on the HOST, so they need a +# host-runnable kit. Build the release native binary first and use it as the +# packaging tool. (Mirrors mk/dist.mk, which packages with build/release/kit.) +log "building native bootstrap kit (make RELEASE=1 bin)" +make RELEASE=1 bin +KIT="${KIT:-$ROOT/build/release/kit}" +[ -x "$KIT" ] || die "native kit not found at $KIT after 'make RELEASE=1 bin'" + +# ---- target set ------------------------------------------------------------ +# The release set is the hosted (runnable) triples: every target in the support +# set that has an OS to run on. scripts/hosted.sh's `expand --mode=cross` is the +# cross-buildable set (includes freestanding + non-host-arch macOS + android); +# we drop the freestanding tokens, which by design ship no kit artifact. KIT_VM +# is honored by hosted.sh itself (drops freebsd/windows when 0). +SELECTOR="${KIT_RELEASE_TARGETS:-all}" +TARGETS=() +while IFS= read -r tok; do + [ -n "$tok" ] || continue + case "$tok" in + freestanding-*) continue ;; # no host to run on → no release artifact + esac + TARGETS+=("$tok") +done < <(KIT_VM="${KIT_VM:-1}" bash "$HOSTED" expand "$SELECTOR" --mode=cross) + +[ "${#TARGETS[@]}" -gt 0 ] || die "no hosted targets selected (selector='$SELECTOR', KIT_VM=${KIT_VM:-1})" + +log "version=$VERSION channel=$CHANNEL targets=${TARGETS[*]}" + +# ---- per-target rt variant mapping ----------------------------------------- +# Map a hosted token to the mk/rt.mk RT_VARIANTS entry whose target triple +# matches. The rt variant set is keyed by (arch,os) and is libc-agnostic on +# Linux (musl and glibc share the *-linux rt), so both linux libcs collapse to +# the same variant. Windows x64's rt variant is the msvc-ABI `x86_64-pc-windows` +# (the only x86_64 windows rt in the table); Android maps to the dedicated +# `aarch64-linux-android` variant. (Source of truth: mk/rt.mk RT_VARIANTS.) +rt_variant_of() { + case "$1" in + linux-glibc-aa64|linux-musl-aa64) echo aarch64-linux ;; + linux-glibc-x64|linux-musl-x64) echo x86_64-linux ;; + linux-glibc-rv64|linux-musl-rv64) echo riscv64-linux ;; + freebsd-aa64) echo aarch64-freebsd ;; + freebsd-x64) echo x86_64-freebsd ;; + freebsd-rv64) echo riscv64-freebsd ;; + windows-aa64) echo aarch64-windows ;; + windows-x64) echo x86_64-pc-windows ;; + android-aa64) echo aarch64-linux-android ;; + macos-aa64) echo aarch64-apple-darwin ;; + macos-x64) echo x86_64-apple-darwin ;; + *) die "no rt variant mapping for token '$1' (extend rt_variant_of + mk/rt.mk)" ;; + esac +} + +# ---- output staging -------------------------------------------------------- +rm -rf "$OUT_DIR" +mkdir -p "$OUT_DIR" + +# Accumulate one index "[host]" block per target into a temp dir, one file per +# target named by triple, so the final index can be assembled in sorted (i.e. +# canonical, strictly-ascending-by-target) order regardless of build order. +BLOCK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/kit-release-blocks.XXXXXX")" +cleanup() { rm -rf "$BLOCK_DIR"; } +trap cleanup EXIT + +# Track artifacts produced, for the closing summary. +SUMMARY=() + +# ---- per-target build/stage/package ---------------------------------------- +build_target() { + local token="$1" + local triple sysroot rt_variant + triple="$(bash "$HOSTED" triple "$token")" || die "bad target '$token'" + sysroot="$(bash "$HOSTED" path "$token" 2>/dev/null || true)" + rt_variant="$(rt_variant_of "$token")" + + # macOS has no cross sysroot — compile against the host SDK. + local os="${token%%-*}" + if [ "$os" = macos ] && [ -z "$sysroot" ]; then + sysroot="$(xcrun --show-sdk-path 2>/dev/null || true)" + fi + + log "=== $token (triple=$triple rt=$rt_variant) ===" + + # 1) Cross-build the kit binary for the target. kit_cross.sh wipes its own + # build dir per run and writes the binary to build/kit-cross/kit/<token>/ + # (kit.exe for windows), so capture it immediately into the per-target + # staging tree before any later run can clobber it. + log "cross-building kit binary" + bash "$ROOT/scripts/kit_cross.sh" "$token" --cc=kit + local cross_dir="$ROOT/build/kit-cross/kit/$token" + local cross_bin bin_name + if [ "$os" = windows ]; then + cross_bin="$cross_dir/kit.exe"; bin_name="kit.exe" + else + cross_bin="$cross_dir/kit"; bin_name="kit" + fi + [ -x "$cross_bin" ] || die "$token: cross-built kit missing at $cross_bin" + + # 2) Build the per-target rt archive(s). These are TARGET objects, so they + # must be produced by a HOST-runnable kit emitting target code — i.e. the + # native packaging kit driven with -target/--sysroot — NOT the just-built + # foreign binary (which cannot execute on the build host). This matches + # mk/dist.mk, where the rt is built by the native `$(BIN) cc`. The cross + # binary is the SHIPPED compiler; this host kit is only the BUILD tool. + # Each target gets an isolated RT_BUILD_DIR so per-arch objects never mix. + log "building rt variant $rt_variant" + local rt_build_dir="$ROOT/build/release-rt/$token" + rm -rf "$rt_build_dir" + local rt_cc="$KIT cc -target $triple" + [ -n "$sysroot" ] && rt_cc="$rt_cc --sysroot $sysroot" + make "rt-$rt_variant" \ + BIN="$KIT" \ + RT_BUILD_DIR="$rt_build_dir" \ + RT_CC="$rt_cc" \ + RT_AR="$KIT ar" \ + RT_AS="$KIT as" + local rt_archive="$rt_build_dir/$rt_variant/libkit_rt.a" + [ -f "$rt_archive" ] || die "$token: rt archive missing at $rt_archive" + + # 3) Stage the self-contained tree, exactly as mk/dist.mk does: bin/kit[.exe], + # lib/libkit.a, include/, support/rt/{include,lib}, VERSION, README/license. + # libkit.a is the TARGET-arch static lib that kit_cross.sh produced next to + # the cross binary (its `make bin BUILD_DIR=$cross_dir` leaves both there), + # so the shipped lib matches the shipped binary. The rt archive is staged + # under support/rt/lib/ next to the rt sources, named for the target so a + # multi-arch tree stays unambiguous. + log "staging tree" + local stage="$OUT_DIR/stage/$triple" + local cross_lib="$cross_dir/libkit.a" + [ -f "$cross_lib" ] || die "$token: cross-built libkit.a missing at $cross_lib" + rm -rf "$stage" + mkdir -p "$stage/bin" "$stage/lib" "$stage/support/rt" + cp "$cross_bin" "$stage/bin/$bin_name" + cp "$cross_lib" "$stage/lib/libkit.a" + cp -r "$ROOT/include" "$stage/include" + cp -r "$ROOT/rt/include" "$stage/support/rt/include" + cp -r "$ROOT/rt/lib" "$stage/support/rt/lib" + cp "$ROOT/VERSION" "$stage/VERSION" + cp "$rt_archive" "$stage/support/rt/lib/libkit_rt-$rt_variant.a" + cp "$ROOT/README.md" "$stage/README.txt" 2>/dev/null || true + local f + for f in LICENSE LICENSE.txt LICENSE.md NOTICE NOTICE.txt; do + [ -f "$ROOT/$f" ] && cp "$ROOT/$f" "$stage/$f" + done + : # keep set -e happy after the optional copies + + # 4) Package + sign, mirroring mk/dist.mk's invocations (real key, no test-key + # fallback). Capture the package-id (`id <64hex>`) printed by pkg create. + local base="kit-$VERSION-$triple" + local kpkg="$OUT_DIR/$base.kpkg" + local targz="$OUT_DIR/$base.tar.gz" + + log "packaging $base.kpkg (fat)" + local kpkg_out kpkg_id + kpkg_out="$("$KIT" pkg create --name kit --version "$VERSION" \ + --format kpkg --native-shape fat -s "$KIT_SIGN_KEY" \ + --root "$stage" -o "$kpkg")" + printf '%s\n' "$kpkg_out" >&2 + kpkg_id="$(printf '%s\n' "$kpkg_out" | sed -n 's/.*id \([0-9a-f]\{64\}\).*/\1/p')" + [ -n "$kpkg_id" ] || die "$token: could not parse kpkg package-id from pkg create output" + + log "packaging $base.tar.gz (portable)" + local targz_out targz_id + targz_out="$("$KIT" pkg create --name kit --version "$VERSION" \ + --format tar.gz -s "$KIT_SIGN_KEY" \ + --root "$stage" -o "$targz")" + printf '%s\n' "$targz_out" >&2 + targz_id="$(printf '%s\n' "$targz_out" | sed -n 's/.*id \([0-9a-f]\{64\}\).*/\1/p')" + [ -n "$targz_id" ] || die "$token: could not parse tar.gz package-id from pkg create output" + + log "signing $base.tar.gz.minisig (detached, bootstrap path)" + "$KIT" pkg sign -s "$KIT_SIGN_KEY" \ + -o "$targz.minisig" --comment "kit $VERSION $triple" \ + "$targz" + + # kpkg byte size, for the index `size` field. + local kpkg_size + kpkg_size="$(wc -c < "$kpkg" | tr -d ' ')" + + # 5) Emit this target's index [host] block into the sort accumulator. The + # block file is named by triple so the final assembly sorts canonically. + # Index field order matches test/api/release_index_test.c's golden: + # target, kpkg, targz, size, then one url line per mirror base. + { + printf '[host]\n' + printf 'target = %s\n' "$triple" + printf 'kpkg = %s\n' "$kpkg_id" + printf 'targz = %s\n' "$targz_id" + printf 'size = %s\n' "$kpkg_size" + for base_url in $URL_BASES; do + printf 'url = %s/%s.kpkg\n' "${base_url%/}" "$base" + done + } > "$BLOCK_DIR/$triple" + + SUMMARY+=("$triple: kpkg=$kpkg ($kpkg_size bytes, id ${kpkg_id:0:12}…) targz=$targz minisig=$targz.minisig") +} + +for token in "${TARGETS[@]}"; do + build_target "$token" +done + +# ---- channel index --------------------------------------------------------- +# Assemble the canonical `kit-release 1` index: a fixed header, a blank line, +# then the [host] blocks sorted ASCENDING by target triple (so the file is +# byte-stable and parseable by kit_release_index_parse, which requires strictly +# ascending unique targets). Blocks are separated by a blank line. The block +# files are named by triple, so `sort` over the directory listing yields the +# canonical order. +log "emitting channel index $INDEX_FILE" +{ + printf 'kit-release 1\n' + printf 'channel = %s\n' "$CHANNEL" + printf 'version = %s\n' "$VERSION" + printf 'hash = blake2b-256\n' + first=1 + while IFS= read -r triple; do + [ -n "$triple" ] || continue + printf '\n' # blank line before each [host] block + cat "$BLOCK_DIR/$triple" + first=0 + done < <(find "$BLOCK_DIR" -type f -exec basename {} \; | LC_ALL=C sort) + : "$first" +} > "$INDEX_FILE" + +log "signing channel index $INDEX_FILE.minisig" +"$KIT" pkg sign -s "$KIT_SIGN_KEY" \ + -o "$INDEX_FILE.minisig" --comment "kit-release $CHANNEL $VERSION" \ + "$INDEX_FILE" + +# ---- summary --------------------------------------------------------------- +printf '\nrelease: %s channel %s, version %s — %d target(s)\n' \ + "DONE" "$CHANNEL" "$VERSION" "${#TARGETS[@]}" >&2 +for line in "${SUMMARY[@]}"; do + printf 'release: %s\n' "$line" >&2 +done +printf 'release: index=%s\n' "$INDEX_FILE" >&2 +printf 'release: index.sig=%s\n' "$INDEX_FILE.minisig" >&2 +printf 'release: all artifacts under %s\n' "$OUT_DIR" >&2 diff --git a/test/dist/run.sh b/test/dist/run.sh @@ -48,22 +48,21 @@ kh= kit_home() { kh="$work/$1"; export KIT_HOME="$kh"; } # stage_kpkg VER : build a signed fat kit kpkg containing bin/kit (= $KIT), a -# VERSION file, and a small lib/ payload. Echoes the kpkg path; records the -# package id in $LAST_KPKG_ID for the channel-index cases. -LAST_KPKG_ID= +# VERSION file, and a small lib/ payload. Records a pass/fail through run_ok and +# leaves the kpkg path in $LAST_KPKG (NOT echoed — calling this in a command +# substitution would swallow the run_ok PASS line into the captured value). +LAST_KPKG= stage_kpkg() { ver=$1 sdir="$work/stage/$ver" - out="$work/pkg/kit-$ver.kpkg" + LAST_KPKG="$work/pkg/kit-$ver.kpkg" mkdir -p "$sdir/bin" "$sdir/lib" cp "$KIT" "$sdir/bin/kit" printf '%s\n' "$ver" > "$sdir/VERSION" printf 'self-distribution payload for %s\n' "$ver" > "$sdir/lib/data.txt" run_ok "create-$ver" "$KIT" pkg create \ --name kit --version "$ver" --format kpkg --native-shape fat \ - -s "$SECKEY" --root "$sdir" -o "$out" - LAST_KPKG_ID=$(first_hex_id "$work/create-$ver.out") - printf '%s\n' "$out" + -s "$SECKEY" --root "$sdir" -o "$LAST_KPKG" } # current_is NAME KH_DIR WANT : assert $KH_DIR/current resolves to versions/WANT. @@ -77,9 +76,12 @@ current_is() { fi } -# version_runs NAME EXE WANT_VER : run EXE --version and assert WANT_VER appears. -version_runs() { - name=$1; exe=$2; wantver=$3 +# runs_kit NAME EXE [WANTVER] : run EXE --version; assert it executes and prints +# a "kit ..." banner. WANTVER (optional) additionally requires that string. (All +# staged packages bundle the SAME test binary, so per-version banner text is +# only asserted where that binary's own version is known to match.) +runs_kit() { + name=$1; exe=$2; wantver=${3:-kit } if "$exe" --version > "$work/$name.out" 2>&1 && grep -F "$wantver" "$work/$name.out" >/dev/null 2>&1; then ok "$name" @@ -102,7 +104,8 @@ host_triple() { # (a) offline install of 2026.6.0 # =========================================================================== # kit_home kh -kpkg_60=$(stage_kpkg 2026.6.0) +stage_kpkg 2026.6.0 +kpkg_60="$LAST_KPKG" run_ok "a-install-60" "$KIT" update "$kpkg_60" contains "a-install-60-msg" "$work/a-install-60.out" "installed kit 2026.6.0" @@ -112,15 +115,18 @@ assert_file_exists "a-link-kit" "$kh/bin/kit" assert_file_exists "a-link-cc" "$kh/bin/cc" is_executable "a-link-kit-exec" "$kh/bin/kit" is_executable "a-link-cc-exec" "$kh/bin/cc" -version_runs "a-link-kit-runs" "$kh/bin/kit" 2026.6.0 +runs_kit "a-link-kit-runs" "$kh/bin/kit" "kit 2026.6.0" +contains "a-version-file" "$kh/versions/2026.6.0/VERSION" "2026.6.0" # =========================================================================== # # (b) second install + atomic flip; --list marks the new current # =========================================================================== # -kpkg_61=$(stage_kpkg 2026.6.1) +stage_kpkg 2026.6.1 +kpkg_61="$LAST_KPKG" run_ok "b-install-61" "$KIT" update "$kpkg_61" current_is "b-current-61" "$kh" 2026.6.1 -version_runs "b-link-kit-runs-61" "$kh/bin/kit" 2026.6.1 +runs_kit "b-link-kit-runs-61" "$kh/bin/kit" +contains "b-version-file-61" "$kh/versions/2026.6.1/VERSION" "2026.6.1" run_ok "b-list" "$KIT" update --list contains "b-list-has-60" "$work/b-list.out" "2026.6.0" contains "b-list-star-61" "$work/b-list.out" "* 2026.6.1"