/* * pkg - Package management utility for rawnix.org * https://rawnix.org * * Copyright (c) 2025 zorz@gmx.com * SPDX-License-Identifier: BSD-2-Clause */ #include "pkg.h" #include "paths.h" #include "config.h" #include "pkgdb.h" #include "depdb.h" #include "repo.h" #include "resolve.h" #include "orphan.h" #include "builder.h" #include "locker.h" #include #include #include #include #include #include #include /* ---- Global context ---- */ typedef struct { config_t *config; pkgdb_t *pkgdb; depdb_t *depdb; repo_t *repo; locker_t *locker; char *root; bool verbose; bool no_deps; bool use_bulk; bool assume_yes; bool force; } ctx_t; /* set from ctx in main(); checked by ask_confirm */ static bool g_assume_yes = false; /* ---- Helpers ---- */ static void version(void) { printf("pkg %s\n", PKG_VERSION); } static void usage(void) { printf( "usage: pkg [options] [args]\n" "\n" "commands:\n" " add [...] install package(s) with dependencies\n" " del [...] remove package(s)\n" " list list installed packages\n" " isinst check if a package is installed\n" " update [...] update package(s)\n" " build [port] build package without installing\n" " prepare [port] install build dependencies\n" " bulk [...] fetch binaries (with deps), no install\n" " sysup update all outdated packages\n" " autoremove remove orphaned packages\n" " orphans list orphaned packages\n" " depends show dependencies\n" " rdepends show reverse dependencies\n" " diff show version differences\n" " lock lock package\n" " unlock unlock package\n" " locked list locked packages\n" " checkdb check dependency database\n" " initdb populate dep database from installed pkgs\n" "\noptions:\n" " -r, --root alternative root\n" " -N, --no-deps single package only\n" " -b, --use-bulk fetch binaries before add/update/sysup\n" " -y, --yes assume yes on confirmation prompts\n" " -f, --force force removal despite reverse dependencies\n" " -v, --verbose verbose output\n" " -V, --version print version\n" " -h, --help print help\n" ); } static bool ask_confirm(const char *msg) { char buf[8]; if (g_assume_yes) { printf("%s y\n", msg); return true; } printf("%s [y/N] ", msg); fflush(stdout); if (!fgets(buf, sizeof(buf), stdin)) return false; return buf[0] == 'y' || buf[0] == 'Y'; } /* * Single-instance lock for db-mutating commands. * * Two concurrent pkg runs can't corrupt deps.db (rename is atomic) * but the last writer silently discards the other's entries. A * POSIX fcntl write-lock on PROC_LOCK_FILE serializes them: if the * lock is busy we say so and wait. The lock is released by the * kernel when the process exits — the fd is held for our lifetime, * so there is nothing to clean up. */ static int acquire_proc_lock(const char *root) { char *path = build_root_path(root, PROC_LOCK_FILE); struct flock fl; int fd; ensure_parent_dir(path); fd = open(path, O_WRONLY | O_CREAT, 0644); if (fd < 0) { warn("cannot open lock file %s", path); free(path); return -1; } free(path); memset(&fl, 0, sizeof(fl)); fl.l_type = F_WRLCK; fl.l_whence = SEEK_SET; if (fcntl(fd, F_SETLK, &fl) != 0) { fprintf(stderr, "pkg: waiting for lock (another instance is running)\n"); if (fcntl(fd, F_SETLKW, &fl) != 0) { warn("lock failed"); close(fd); return -1; } } return fd; /* intentionally kept open until exit */ } static void reload_pkgdb(ctx_t *ctx) { pkgdb_close(ctx->pkgdb); ctx->pkgdb = pkgdb_open(ctx->root); } /* * ── Locked recipes ─────────────────────────────────────────────── * * A locked port keeps the operator's own recipe beside the port's * MAKEPKG as MAKEPKG.lock. Three tools cooperate through that one * file, none of them reading each other's state: * * ports -u carries --exclude=*.lock, so the snapshot survives * the sync that overwrites MAKEPKG with upstream's * mkpkg builds MAKEPKG.lock in preference to MAKEPKG, with * .footprint.lock and .signature.lock beside it * pkg reads MAKEPKG.lock in preference to MAKEPKG * (port_load), so resolve, pkg_is_built and depdb all * see the recipe mkpkg builds, and sysup/update build * edits to it. Only `pkg diff` also reads upstream's * MAKEPKG, to report that it moved. * bulk refuses a binary fetch while it exists, since the * server's binary was built from upstream's recipe * * pkg is the only one that reads var/lib/pkg/locked, and only for * decisions about installed state. */ static void recipe_lock_path(char *buf, size_t len, const char *port_path) { snprintf(buf, len, "%s/MAKEPKG.lock", port_path); } static bool recipe_lock_exists(const char *port_path) { char path[PATH_MAX]; recipe_lock_path(path, sizeof(path), port_path); return access(path, F_OK) == 0; } /* * Frozen = never upgraded. Only a lock with no MAKEPKG.lock behind * it: a version-only pin (the port left the tree). A lock with a * recipe pins the BUILD, not the version — pkg reads MAKEPKG.lock, * upstream is invisible to it, and the only way versions can differ * is an edit to the pinned recipe, which is exactly what should be * built. del and autoremove still protect every locked package; * they check the locker directly. */ static bool pkg_frozen(ctx_t *ctx, const char *name) { if (!locker_is_locked(ctx->locker, name)) return false; return !(ctx->repo && port_recipe_locked(repo_find(ctx->repo, name))); } /* * Copy port_path/src to port_path/dst, preserving mode. Writes a temp * file and renames, so an interrupted copy never leaves a truncated * file behind for mkpkg to trust. */ static int copy_port_file(const char *port_path, const char *src_name, const char *dst_name) { char src[PATH_MAX], dst[PATH_MAX], tmp[PATH_MAX]; FILE *in, *out; char buf[8192]; size_t n; struct stat st; snprintf(src, sizeof(src), "%s/%s", port_path, src_name); snprintf(dst, sizeof(dst), "%s/%s", port_path, dst_name); snprintf(tmp, sizeof(tmp), "%s/%s.tmp", port_path, dst_name); in = fopen(src, "r"); if (!in) return -1; out = fopen(tmp, "w"); if (!out) { fclose(in); return -1; } while ((n = fread(buf, 1, sizeof(buf), in)) > 0) { if (fwrite(buf, 1, n, out) != n) { fclose(in); fclose(out); unlink(tmp); return -1; } } if (ferror(in)) { fclose(in); fclose(out); unlink(tmp); return -1; } fclose(in); if (file_close_checked(out) != 0) { unlink(tmp); return -1; } if (stat(src, &st) == 0) chmod(tmp, st.st_mode & 07777); if (rename(tmp, dst) != 0) { unlink(tmp); return -1; } return 0; } /* * The files a lock pins beside MAKEPKG.lock. mkpkg (>= 1.5.8) reads * all three in place of MAKEPKG / .footprint / .signature whenever * MAKEPKG.lock exists; addpkg checks .footprint.lock. `ports -u` * excludes *.lock, so all three survive the sync. */ static const char *lock_companions[][2] = { { ".footprint", ".footprint.lock" }, { ".signature", ".signature.lock" }, }; #define LOCK_COMPANIONS (sizeof(lock_companions) / sizeof(lock_companions[0])) /* * Snapshot MAKEPKG, .footprint and .signature as one set. The * companions go first and MAKEPKG.lock last: MAKEPKG.lock existing is * what switches mkpkg and pkg to the lock, so it must never appear * without the files that belong to it. A companion upstream doesn't * have (unsigned port) is removed rather than left stale — a stale * .signature.lock would describe some other recipe. */ static int snapshot_recipe(const char *port_path) { char path[PATH_MAX]; size_t i; for (i = 0; i < LOCK_COMPANIONS; i++) { snprintf(path, sizeof(path), "%s/%s", port_path, lock_companions[i][0]); if (access(path, F_OK) == 0) { if (copy_port_file(port_path, lock_companions[i][0], lock_companions[i][1]) != 0) goto fail; } else { snprintf(path, sizeof(path), "%s/%s", port_path, lock_companions[i][1]); unlink(path); } } if (copy_port_file(port_path, "MAKEPKG", "MAKEPKG.lock") != 0) goto fail; return 0; fail: for (i = 0; i < LOCK_COMPANIONS; i++) { snprintf(path, sizeof(path), "%s/%s", port_path, lock_companions[i][1]); unlink(path); } return -1; } /* * Reason to record for a package being installed as a dependency. * Fresh installs are REASON_AUTO. An already-installed package keeps * whatever it had: now that resolve() pulls outdated deps into the * order, a manually installed rocm-device-libs upgraded on the way to * hipblas must not be demoted to auto and handed to autoremove. */ static install_reason_t dep_reason(ctx_t *ctx, const char *name, bool installed) { dep_entry_t *de; if (!installed) return REASON_AUTO; de = depdb_find(ctx->depdb, name); return de ? de->reason : REASON_AUTO; } /* * Record a package in the dependency database. * Only runtime depends are recorded — makedeps are build-time only * and irrelevant for orphan detection / reverse-dep tracking. * * Virtual package names in depends are resolved to the real * provider name so reverse-dependency lookups work correctly. * E.g. if cmake depends=(ninja) and samurai provides=(ninja), * we store "samurai" in the depends list, not "ninja". */ /* * Resolve a depends entry to the name to record in depdb. * If 'dep' is installed under its own name, keep it. Otherwise, * if an INSTALLED provider exists, record the provider's real name * so reverse-dependency lookups work. Falls back to the virtual * name unchanged when nothing matches. */ static const char *resolve_dep_provider(ctx_t *ctx, const char *dep) { strarray_t *prov; size_t i; if (pkgdb_installed(ctx->pkgdb, dep) || !ctx->repo) return dep; prov = repo_providers(ctx->repo, dep); if (prov) for (i = 0; i < prov->size; i++) if (pkgdb_installed(ctx->pkgdb, prov->data[i])) return prov->data[i]; return dep; } static void record_depdb(ctx_t *ctx, const char *name, port_t *p, install_reason_t reason) { dep_entry_t *e; size_t j; port_load(p); e = dep_entry_new(name, p->pkg ? p->pkg->version : "unknown", p->pkg ? p->pkg->release : "1", reason); /* only record runtime depends */ if (p->pkg && p->pkg->depends) { for (j = 0; j < p->pkg->depends->size; j++) strarray_add(e->depends, resolve_dep_provider(ctx, p->pkg->depends->data[j])); } depdb_add(ctx->depdb, e); } /* * Build and install packages in the given dependency order. * * - 'target' receives 'target_reason'; every other package is * REASON_AUTO if newly installed, or keeps its existing reason * if this is an upgrade (see dep_reason). * - Packages already installed at the resolved version are skipped * after a depdb refresh (the depends list may have changed without * a version bump). * - 'installed' (if non-NULL) collects the names of newly installed * packages so the caller can emit a summary. * * Returns 0 on success, -1 on the first build or install failure. */ static void bulk_collect(repo_t *repo, pkgdb_t *pkgdb, const char *name, strarray_t *out, strarray_t *missing, strarray_t *visited); static int install_order(ctx_t *ctx, strarray_t *order, const char *target, install_reason_t target_reason, strarray_t *installed) { size_t i; port_t *p; bool upgrade; install_reason_t reason; for (i = 0; i < order->size; i++) { const char *name = order->data[i]; p = repo_find(ctx->repo, name); if (!p) continue; if (port_load(p) != 0) return -1; /* port_load() said why */ upgrade = pkgdb_installed(ctx->pkgdb, name); reason = (strcmp(name, target) == 0) ? target_reason : dep_reason(ctx, name, upgrade); /* * Frozen (version-only pin): satisfies the dependency at its * installed version and is never upgraded — same rule as * sysup. Deliberately does not record_depdb(): the port's * depends list belongs to the version we are NOT installing, * unlike the branch below which only runs when the versions * already match. */ if (upgrade && pkg_frozen(ctx, name)) { printf("pkg: %s is locked, keeping installed version\n", name); continue; } /* skip if already at the resolved version */ if (upgrade) { installed_pkg_t *ip = pkgdb_find(ctx->pkgdb, name); if (ip && p->pkg && !version_differs(ip, p)) { record_depdb(ctx, name, p, reason); depdb_save(ctx->depdb); continue; } } if (!pkg_is_built(p)) { printf("pkg: building %s\n", name); if (pkg_build(p) != 0) { warnx("build failed: %s", name); return -1; } } printf("pkg: installing %s\n", name); if (pkg_install(p, upgrade, ctx->root) != 0) { warnx("install failed: %s", name); return -1; } if (installed) strarray_add(installed, name); record_depdb(ctx, name, p, reason); depdb_save(ctx->depdb); reload_pkgdb(ctx); } return 0; } /* ---- Commands ---- */ /* * add and update. `pkg add` makes the target a manual install; `pkg * update` keeps whatever reason it had, so updating a dependency does * not take it away from autoremove. */ static int cmd_install(ctx_t *ctx, const char *pkg, bool keep_reason) { resolution_t *res; strarray_t installed; strarray_t *variants; size_t i; port_t *p; bool upgrade; const char *selected_pkg = pkg; char *allocated_pkg = NULL; /* track heap allocation for cleanup */ install_reason_t target_reason; if (!pkg) errx(1, "no package specified"); /* * Named explicitly: refuse a frozen package, or `pkg update` * would upgrade straight through a version-only pin to upstream. * A recipe-locked package goes ahead: it builds from MAKEPKG.lock. * (Never suggest unlock for those — it deletes the recipe.) */ if (pkg_frozen(ctx, pkg)) errx(1, "package '%s' is locked with no MAKEPKG.lock " "(version-only pin; pkg unlock %s to upgrade)", pkg, pkg); /* check for variants (mesa-amdgpu, mesa-intel, etc.) */ variants = repo_find_variants(ctx->repo, pkg); if (variants && variants->size > 0) { port_t *base = repo_find(ctx->repo, pkg); size_t total = variants->size + (base ? 1 : 0); char buf[16]; int choice; size_t idx = 0; printf("Multiple variants available:\n"); if (base) { port_load(base); printf(" [1] %-16s - ", pkg); if (base->pkg && base->pkg->variant && base->pkg->variant->size > 0) { size_t vi; for (vi = 0; vi < base->pkg->variant->size; vi++) { if (vi > 0) printf(" "); printf("%s", base->pkg->variant->data[vi]); } printf("\n"); } else { printf("base package\n"); } idx = 1; } for (i = 0; i < variants->size; i++) { port_t *vp = repo_find(ctx->repo, variants->data[i]); if (vp) { port_load(vp); printf(" [%zu] %-16s - ", idx + i + 1, variants->data[i]); if (vp->pkg && vp->pkg->variant && vp->pkg->variant->size > 0) { size_t vi; for (vi = 0; vi < vp->pkg->variant->size; vi++) { if (vi > 0) printf(" "); printf("%s", vp->pkg->variant->data[vi]); } printf("\n"); } else { printf("no description\n"); } } } if (g_assume_yes) { strarray_free(variants); free(variants); errx(1, "'%s' has variants; name the one to install", pkg); } printf("Select [1-%zu]: ", total); fflush(stdout); if (!fgets(buf, sizeof(buf), stdin)) { strarray_free(variants); free(variants); errx(1, "no selection made"); } choice = atoi(buf); if (choice < 1 || choice > (int)total) { strarray_free(variants); free(variants); errx(1, "invalid selection"); } if (base && choice == 1) { selected_pkg = pkg; p = base; } else { int variant_idx = base ? choice - 2 : choice - 1; allocated_pkg = xstrdup(variants->data[variant_idx]); selected_pkg = allocated_pkg; p = repo_find(ctx->repo, selected_pkg); } strarray_free(variants); free(variants); } else { p = repo_find(ctx->repo, pkg); if (variants) { strarray_free(variants); free(variants); } } if (!p) errx(1, "package '%s' not found", selected_pkg); if (port_load(p) != 0) { free(allocated_pkg); return 1; } if (!keep_reason) target_reason = REASON_MANUAL; else { /* update: keep the reason; an untracked package counts as manual */ dep_entry_t *de = depdb_find(ctx->depdb, selected_pkg); target_reason = de ? de->reason : REASON_MANUAL; } /* * If already installed at the same version, nothing to do. * This prevents wasteful reinstalls on 'pkg add' * when the package is already up to date. * 'pkg update' also benefits — same version means no work. * * Still refresh the depdb entry — the port's depends list * may have changed without a version bump. */ { installed_pkg_t *ip = pkgdb_find(ctx->pkgdb, selected_pkg); if (ip && p->pkg && !version_differs(ip, p)) { record_depdb(ctx, selected_pkg, p, target_reason); depdb_save(ctx->depdb); printf("pkg: %s is up to date (%s)\n", selected_pkg, ip->version); free(allocated_pkg); return 0; } } /* resolve dependencies */ /* * --no-deps: skip dependency resolution entirely, * just build + install this one package. */ if (ctx->no_deps) { upgrade = pkgdb_installed(ctx->pkgdb, selected_pkg); /* -b means binaries: fetch this one package, as -b does for a * whole resolution, unless its recipe is locked */ if (ctx->use_bulk && !port_recipe_locked(p) && !pkg_is_built(p)) { strarray_t one; int br; strarray_init(&one); strarray_add(&one, selected_pkg); br = pkg_bulk_fetch(&one); strarray_free(&one); if (br != 0) { warnx("bulk fetch failed: %s", selected_pkg); free(allocated_pkg); return 1; } } if (!pkg_is_built(p)) { printf("pkg: building %s\n", selected_pkg); if (pkg_build(p) != 0) { warnx("build failed: %s", selected_pkg); free(allocated_pkg); return 1; } } printf("pkg: installing %s\n", selected_pkg); if (pkg_install(p, upgrade, ctx->root) != 0) { warnx("install failed: %s", selected_pkg); free(allocated_pkg); return 1; } record_depdb(ctx, selected_pkg, p, target_reason); depdb_save(ctx->depdb); reload_pkgdb(ctx); printf("pkg: installed %s\n", selected_pkg); free(allocated_pkg); return 0; } /* * Include makedeps only if the package actually needs to be built. * In -b (bulk) mode we never compile — binaries come down pre-built — * so makedeps are unconditionally skipped regardless of pkg_is_built. */ res = resolve(ctx->repo, selected_pkg, ctx->pkgdb, ctx->locker, ctx->use_bulk ? false : !pkg_is_built(p)); if (!res) { free(allocated_pkg); errx(1, "failed to resolve dependencies"); } if (res->has_missing) { warnx("missing dependencies:"); for (i = 0; i < res->missing->size; i++) fprintf(stderr, " %s\n", res->missing->data[i]); resolution_free(res); free(allocated_pkg); return 1; } if (res->has_cycle) { warnx("dependency cycle:"); for (i = 0; i < res->cycle->size; i++) fprintf(stderr, " %s\n", res->cycle->data[i]); resolution_free(res); free(allocated_pkg); return 1; } if (res->order->size == 0) { printf("pkg: nothing to do\n"); resolution_free(res); free(allocated_pkg); return 0; } /* * In -b mode, fetch the FULL binary closure first — same set * `pkg bulk ` would fetch. This walks the dep tree * regardless of install state, so already-installed deps still * get their binaries cached on disk. Equivalent to running * `pkg bulk && pkg add ` as separate steps. * * Strict failure: any bulk error aborts the whole operation — * the user asked for binaries, source-fallback would be a lie. * install_order's pkg_is_built() check then skips the build * step for each entry and goes straight to addpkg. */ if (ctx->use_bulk) { strarray_t bulk_set, bulk_missing, bulk_visited; int bulk_ret = 0; strarray_init(&bulk_set); strarray_init(&bulk_missing); strarray_init(&bulk_visited); bulk_collect(ctx->repo, ctx->pkgdb, selected_pkg, &bulk_set, &bulk_missing, &bulk_visited); if (bulk_missing.size > 0) { size_t k; warnx("missing ports for bulk fetch:"); for (k = 0; k < bulk_missing.size; k++) fprintf(stderr, " %s\n", bulk_missing.data[k]); bulk_ret = 1; } if (bulk_ret == 0 && bulk_set.size > 0) { if (pkg_bulk_fetch(&bulk_set) != 0) { warnx("bulk fetch failed; aborting"); bulk_ret = 1; } } strarray_free(&bulk_set); strarray_free(&bulk_missing); strarray_free(&bulk_visited); if (bulk_ret != 0) { resolution_free(res); free(allocated_pkg); return 1; } } /* build and install each package in dependency order */ strarray_init(&installed); if (install_order(ctx, res->order, selected_pkg, target_reason, &installed) != 0) { strarray_free(&installed); resolution_free(res); free(allocated_pkg); return 1; } /* summary */ printf("pkg: installed:"); for (i = 0; i < installed.size; i++) printf(" %s", installed.data[i]); printf(" (%zu packages)\n", installed.size); strarray_free(&installed); resolution_free(res); free(allocated_pkg); return 0; } static int cmd_remove(ctx_t *ctx, const char *pkg) { strarray_t *orphans; size_t i; if (!pkg) errx(1, "no package specified"); if (!pkgdb_installed(ctx->pkgdb, pkg)) { warnx("package '%s' not installed", pkg); return 1; } if (locker_is_locked(ctx->locker, pkg)) { warnx("package '%s' is locked", pkg); return 1; } /* * Refuse to remove a package that installed packages still * depend on — we have the reverse-dep data, use it. Only * INSTALLED dependents count (stale depdb entries don't block). * -f overrides for the "I know what I'm doing" case. */ if (!ctx->force) { strarray_t *rdeps = depdb_rdeps(ctx->depdb, pkg); size_t needed = 0; for (i = 0; i < rdeps->size; i++) { if (pkgdb_installed(ctx->pkgdb, rdeps->data[i])) { if (needed == 0) fprintf(stderr, "pkg: '%s' is required by:\n", pkg); fprintf(stderr, " %s\n", rdeps->data[i]); needed++; } } strarray_free(rdeps); free(rdeps); if (needed > 0) { warnx("not removing '%s' (use -f to force)", pkg); return 1; } } printf("pkg: removing %s\n", pkg); if (pkg_remove(pkg, ctx->root) != 0) { warnx("remove failed: %s", pkg); return 1; } depdb_remove(ctx->depdb, pkg); depdb_save(ctx->depdb); reload_pkgdb(ctx); printf("pkg: removed %s\n", pkg); /* suggest orphan cleanup */ orphans = find_orphans(ctx->pkgdb, ctx->depdb); if (orphans && orphans->size > 0) { printf("pkg: orphaned packages:\n"); for (i = 0; i < orphans->size; i++) { installed_pkg_t *ip = pkgdb_find(ctx->pkgdb, orphans->data[i]); if (ip) printf(" %s %s\n", ip->name, ip->version); } printf("pkg: use 'autoremove' to clean up\n"); } if (orphans) { strarray_free(orphans); free(orphans); } return 0; } static int cmd_autoremove(ctx_t *ctx) { strarray_t *orphans; strarray_t removable; size_t i, failed = 0; orphans = find_orphans(ctx->pkgdb, ctx->depdb); if (!orphans || orphans->size == 0) { printf("pkg: no orphaned packages\n"); if (orphans) { strarray_free(orphans); free(orphans); } return 0; } /* filter out locked packages */ strarray_init(&removable); for (i = 0; i < orphans->size; i++) { if (!locker_is_locked(ctx->locker, orphans->data[i])) strarray_add(&removable, orphans->data[i]); } if (removable.size == 0) { printf("pkg: no removable orphans\n"); strarray_free(orphans); free(orphans); strarray_free(&removable); return 0; } printf("pkg: orphaned packages:\n"); for (i = 0; i < removable.size; i++) { installed_pkg_t *ip = pkgdb_find(ctx->pkgdb, removable.data[i]); if (ip) printf(" %s %s\n", ip->name, ip->version); } if (!ask_confirm("pkg: remove?")) { strarray_free(orphans); free(orphans); strarray_free(&removable); return 0; } for (i = 0; i < removable.size; i++) { printf("pkg: removing %s\n", removable.data[i]); if (pkg_remove(removable.data[i], ctx->root) != 0) { warnx("failed: %s", removable.data[i]); failed++; } else { depdb_remove(ctx->depdb, removable.data[i]); } } depdb_save(ctx->depdb); printf("pkg: removed: %zu packages\n", removable.size - failed); strarray_free(orphans); free(orphans); strarray_free(&removable); return failed > 0 ? 1 : 0; } static int cmd_orphans(ctx_t *ctx) { strarray_t *orphans; size_t i; orphans = find_orphans(ctx->pkgdb, ctx->depdb); if (!orphans || orphans->size == 0) { printf("pkg: no orphaned packages\n"); if (orphans) { strarray_free(orphans); free(orphans); } return 0; } for (i = 0; i < orphans->size; i++) { installed_pkg_t *ip = pkgdb_find(ctx->pkgdb, orphans->data[i]); if (ip) printf("%s %s\n", ip->name, ip->version); } strarray_free(orphans); free(orphans); return 0; } static int cmd_depends(ctx_t *ctx, const char *pkg) { port_t *p; size_t i; if (!pkg) errx(1, "no package specified"); p = repo_find(ctx->repo, pkg); if (!p) errx(1, "package '%s' not found", pkg); if (port_load(p) != 0) errx(1, "failed to load package info"); if (p->pkg->depends && p->pkg->depends->size > 0) { printf("depends:"); for (i = 0; i < p->pkg->depends->size; i++) printf(" %s", p->pkg->depends->data[i]); printf("\n"); } if (p->pkg->makedeps && p->pkg->makedeps->size > 0) { printf("makedeps:"); for (i = 0; i < p->pkg->makedeps->size; i++) printf(" %s", p->pkg->makedeps->data[i]); printf("\n"); } return 0; } static int cmd_rdepends(ctx_t *ctx, const char *pkg) { strarray_t *rdeps; size_t i; if (!pkg) errx(1, "no package specified"); rdeps = depdb_rdeps(ctx->depdb, pkg); if (!rdeps || rdeps->size == 0) { printf("pkg: no packages depend on %s\n", pkg); if (rdeps) { strarray_free(rdeps); free(rdeps); } return 0; } printf("pkg: packages that depend on %s:\n", pkg); for (i = 0; i < rdeps->size; i++) { installed_pkg_t *ip = pkgdb_find(ctx->pkgdb, rdeps->data[i]); if (ip) printf(" %s %s\n", ip->name, ip->version); else printf(" %s\n", rdeps->data[i]); } strarray_free(rdeps); free(rdeps); return 0; } /* * Parse upstream's MAKEPKG directly (port_load would give the lock) * into buf as version-release. Returns false if unreadable. */ static bool upstream_version(port_t *p, char *buf, size_t len) { char path[PATH_MAX]; package_t *up = package_new(p->name, p->path); bool ok; snprintf(path, sizeof(path), "%s/MAKEPKG", p->path); ok = makepkg_parse(path, up) == 0 && up->version; if (ok) version_build(buf, len, up->version, up->release); package_free(up); return ok; } /* * One line per package that sysup would touch or that you should * know about. "a -> b" is always what sysup would install: * * zlib 1.3.1-1 -> 1.3.2-1 normal port * hip 7.1.4-1 -> 7.1.4-2 [MAKEPKG.lock] pinned recipe was edited * hip 7.1.4-1 [MAKEPKG.lock] upstream 7.2.0-1 * pinned, upstream moved on * hsa 7.1.4-1 -> 7.2.0-1 [locked] version-only pin, frozen * * Upstream is compared against the pinned recipe, so it is reported * for as long as upstream differs from what you pinned. */ static int cmd_diff(ctx_t *ctx) { strarray_t *installed; size_t i; int count = 0; installed = pkgdb_list(ctx->pkgdb); for (i = 0; i < installed->size; i++) { const char *name = installed->data[i]; installed_pkg_t *ip = pkgdb_find(ctx->pkgdb, name); port_t *p = repo_find(ctx->repo, name); char repo_ver[256], up_ver[256]; bool lock_diff, up_diff; if (!ip || !p) continue; if (port_load(p) != 0 || !p->pkg || !p->pkg->version) continue; version_build(repo_ver, sizeof(repo_ver), p->pkg->version, p->pkg->release); lock_diff = strcmp(ip->version, repo_ver) != 0; if (!port_recipe_locked(p)) { if (lock_diff) { printf("%s %s -> %s%s\n", name, ip->version, repo_ver, pkg_frozen(ctx, name) ? " [locked]" : ""); count++; } continue; } up_diff = upstream_version(p, up_ver, sizeof(up_ver)) && strcmp(repo_ver, up_ver) != 0; if (!lock_diff && !up_diff) continue; if (lock_diff) printf("%s %s -> %s [MAKEPKG.lock]", name, ip->version, repo_ver); else printf("%s %s [MAKEPKG.lock]", name, ip->version); if (up_diff) printf(" upstream %s", up_ver); putchar('\n'); count++; } if (count == 0) printf("pkg: no differences\n"); strarray_free(installed); free(installed); return 0; } /* * Map a dependency name to the port that satisfies it, in the same * order of preference resolve() uses: installed under that name, * then an installed provider, then a port by that name, then the * first provider. Without the installed checks `llvm` would map to * the uninstalled llvm port instead of llvm-toolchain. * * Takes repo/pkgdb rather than ctx so bulk_collect() can use it: * the bulk closure must pick the same provider the install will. */ static port_t *repo_dep_port(repo_t *repo, pkgdb_t *pkgdb, const char *dep) { strarray_t *prov; port_t *p; size_t i; if (pkgdb_installed(pkgdb, dep)) return repo_find(repo, dep); prov = repo_providers(repo, dep); if (prov) for (i = 0; i < prov->size; i++) if (pkgdb_installed(pkgdb, prov->data[i])) return repo_find(repo, prov->data[i]); p = repo_find(repo, dep); if (p) return p; return (prov && prov->size > 0) ? repo_find(repo, prov->data[0]) : NULL; } /* * Recursive collector for cmd_bulk. Unlike resolve() — which drops * deps that are already installed, since the install path doesn't * need to reinstall them — bulk wants the *whole* binary closure on * disk regardless of install state. Useful for staging mirrors or * pre-populating the cache before going offline. * * Post-order DFS: leaves first, root last. `out` is the dedup'd * result list, `missing` collects any deps not present in the repo. * `visited` tracks names we've already entered to handle DAG joins * and cycles (we just stop recursing — bulk doesn't care about * install ordering since each fetch is independent). * * A port with MAKEPKG.lock is never put in `out`: the server's binary * was built from upstream's recipe, bulk refuses it and exits 1, and * pkg treated that as a failed fetch — one locked port anywhere in * the closure aborted `pkg -b add` even when it was already installed. * Its deps are still collected (from the lock recipe), and when it * isn't built yet so are its makedeps: it is the one source build in * the run, and its build deps can come down as binaries. */ static void bulk_collect(repo_t *repo, pkgdb_t *pkgdb, const char *name, strarray_t *out, strarray_t *missing, strarray_t *visited) { port_t *p; size_t i; bool locked, build = false; for (i = 0; i < visited->size; i++) if (strcmp(visited->data[i], name) == 0) return; strarray_add(visited, name); /* virtual names (depends=(mesa) -> provides=(mesa)) resolve to * the real provider, exactly as resolve() will at install time */ p = repo_dep_port(repo, pkgdb, name); if (!p) { strarray_add(missing, name); return; } if (strcmp(p->name, name) != 0) { /* a direct dep on the provider may already have entered it */ for (i = 0; i < visited->size; i++) if (strcmp(visited->data[i], p->name) == 0) return; strarray_add(visited, p->name); name = p->name; } if (port_load(p) != 0) { warnx("cannot load port: %s", name); return; } locked = port_recipe_locked(p); if (locked) { /* will it actually be built? not if its lock version is * already installed, even with no tarball on disk */ installed_pkg_t *ip = pkgdb_find(pkgdb, name); build = !pkg_is_built(p) && (!ip || version_differs(ip, p)); } if (p->pkg && p->pkg->depends) { for (i = 0; i < p->pkg->depends->size; i++) bulk_collect(repo, pkgdb, p->pkg->depends->data[i], out, missing, visited); } if (build && p->pkg && p->pkg->makedeps) { for (i = 0; i < p->pkg->makedeps->size; i++) bulk_collect(repo, pkgdb, p->pkg->makedeps->data[i], out, missing, visited); } if (locked) { printf("pkg: %s has MAKEPKG.lock, not fetched%s\n", name, build ? " (builds from source)" : ""); return; } /* dedup against out (visited tracks all entries, but only * successfully-loaded ones reach here) */ for (i = 0; i < out->size; i++) if (strcmp(out->data[i], name) == 0) return; strarray_add(out, name); } /* * Standalone fetch: walk each requested package's full dep closure * (regardless of install state), dedup across multiple targets, hand * the union to bulk in one fork, do not install. Useful for staging. * * Strict on missing deps; bulk-fetch failures bubble up as exit 1. */ static int cmd_bulk(ctx_t *ctx, char **names, int count) { strarray_t union_order; strarray_t missing; strarray_t visited; int i, ret = 0; if (count <= 0) { warnx("no package specified"); return 1; } strarray_init(&union_order); strarray_init(&missing); strarray_init(&visited); for (i = 0; i < count; i++) bulk_collect(ctx->repo, ctx->pkgdb, names[i], &union_order, &missing, &visited); if (missing.size > 0) { size_t k; warnx("missing ports:"); for (k = 0; k < missing.size; k++) fprintf(stderr, " %s\n", missing.data[k]); ret = 1; } if (union_order.size > 0) { if (pkg_bulk_fetch(&union_order) != 0) { warnx("bulk fetch failed"); ret = 1; } } else if (missing.size == 0) { printf("pkg: nothing to fetch\n"); } strarray_free(&union_order); strarray_free(&missing); strarray_free(&visited); return ret; } /* ctx wrapper; see repo_dep_port() */ static port_t *dep_port(ctx_t *ctx, const char *dep) { return repo_dep_port(ctx->repo, ctx->pkgdb, dep); } /* Collect the runtime closure of 'dep' into 'seen' (port names). */ static void runtime_closure(ctx_t *ctx, const char *dep, strarray_t *seen) { port_t *p = dep_port(ctx, dep); size_t i; if (!p || strarray_contains(seen, p->name)) return; strarray_add(seen, p->name); if (port_load(p) != 0 || !p->pkg || !p->pkg->depends) return; for (i = 0; i < p->pkg->depends->size; i++) runtime_closure(ctx, p->pkg->depends->data[i], seen); } /* * Sort outdated packages so every package is upgraded after anything * it builds or runs against. * * Edge i -> j when outdated package j is reachable from i: through * i's own depends or makedeps, then through runtime depends only, * across the WHOLE repo graph. Walking through packages that are * not being updated is the point: hipblas -> hip -> rocm-device-libs * must order rocm-device-libs first even when hip didn't bump. * Direct-only edges between outdated packages missed that. * * makedeps are followed at the first level only: building C needs * C's build tools and their runtime closure, not the tools that * built them. Following them transitively drags in whole toolchains * and manufactures cycles. * * Cycles within the outdated set (freetype <-> harfbuzz) are broken * by emitting the stuck node with the fewest outstanding edges and * carrying on, so dependents of the cycle still come after it. */ static void sort_outdated_topo(ctx_t *ctx, strarray_t *outdated) { size_t n = outdated->size; bool *reach, *taken; int *in_degree; char **out; size_t i, j, head = 0; if (n < 2) return; reach = xmalloc(n * n * sizeof(bool)); in_degree = xmalloc(n * sizeof(int)); taken = xmalloc(n * sizeof(bool)); out = xmalloc(n * sizeof(char *)); for (i = 0; i < n; i++) { port_t *pi = repo_find(ctx->repo, outdated->data[i]); strarray_t seen; in_degree[i] = 0; taken[i] = false; for (j = 0; j < n; j++) reach[i * n + j] = false; if (!pi || port_load(pi) != 0 || !pi->pkg) continue; strarray_init(&seen); if (pi->pkg->depends) for (j = 0; j < pi->pkg->depends->size; j++) runtime_closure(ctx, pi->pkg->depends->data[j], &seen); if (pi->pkg->makedeps) for (j = 0; j < pi->pkg->makedeps->size; j++) runtime_closure(ctx, pi->pkg->makedeps->data[j], &seen); for (j = 0; j < n; j++) { if (i != j && strarray_contains(&seen, outdated->data[j])) { reach[i * n + j] = true; in_degree[i]++; } } strarray_free(&seen); } while (head < n) { size_t pick = n; /* first ready node in original order */ for (i = 0; i < n; i++) if (!taken[i] && in_degree[i] == 0) { pick = i; break; } /* stuck: cycle — take the node closest to ready */ if (pick == n) for (i = 0; i < n; i++) if (!taken[i] && (pick == n || in_degree[i] < in_degree[pick])) pick = i; out[head++] = outdated->data[pick]; taken[pick] = true; for (i = 0; i < n; i++) if (!taken[i] && reach[i * n + pick]) in_degree[i]--; } for (i = 0; i < n; i++) outdated->data[i] = out[i]; free(reach); free(in_degree); free(taken); free(out); } static int cmd_sysup(ctx_t *ctx) { strarray_t *installed, outdated, done; size_t i, failed = 0; strarray_init(&outdated); strarray_init(&done); installed = pkgdb_list(ctx->pkgdb); /* find outdated packages — frozen packages are excluded from * upgrades but are still considered "satisfied" by dependency * resolution (they stay at their current version). Recipe-locked * packages are compared against MAKEPKG.lock like any port. */ for (i = 0; i < installed->size; i++) { const char *name = installed->data[i]; installed_pkg_t *ip = pkgdb_find(ctx->pkgdb, name); port_t *p = repo_find(ctx->repo, name); if (!ip || !p) continue; if (pkg_frozen(ctx, name)) continue; if (port_load(p) != 0) continue; if (version_differs(ip, p)) strarray_add(&outdated, name); } if (outdated.size == 0) { printf("pkg: system is up to date\n"); strarray_free(&outdated); strarray_free(installed); free(installed); return 0; } /* deps before dependents */ sort_outdated_topo(ctx, &outdated); printf("pkg: packages to update:\n"); for (i = 0; i < outdated.size; i++) printf(" %s%s\n", outdated.data[i], port_recipe_locked(repo_find(ctx->repo, outdated.data[i])) ? " [MAKEPKG.lock]" : ""); if (!ask_confirm("pkg: proceed?")) { strarray_free(&outdated); strarray_free(installed); free(installed); return 0; } /* * In -b mode, run `bulk -u` once upfront — bulk reads pkgdb and * the ports tree itself, so it knows exactly which installed * packages have outdated binaries and fetches them in one shot. * The sysup loop below then sees binaries on disk and skips * builds via pkg_is_built(). Equivalent to running * `bulk -u && pkg sysup` by hand. * * If a new dep gets pulled in during install_order's resolve, * bulk -u doesn't know about it (it only walks the installed * set), so that dep would be source-built. This is the * intentional looser semantic — strict-binary cases should use * `pkg -b add ` first, or accept the source build. */ if (ctx->use_bulk) { char *bulk_argv[] = { (char *)BULK_CMD, "-u", NULL }; if (run_cmd(BULK_CMD, bulk_argv, NULL) != 0) { warnx("bulk -u failed; aborting sysup"); strarray_free(&outdated); strarray_free(installed); free(installed); return 1; } } /* * Upgrade each outdated target through full dependency * resolution. This pulls in any newly required runtime * depends and — when the binary tarball is missing — * makedeps as well, matching the behaviour of 'pkg add'. * * Packages that get upgraded transitively as part of an * earlier target's tree are detected by the version check * and skipped on their own iteration. */ for (i = 0; i < outdated.size; i++) { const char *name = outdated.data[i]; port_t *p; resolution_t *res; dep_entry_t *de; install_reason_t target_reason; installed_pkg_t *ip; p = repo_find(ctx->repo, name); if (!p) continue; if (port_load(p) != 0) continue; /* may have been upgraded as a transitive dep already */ ip = pkgdb_find(ctx->pkgdb, name); if (ip && p->pkg && !version_differs(ip, p)) continue; /* preserve the original install reason across the upgrade */ de = depdb_find(ctx->depdb, name); target_reason = de ? de->reason : REASON_MANUAL; /* * Resolve the target's tree. In normal source mode, makedeps * are pulled in only when no current binary exists. In -b * (bulk) mode we never compile, so makedeps are always skipped. */ res = resolve(ctx->repo, name, ctx->pkgdb, ctx->locker, ctx->use_bulk ? false : !pkg_is_built(p)); if (!res) { warnx("resolve failed: %s", name); failed++; continue; } if (res->has_missing) { size_t j; warnx("missing dependencies for %s:", name); for (j = 0; j < res->missing->size; j++) fprintf(stderr, " %s\n", res->missing->data[j]); resolution_free(res); failed++; continue; } if (res->has_cycle) { size_t j; warnx("dependency cycle for %s:", name); for (j = 0; j < res->cycle->size; j++) fprintf(stderr, " %s\n", res->cycle->data[j]); resolution_free(res); failed++; continue; } /* * In -b mode, fetch this target's full binary closure first * (same set `pkg bulk ` would fetch — full tree, ignoring * install state). A bulk failure for one target skips that * target but lets sysup continue; matches the existing * per-target isolation, no all-or-nothing. */ if (ctx->use_bulk) { strarray_t bulk_set, bulk_missing, bulk_visited; int bulk_ret = 0; strarray_init(&bulk_set); strarray_init(&bulk_missing); strarray_init(&bulk_visited); bulk_collect(ctx->repo, ctx->pkgdb, name, &bulk_set, &bulk_missing, &bulk_visited); if (bulk_missing.size > 0) { size_t k; warnx("missing ports for bulk fetch (%s):", name); for (k = 0; k < bulk_missing.size; k++) fprintf(stderr, " %s\n", bulk_missing.data[k]); bulk_ret = 1; } if (bulk_ret == 0 && bulk_set.size > 0) { if (pkg_bulk_fetch(&bulk_set) != 0) { warnx("bulk fetch failed: %s — skipping", name); bulk_ret = 1; } } strarray_free(&bulk_set); strarray_free(&bulk_missing); strarray_free(&bulk_visited); if (bulk_ret != 0) { resolution_free(res); failed++; continue; } } /* 'done' counts every addpkg, including deps upgraded inside * another target's tree — those are skipped on their own * iteration, so counting targets would under-report */ if (install_order(ctx, res->order, name, target_reason, &done) != 0) { warnx("upgrade failed: %s", name); failed++; } resolution_free(res); } printf("pkg: updated: %zu packages", done.size); if (failed > 0) printf(", %zu failed", failed); putchar('\n'); strarray_free(&done); strarray_free(&outdated); strarray_free(installed); free(installed); return failed > 0 ? 1 : 0; } /* * Detect target package from explicit argument or MAKEPKG in cwd. * Returns a strdup'd name the caller must free, or NULL on failure. */ static char *detect_target(const char *arg) { char cwd[PATH_MAX], path[PATH_MAX]; package_t *tmp; char *name; if (arg) return xstrdup(arg); if (!getcwd(cwd, sizeof(cwd))) return NULL; snprintf(path, sizeof(path), "%s/MAKEPKG", cwd); tmp = package_new(NULL, cwd); /* name comes from the file */ if (makepkg_parse(path, tmp) != 0 || !tmp->name) { package_free(tmp); return NULL; } name = xstrdup(tmp->name); package_free(tmp); return name; } static int cmd_prepare(ctx_t *ctx, const char *arg) { char *target; port_t *p; resolution_t *res; strarray_t installed_deps; size_t i; target = detect_target(arg); if (!target) errx(1, "no package specified and no MAKEPKG in current directory"); p = repo_find(ctx->repo, target); if (!p) { warnx("package '%s' not found", target); free(target); return 1; } if (port_load(p) != 0) { free(target); return 1; } if (!p->pkg || ((!p->pkg->depends || p->pkg->depends->size == 0) && (!p->pkg->makedeps || p->pkg->makedeps->size == 0))) { printf("pkg: no dependencies for %s\n", target); free(target); return 0; } if (ctx->verbose) { if (p->pkg->depends && p->pkg->depends->size > 0) { printf("depends:"); for (i = 0; i < p->pkg->depends->size; i++) printf(" %s", p->pkg->depends->data[i]); printf("\n"); } if (p->pkg->makedeps && p->pkg->makedeps->size > 0) { printf("makedeps:"); for (i = 0; i < p->pkg->makedeps->size; i++) printf(" %s", p->pkg->makedeps->data[i]); printf("\n"); } } /* resolve full dependency tree including makedeps */ res = resolve(ctx->repo, target, ctx->pkgdb, ctx->locker, true); if (!res) { free(target); errx(1, "failed to resolve dependencies"); } if (res->has_missing) { warnx("missing dependencies:"); for (i = 0; i < res->missing->size; i++) fprintf(stderr, " %s\n", res->missing->data[i]); resolution_free(res); free(target); return 1; } if (res->has_cycle) { warnx("dependency cycle:"); for (i = 0; i < res->cycle->size; i++) fprintf(stderr, " %s\n", res->cycle->data[i]); resolution_free(res); free(target); return 1; } /* * Install every resolved package EXCEPT the target itself. * New installs are marked REASON_AUTO so they become orphans if * the target is never installed via 'pkg add'; upgrades of * installed deps keep their existing reason. */ strarray_init(&installed_deps); for (i = 0; i < res->order->size; i++) { const char *name = res->order->data[i]; port_t *dp; bool upgrade; /* skip the target — we're only preparing deps */ if (strcmp(name, target) == 0) continue; dp = repo_find(ctx->repo, name); if (!dp) continue; if (port_load(dp) != 0) { strarray_free(&installed_deps); resolution_free(res); free(target); return 1; } upgrade = pkgdb_installed(ctx->pkgdb, name); /* skip already-installed at same version */ if (upgrade) { installed_pkg_t *ip = pkgdb_find(ctx->pkgdb, name); if (ip && dp->pkg && !version_differs(ip, dp)) { /* ensure tracked in depdb */ if (!depdb_find(ctx->depdb, name)) { record_depdb(ctx, name, dp, REASON_AUTO); depdb_save(ctx->depdb); } continue; } } if (!pkg_is_built(dp)) { printf("pkg: building %s\n", name); if (pkg_build(dp) != 0) { warnx("build failed: %s", name); strarray_free(&installed_deps); resolution_free(res); free(target); return 1; } } printf("pkg: installing %s\n", name); if (pkg_install(dp, upgrade, ctx->root) != 0) { warnx("install failed: %s", name); strarray_free(&installed_deps); resolution_free(res); free(target); return 1; } strarray_add(&installed_deps, name); record_depdb(ctx, name, dp, dep_reason(ctx, name, upgrade)); depdb_save(ctx->depdb); reload_pkgdb(ctx); } if (installed_deps.size > 0) { printf("pkg: %s -- installed", target); for (i = 0; i < installed_deps.size; i++) printf(" %s", installed_deps.data[i]); printf(" (%zu)\n", installed_deps.size); } else { printf("pkg: all dependencies for %s satisfied\n", target); } strarray_free(&installed_deps); resolution_free(res); free(target); return 0; } static int cmd_build(ctx_t *ctx, const char *arg) { char *target; port_t *p; target = detect_target(arg); if (!target) errx(1, "no package specified and no MAKEPKG in current directory"); p = repo_find(ctx->repo, target); if (!p) { warnx("package '%s' not found", target); free(target); return 1; } /* locked ports are fine: port_load() reads MAKEPKG.lock, the * same recipe mkpkg builds, so pkg_is_built() looks for the * right version */ if (port_load(p) != 0) { free(target); return 1; } if (pkg_is_built(p)) { printf("pkg: %s is already built\n", target); free(target); return 0; } printf("pkg: building %s\n", target); if (pkg_build(p) != 0) { warnx("build failed: %s", target); free(target); return 1; } printf("pkg: built %s\n", target); free(target); return 0; } static int cmd_lock(ctx_t *ctx, const char *pkg) { port_t *p; if (!pkg) errx(1, "no package specified"); if (!pkgdb_installed(ctx->pkgdb, pkg)) errx(1, "package '%s' not installed", pkg); /* * Snapshot before recording the lock. If the copy fails nothing * has changed; recording first and failing the copy would leave * pkg refusing upgrades for a port with no pinned recipe behind * the refusal. An existing lock is adopted, never rewritten. * * The !p case is reachable: locking only requires the package to * be INSTALLED, and a port can leave the tree while its package * stays on the system. */ p = repo_find(ctx->repo, pkg); if (p && recipe_lock_exists(p->path)) { /* * Never overwrite an existing lock. unlock removes the * snapshot BEFORE the list entry, so an interrupted unlock * leaves none behind: a MAKEPKG.lock here is a live pinned * recipe (possibly edited, possibly signed), and re-running * `pkg lock` after `ports -u` used to replace it with * upstream's. Adopt it as it stands. */ printf("pkg: %s: keeping existing %s/MAKEPKG.lock\n", pkg, p->path); } else if (p) { if (snapshot_recipe(p->path) != 0) errx(1, "cannot write the lock snapshot in %s", p->path); } else { warnx("no port for '%s' — locking the installed version only, " "no recipe snapshot", pkg); } if (!locker_lock(ctx->locker, pkg)) errx(1, "failed to lock package"); if (p) printf("pkg: locked %s (%s: MAKEPKG.lock)\n", pkg, p->path); else printf("pkg: locked %s\n", pkg); return 0; } static int cmd_unlock(ctx_t *ctx, const char *pkg) { port_t *p; if (!pkg) errx(1, "no package specified"); /* * locker_unlock() returns false both for "was not locked" and * for "could not write the lock file", so ask first — otherwise * a failed rename reports the opposite of what happened. */ if (!locker_is_locked(ctx->locker, pkg)) errx(1, "package '%s' not locked", pkg); /* * Remove the snapshot first. Unlocking means taking upstream's * recipe, and mkpkg keeps building MAKEPKG.lock for as long as * it exists. If the save below then fails, the package stays * listed as locked — the conservative direction: pkg keeps * refusing upgrades and a second `pkg unlock` finishes the job. */ p = repo_find(ctx->repo, pkg); if (p) { char lockfile[PATH_MAX]; size_t i; /* MAKEPKG.lock first: it is the switch. Companions left by a * crash here are inert — mkpkg reads them only while * MAKEPKG.lock exists — and the next lock rewrites them. */ recipe_lock_path(lockfile, sizeof(lockfile), p->path); if (unlink(lockfile) != 0 && errno != ENOENT) err(1, "cannot remove %s", lockfile); for (i = 0; i < LOCK_COMPANIONS; i++) { snprintf(lockfile, sizeof(lockfile), "%s/%s", p->path, lock_companions[i][1]); if (unlink(lockfile) != 0 && errno != ENOENT) warn("cannot remove %s", lockfile); } } if (!locker_unlock(ctx->locker, pkg)) errx(1, "cannot update %s", LOCK_FILE); printf("pkg: unlocked %s\n", pkg); return 0; } static int cmd_locked(ctx_t *ctx) { strarray_t *locked; size_t i; locked = locker_list(ctx->locker); if (!locked || locked->size == 0) { printf("pkg: no locked packages\n"); if (locked) { strarray_free(locked); free(locked); } return 0; } for (i = 0; i < locked->size; i++) { const char *name = locked->data[i]; installed_pkg_t *ip = pkgdb_find(ctx->pkgdb, name); port_t *p = repo_find(ctx->repo, name); char repo_ver[256]; bool have_repo = false; if (p && port_load(p) == 0 && p->pkg && p->pkg->version) { version_build(repo_ver, sizeof(repo_ver), p->pkg->version, p->pkg->release); have_repo = true; } printf("%-24s %s", name, ip ? ip->version : "(not installed)"); /* port_load() reads MAKEPKG.lock when present, so this is * the pinned recipe's version, not upstream's */ if (have_repo && (!ip || strcmp(ip->version, repo_ver) != 0)) printf(" -> %s %s", recipe_lock_exists(p->path) ? "lock" : "repo", repo_ver); /* * Flag the states that make a lock a lie: an entry for a * package that is no longer installed, and a lock with no * pinned recipe behind it. */ if (!ip) printf(" [stale]"); if (p && !recipe_lock_exists(p->path)) printf(" [no MAKEPKG.lock]"); putchar('\n'); } strarray_free(locked); free(locked); return 0; } /* * Populate depends for depdb entries that have an empty depends list. * Looks up each package in the repo and copies its runtime depends. * Returns number of entries updated. */ static int initdb_populate_depends(ctx_t *ctx) { strarray_t *tracked; size_t i, j; int updated = 0; if (!ctx->repo) return 0; tracked = depdb_list(ctx->depdb); for (i = 0; i < tracked->size; i++) { const char *name = tracked->data[i]; dep_entry_t *e = depdb_find(ctx->depdb, name); port_t *p; if (!e) continue; /* skip entries that already have depends */ if (e->depends && e->depends->size > 0) continue; p = repo_find(ctx->repo, name); if (!p || port_load(p) != 0 || !p->pkg || !p->pkg->depends) continue; for (j = 0; j < p->pkg->depends->size; j++) strarray_add(e->depends, resolve_dep_provider(ctx, p->pkg->depends->data[j])); updated++; } strarray_free(tracked); free(tracked); return updated; } static int cmd_initdb(ctx_t *ctx) { int added, populated; if (ctx->depdb->size > 0) { printf("pkg: dependency database: %zu entries\n", ctx->depdb->size); if (!ask_confirm("pkg: add missing packages?")) return 0; } added = depdb_initdb(ctx->depdb, ctx->pkgdb); populated = initdb_populate_depends(ctx); if (added > 0 || populated > 0) { depdb_save(ctx->depdb); if (added > 0) printf("pkg: added %d packages to dependency database\n", added); if (populated > 0) printf("pkg: populated dependencies for %d packages\n", populated); } else { printf("pkg: all packages already tracked\n"); } return 0; } /* * Auto-initialise the dep database on first use. * Called once from main() when depdb is empty but pkgdb has packages. * This silently seeds the database so orphan detection and * reverse-dependency tracking work out of the box. * NEVER overwrites existing entries (depdb_initdb guarantees this). */ static void auto_initdb(ctx_t *ctx, bool have_lock) { int added; /* only trigger when depdb is completely empty */ if (ctx->depdb->size > 0) return; if (ctx->pkgdb->size == 0) return; /* * This writes deps.db, so it needs the instance lock even under a * read-only command; then re-read, another pkg may have done it. */ if (!have_lock) { if (acquire_proc_lock(ctx->root) < 0) return; depdb_close(ctx->depdb); ctx->depdb = depdb_open(ctx->root); if (ctx->depdb->size > 0) return; } /* the depends lists come from the ports: without them rdepends, * del's reverse-dependency check and orphans see nothing */ if (!ctx->repo) { ctx->repo = repo_new(); repo_scan_all(ctx->repo, ctx->config->portdirs, ctx->config->portdir_count); } added = depdb_initdb(ctx->depdb, ctx->pkgdb); if (added > 0) { initdb_populate_depends(ctx); depdb_save(ctx->depdb); printf("pkg: initialised dependency database with %d " "packages\n", added); } } static int cmd_checkdb(ctx_t *ctx) { strarray_t *installed, *tracked; size_t i; int stale = 0, untracked = 0; installed = pkgdb_list(ctx->pkgdb); tracked = depdb_list(ctx->depdb); /* find stale entries (in depdb but not installed) */ for (i = 0; i < tracked->size; i++) { if (!pkgdb_installed(ctx->pkgdb, tracked->data[i])) { if (stale == 0) printf("pkg: stale entries:\n"); printf(" %s\n", tracked->data[i]); stale++; } } /* find untracked (installed but not in depdb) */ for (i = 0; i < installed->size; i++) { if (!depdb_find(ctx->depdb, installed->data[i])) { if (untracked == 0) printf("pkg: untracked packages:\n"); printf(" %s\n", installed->data[i]); untracked++; } } if (stale == 0 && untracked == 0) { printf("pkg: database is clean\n"); } else { printf("pkg: stale: %d, untracked: %d\n", stale, untracked); if (stale > 0 && ask_confirm("pkg: remove stale entries?")) { for (i = 0; i < tracked->size; i++) { if (!pkgdb_installed(ctx->pkgdb, tracked->data[i])) depdb_remove(ctx->depdb, tracked->data[i]); } depdb_save(ctx->depdb); printf("pkg: removed %d stale entries\n", stale); } } strarray_free(installed); free(installed); strarray_free(tracked); free(tracked); return 0; } static int cmd_list(ctx_t *ctx) { strarray_t *installed; size_t i; installed = pkgdb_list(ctx->pkgdb); for (i = 0; i < installed->size; i++) { installed_pkg_t *ip = pkgdb_find(ctx->pkgdb, installed->data[i]); if (ip) printf("%s %s\n", ip->name, ip->version); } strarray_free(installed); free(installed); return 0; } static int cmd_isinst(ctx_t *ctx, const char *pkg) { installed_pkg_t *ip; if (!pkg) errx(1, "no package specified"); ip = pkgdb_find(ctx->pkgdb, pkg); if (!ip) return 1; printf("%s %s\n", ip->name, ip->version); return 0; } /* ---- Main ---- */ int main(int argc, char **argv) { ctx_t ctx = {0}; const char *cmd, *arg = NULL; int opt, ret = 0; bool have_lock = false; static struct option longopts[] = { {"root", required_argument, 0, 'r'}, {"no-deps", no_argument, 0, 'N'}, {"use-bulk", no_argument, 0, 'b'}, {"yes", no_argument, 0, 'y'}, {"force", no_argument, 0, 'f'}, {"verbose", no_argument, 0, 'v'}, {"version", no_argument, 0, 'V'}, {"help", no_argument, 0, 'h'}, {0, 0, 0, 0} }; ctx.root = "/"; while ((opt = getopt_long(argc, argv, "r:NbyfvVh", longopts, NULL)) != -1) { switch (opt) { case 'r': ctx.root = optarg; break; case 'N': ctx.no_deps = true; break; case 'b': ctx.use_bulk = true; break; case 'y': ctx.assume_yes = true; break; case 'f': ctx.force = true; break; case 'v': ctx.verbose = true; break; case 'V': version(); return 0; case 'h': usage(); return 0; default: usage(); return 1; } } if (optind >= argc) { usage(); return 1; } cmd = argv[optind]; if (optind + 1 < argc) arg = argv[optind + 1]; g_assume_yes = ctx.assume_yes; /* * Serialize db-mutating commands across pkg instances. * Read-only commands (list, depends, orphans, ...) stay * lock-free so they never block behind a long sysup. * The lock must be taken BEFORE the dbs are read, otherwise * we could load a snapshot another instance is about to replace. */ if (strcmp(cmd, "add") == 0 || strcmp(cmd, "del") == 0 || strcmp(cmd, "update") == 0 || strcmp(cmd, "sysup") == 0 || strcmp(cmd, "autoremove") == 0 || strcmp(cmd, "prepare") == 0 || strcmp(cmd, "initdb") == 0 || strcmp(cmd, "checkdb") == 0 || strcmp(cmd, "lock") == 0 || strcmp(cmd, "unlock") == 0) { if (acquire_proc_lock(ctx.root) < 0) errx(1, "cannot acquire instance lock"); have_lock = true; } /* load config */ ctx.config = config_new(); config_load(ctx.config, NULL); /* open databases */ ctx.pkgdb = pkgdb_open(ctx.root); ctx.depdb = depdb_open(ctx.root); ctx.locker = locker_open(ctx.root); /* * Scan repository for commands that need it. * * lock/unlock/locked are in this list because they now touch the * port directory: lock snapshots MAKEPKG to MAKEPKG.lock, unlock * removes it, and locked reports whether the snapshot is there. * Without the scan repo_find() returns NULL and every lock * silently degrades to a version-only pin. */ if (strcmp(cmd, "add") == 0 || strcmp(cmd, "update") == 0 || strcmp(cmd, "sysup") == 0 || strcmp(cmd, "depends") == 0 || strcmp(cmd, "diff") == 0 || strcmp(cmd, "prepare") == 0 || strcmp(cmd, "build") == 0 || strcmp(cmd, "bulk") == 0 || strcmp(cmd, "lock") == 0 || strcmp(cmd, "unlock") == 0 || strcmp(cmd, "locked") == 0) { ctx.repo = repo_new(); repo_scan_all(ctx.repo, ctx.config->portdirs, ctx.config->portdir_count); } /* auto-populate depdb on first use (never overwrites) */ if (strcmp(cmd, "version") != 0 && strcmp(cmd, "help") != 0) auto_initdb(&ctx, have_lock); /* dispatch */ if (strcmp(cmd, "add") == 0) { /* multi-package: pkg add port1 port2 ... */ int i; if (optind + 1 >= argc) errx(1, "no package specified"); for (i = optind + 1; i < argc; i++) { ret = cmd_install(&ctx, argv[i], false); if (ret != 0) break; reload_pkgdb(&ctx); } } else if (strcmp(cmd, "del") == 0) { /* multi-package: pkg del port1 port2 ... */ int i, failed = 0; if (optind + 1 >= argc) errx(1, "no package specified"); for (i = optind + 1; i < argc; i++) { if (cmd_remove(&ctx, argv[i]) != 0) failed++; else reload_pkgdb(&ctx); } ret = failed > 0 ? 1 : 0; } else if (strcmp(cmd, "list") == 0) { ret = cmd_list(&ctx); } else if (strcmp(cmd, "isinst") == 0) { ret = cmd_isinst(&ctx, arg); } else if (strcmp(cmd, "update") == 0) { /* multi-package: pkg update port1 port2 ... */ int i; if (optind + 1 >= argc) errx(1, "no package specified"); for (i = optind + 1; i < argc; i++) { if (!pkgdb_installed(ctx.pkgdb, argv[i])) errx(1, "package '%s' not installed", argv[i]); ret = cmd_install(&ctx, argv[i], true); if (ret != 0) break; reload_pkgdb(&ctx); } } else if (strcmp(cmd, "sysup") == 0) { ret = cmd_sysup(&ctx); } else if (strcmp(cmd, "bulk") == 0) { if (optind + 1 >= argc) errx(1, "no package specified"); ret = cmd_bulk(&ctx, &argv[optind + 1], argc - optind - 1); } else if (strcmp(cmd, "prepare") == 0) { ret = cmd_prepare(&ctx, arg); } else if (strcmp(cmd, "build") == 0) { ret = cmd_build(&ctx, arg); } else if (strcmp(cmd, "autoremove") == 0) { ret = cmd_autoremove(&ctx); } else if (strcmp(cmd, "orphans") == 0) { ret = cmd_orphans(&ctx); } else if (strcmp(cmd, "depends") == 0) { ret = cmd_depends(&ctx, arg); } else if (strcmp(cmd, "rdepends") == 0) { ret = cmd_rdepends(&ctx, arg); } else if (strcmp(cmd, "diff") == 0) { ret = cmd_diff(&ctx); } else if (strcmp(cmd, "lock") == 0) { ret = cmd_lock(&ctx, arg); } else if (strcmp(cmd, "unlock") == 0) { ret = cmd_unlock(&ctx, arg); } else if (strcmp(cmd, "locked") == 0) { ret = cmd_locked(&ctx); } else if (strcmp(cmd, "checkdb") == 0) { ret = cmd_checkdb(&ctx); } else if (strcmp(cmd, "initdb") == 0) { ret = cmd_initdb(&ctx); } else if (strcmp(cmd, "version") == 0) { version(); } else if (strcmp(cmd, "help") == 0) { usage(); } else { errx(1, "unknown command: %s", cmd); } /* cleanup */ if (ctx.repo) repo_free(ctx.repo); locker_close(ctx.locker); depdb_close(ctx.depdb); pkgdb_close(ctx.pkgdb); config_free(ctx.config); return ret; }