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:
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