LICENSE=BSD-3-ClauseCGO_ENABLED=0The pkgx world rebuilt in pure CGO_ENABLED=0 Go: a runtime that runs packages on the fly, an installer that mirrors the reference CLI, and the bottle backend they share. The reference tools shell out to pkgx + deno + curl + openssl + xz โ on the order of 515 MB of runtime closure โ a figure this page has carried without a date or an instrument beside it, unlike every other number here, and it is the one claim about somebody else's software that nobody has re-measured. These replace all of it with single 9.1 MB static binaries that need no libc at all, so they run on a literally empty FROM scratch image and materialise real packages there. BSD-3-Clause.
# an image whose ONLY file is the pkgx binary FROM scratch COPY pkgx /bin/pkgx ENV PKGX_DIR=/pkgx ENTRYPOINT ["/bin/pkgx"] # there is no shell in there, and no libc either $ docker run --rm pkgx-scratch gnu.org/wget --version GNU Wget 1.25.0 built on linux-gnu. # bring several packages into one environment $ docker run --rm pkgx-scratch +sqlite.org -- sqlite3 --version 3.53.4 2026-07-24 โฆ
It downloads the static binary for your os/arch from a named GitHub release, verifies it against that release's SHA256SUMS, and puts it on your PATH. A line copied today and the same line copied in six months install the same bytes โ and a bad release does not reach everyone who happens to install that hour.
pkgm installs, pkgx runs without installing, and pkgs answers about packages โ ls, search, why, catalog, store, browse, and request when a package is not there, which prints a report you can open as an issue and sends nothing. The last two ship from one release: the word after pkgx is a package, as it is upstream, so pkgx ls runs GNU ls and the verbs needed a command of their own.
curl -fsSL https://go-pkgx.github.io/install.sh | sh -s -- pkgm v0.5.1
curl -fsSL https://go-pkgx.github.io/install.sh | sh -s -- pkgx v0.27.1
curl -fsSL https://go-pkgx.github.io/install.sh | sh -s -- pkgs v0.27.1$env:PKGM_VERSION='v0.5.1'
irm https://go-pkgx.github.io/install.ps1 | iexWrite latest where the version goes โ which is also what a bare | sh has always done. Re-running is the updater: it skips the download when the target version is already installed, and PKGM_FORCE=1 reinstalls anyway. Go users: go install github.com/go-pkgx/pkgm@latest.
Every dependency a packaging tool shells out to is a dependency the packages it installs have to coexist with. Removing them is not tidiness โ it is what makes an empty image a valid target.
The runtime. Run any package on the fly โ pkgx node@22 --version โ or bring several into an environment โ pkgx +git +gnu.org/bash -- ./build.sh. It materialises each package’s full dependency closure on demand and execs through the pkgx glibc loader, so it works on a literally-empty FROM scratch image. A single CGO_ENABLED=0 binary.
The installer. Resolves a package’s runtime closure from the pkgx pantry, downloads the bottles, and installs them โ mirroring the reference pkgm CLI (install/uninstall/shim/list/outdated/update/pin) as a drop-in replacement, plus a run that completes the closure from ELF DT_NEEDED and runs it on FROM scratch.
The shared backend both tools import โ one source of truth for the pkgx bottle protocol: resolution, download, soname-exact FROM scratch closure completion, and loader-aware exec. net/http with an embedded CA bundle replaces curl+openssl; compress/gzip + a pure-Go xz decoder replace info-zip+xz.
Sync a local mirror of pkgx bottles from the official dist into the same servable tree layout โ per os/arch, incremental, with --closure to pull a package’s full runtime closure (plus the implicit FROM scratch system libs). Serve it statically and point pkgm/pkgx at it with PKGX_DIST=<mirror-url>.
The build tool, a from-scratch CGO_ENABLED=0 reimplementation of pkgx’s brewkit โ which was Deno/TypeScript with Ruby and patchelf helpers. It reads a pantry package.yml, builds in a pkgx environment, relocates the install tree and packages a bottle. The reference tool is native: everything pivots on the host being the thing it builds for. Here Target is not Host by design, so the target platform is a first-class value and a Windows PE bottle built on a Linux or macOS host is a supported path rather than a retrofit, and so is a pure-Go wasip1/wasm module (target triple wasm32-wasip1, no C toolchain, no POSIX relocation).
The factory. It runs bk over the pkgx pantry and publishes to ghcr.io/go-pkgx/packages, each package an OCI artifact carrying a CycloneDX SBOM and an in-toto SLSA provenance statement as OCI referrers. Measured from the published registry.json on 2026-09-26: 1 584 projects, 42 530 bottles across linux/aarch64 (15 135), linux/x86-64 (13 253), darwin/x86-64 (7 029), darwin/aarch64 (6 559) and windows/x86-64 (554). It is a moving target, so re-measure rather than trust this line. A pilot wasip1/wasm lane (wasip1.yml) publishes pure-Go modules โ age, go-md2man, shfmt so far, each run under wasmtime before publishing โ under the OCI platform os=wasip1, architecture=wasm, for an in-browser container engine.
The corrections, consumed through PKGX_PANTRY_OVERLAY: each recipe here overrides the matching upstream one and everything else falls back. It exists because an upstream recipe can be unsatisfiable against the current registry โ curl, wget and git pin openssl.org: ^1.1 while openssl is 3.x and 4.x โ so without the overlay pkgm install cannot resolve the closure the published bottles were actually built against.
The tree you are already using. A CGO_ENABLED=0 WASM browser app that renders the registry as a filterable TreeTable, drawn to a <canvas> with go-widgets/toolkit and go-widgets/painter. It powers https://go-pkgx.github.io/packages/, reading the registry.json the factory emits.
Documentation (MkDocs Material): usage, the full-pantry FROM-scratch conformance sweep (1818 projects), and the maintained soname โ project fix list. A pantry-wide audit on 2026-09-26 found 1818 of 1896 projects published a linux bottle (the pantry held 1899 on 2026-10-04), and after the soname-exact fix 73 of 81 previously-failing projects run on scratch.
nix, guix and spack all let you browse what exists before you install it. Here the same thing works where none of them run: a single static binary, a catalogue cached as one file, and no network after the first fetch. <TAB> answers in 3.8 ms โ median of 300 runs in a FROM scratch container with the network off, measured 2026-10-11 against a floor of 3.05 ms for starting the process at all โ because nothing on that path opens a socket, and because since v0.16.0 it reads a 38 KB name index instead of decoding the 470 KB catalogue.
# once, with a network. Then never again. $ pkgx catalog update 1908 project(s) โ ~/.pkgx/catalog/linux-x86-64.json # a namespace lists what it CONTAINS, a package what it NEEDS $ pkgx ls --tree curl.se curl.se โ 8.17.0 โ curl.se/ca-certs 2026.09.25 โ doxygen.nl (no bottle here) openssl.org 3.6.0 โ 3.5.1 # you know the command, not the package $ pkgx search rg crates.io/ripgrep rg 14.1.1 # and the other direction: who NEEDS this, before you change it $ pkgx ls --dependents openssl.org | wc -l 164 $ pkgx why git-scm.org zlib.net git-scm.org โ 2.55.0 โ zlib.net โ 1.3.2
gnu.org contains gnu.org/bash because of how it is named; curl.se has openssl.org under it because of what it needs. nix and guix keep the two in separate commands; here the node decides, because the person typing already knows which they named.
Spack generates a completion file and commits it โ spack-completion.bash says “auto-generated… please do not manually modify” in its own header โ and keeps it honest with a test called test_updated_completion_scripts. guix hand-writes one per shell: three files, 9 KB of bash, 33 KB of zsh, 78 KB of fish, each with its own authors and no generator to compare against. Nix has the binary answer โ NIX_GET_COMPLETIONS carries the argument index, in src/libutil/args.cc โ and so does this โ because what is being completed is the registry, which changes without the binary changing, and because there is no completion framework in an empty image.
A catalogue is published per platform: 537 of 1908 projects have no bottle for darwin/aarch64. Dropping those names would say they do not exist, which is false. Marking them says what to build next.
pkgx browse walks it with the arrow keys, / searches, space pins โ and q prints what you pinned on stdout, so pkgx +$(pkgx browse) composes. tab cycles three views of the node you are standing on: what it contains, what it needs, and who needs it. Without a terminal it prints the tree and exits.
A โ is per project; pkgx store is the whole disk. One developer's machine on 2026-10-08: 41.2 GiB over 363 versions of 302 projects, 13 versions of gohugo.io, 12.8 GiB in versions that are not the newest present. Every ephemeral run leaves its bottles behind and nothing removes one. It reports and deletes nothing โ there is no profile here, so no roots, and nix, guix and spack all draw that line too.
ls --dependents asks who needs this โ the question before changing something, where every tree above answers the one before installing. 164 of 1908 projects need openssl on linux/aarch64, 854 transitively; on darwin/aarch64 it is 153 and 789, because a catalogue records what a platform really carries. pkgx why A B is Nix's third question: not what is in the closure but which link put it there, as a shortest path.
The same pantry commit resolved tcl to 9.0.4 and then to 9.1.0 four hours apart, because a recipe asks GitHub at resolution time. bk lock writes down what a set means today โ Spack's shape, not Spack's contents โ Spack calls its own the dag_hash, “Spack's default hash, used to identify installations”, and its code notes that what the hash covers has changed twice across releases. Ours is over the platform, the version and the parsed recipe, so a recipe edited under a pin is visible where a version alone would say nothing.
pkgx --lock f.hcl runs exactly those versions and bk factory --lock f.hcl builds them โ not just the lock's own roots, so a transitive dependency cannot move underneath. A set that will not resolve exactly fails, as cargo's --locked does.
A lock records its platform and refuses to be honoured elsewhere, by name. The versions a platform carries are not the versions another has, and failing late inside a resolver would blame a version instead of the lock.
A module system does two things: it resolves what a package needs, and it edits your shell. pkgx +a +b already did the first. pkge does the second โ a front-end shaped like Lmod's, over pkgx's own resolution, with no Lua interpreter and no modulefile tree. It ships inside the pkgx binary; there is nothing else to install.
$ eval "$(pkgx env init)" # in a profile $ pkge load stedolan.github.io/jq $ command -v jq ~/.pkgx/stedolan.github.io/jq/v1.8.2/bin/jq $ pkge unload stedolan.github.io/jq $ command -v jq /usr/bin/jq # back, exactly as it was
Because loading never edits incrementally: it restores the environment it first saw and recomposes from the whole spec set. So unload b after load a b c leaves precisely what load a c would have โ whatever order things happened in.
Where a site already runs one, publish modulefiles โ pkgx --modulefile +openmpi.org@5 โ and let the site's own Lmod load them, so conflicts, hierarchies and spider keep working.
pkgx env init --module defines module and ml only where there is no Lmod. Two implementations answering one command is how a support ticket becomes unanswerable.
The org runs its own bottle registry at ghcr.io/go-pkgx/packages. A pure-Go factory builds pkgx pantry recipes with bk, a CGO-free re-implementation of brewkit, and publishes through two channels: the signed OCI registry and a GitHub Pages pkgx-dist mirror.
Each bottle carries, as OCI referrers, a CycloneDX SBOM, an in-toto SLSA provenance statement and a cosign-style signature verified against a pinned key. On install PKGX_VERIFY=1 refuses an unsigned or badly-signed bottle.
The provenance records the source archive's SHA-256 (or a git commit) as well as the artefact. Over half this pantry builds from tarballs GitHub generates on request rather than stores, so for those it is the first stored record they have ever had.
Linux builds run inside a FROM-scratch rootfs staged from bottles โ no distribution libc and no distribution compiler take part. The output carries its PT_INTERP into the pkgx prefix and no distribution strings at all.
Publishing uses the workflow's native GITHUB_TOKEN, never a personal token, and every (project, version, platform) already present is skipped, so the catalogue grows progressively rather than being rebuilt.
No cgo, no interpreter, no runtime to install first. One static binary per job, a backend they share, and bottles that say where they came from.
Read the source on GitHub โ