kit

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

Self-distribution (kit ships kit)

This is the design and implementation plan for distributing the kit toolchain itself — versioning it, building signed release artifacts for every supported host, and updating an installed kit in place. It is the detailed design behind the "release packaging: version, …, kit install default tool set" line in RELEASE.md. All design decisions below are settled; the remaining work is implementation (see Work items).

It deliberately reuses the signed content-addressed packaging subsystem already in the tree (see ../DISTRIBUTE.md) rather than inventing a second mechanism: a kit release is just a signed kit package whose payload happens to be the toolchain. The asymmetry today is that kit pkg / kit cas package user code, while kit's own make dist is an unsigned, unversioned, native-host-only tar czf (mk/dist.mk). This plan closes that gap by dogfooding the package pipeline onto kit's own release.

Goals

Non-goals / deferred to later milestones

Relationship to existing subsystems

Concern Where it lives This plan
Content store, blobs, trees, signing, .kpkg/.tar.gz ../DISTRIBUTE.md, src/dist/, include/kit/{cas,package}.h reused unchanged as the mechanism
Per-host build / cross-compile matrix scripts/hosted.sh, ../PORT.md drives the multi-target release build
Cross-compile sysroots (needed to build kit for a foreign host) SYSROOTS.md a build-time dependency of the release pipeline; its ~/.kit/sysroots path becomes $KIT_HOME/sysroots under the single-root layout below
Release punchlist RELEASE.md this is the design for its "version/packaging" item
Install layout / tool symlinks driver/cmd/install.c extended with a versioned single-root install ($KIT_HOME/bin links)

libkit stays the mechanism (verify, unpack, materialize, hash, sign, plus the new index format + version compare); the driver owns the policy (version semantics, channel selection, trust anchor, fetch, atomic swap). This matches the existing split where "argument parsing and trusted-keys path/pin policy stay in the driver" (DISTRIBUTE.md).

Versioning

Source of truth: CalVer in VERSION

A committed top-level VERSION file holds a CalVer string YYYY.MINOR.PATCH:

2026.6.0

CalVer signals cadence over API-compat promises, which fits a pre-1.0 self-hosting toolchain better than semver. The file is the single source of truth; nothing computes the version from git tags. (A matching annotated git tag v2026.6.0 is still created at release time for provenance, but it is derived from VERSION, not the other way around.)

Build id and kit --version

kit --version prints the version plus provenance:

$ kit --version
kit 2026.6.0 (a3f91c2, aarch64-macos)

Mechanism: the Makefile reads VERSION and the git short hash once and passes them as -DKIT_VERSION_STR=… / -DKIT_BUILD_ID=… to a single small translation unit driver/version.c exposing kit_version_string(), kit_build_id(), and kit_host_triple(). Only that TU recompiles when VERSION or the commit changes, so version churn does not rebuild the world.

--version everywhere

--version / version routing slots into kit_driver_main_ex (driver/main.c) alongside the existing --help / help <tool> handling. Every tool entry point answers --version with the same banner, prefixed by the tool name, so toolchain probes like cc --version work uniformly:

$ cc --version
kit cc 2026.6.0 (a3f91c2, aarch64-macos)
$ ld --version
kit ld 2026.6.0 (a3f91c2, aarch64-macos)

Compiler identity for configure scripts comes from predefined macros, not this string, so a clear kit banner is correct and not a compatibility risk.

Reproducibility contract

The self-host byte-identity gate compiles kit with kit from one tree and compares stage2 vs stage3. To keep it intact:

Where the version flows

VERSION → (1) the binary's --version; (2) the channel index entries; (3) the git tag at release time.

Release artifacts (make dist → signed packages)

One package model, per-host fat artifacts

A kit release is a package named kit, version = cat VERSION, produced once per host triple as a self-contained fat package (everything embedded — no external CAS). The package manifest's per-[output] target triple records the host; the native host's output is flagged default. (The kit-package 3 manifest already models per-output target triples, a default flag, and exe/lib/data artifact overlays — DISTRIBUTE.md — so no format change is needed.)

Per-host fat packages keep the model simple: each download is self-contained, kit update <file.kpkg> works fully offline, and the download is only the user's own host. Common headers/rt-source are duplicated across the ~dozen server-side artifacts, but that storage is cheap; the thin/external-CAS shape (cross-host dedup, delta updates) is a later evolution.

Staging tree (what gets hashed into the package), aligned with today's mk/dist.mk and kit install's expectations:

kit/
  bin/kit                          the multi-call binary (exe)
  lib/libkit.a                     static library (lib)
  include/                         public headers (data)
  support/rt/include/              freestanding + kit rt headers (data)
  support/rt/lib/                  kit rt sources (data)
  VERSION                          the release version (data)
  README.txt, LICENSE.txt, NOTICE.txt       (data)

Both formats, both signed

For each host triple the pipeline emits, all signed by the release key:

build/dist/
  kit-2026.6.0-aarch64-macos.kpkg            native fat, signed (manifest+descriptor)
  kit-2026.6.0-aarch64-macos.tar.gz          portable, signed (embedded manifest.minisig)
  kit-2026.6.0-aarch64-macos.tar.gz.minisig  detached sig over the tarball bytes (bootstrap)
  …one set per triple…
  stable.index                               signed channel index (see below)
  stable.index.minisig

make dist vs the release pipeline

Two layers, so the everyday target needs no secret key:

Artifacts are named by the scripts/hosted.sh selector triples (e.g. aarch64-macos, x86_64-linux-gnu) for one consistent naming scheme across the build matrix, the index, and the file names.

Hosting and the mirror model

Hosting is not a single fixed location. The signed channel index identifies each artifact by its content id (package-id) and carries an ordered list of mirror URLs for it; the fetcher tries them in order and accepts bytes only after they match the signed content id and signature. Because identity is the hash, it does not matter who serves the bytes — GitHub Releases, a CDN, an S3/R2 bucket, or a private mirror are all interchangeable, and any subset can be listed.

For v1 fat packages the "blob" is the whole .kpkg file. The same mechanism generalizes to per-blob/per-chunk mirror lists when thin packages arrive later — same principle, finer grain. GitHub Releases is a fine default first mirror (durable URLs, free, signatures make its trust irrelevant), but the design pins no host.

Manifest, signing, and trust anchor

The release key set

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, static const KitReleaseKey kit_release_keys[]); a signature from any embedded key verifies an update. The public key(s) are also published out of band for the stock-minisign bootstrap path.

Verifying an update requires (a) the signature validates against an embedded (or --key-supplied) key, and (b) the fetched package-id matches the channel index entry, and (c) the version is acceptable per policy (below). The existing pkgid= trusted-comment binding (DISTRIBUTE.md) ties the signature to exact manifest bytes.

Key rotation: overlap windows

Rotation needs no extra protocol — just overlapping embedded sets:

release N    embeds {A, B}, signed by A   → users now trust both A and B
release N+1  signed by B                  → trusted (users from N hold B)
release N+2  embeds {B, C}, signed by B   → introduces C the same way

A user always validates a new artifact against a key their already-trusted kit holds. Hard rotation after a key compromise falls back to the published bootstrap key (re-acquire + verify with stock minisign). A data-driven (TUF-style) key set in the index was considered and rejected as over-built for v1.

Bootstrap trust (first install)

No kit yet, so kit cannot verify itself. The published release key + the detached .tar.gz.minisig let the user verify with stock minisign:

$ 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.

Channel index format

A small, signed, byte-stable text file per channel (stable for v1), in the same canonical-text + detached-minisig idiom as the package manifest, and emitted / parsed by libkit (src/dist/release.c). Proposed kit-release 1:

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   = 31457280
url    = https://github.com/<org>/kit/releases/download/v2026.6.0/kit-2026.6.0-aarch64-macos.kpkg
url    = https://dl.kit.dev/2026.6.0/kit-2026.6.0-aarch64-macos.kpkg
url    = https://mirror.example/kit/2026.6.0/kit-2026.6.0-aarch64-macos.kpkg

[host]
target = x86_64-linux-gnu
…

Installation layout (single root)

Everything kit manages for a user lives under one self-contained root, with each concern in a subdir — kit never scatters files across separate config/cache/data trees. The root defaults to $XDG_DATA_HOME/kit (i.e. ~/.local/share/kit when XDG_DATA_HOME is unset), so it is a single tree under the conventional data path rather than a bare dotdir in $HOME; $KIT_HOME overrides it outright. On Windows the default is %LOCALAPPDATA%\kit.

$KIT_HOME/                                  (default $XDG_DATA_HOME/kit ≡ ~/.local/share/kit)
  versions/
    2026.5.3/{bin,lib,include,support,VERSION,…}
    2026.6.0/{bin,lib,include,support,VERSION,…}
  current  ->  versions/2026.6.0            atomic active-toolchain pointer
  bin/                                      PATH links -> ../current/bin/kit
  config/
    config                                  tracked channel, retention, prefs
    trusted_keys                            pkg trust anchors (kit pkg trust)
  sysroots/                                 cross-compile sysroots (SYSROOTS.md)
  cache/
    downloads/                              fetched kpkgs / scratch

Consolidating config/ and cache/ inside the root is a deliberate deviation from XDG's three-root split, in favor of one tree that is trivial to locate, back up, or delete. The user puts $KIT_HOME/bin on PATH once (cargo/rustup style); kit install <dir> still lays links into any other directory on demand.

Two reconciliations of already-shipped/sketched design follow from the single root:

Self-update operates only on this user-writable root. A system-wide / root-owned install (e.g. under /usr/local) is out of scope for self-update v1; there, kit update reports that the install is not user-managed and points to re-acquisition or the OS package manager.

Updating (kit update)

Flow and atomic swap

  1. Resolve target version (channel index, explicit --version, or local file) and confirm it matches the running host triple.
  2. If the target version is already installed under versions/, skip to the flip (offline). Otherwise fetch the .kpkg (see fetch model).
  3. kit_pkg_verify against the embedded (or --key) release key; confirm id matches the index; confirm version policy.
  4. kit_pkg_unpack into a temp dir under the data root, then rename into versions/<new> (rename within a filesystem is atomic).
  5. Flip current to the new version dir atomically (symlink swap on POSIX).
  6. Refresh $KIT_HOME/bin links via the existing kit install logic so new tools appear and removed ones go away.

Because the live binary is reached through current, the swap does not mutate the running image. All installed versions are retained by default (kit update --prune removes the non-current ones), so any prior version is an instant offline flip via kit update --version <X> or --rollback.

Platform notes:

Fetch model: offline-first, pluggable, curl/wget first

Version policy

Default is monotonic for channel-driven updates: kit update refuses to move to a version older than the running one. Intentional moves bypass the guard: kit update --version <X> (flip to an installed version), --rollback (flip to the immediately-previous installed version), and local-file installs are deliberate and allowed (still fully verified); --allow-downgrade permits a channel-driven downgrade. --rollback reverts the active version only — channel tracking is untouched, and the command warns that the next kit update will re-upgrade unless the user pins with --version. CalVer compare is numeric per field (YYYY, then MINOR, then PATCH).

Command surface

kit update                       # latest on the tracked channel (default: stable)
kit update --version 2026.6.0    # specific version: offline flip if installed, else fetch
kit update <file.kpkg>           # offline: verify + install a local package
kit update --from <file.kpkg>    # explicit local-file form
kit update --check               # report available vs installed; change nothing
kit update --rollback            # flip to the previous installed version (warns re-upgrade)
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

macOS Gatekeeper

A downloaded Mach-O is quarantined and Gatekeeper blocks it on first run unless notarized. v1 ships without Apple notarization and documents the one-time removal:

$ xattr -dr com.apple.quarantine kit/bin/kit
$ kit/bin/kit --version

kit already ad-hoc-signs its own Mach-O output (required to run on Apple Silicon), so the binary itself is valid code; this is purely the download-quarantine prompt. Developer ID signing + notarization is a later milestone once a signing identity exists.

Security / threat model

Code boundaries

Work items

Versioning

Release artifacts

Channel index + trust

kit update

Tests