commit 2fd574b3dbb533dc05f32f8aaa52812579e193c8
parent 0b2ac3d8b7b48659b680243876657755192b1f9a
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 17 Jun 2026 13:28:39 -0700
selfdist/doc: reconcile trusted_keys + sysroots paths to the single-root layout (DISTRIBUTE.md, SYSROOTS.md)
Diffstat:
3 files changed, 299 insertions(+), 5 deletions(-)
diff --git a/doc/DISTRIBUTE.md b/doc/DISTRIBUTE.md
@@ -186,10 +186,16 @@ recomputes the manifest hash and rejects the package if the trusted comment's
pkgid does not match. This binds the signature to the exact manifest content,
not merely to a name.
-Trust anchors live in a trusted-keys file (`$KIT_TRUSTED_KEYS`, else
-`$HOME/.config/kit/trusted_keys`; `src/dist/trust.c`), one
-`keyid pubkey label` line each. A `.pub` bundled inside a package is never
-trusted on its own. The verifier picks a key by the signature's key id:
+Trust anchors live in a trusted-keys file. Under the single-root layout
+(see [plan/SELFDIST.md](plan/SELFDIST.md)) the path is `$KIT_TRUSTED_KEYS`,
+else `$KIT_HOME/config/trusted_keys` (with `$KIT_HOME` defaulting to
+`$XDG_DATA_HOME/kit` ≡ `~/.local/share/kit`, `%LOCALAPPDATA%\kit` on Windows);
+the legacy `$HOME/.config/kit/trusted_keys` is still read when the preferred
+file is absent, so existing setups keep working. The path policy lives in the
+driver (`driver/cmd/pkg.c`); the file format helpers are path-agnostic in
+`src/dist/trust.c`. Each line is one `keyid pubkey label`. A `.pub` bundled
+inside a package is never trusted on its own. The verifier picks a key by the
+signature's key id:
```
-p PUBKEY verify against an explicitly supplied public key
diff --git a/doc/plan/SYSROOTS.md b/doc/plan/SYSROOTS.md
@@ -35,7 +35,15 @@ Freestanding targets require no sysroot kpkg; `libkit_rt.a` and
## Sysroot kpkg layout
-Each installed sysroot unpacks into `~/.kit/sysroots/<triple>/`:
+> Single-root layout note (see [SELFDIST.md](SELFDIST.md)): kit consolidates
+> everything it manages under one root, `$KIT_HOME` (default `$XDG_DATA_HOME/kit`
+> ≡ `~/.local/share/kit`). Installed sysroots therefore belong at
+> `$KIT_HOME/sysroots/<triple>/`. The build/test provisioning today caches them
+> under `$XDG_CACHE_HOME/kit/sysroots` (`KIT_SYSROOTS_DIR` in `mk/env.mk`); the
+> SYSROOTS implementation should adopt the `$KIT_HOME/sysroots` location when it
+> lands as a user-facing feature.
+
+Each installed sysroot unpacks into `$KIT_HOME/sysroots/<triple>/`:
```
<triple>/
diff --git a/test/dist/run.sh b/test/dist/run.sh
@@ -0,0 +1,280 @@
+#!/bin/sh
+# Hermetic self-distribution suite — exercises `kit update` over packages built
+# the same way `make dist` does (kit pkg create), without any network or any
+# dependency on `make dist`. Every install root lives under the sandbox, so the
+# whole flow is offline-first and reproducible (SOURCE_DATE_EPOCH pinned).
+#
+# Type-K mode-P harness: shared assert verbs route through the unified counters
+# over $work. SERIAL by design — the version layout under $KIT_HOME is mutated
+# in place across cases, so do not parallelize.
+#
+# Run by: make test-selfdist (KIT=<binary> sh test/dist/run.sh)
+
+set -u
+
+script_dir=$(cd "$(dirname "$0")" && pwd)
+repo_root=$(cd "$script_dir/../.." && pwd)
+
+KIT="${KIT:-$repo_root/build/kit}"
+if [ ! -x "$KIT" ]; then
+ echo "dist: kit binary not found at $KIT" >&2
+ exit 2
+fi
+
+SECKEY="$repo_root/test/dist/keys/nonrelease.key"
+PUBKEY="$repo_root/test/dist/keys/nonrelease.pub"
+
+work=$(mktemp -d "${TMPDIR:-/tmp}/kit-dist-test.XXXXXX")
+trap 'rm -rf "$work"' EXIT
+
+# HOME + KIT_HOME live inside the sandbox so nothing escapes $work and the host
+# user's real ~/.local/share/kit is never touched. KIT_HOME is repointed per
+# scenario via the kit_home() helper below; HOME stays put as a fallback root.
+HOME="$work/home"
+SOURCE_DATE_EPOCH=1
+export HOME SOURCE_DATE_EPOCH
+mkdir -p "$HOME" "$work/stage" "$work/pkg"
+
+KIT_KIT_DIR="$repo_root/test/lib"
+. "$repo_root/test/lib/kit_sh_kit.sh"
+kit_report_init
+
+# ---- local helpers ---------------------------------------------------------
+
+have_cmd() { command -v "$1" >/dev/null 2>&1; }
+
+# kit_home DIR : point every subsequent `kit update` at this install root.
+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=
+stage_kpkg() {
+ ver=$1
+ sdir="$work/stage/$ver"
+ out="$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"
+}
+
+# current_is NAME KH_DIR WANT : assert $KH_DIR/current resolves to versions/WANT.
+current_is() {
+ name=$1; kdir=$2; want=$3
+ got=$(readlink "$kdir/current" 2>/dev/null)
+ if [ "$got" = "versions/$want" ]; then ok "$name"
+ else
+ echo "current -> '$got', want 'versions/$want'" > "$work/$name.diag"
+ not_ok "$name" "$work/$name.diag"
+ fi
+}
+
+# version_runs NAME EXE WANT_VER : run EXE --version and assert WANT_VER appears.
+version_runs() {
+ name=$1; exe=$2; wantver=$3
+ if "$exe" --version > "$work/$name.out" 2>&1 &&
+ grep -F "$wantver" "$work/$name.out" >/dev/null 2>&1; then
+ ok "$name"
+ else
+ not_ok "$name" "$work/$name.out"
+ fi
+}
+
+flip_byte() {
+ file=$1; off=$2
+ printf '\377' | dd of="$file" bs=1 seek="$off" count=1 conv=notrunc >/dev/null 2>&1
+}
+
+# host_triple : 3rd field of `kit <ver> (<build>, <TRIPLE>)`.
+host_triple() {
+ "$KIT" --version 2>/dev/null | sed -n 's/^kit [^ ]* (.*, \(.*\))/\1/p' | sed -n '1p'
+}
+
+# =========================================================================== #
+# (a) offline install of 2026.6.0
+# =========================================================================== #
+kit_home kh
+kpkg_60=$(stage_kpkg 2026.6.0)
+
+run_ok "a-install-60" "$KIT" update "$kpkg_60"
+contains "a-install-60-msg" "$work/a-install-60.out" "installed kit 2026.6.0"
+current_is "a-current-60" "$kh" 2026.6.0
+assert_file_exists "a-versioned-bin" "$kh/versions/2026.6.0/bin/kit"
+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
+
+# =========================================================================== #
+# (b) second install + atomic flip; --list marks the new current
+# =========================================================================== #
+kpkg_61=$(stage_kpkg 2026.6.1)
+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
+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"
+
+# =========================================================================== #
+# (c) offline --version flip back to 2026.6.0 (no network)
+# =========================================================================== #
+run_ok "c-flip-60" "$KIT" update --version 2026.6.0
+contains "c-flip-60-msg" "$work/c-flip-60.out" "kit 2026.6.0 is now current"
+current_is "c-current-60" "$kh" 2026.6.0
+
+# =========================================================================== #
+# (d) --rollback from a known current to the previous version
+# =========================================================================== #
+# Make 2026.6.1 current first, then roll back to 2026.6.0 (the prior CalVer).
+run_ok "d-prep-flip-61" "$KIT" update --version 2026.6.1
+current_is "d-prep-current-61" "$kh" 2026.6.1
+run_ok "d-rollback" "$KIT" update --rollback
+current_is "d-current-60" "$kh" 2026.6.0
+contains "d-rollback-warn" "$work/d-rollback.out" "re-upgrade"
+
+# =========================================================================== #
+# (e) --prune removes every non-current version
+# =========================================================================== #
+# current is 2026.6.0; prune must drop versions/2026.6.1.
+run_ok "e-prune" "$KIT" update --prune
+contains "e-prune-msg" "$work/e-prune.out" "removed kit 2026.6.1"
+if [ -e "$kh/versions/2026.6.1" ]; then
+ echo "versions/2026.6.1 still present after prune" > "$work/e-pruned-gone.diag"
+ not_ok "e-pruned-gone" "$work/e-pruned-gone.diag"
+else
+ ok "e-pruned-gone"
+fi
+run_ok "e-list-after" "$KIT" update --list
+contains "e-list-only-60" "$work/e-list-after.out" "* 2026.6.0"
+if grep -F "2026.6.1" "$work/e-list-after.out" >/dev/null 2>&1; then
+ echo "2026.6.1 still listed after prune" > "$work/e-list-no-61.diag"
+ not_ok "e-list-no-61" "$work/e-list-no-61.diag"
+else
+ ok "e-list-no-61"
+fi
+
+# =========================================================================== #
+# (f) explicit-key verify paths (pkg verify + update --key)
+# =========================================================================== #
+run_ok "f-verify-pubkey" "$KIT" pkg verify -p "$PUBKEY" "$kpkg_60"
+contains "f-verify-name" "$work/f-verify-pubkey.out" "ok: kit 2026.6.0"
+kit_home kh-f
+run_ok "f-update-key" "$KIT" update --key "$PUBKEY" "$kpkg_60"
+current_is "f-current-60" "$work/kh-f" 2026.6.0
+
+# =========================================================================== #
+# (g) wrong key is rejected and leaves current untouched
+# =========================================================================== #
+# Fresh root with 2026.6.0 already current, then try installing 2026.6.1 with a
+# foreign key — must fail and keep current at 2026.6.0.
+kit_home kh-g
+run_ok "g-seed-60" "$KIT" update "$kpkg_60"
+current_is "g-seed-current-60" "$work/kh-g" 2026.6.0
+run_ok "g-keygen-other" "$KIT" pkg keygen -o "$work/other"
+run_fail "g-update-wrong-key" "$KIT" update --key "$work/other.pub" "$kpkg_61"
+current_is "g-current-unchanged" "$work/kh-g" 2026.6.0
+
+# =========================================================================== #
+# (h) tamper is rejected and leaves current untouched
+# =========================================================================== #
+# Reuse kh-g (current = 2026.6.0). Flip one byte well inside the kpkg.
+cp "$kpkg_61" "$work/pkg/tampered.kpkg"
+tsize=$(wc -c < "$work/pkg/tampered.kpkg")
+flip_byte "$work/pkg/tampered.kpkg" $((tsize / 2))
+run_fail "h-update-tampered" "$KIT" update "$work/pkg/tampered.kpkg"
+current_is "h-current-unchanged" "$work/kh-g" 2026.6.0
+
+# =========================================================================== #
+# (i) bootstrap: stock minisign verifies a kit-signed tar.gz
+# =========================================================================== #
+run_ok "i-create-targz" "$KIT" pkg create \
+ --name kit --version 2026.6.0 --format tar.gz --native-shape fat \
+ -s "$SECKEY" --root "$work/stage/2026.6.0" -o "$work/pkg/kit-boot.tar.gz"
+run_ok "i-sign-targz" "$KIT" pkg sign -s "$SECKEY" "$work/pkg/kit-boot.tar.gz"
+assert_file_exists "i-sig-written" "$work/pkg/kit-boot.tar.gz.minisig"
+if have_cmd minisign; then
+ run_ok "i-minisign-verify" minisign -Vm "$work/pkg/kit-boot.tar.gz" -p "$PUBKEY"
+else
+ skip_test "i-minisign-verify" "minisign not installed"
+fi
+
+# =========================================================================== #
+# (j) networked + monotonic via file:// channel index
+# =========================================================================== #
+# A signed package's trust comes from its own signature; the index is only a
+# fetch hint. We hand-write a `kit-release 1` index (format per
+# test/api/release_index_test.c) pointing at the local 2026.6.0 kpkg over a
+# file:// URL. If the fetch transport is unavailable here, the whole block is
+# skipped rather than failed.
+if ! have_cmd curl; then
+ skip_test "j-network" "curl not available for file:// fetch"
+else
+ triple=$(host_triple)
+ if [ -z "$triple" ]; then
+ skip_test "j-network" "could not determine host triple"
+ else
+ # Re-derive the 2026.6.0 kpkg id from its create output.
+ id60=$(first_hex_id "$work/create-2026.6.0.out")
+ idx="$work/channel-60.index"
+ {
+ printf 'kit-release 1\n'
+ printf 'channel = stable\n'
+ printf 'version = 2026.6.0\n'
+ printf 'hash = blake2b-256\n'
+ printf '\n'
+ printf '[host]\n'
+ printf 'target = %s\n' "$triple"
+ printf 'kpkg = %s\n' "$id60"
+ printf 'url = file://%s\n' "$kpkg_60"
+ } > "$idx"
+
+ # Probe the file:// transport against a fresh root before asserting:
+ # if it cannot fetch+install here, skip the block gracefully.
+ kit_home kh-j-probe
+ if "$KIT" update --index "file://$idx" > "$work/j-probe.out" 2>&1; then
+ current_is "j-probe-install-60" "$work/kh-j-probe" 2026.6.0
+
+ # --check reports the latest channel version, changing nothing.
+ kit_home kh-j-check
+ run_ok "j-check" "$KIT" update --index "file://$idx" --check
+ contains "j-check-latest" "$work/j-check.out" "latest 2026.6.0"
+ contains "j-check-none" "$work/j-check.out" "installed current: (none)"
+ if [ -e "$work/kh-j-check/current" ]; then
+ echo "--check created a current pointer" > "$work/j-check-nochange.diag"
+ not_ok "j-check-nochange" "$work/j-check-nochange.diag"
+ else
+ ok "j-check-nochange"
+ fi
+
+ # Fresh root: networked fetch + install 2026.6.0.
+ kit_home kh-j-fetch
+ run_ok "j-fetch-install" "$KIT" update --index "file://$idx"
+ current_is "j-fetch-current-60" "$work/kh-j-fetch" 2026.6.0
+
+ # Monotonic guard: with current = 2026.6.1, an index advertising the
+ # older 2026.6.0 is refused, but --allow-downgrade succeeds.
+ run_ok "j-fetch-up-to-61" "$KIT" update "$kpkg_61"
+ current_is "j-fetch-current-61" "$work/kh-j-fetch" 2026.6.1
+ run_fail "j-monotonic-reject" "$KIT" update --index "file://$idx"
+ contains "j-monotonic-msg" "$work/j-monotonic-reject.err" "older than installed"
+ current_is "j-monotonic-unchanged" "$work/kh-j-fetch" 2026.6.1
+ run_ok "j-allow-downgrade" "$KIT" update --index "file://$idx" --allow-downgrade
+ current_is "j-downgrade-current-60" "$work/kh-j-fetch" 2026.6.0
+ else
+ skip_test "j-network" "file:// fetch not supported in this environment"
+ fi
+ fi
+fi
+
+kit_summary dist
+kit_exit