kit

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

commit 29589741aac6f8b540851739e81997479a19ad73
parent f150a1ada14451dc7a989d3988055686c4ca413e
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 17 Jun 2026 13:16:57 -0700

selfdist: kit update tool (offline-first install/flip/rollback/prune/list + networked channel), unbounded blob Merkle root, remove_tree env primitive

Diffstat:
Adriver/cmd/update.c | 768+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdriver/env.h | 7+++++++
Mdriver/env/posix.c | 33+++++++++++++++++++++++++++++++++
Mdriver/env/windows.c | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdriver/main.c | 5+++++
Minclude/kit/config.h | 1+
Mmk/driver_srcs.mk | 6++++--
Msrc/core/config_assert.c | 3+++
Msrc/dist/blob.c | 42+++++++++++++++++++++++++++---------------
Msrc/dist/dist.h | 6++----
10 files changed, 910 insertions(+), 21 deletions(-)

diff --git a/driver/cmd/update.c b/driver/cmd/update.c @@ -0,0 +1,768 @@ +#include <kit/cas.h> +#include <kit/core.h> +#include <kit/package.h> +#include <stddef.h> +#include <stdint.h> +#include <stdio.h> +#include <string.h> + +#include "dist_host.h" +#include "driver.h" +#include "env.h" +#include "install_links.h" + +/* `kit update` — verify and install a kit release into the single-root data + * layout, and manage installed versions. See doc/plan/SELFDIST.md. + * + * $KIT_HOME/ (default $XDG_DATA_HOME/kit ≡ ~/.local/share/kit) + * versions/<ver>/{bin,lib,include,support,VERSION,...} + * current -> versions/<ver> atomic active-toolchain pointer + * bin/ PATH links -> ../current/bin/kit + * config/ cache/downloads/ + * + * Offline-first: `kit update <file.kpkg>` verifies a local package against the + * embedded release key set (driver/release_key.c) — or an explicit --key — and + * installs it with no network. The networked forms resolve a signed channel + * index (kit-release) through a fetch hint URL and download the .kpkg, but the + * trust always comes from the package signature, never the transport. */ + +#define UPDATE_TOOL "update" +#define UP_PATH 4096u +#define UP_MAX_VERS 256 + +/* ---------------------------------------------------------------------- */ +/* layout */ +/* ---------------------------------------------------------------------- */ + +typedef struct UpPaths { + char home[UP_PATH]; + char versions[UP_PATH]; + char current[UP_PATH]; + char bin[UP_PATH]; + char config[UP_PATH]; + char downloads[UP_PATH]; +} UpPaths; + +static int up_resolve_paths(UpPaths* p) { + if (driver_kit_home(p->home, sizeof p->home) != 0) { + driver_errf(UPDATE_TOOL, + "cannot determine kit home (set KIT_HOME, XDG_DATA_HOME, or HOME)"); + return 1; + } + snprintf(p->versions, sizeof p->versions, "%s/versions", p->home); + snprintf(p->current, sizeof p->current, "%s/current", p->home); + snprintf(p->bin, sizeof p->bin, "%s/bin", p->home); + snprintf(p->config, sizeof p->config, "%s/config", p->home); + snprintf(p->downloads, sizeof p->downloads, "%s/cache/downloads", p->home); + return 0; +} + +static int up_ensure_dirs(DriverEnv* env, const UpPaths* p) { + if (driver_mkdir_p(env, p->versions) != 0) return 1; + if (driver_mkdir_p(env, p->bin) != 0) return 1; + if (driver_mkdir_p(env, p->config) != 0) return 1; + if (driver_mkdir_p(env, p->downloads) != 0) return 1; + return 0; +} + +/* Active version from the `current` symlink target ("versions/<ver>"). Returns + * 0 and fills ver[] when current resolves, 1 when there is no current. */ +static int up_current_version(const UpPaths* p, char* ver, size_t cap) { + char target[UP_PATH]; + size_t i, last = 0; + int saw = 0; + if (driver_readlink(p->current, target, sizeof target) != 0) return 1; + for (i = 0; target[i]; ++i) + if (target[i] == '/') { + last = i + 1u; + saw = 1; + } + snprintf(ver, cap, "%s", saw ? target + last : target); + return 0; +} + +static int up_version_installed(const UpPaths* p, const char* ver) { + char dir[UP_PATH]; + snprintf(dir, sizeof dir, "%s/%s", p->versions, ver); + return driver_path_lexists(dir); +} + +/* Snapshot installed version names into vers[] (ascending CalVer). Returns the + * count. Non-CalVer entries keep insertion order relative to each other. */ +static int up_list_installed(DriverEnv* env, const UpPaths* p, + char vers[][KIT_PKG_VERSION_MAX], int cap) { + DriverDirHandle* d = driver_open_dir(env, p->versions); + int n = 0, a; + uint64_t idx = 0; + if (!d) return 0; + for (;;) { + const char* name; + uint32_t nl; + uint64_t ino, sz, mt; + uint8_t ft; + if (driver_read_dir_entry(d, idx++, &name, &nl, &ino, &sz, &mt, &ft) != 0) + break; + if (n >= cap) break; + if (nl == 0 || nl >= KIT_PKG_VERSION_MAX) continue; + memcpy(vers[n], name, nl); + vers[n][nl] = '\0'; + ++n; + } + driver_close_dir(env, d); + for (a = 1; a < n; ++a) { /* insertion sort by CalVer */ + char tmp[KIT_PKG_VERSION_MAX]; + int b = a - 1, cmp; + memcpy(tmp, vers[a], sizeof tmp); + while (b >= 0 && + kit_calver_compare(vers[b], tmp, &cmp) == KIT_OK && cmp > 0) { + memcpy(vers[b + 1], vers[b], KIT_PKG_VERSION_MAX); + --b; + } + memcpy(vers[b + 1], tmp, KIT_PKG_VERSION_MAX); + } + return n; +} + +/* ---------------------------------------------------------------------- */ +/* verify */ +/* ---------------------------------------------------------------------- */ + +static int up_read(const KitContext* ctx, const char* path, KitFileData* out) { + out->data = NULL; + out->size = 0; + out->token = NULL; + return ctx->file_io->read_all(ctx->file_io->user, path, out) == KIT_OK; +} + +static void up_release(const KitContext* ctx, KitFileData* fd) { + if ((fd->token || fd->data) && ctx->file_io->release) + ctx->file_io->release(ctx->file_io->user, fd); +} + +/* Pull the 64-hex package id out of a verified trusted comment ("...pkgid=<hex> + * ..."). Returns 0 + fills out[65] on success. */ +static int up_extract_pkgid(const char* trusted, char* out) { + const char* s = trusted; + const char* tag = "pkgid="; + int i; + out[0] = '\0'; + for (; *s; ++s) { + const char* a = s; + const char* b = tag; + while (*b && *a == *b) { + ++a; + ++b; + } + if (!*b) { /* matched "pkgid=" at s */ + for (i = 0; i < 64; ++i) { + char c = a[i]; + int hex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'); + if (!hex) return 1; + out[i] = (c >= 'A' && c <= 'F') ? (char)(c - 'A' + 'a') : c; + } + out[64] = '\0'; + return 0; + } + } + return 1; +} + +/* Verify `data`/`len` as a fat kit kpkg. With key_bytes != NULL, verify against + * just that minisign public-key file content; otherwise try every embedded + * release key until one verifies. When unpack_dir != NULL, materialize the + * default output there. On success returns 0 and fills name/version/pkgid. */ +static int up_verify(DriverEnv* env, const KitContext* ctx, const uint8_t* data, + size_t len, const uint8_t* key_bytes, size_t key_len, + const char* unpack_dir, char* name, size_t namecap, + char* version, size_t vercap, char* pkgid_hex) { + KitCasHost host = driver_cas_host(env); + KitPkgVerifyOptions opts; + KitPkgVerifyResult res; + const KitReleaseKey* keys; + unsigned nkeys = 0, i; + + if (key_bytes) { + memset(&opts, 0, sizeof opts); + opts.pkg_data = data; + opts.pkg_len = len; + opts.format = KIT_PKG_FORMAT_KPKG; + opts.unpack_dir = unpack_dir; + opts.pubkey_bytes = key_bytes; + opts.pubkey_len = key_len; + if (kit_pkg_verify(ctx, &host, &opts, &res) != KIT_OK) return 1; + snprintf(name, namecap, "%s", res.name); + snprintf(version, vercap, "%s", res.version); + return up_extract_pkgid(res.trusted, pkgid_hex); + } + + keys = driver_release_keys(&nkeys); + for (i = 0; i < nkeys; ++i) { + memset(&opts, 0, sizeof opts); + opts.pkg_data = data; + opts.pkg_len = len; + opts.format = KIT_PKG_FORMAT_KPKG; + opts.unpack_dir = unpack_dir; + opts.pubkey_bytes = (const uint8_t*)keys[i].pubkey; + opts.pubkey_len = (size_t)strlen(keys[i].pubkey); + if (kit_pkg_verify(ctx, &host, &opts, &res) == KIT_OK) { + snprintf(name, namecap, "%s", res.name); + snprintf(version, vercap, "%s", res.version); + return up_extract_pkgid(res.trusted, pkgid_hex); + } + } + return 1; +} + +/* ---------------------------------------------------------------------- */ +/* swap + links */ +/* ---------------------------------------------------------------------- */ + +/* Atomically point `current` at versions/<version> (temp symlink + rename). */ +static int up_flip_current(const UpPaths* p, const char* version) { + char tmplink[UP_PATH], target[UP_PATH]; + snprintf(tmplink, sizeof tmplink, "%s/.current.tmp", p->home); + snprintf(target, sizeof target, "versions/%s", version); /* relative target */ + driver_remove_file(tmplink); + if (driver_create_symlink(target, tmplink) != 0) return 1; + if (driver_rename(tmplink, p->current) != 0) { + driver_remove_file(tmplink); + return 1; + } + return 0; +} + +/* Rebuild $KIT_HOME/bin from scratch so tools that went away disappear and new + * ones appear, each link resolving through `current` to the active version. */ +static int up_refresh_links(DriverEnv* env, const UpPaths* p) { + char target_exe[UP_PATH]; + DriverInstallLinkOpts opts; + unsigned done = 0, fail = 0; + driver_remove_tree(p->bin); + if (driver_mkdir_p(env, p->bin) != 0) return 1; + snprintf(target_exe, sizeof target_exe, "%s/bin/kit", p->current); + memset(&opts, 0, sizeof opts); + opts.target_exe = target_exe; + opts.tool_tag = UPDATE_TOOL; + opts.use_hardlink = (driver_host_target().os == KIT_OS_WINDOWS) ? 1 : 0; + opts.force = 1; + if (driver_install_link_one(env, p->bin, "kit", &opts) != 0) ++fail; + fail += driver_install_links( + env, p->bin, DRIVER_GROUP_TOOLCHAIN | DRIVER_GROUP_BYTEUTIL, &opts, &done); + return fail ? 1 : 0; +} + +/* Flip to an already-installed version (offline) and refresh links. */ +static int up_activate(DriverEnv* env, const UpPaths* p, const char* version, + int dry_run) { + if (!up_version_installed(p, version)) { + driver_errf(UPDATE_TOOL, "version %s is not installed", version); + return 1; + } + if (dry_run) { + driver_printf("would make kit %s current\n", version); + return 0; + } + if (up_ensure_dirs(env, p) != 0) { + driver_errf(UPDATE_TOOL, "cannot create install layout under %s", p->home); + return 1; + } + if (up_flip_current(p, version) != 0) { + driver_errf(UPDATE_TOOL, "cannot update the current pointer"); + return 1; + } + if (up_refresh_links(env, p) != 0) + driver_errf(UPDATE_TOOL, "warning: one or more bin links could not be laid"); + driver_printf("kit %s is now current\n", version); + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* install from verified bytes */ +/* ---------------------------------------------------------------------- */ + +/* Verify `data`/`len`, then unpack + activate. expect_version (or NULL) and + * expect_pkgid_hex (or NULL) are post-verification cross-checks for the + * networked path (the fetched bytes must match the signed channel index). */ +static int up_install_bytes(DriverEnv* env, const KitContext* ctx, + const UpPaths* p, const uint8_t* data, size_t len, + const uint8_t* key_bytes, size_t key_len, + int dry_run, const char* expect_version, + const char* expect_pkgid_hex) { + char name[KIT_PKG_NAME_MAX], version[KIT_PKG_VERSION_MAX], pkgid[65]; + char tmp[UP_PATH], verdir[UP_PATH]; + + /* Discovery pass: verify only (no materialize), to learn name/version/id and + * confirm a key matches before touching the filesystem. */ + if (up_verify(env, ctx, data, len, key_bytes, key_len, NULL, name, + sizeof name, version, sizeof version, pkgid) != 0) { + driver_errf(UPDATE_TOOL, + "signature verification failed (no trusted key matched)"); + return 1; + } + if (strcmp(name, "kit") != 0) { + driver_errf(UPDATE_TOOL, "not a kit release (package name is '%s')", name); + return 1; + } + if (expect_version && strcmp(expect_version, version) != 0) { + driver_errf(UPDATE_TOOL, "package version %s does not match expected %s", + version, expect_version); + return 1; + } + if (expect_pkgid_hex && strcmp(expect_pkgid_hex, pkgid) != 0) { + driver_errf(UPDATE_TOOL, + "package id does not match the channel index entry"); + return 1; + } + if (dry_run) { + driver_printf("would install kit %s and make it current\n", version); + return 0; + } + + if (up_ensure_dirs(env, p) != 0) { + driver_errf(UPDATE_TOOL, "cannot create install layout under %s", p->home); + return 1; + } + + /* Materialize into a clean scratch dir, then rename into versions/<ver>. */ + snprintf(tmp, sizeof tmp, "%s/.unpack-tmp", p->downloads); + driver_remove_tree(tmp); + if (driver_mkdir_p(env, tmp) != 0) { + driver_errf(UPDATE_TOOL, "cannot create unpack dir %s", tmp); + return 1; + } + if (up_verify(env, ctx, data, len, key_bytes, key_len, tmp, name, sizeof name, + version, sizeof version, pkgid) != 0) { + driver_errf(UPDATE_TOOL, "verification failed while unpacking"); + driver_remove_tree(tmp); + return 1; + } + snprintf(verdir, sizeof verdir, "%s/%s", p->versions, version); + driver_remove_tree(verdir); /* replace on reinstall */ + if (driver_rename(tmp, verdir) != 0) { + driver_errf(UPDATE_TOOL, "cannot move new version into place: %s", verdir); + driver_remove_tree(tmp); + return 1; + } + if (up_flip_current(p, version) != 0) { + driver_errf(UPDATE_TOOL, "installed kit %s but could not make it current", + version); + return 1; + } + if (up_refresh_links(env, p) != 0) + driver_errf(UPDATE_TOOL, "warning: one or more bin links could not be laid"); + driver_printf("installed kit %s (now current)\n", version); + return 0; +} + +/* Install from a local .kpkg file (offline). A local file is a deliberate + * choice, so no monotonic version guard applies (it is still fully verified). */ +static int up_install_file(DriverEnv* env, const KitContext* ctx, + const UpPaths* p, const char* file, + const uint8_t* key_bytes, size_t key_len, + int dry_run) { + KitFileData fd; + int rc; + if (!up_read(ctx, file, &fd)) { + driver_errf(UPDATE_TOOL, "cannot read package: %s", file); + return 1; + } + rc = up_install_bytes(env, ctx, p, fd.data, fd.size, key_bytes, key_len, + dry_run, NULL, NULL); + up_release(ctx, &fd); + return rc; +} + +/* ---------------------------------------------------------------------- */ +/* list / prune / rollback */ +/* ---------------------------------------------------------------------- */ + +static int up_list(DriverEnv* env, const UpPaths* p) { + char vers[UP_MAX_VERS][KIT_PKG_VERSION_MAX]; + char cur[KIT_PKG_VERSION_MAX]; + int n = up_list_installed(env, p, vers, UP_MAX_VERS), i, have_cur; + have_cur = (up_current_version(p, cur, sizeof cur) == 0); + if (n == 0) { + driver_printf("no kit versions installed under %s\n", p->home); + return 0; + } + for (i = 0; i < n; ++i) + driver_printf("%s %s\n", + (have_cur && strcmp(vers[i], cur) == 0) ? "*" : " ", vers[i]); + return 0; +} + +static int up_prune(DriverEnv* env, const UpPaths* p, int dry_run) { + char vers[UP_MAX_VERS][KIT_PKG_VERSION_MAX]; + char cur[KIT_PKG_VERSION_MAX]; + int n = up_list_installed(env, p, vers, UP_MAX_VERS), i, removed = 0; + if (up_current_version(p, cur, sizeof cur) != 0) { + driver_errf(UPDATE_TOOL, "no current version; refusing to prune"); + return 1; + } + for (i = 0; i < n; ++i) { + char dir[UP_PATH]; + if (strcmp(vers[i], cur) == 0) continue; + if (dry_run) { + driver_printf("would remove kit %s\n", vers[i]); + ++removed; + continue; + } + snprintf(dir, sizeof dir, "%s/%s", p->versions, vers[i]); + if (driver_remove_tree(dir) != 0) + driver_errf(UPDATE_TOOL, "warning: could not remove %s", dir); + else { + driver_printf("removed kit %s\n", vers[i]); + ++removed; + } + } + if (removed == 0) driver_printf("nothing to prune (only kit %s)\n", cur); + return 0; +} + +static int up_rollback(DriverEnv* env, const UpPaths* p, int dry_run) { + char vers[UP_MAX_VERS][KIT_PKG_VERSION_MAX]; + char cur[KIT_PKG_VERSION_MAX]; + int n = up_list_installed(env, p, vers, UP_MAX_VERS), i, ci = -1; + if (up_current_version(p, cur, sizeof cur) != 0) { + driver_errf(UPDATE_TOOL, "no current version to roll back from"); + return 1; + } + for (i = 0; i < n; ++i) + if (strcmp(vers[i], cur) == 0) ci = i; + if (ci <= 0) { + driver_errf(UPDATE_TOOL, "no previous version installed to roll back to"); + return 1; + } + if (up_activate(env, p, vers[ci - 1], dry_run) != 0) return 1; + driver_printf( + "note: rolled back the active version only; channel tracking is " + "unchanged, so the next `kit update` will re-upgrade unless you pin " + "with --version\n"); + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* networked: channel index */ +/* ---------------------------------------------------------------------- */ + +static const char* up_index_url(const char* index_opt) { + if (index_opt) return index_opt; + return driver_getenv("KIT_UPDATE_INDEX_URL"); +} + +/* Fetch + parse the channel index and locate this host's entry. Returns 0 on + * success (idx filled, *host points into idx). The index is an untrusted fetch + * hint: trust comes from the per-package signature plus the kpkg-id match the + * caller performs after download. */ +static int up_load_index(DriverEnv* env, const KitContext* ctx, + const UpPaths* p, const char* index_url, + KitReleaseIndex* idx, const KitReleaseHost** host) { + char dest[UP_PATH]; + KitFileData fd; + const char* triple = kit_host_triple(); + unsigned i; + snprintf(dest, sizeof dest, "%s/channel.index", p->downloads); + if (up_ensure_dirs(env, p) != 0) return 1; + if (driver_fetch_url(index_url, dest) != 0) { + driver_errf(UPDATE_TOOL, "could not fetch channel index from %s", index_url); + return 1; + } + if (!up_read(ctx, dest, &fd)) { + driver_errf(UPDATE_TOOL, "could not read fetched channel index"); + return 1; + } + if (kit_release_index_parse(ctx, fd.data, fd.size, idx) != KIT_OK) { + up_release(ctx, &fd); + return 1; /* parse already emitted detail via ctx->diag */ + } + up_release(ctx, &fd); + *host = NULL; + for (i = 0; i < idx->n_hosts; ++i) + if (strcmp(idx->hosts[i].target, triple) == 0) { + *host = &idx->hosts[i]; + return 0; + } + driver_errf(UPDATE_TOOL, "channel has no release for this host (%s)", triple); + return 1; +} + +/* Download the host's .kpkg (trying each mirror), verify, and install. */ +static int up_fetch_install(DriverEnv* env, const KitContext* ctx, + const UpPaths* p, const KitReleaseIndex* idx, + const KitReleaseHost* host, const uint8_t* key_bytes, + size_t key_len, int dry_run) { + char dest[UP_PATH], expect_id[65]; + KitFileData fd; + unsigned i; + int got = 0, rc; + if (!host->has_kpkg || host->n_urls == 0) { + driver_errf(UPDATE_TOOL, "channel index has no .kpkg URL for this host"); + return 1; + } + kit_hex_encode(expect_id, host->kpkg_id, KIT_CAS_HASH_LEN); + if (dry_run) { + driver_printf("would fetch and install kit %s for %s\n", idx->version, + host->target); + return 0; + } + snprintf(dest, sizeof dest, "%s/kit-%s-%s.kpkg", p->downloads, idx->version, + host->target); + for (i = 0; i < host->n_urls; ++i) { + if (driver_fetch_url(host->urls[i], dest) == 0) { + got = 1; + break; + } + driver_errf(UPDATE_TOOL, "mirror failed: %s", host->urls[i]); + } + if (!got) { + driver_errf(UPDATE_TOOL, "all mirrors failed for kit %s", idx->version); + return 1; + } + if (!up_read(ctx, dest, &fd)) { + driver_errf(UPDATE_TOOL, "cannot read downloaded package: %s", dest); + return 1; + } + rc = up_install_bytes(env, ctx, p, fd.data, fd.size, key_bytes, key_len, 0, + idx->version, expect_id); + up_release(ctx, &fd); + return rc; +} + +/* ---------------------------------------------------------------------- */ +/* help + main */ +/* ---------------------------------------------------------------------- */ + +void driver_help_update(void) { + driver_printf( + "kit update — verify and install kit, and manage installed versions\n" + "\n" + "USAGE\n" + " kit update [OPTIONS] [<file.kpkg>]\n" + "\n" + "DESCRIPTION\n" + " Installs a kit release into the single-root layout under $KIT_HOME\n" + " (default $XDG_DATA_HOME/kit, i.e. ~/.local/share/kit), verifying its\n" + " signature against the built-in release key set (or --key) before any\n" + " change. A local <file.kpkg> installs fully offline; without one, the\n" + " tracked channel index is resolved over the network (curl/wget).\n" + "\n" + " Put $KIT_HOME/bin on PATH once; `current` is flipped atomically and\n" + " the bin links refreshed on each install. All versions are retained,\n" + " so any prior one is an instant offline flip via --version / --rollback.\n" + "\n" + "OPTIONS\n" + " <file.kpkg> verify + install a local package (offline)\n" + " --from <file.kpkg> explicit local-file form\n" + " --version <ver> flip to an installed version, else fetch it\n" + " --check report installed vs available; change nothing\n" + " --list list installed versions, mark current\n" + " --rollback flip to the previous installed version\n" + " --prune remove all non-current installed versions\n" + " --key <pubkey> verify against an explicit minisign public key\n" + " --index <url> channel index URL (else $KIT_UPDATE_INDEX_URL)\n" + " --allow-downgrade permit a channel-driven downgrade\n" + " --dry-run print the plan; change nothing\n" + " -h, --help show this help\n" + "\n" + "EXIT CODES\n" + " 0 success 1 verify/install/IO error 2 bad usage\n"); +} + +int driver_update(int argc, char** argv) { + DriverEnv env; + KitContext ctx; + UpPaths paths; + const char *file = NULL, *want_version = NULL, *keyfile = NULL; + const char* index_opt = NULL; + int do_check = 0, do_list = 0, do_prune = 0, do_rollback = 0; + int dry_run = 0, allow_downgrade = 0, opts_done = 0, i, rc = 2; + KitFileData keyfd; + int key_loaded = 0; + const uint8_t* key_bytes = NULL; + size_t key_len = 0; + + if (driver_argv_wants_help(argc, argv, 1)) { + driver_help_update(); + return 0; + } + + for (i = 1; i < argc; ++i) { + const char* a = argv[i]; + if (!opts_done && driver_streq(a, "--")) { + opts_done = 1; + continue; + } + if (!opts_done && a[0] == '-' && a[1] != '\0') { + if (driver_streq(a, "--from") && i + 1 < argc) + file = argv[++i]; + else if (driver_streq(a, "--version") && i + 1 < argc) + want_version = argv[++i]; + else if (driver_streq(a, "--key") && i + 1 < argc) + keyfile = argv[++i]; + else if (driver_streq(a, "--index") && i + 1 < argc) + index_opt = argv[++i]; + else if (driver_streq(a, "--check")) + do_check = 1; + else if (driver_streq(a, "--list")) + do_list = 1; + else if (driver_streq(a, "--prune")) + do_prune = 1; + else if (driver_streq(a, "--rollback")) + do_rollback = 1; + else if (driver_streq(a, "--allow-downgrade")) + allow_downgrade = 1; + else if (driver_streq(a, "--dry-run") || driver_streq(a, "-n")) + dry_run = 1; + else { + driver_errf(UPDATE_TOOL, "unknown option: %s", a); + return 2; + } + continue; + } + if (!file) + file = a; + else { + driver_errf(UPDATE_TOOL, "unexpected argument: %s", a); + return 2; + } + } + + driver_env_init(&env); + ctx = driver_env_to_context(&env); + if (up_resolve_paths(&paths) != 0) { + rc = 1; + goto done; + } + + if (keyfile) { + if (!up_read(&ctx, keyfile, &keyfd)) { + driver_errf(UPDATE_TOOL, "cannot read public key: %s", keyfile); + rc = 1; + goto done; + } + key_loaded = 1; + key_bytes = keyfd.data; + key_len = keyfd.size; + } + + /* Local operations first. */ + if (do_list) { + rc = up_list(&env, &paths); + goto done; + } + if (do_prune) { + rc = up_prune(&env, &paths, dry_run); + goto done; + } + if (do_rollback) { + rc = up_rollback(&env, &paths, dry_run); + goto done; + } + + /* Local-file install (offline; deliberate, so no monotonic guard). */ + if (file) { + rc = up_install_file(&env, &ctx, &paths, file, key_bytes, key_len, dry_run); + goto done; + } + + /* --version: offline flip if installed, else fetch that exact version. */ + if (want_version && up_version_installed(&paths, want_version)) { + rc = up_activate(&env, &paths, want_version, dry_run); + goto done; + } + + /* Networked paths need a channel index URL. */ + { + const char* idx_url = up_index_url(index_opt); + KitReleaseIndex* idx; + const KitReleaseHost* host = NULL; + char cur[KIT_PKG_VERSION_MAX]; + int have_cur, cmp; + + if (!idx_url) { + if (want_version) + driver_errf(UPDATE_TOOL, + "version %s is not installed; networked fetch needs " + "--index <url> or $KIT_UPDATE_INDEX_URL", + want_version); + else + driver_errf(UPDATE_TOOL, + "no channel index configured; pass a local <file.kpkg>, " + "or set --index <url> / $KIT_UPDATE_INDEX_URL"); + rc = (do_check) ? 0 : 1; + if (do_check) { /* still report what is installed locally */ + up_list(&env, &paths); + } + goto done; + } + + idx = (KitReleaseIndex*)driver_alloc_zeroed(&env, sizeof *idx); + if (!idx) { + driver_errf(UPDATE_TOOL, "out of memory"); + rc = 1; + goto done; + } + if (up_load_index(&env, &ctx, &paths, idx_url, idx, &host) != 0) { + driver_free(&env, idx, sizeof *idx); + rc = 1; + goto done; + } + + have_cur = (up_current_version(&paths, cur, sizeof cur) == 0); + + if (do_check) { + driver_printf("channel %s: latest %s for %s\n", idx->channel, + idx->version, host->target); + if (have_cur) + driver_printf("installed current: %s\n", cur); + else + driver_printf("installed current: (none)\n"); + driver_free(&env, idx, sizeof *idx); + rc = 0; + goto done; + } + + if (want_version) { + /* The single-version channel index can only fetch its own version. */ + if (strcmp(want_version, idx->version) != 0) { + driver_errf(UPDATE_TOOL, + "version %s is not installed and not the current channel " + "release (%s)", + want_version, idx->version); + driver_free(&env, idx, sizeof *idx); + rc = 1; + goto done; + } + } else { + /* No-arg channel update: monotonic unless --allow-downgrade. */ + if (have_cur && kit_calver_compare(idx->version, cur, &cmp) == KIT_OK) { + if (cmp == 0) { + driver_printf("already up to date (kit %s)\n", cur); + driver_free(&env, idx, sizeof *idx); + rc = 0; + goto done; + } + if (cmp < 0 && !allow_downgrade) { + driver_errf(UPDATE_TOOL, + "channel offers kit %s, older than installed %s; pass " + "--allow-downgrade to move back", + idx->version, cur); + driver_free(&env, idx, sizeof *idx); + rc = 1; + goto done; + } + } + } + + rc = up_fetch_install(&env, &ctx, &paths, idx, host, key_bytes, key_len, + dry_run); + driver_free(&env, idx, sizeof *idx); + goto done; + } + +done: + if (key_loaded) up_release(&ctx, &keyfd); + driver_env_fini(&env); + return rc; +} diff --git a/driver/env.h b/driver/env.h @@ -255,6 +255,13 @@ int driver_path_lexists(const char* path); * failure. POSIX rename(2); Windows MoveFileExW(MOVEFILE_REPLACE_EXISTING). */ int driver_rename(const char* from, const char* to); +/* Recursively remove `path` and everything beneath it (a file, symlink, or + * directory tree). Used by `kit update` to clear a scratch unpack dir, replace + * a reinstalled version, and prune old versions. Does NOT follow symlinks (it + * unlinks the link itself). Returns 0 on success or when `path` is already + * absent, nonzero on any other failure. */ +int driver_remove_tree(const char* path); + /* Resolve the single-root kit home directory into `buf` (capacity `cap`, * NUL-terminated, no trailing slash): `$KIT_HOME`, else `$XDG_DATA_HOME/kit`, * else `$HOME/.local/share/kit` on POSIX (`%LOCALAPPDATA%\kit` on Windows). diff --git a/driver/env/posix.c b/driver/env/posix.c @@ -761,6 +761,39 @@ int driver_rename(const char* from, const char* to) { return rename(from, to) == 0 ? 0 : 1; /* same-fs rename is atomic */ } +static int posix_remove_tree(const char* path) { + struct stat sb; + if (lstat(path, &sb) != 0) return errno == ENOENT ? 0 : 1; + if (S_ISDIR(sb.st_mode)) { /* lstat: a symlink-to-dir is unlinked, not entered */ + DIR* d = opendir(path); + struct dirent* ent; + int rc = 0; + if (!d) return 1; + while ((ent = readdir(d)) != NULL) { + char child[4096]; + const char* name = ent->d_name; + if (name[0] == '.' && + (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) + continue; + if ((size_t)snprintf(child, sizeof child, "%s/%s", path, name) >= + sizeof child) { + rc = 1; + continue; + } + if (posix_remove_tree(child) != 0) rc = 1; + } + closedir(d); + if (rmdir(path) != 0) rc = 1; + return rc; + } + return unlink(path) == 0 ? 0 : 1; +} + +int driver_remove_tree(const char* path) { + if (!path) return 1; + return posix_remove_tree(path); +} + int driver_kit_home(char* buf, size_t cap) { const char* v; int n = -1; diff --git a/driver/env/windows.c b/driver/env/windows.c @@ -1077,6 +1077,66 @@ int driver_rename(const char* from, const char* to) { return ok ? 0 : 1; } +static int win_remove_tree(const char* path) { + wchar_t* wpath = widen(path); + DWORD attr; + int rc = 0; + if (!wpath) return 1; + attr = GetFileAttributesW(wpath); + if (attr == INVALID_FILE_ATTRIBUTES) { + DWORD e = GetLastError(); + free(wpath); + return (e == ERROR_FILE_NOT_FOUND || e == ERROR_PATH_NOT_FOUND) ? 0 : 1; + } + if ((attr & FILE_ATTRIBUTE_DIRECTORY) && + !(attr & FILE_ATTRIBUTE_REPARSE_POINT)) { + char pattern[4096]; + WIN32_FIND_DATAW fd; + HANDLE h; + wchar_t* wpattern; + snprintf(pattern, sizeof pattern, "%s\\*", path); + wpattern = widen(pattern); + if (!wpattern) { + free(wpath); + return 1; + } + h = FindFirstFileW(wpattern, &fd); + free(wpattern); + if (h != INVALID_HANDLE_VALUE) { + do { + char child[4096], nameu8[1024]; + if (WideCharToMultiByte(CP_UTF8, 0, fd.cFileName, -1, nameu8, + (int)sizeof nameu8, NULL, NULL) <= 0) { + rc = 1; + continue; + } + if (nameu8[0] == '.' && (nameu8[1] == '\0' || + (nameu8[1] == '.' && nameu8[2] == '\0'))) + continue; + if ((size_t)snprintf(child, sizeof child, "%s\\%s", path, nameu8) >= + sizeof child) { + rc = 1; + continue; + } + if (win_remove_tree(child) != 0) rc = 1; + } while (FindNextFileW(h, &fd)); + FindClose(h); + } + if (!RemoveDirectoryW(wpath)) rc = 1; + } else if (attr & FILE_ATTRIBUTE_DIRECTORY) { + if (!RemoveDirectoryW(wpath)) rc = 1; /* reparse point: remove link only */ + } else { + if (!DeleteFileW(wpath)) rc = 1; + } + free(wpath); + return rc; +} + +int driver_remove_tree(const char* path) { + if (!path) return 1; + return win_remove_tree(path); +} + int driver_kit_home(char* buf, size_t cap) { const char* v; int n = -1; diff --git a/driver/main.c b/driver/main.c @@ -196,6 +196,11 @@ static const DriverToolDesc driver_tools[] = { {"gram", driver_gram, NULL, driver_help_gram, "Generate a C parser/lexer from an EBNF grammar", DRIVER_GROUP_OTHER}, #endif +#if KIT_TOOL_UPDATE_ENABLED + {"update", driver_update, NULL, driver_help_update, + "Verify and install a newer kit; manage installed versions", + DRIVER_GROUP_OTHER}, +#endif {NULL, NULL, NULL, NULL, NULL, DRIVER_GROUP_OTHER}, }; diff --git a/include/kit/config.h b/include/kit/config.h @@ -144,5 +144,6 @@ #define KIT_TOOL_DISAS_ENABLED 1 #define KIT_TOOL_MC_ENABLED 1 #define KIT_TOOL_GRAM_ENABLED 1 +#define KIT_TOOL_UPDATE_ENABLED 1 #endif /* KIT_CONFIG_H */ diff --git a/mk/driver_srcs.mk b/mk/driver_srcs.mk @@ -59,7 +59,8 @@ DRIVER_TOOL_SRCS = \ $(call tool-cmd,COMPRESS,compress) \ $(call tool-cmd,DISAS,disas) \ $(call tool-cmd,MC,mc) \ - $(call tool-cmd,GRAM,gram) + $(call tool-cmd,GRAM,gram) \ + $(call tool-cmd,UPDATE,update) DRIVER_SRCS += $(sort $(DRIVER_TOOL_SRCS)) # Shared driver/lib support, each compiled in when any of its consumer tools @@ -83,8 +84,9 @@ DRIVER_SRCS += $(call need-any,CC CHECK AR RANLIB STRIP DBG RUN BUILD_EXE BUILD_ DRIVER_SRCS += $(call need-any,STRIP OBJCOPY,driver/lib/objedit.c) DRIVER_SRCS += $(call need-any,RUN,driver/lib/wasm_run.c) DRIVER_SRCS += $(call need-any,CC CHECK BUILD_EXE BUILD_LIB BUILD_OBJ,driver/lib/link_inputs.c) -DRIVER_SRCS += $(call need-any,CAS PKG,driver/lib/dist_host.c) +DRIVER_SRCS += $(call need-any,CAS PKG UPDATE,driver/lib/dist_host.c) DRIVER_SRCS += $(call need-any,INSTALL UPDATE,driver/lib/install_links.c) +DRIVER_SRCS += $(call need-any,UPDATE,driver/release_key.c) DRIVER_SRCS += $(call need-any,ADDR2LINE SYMBOLIZE,driver/lib/dwarfsym.c) DRIVER_SRCS += $(call need-any,DBG RUN,driver/lib/backtrace.c) diff --git a/src/core/config_assert.c b/src/core/config_assert.c @@ -81,6 +81,7 @@ KIT_ASSERT_BOOL(KIT_TOOL_COMPRESS_ENABLED); KIT_ASSERT_BOOL(KIT_TOOL_DISAS_ENABLED); KIT_ASSERT_BOOL(KIT_TOOL_MC_ENABLED); KIT_ASSERT_BOOL(KIT_TOOL_GRAM_ENABLED); +KIT_ASSERT_BOOL(KIT_TOOL_UPDATE_ENABLED); #undef KIT_ASSERT_BOOL @@ -159,3 +160,5 @@ _Static_assert(!KIT_TOOL_MC_ENABLED || KIT_DISASM_ENABLED, "KIT_TOOL_MC_ENABLED requires disasm support"); _Static_assert(!KIT_TOOL_GRAM_ENABLED || KIT_GRAM_ENABLED, "KIT_TOOL_GRAM_ENABLED requires gram support"); +_Static_assert(!KIT_TOOL_UPDATE_ENABLED || (KIT_PKG_ENABLED && KIT_CAS_ENABLED), + "KIT_TOOL_UPDATE_ENABLED requires package + CAS support"); diff --git a/src/dist/blob.c b/src/dist/blob.c @@ -56,8 +56,15 @@ void dist_blob_empty_root(uint8_t out[DIST_BLAKE2B_LEN]) { int dist_blob_root(uint8_t out[DIST_BLAKE2B_LEN], const uint8_t* data, size_t len, size_t chunk_size) { - uint8_t level[DIST_BLOB_MAX_CHUNKS][DIST_BLAKE2B_LEN]; - size_t leaves, i; + /* Bounded-memory Merkle root. This is bit-identical to a level-by-level + * pairing that promotes a lone odd node, but computed in O(1) space: process + * leaves left-to-right keeping a stack of completed perfect-subtree roots + * (one per height), combining equal-height neighbours as they appear, then + * fold the residual peaks left-to-right. A 64-entry stack covers any file up + * to 2^64 leaves, so there is no longer a per-blob size cap. */ + uint8_t peaks[64][DIST_BLAKE2B_LEN]; + uint8_t height[64]; + size_t np = 0, leaves, i; if (chunk_size == 0) return DIST_ERR; if (len && !data) return DIST_ERR; if (len == 0) { @@ -65,30 +72,35 @@ int dist_blob_root(uint8_t out[DIST_BLAKE2B_LEN], const uint8_t* data, return DIST_OK; } leaves = (len + chunk_size - 1u) / chunk_size; - if (leaves > DIST_BLOB_MAX_CHUNKS) return DIST_ERR; for (i = 0; i < leaves; ++i) { size_t off = i * chunk_size; size_t n = len - off; if (n > chunk_size) n = chunk_size; - dist_blob_leaf_hash(level[i], (uint64_t)i, data + off, n); - } - while (leaves > 1u) { - size_t outn = 0; - for (i = 0; i < leaves; i += 2u) { - if (i + 1u < leaves) - dist_blob_node_hash(level[outn], level[i], level[i + 1u]); - else - memcpy(level[outn], level[i], DIST_BLAKE2B_LEN); - ++outn; + dist_blob_leaf_hash(peaks[np], (uint64_t)i, data + off, n); + height[np] = 0; + ++np; + /* Combine equal-height neighbours: builds the same perfect subtrees the + * level-by-level pass would, left-to-right. */ + while (np >= 2u && height[np - 1u] == height[np - 2u]) { + dist_blob_node_hash(peaks[np - 2u], peaks[np - 2u], peaks[np - 1u]); + height[np - 2u] = (uint8_t)(height[np - 2u] + 1u); + --np; } - leaves = outn; + } + /* Bag the residual peaks left-to-right: H(...H(H(p0,p1),p2)..., pk). This + * matches the odd-node-promotion of the level algorithm. */ + while (np > 1u) { + dist_blob_node_hash(peaks[0], peaks[0], peaks[1]); + for (i = 1u; i + 1u < np; ++i) + memcpy(peaks[i], peaks[i + 1u], DIST_BLAKE2B_LEN); + --np; } { static const uint8_t dom[] = "kit blob root v1"; DistBlake2b h; dist_blake2b_init(&h, DIST_BLAKE2B_LEN); dist_blake2b_update(&h, dom, sizeof dom - 1u); - dist_blake2b_update(&h, level[0], DIST_BLAKE2B_LEN); + dist_blake2b_update(&h, peaks[0], DIST_BLAKE2B_LEN); dist_blake2b_final(&h, out); } return DIST_OK; diff --git a/src/dist/dist.h b/src/dist/dist.h @@ -28,10 +28,8 @@ #define DIST_MAX_FILES 256u #define DIST_MAX_OUTPUTS 16u -/* Maximum number of Merkle leaves (chunks) a single blob may have when its - * root is computed in-memory. Distinct from DIST_MAX_FILES (a package member - * count) even though the two currently share a value. */ -#define DIST_BLOB_MAX_CHUNKS 256u +/* (No per-blob chunk cap: dist_blob_root computes the chunk Merkle root in + * bounded memory, so a blob may have any number of chunks. See blob.c.) */ /* String field caps inside in-memory manifest structs. */ #define DIST_NAME_MAX 128u