Releasing and updating kit
kit ships itself. A kit release is just a signed kit package: the same
content-addressed .kpkg/.tar.gz machinery that packages user code
(DISTRIBUTE.md) is dogfooded onto the toolchain, and a running
kit verifies and installs a newer kit with kit update. There is no second
mechanism and no new trusted-byte surface beyond the existing minisign/Ed25519
path. This document is the operational reference; the cross-cutting release
punchlist lives in plan/RELEASE.md.
Versioning
The single source of truth is the committed top-level VERSION file holding
a CalVer string YYYY.MINOR.PATCH (YYYY = release-series year, MINOR =
release within the year, PATCH = fix release). Nothing derives the version
from git tags; an annotated tag v<VERSION> is created at release time from
VERSION, not the reverse.
kit --version prints the version plus provenance, and every tool answers it
the same way (so toolchain probes like cc --version work):
$ kit --version
kit 2026.6.0 (a3f91c2, aarch64-macos)
$ cc --version
kit cc 2026.6.0 (a3f91c2, aarch64-macos)
2026.6.0— contents ofVERSION.a3f91c2— short git hash (the build id),unknown/<hash>-dirtywhen git state is unavailable or dirty.aarch64-macos— the host triple this binary targets (compile-time).
Mechanism: mk/flags.mk reads VERSION + the git short hash once per make
invocation and injects them as -D defines into the single TU driver/version.c
(kit_version_string / kit_build_id / kit_host_triple), gated on a dedicated
.version-config sentinel so a version or commit change recompiles only that one
object. There is no __DATE__/__TIME__ anywhere, so a given tree always
embeds the same bytes and the self-host byte-identity gate is preserved.
Building release artifacts
Both artifact paths require explicit release signing and update-channel trust configuration. Hermetic tests opt into the checked-in non-release key; no release target selects it implicitly.
make dist — native host
Builds RELEASE=1, stages a self-contained kit/ tree, and emits — for the
native host triple — a signed fat .kpkg, a signed portable .tar.gz, and a
detached .tar.gz.minisig:
build/dist/
kit-2026.6.0-aarch64-macos.kpkg native fat, signed
kit-2026.6.0-aarch64-macos.tar.gz portable, signed (manifest sig embedded)
kit-2026.6.0-aarch64-macos.tar.gz.minisig detached sig over the tarball (bootstrap)
The staging tree is bin/kit, lib/libkit.a, include/,
support/rt/{include,lib}/, doc/, VERSION, and README/license. The
documentation tree includes the maintained references plus the explicitly
forward-looking plan/ and ideas/ indexes; the historical release audit is
not shipped. The overview is README.md. The target requires
KIT_SIGN_KEY=<path>,
KIT_RELEASE_PUBKEYS=<path...>, and
KIT_UPDATE_INDEX_URL=<stable-url>. It rejects the in-tree NON-RELEASE test
key unless a hermetic harness explicitly sets KIT_RELEASE_ALLOW_TEST_KEY=1.
For a local packaging smoke test, make dist-dev invokes make dist with the
checked-in non-release keypair and an inert example.invalid update URL.
The .kpkg is the canonical update unit; the .tar.gz carries its signed
manifest internally, and the extra detached .minisig lets a user with only
stock minisign verify before unpacking.
scripts/release.sh — the multi-target matrix
For each hosted (runnable) triple in scripts/hosted.sh's support set,
release.sh cross-builds the kit binary + per-target rt archives (via
kit_cross.sh and, for foreign libc, the sysroots of plan/SYSROOTS.md),
stages and packages each exactly as make dist does, signs everything with the
real release key (KIT_SIGN_KEY, held in CI/secret, never in-tree), and
emits + signs the channel index. It asserts a clean git tree and refuses the
in-tree test key. Freestanding targets have no host to run on and produce no
artifact. Knobs: KIT_SIGN_KEY (required), KIT_RELEASE_URL_BASE (mirror base
URLs), KIT_RELEASE_TARGETS, KIT_VM=0 (drop FreeBSD/Windows on a VM-less
runner), KIT_CHANNEL, KIT_RELEASE_OUT_DIR (artifact output directory,
default build/release-dist). Hermetic tests that intentionally sign with the
in-tree non-release key must also set KIT_RELEASE_ALLOW_TEST_KEY=1.
Targeted validation for the runnable arm64 macOS artifact is
make test-release-macos-aa64: it drives scripts/release.sh for macos-aa64
with the in-tree non-release key, installs the .kpkg under an isolated
KIT_HOME, unpacks the .tar.gz under a separate isolated KIT_HOME, and runs
both resulting kit binaries.
Detached signatures over arbitrary files (the .tar.gz.minisig and the index
signature) are produced by kit pkg sign -s KEY [-o OUT] FILE, which writes a
stock-minisign-compatible signature.
Channel index and hosting
Each channel (normally stable) is described by a small signed, byte-stable text
file, kit-release 1, emitted/parsed by libkit (src/dist/release.c,
kit_release_index_*):
kit-release 1
channel = stable
version = 2026.6.0
hash = blake2b-256
[host]
target = aarch64-macos
kpkg = <64-hex package-id of the .kpkg>
targz = <64-hex package-id of the .tar.gz>
size = 9311461
url = https://github.com/<org>/kit/releases/download/v2026.6.0/kit-2026.6.0-aarch64-macos.kpkg
url = https://mirror.example/kit/2026.6.0/kit-2026.6.0-aarch64-macos.kpkg
Hosting is not a fixed location: each artifact is identified by its content
id (package-id), and the repeated url lines are an ordered, untrusted
mirror list. The fetcher tries them in order and accepts bytes only after
they match the signed content id and the package signature, so any host —
GitHub Releases, a CDN, an S3/R2 bucket, a private mirror — is interchangeable.
The index may be served over plain HTTP.
Trust, keys, and rotation
Release artifacts are signed with a dedicated minisign (Ed25519) keypair whose
secret is held in CI/secret and never enters the tree. The binary embeds an
array of trusted release public keys (driver/release_key.c); a signature
from any embedded key verifies an update, which gives key rotation for free:
ship release N+1 signed by a key already embedded in N, and introduce the next
key in a later overlap window. Verifying an update requires the signature to
validate against an embedded (or --key-supplied) key and the fetched
package-id to match the signed channel-index entry and the version to pass
policy (below).
The published release public key + the detached .tar.gz.minisig give the
bootstrap path when no kit exists yet:
$ minisign -Vm kit-2026.6.0-aarch64-macos.tar.gz -P <published kit release pubkey>
$ tar xzf kit-2026.6.0-aarch64-macos.tar.gz && kit/bin/kit --version
From then on the installed kit's embedded anchor verifies all future updates. (The in-tree default anchor is the NON-RELEASE test key; the real release pubkey replaces/augments it at release time.)
Installing and updating
kit update manages a single self-contained root, $KIT_HOME (default
$XDG_DATA_HOME/kit ≡ ~/.local/share/kit; %LOCALAPPDATA%\kit on Windows):
$KIT_HOME/
versions/2026.6.0/{bin,lib,include,support,VERSION}
current -> versions/2026.6.0 atomic active-toolchain pointer
bin/ PATH links -> ../current/bin/kit
config/trusted_keys pkg trust anchors
cache/downloads/ fetched kpkgs / scratch
Put $KIT_HOME/bin on PATH once. An update verifies the package, unpacks it
into a temp dir, renames it into versions/<new>, flips current atomically,
and refreshes the bin/ links — the live binary is reached through current,
so the swap never mutates the running image. All versions are retained, so any
prior one is an instant offline flip.
kit install DIR [TOOL...] is the standalone link-layout tool for an existing
binary. With no explicit tools it installs the default drop-in compiler/binutils
and standard byte-utility names. kit install --all installs every public tool
compiled into the public registry, while development tools such as emu are
excluded.
kit update uses the same link writer after installing or flipping a version, so
$KIT_HOME/bin and an explicit kit install directory expose the same public
tool names for the selected mode.
kit update # latest on the tracked channel (network)
kit update <file.kpkg> # offline: verify + install a local package
kit update --version 2026.6.0 # offline flip if installed, else fetch
kit update --check # report available vs installed; change nothing
kit update --rollback # flip to the previous installed version
kit update --list # list installed versions, mark current
kit update --prune # remove all non-current installed versions
kit update --key <pubkey> # verify against an explicit key (mirrors/forks)
kit update --allow-downgrade # permit a channel-driven downgrade
kit update --dry-run # print the plan; change nothing
Networked updates resolve the channel index from --index <url> or
$KIT_UPDATE_INDEX_URL and download through a fetch hook in the driver
(driver_fetch_url, which invokes curl directly with no downloader
fallback); libkit itself stays network-free, and since packages are signed the
transport is untrusted.
Version policy: channel-driven updates are monotonic by default — kit update refuses a version older than the running one. Deliberate moves bypass
the guard: --version <X>, --rollback, local-file installs, and
--allow-downgrade (all still fully verified). CalVer compare is numeric per
field.
Self-update only ever writes under the user-writable $KIT_HOME; it never
touches a system/root-owned install (e.g. under /usr/local) — there,
re-acquire or use the OS package manager.
macOS Gatekeeper
A downloaded Mach-O is quarantined; current releases ship without Apple notarization, so remove the quarantine flag once:
$ xattr -dr com.apple.quarantine kit/bin/kit
$ kit/bin/kit --version
kit ad-hoc-signs its own Mach-O output, so the binary is valid code; this is only the download-quarantine prompt. Developer ID signing + notarization is a later milestone.