Fork me on GitHub

No libc of its own,
so there is nowhere it cannot run.

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

9 repositories~9 MB static, no libc5 platforms published
FROM scratch โ€” zsh
# 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 โ€ฆ
9 repositories โ€” one job each, so the backend can be imported without bringing a CLI with it
Install

One line, and it names a version on purpose.

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.

๐Ÿง

Linux / macOS

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
๐ŸชŸ

Windows (PowerShell)

$env:PKGM_VERSION='v0.5.1'
irm https://go-pkgx.github.io/install.ps1 | iex
๐Ÿ”

Tracking releases, and updating

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

Why go-pkgx

A package manager that owns its own environment.

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.

pkgx ready

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.

repo โ†’ ยท pkg.go.dev

pkgm ready

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.

repo โ†’ ยท pkg.go.dev

bottle ready

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.

repo โ†’ ยท pkg.go.dev

mirror ready

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

repo โ†’ ยท pkg.go.dev

bk ready

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

repo โ†’ ยท pkg.go.dev

packages ready

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.

repo โ†’ ยท pkg.go.dev

pantry-overlay ready

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.

repo โ†’ ยท pkg.go.dev

registry-viewer ready

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.

repo โ†’ ยท pkg.go.dev

docs ready

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.

repo โ†’ ยท pkg.go.dev

9
repositories, one job each
~9 MB
static binary, against ~515 MB of reference closure
1430
of 1908 projects carry a linux/aarch64 bottle, read off the catalogue the factory published on 2026-10-06 โ€” and the other 478 are named, marked as having none here, rather than hidden
0
runtime dependencies โ€” no libc, no deno, no curl
Finding things

A tree you can walk, offline, in an empty image.

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.

pkgx โ€” zsh
# 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
๐ŸŒณ

A node has two kinds of thing under it

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.

โŒจ๏ธ

The binary answers <TAB>

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.

๐Ÿ”ญ

Everything is named; what is here is marked

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.

๐Ÿ–ฅ๏ธ

And a full-screen browser

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.

๐Ÿ’พ

And what it has cost you

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.

โ†ฉ๏ธ

The two questions that do not descend

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.

Reproducibility

The same build, later.

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.

๐Ÿ“Œ

Every pin is a root

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.

๐Ÿงญ

It knows where it was taken

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.

Environments and HPC

A module system, without Lmod.

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.

pkge โ€” zsh
$ 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
โ†ฉ๏ธ

Unloading is exact

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.

๐Ÿค

It defers to a real Lmod

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.

๐Ÿšซ

And refuses to shadow it

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.

Provenance

A signed, attested package registry.

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.

๐Ÿ”

Fail-closed by default

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.

๐Ÿ“ฅ

What went in, not only what came out

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.

๐Ÿงฑ

Built where nothing leaks in

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.

๐Ÿ”‘

No long-lived credential

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.

Packaging that ships like Go.

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 โ†’