Realm
blog:gnopm
gno.land/r/moul/blog:gnopm
Render
sharing gnopm
2026-09-28 · #gnopm · #tooling · #packages
gnopm is a package manager I wrote because bumping a version in gno destroyed
the diff. It keeps a package's version in its gnomod.toml instead of in its
directory name, and everything else it does falls out of that.
the diff that started it
A gno import path ends in its version, so the obvious layout gives each version its own directory, and a bump means copying that directory.
git cannot pair a copy. The review diff becomes added files with no content
diff at all, which is backwards: a bump is by definition the compatibility
change that most needs reviewing, and it is the one change you cannot see.
log --follow, blame and bisect stop there too.
One port of 25 realms landed in my repo as +11,103 / -0 across 112 files, every behaviour change invisible.
The toolchain already reads the version from the module line, so the directory does not have to repeat it:
p/alice/md/gnomod.toml module = "gno.land/p/alice/md/v1"
p/alice/md/md.gno edited in place
A bump is one line, then the real diff. The superseded version gets pinned in a
gnomod.lock to the commit that still holds it and rebuilt on demand, so
anything importing .../md/v0 keeps resolving with no directory left.
That lock is source, not a build artefact. A change that bumps a version carries the pin keeping the old one resolvable, so it is the author who commits it and CI that proves it still reproduces.
a version number is a tag, not a counter
This one cost me before I understood it. A number means nothing until the version is published: until then it names no bytes and derives no address. So you bump when the last version shipped, and edit in place when it did not.
Getting it wrong is the default behaviour of a stack of pull requests. The first bumps v0 to v1 and lands. The second is written against it, sees v1 taken, goes to v2. Both merge before either is published, and the package reaches a chain as v2 with v1 existing nowhere.
I did it to four packages in a single stacked pair of pull requests, and the
lock could not show me: a skipped number is never recorded, so it jumps v0 to v2
with nothing in between. gnopm tidy asks the chain, which is the only thing
that knows, and names the unbump that folds it back. That was an 8-line diff
of module lines once I could see it.
325 packages
My own repo was never the real test. examples/ in the gno monorepo is:
thousands of files, many versions, a layout nobody designed with this in mind.
Ran it on 2026-09-28 against master, in a throwaway worktree. 325 packages, 97
lifted out of versioned directories, 987 renames and zero content changes in
19.9 seconds. gnopm verify over the result: 0.08s, all 325 ok. gno lint
and gno test pass on the migrated packages, using gno's own binary.
One package blocked it, correctly: a quarantined realm that exists both
unversioned and under v1/ and v2/, so lifting v2 has nowhere to land and
nothing can say which is current.
I have not opened that as a pull request and would not from a script. It is 994 files in somebody else's repo, and which layout a project wants is a maintainer's call. The rehearsal just means that conversation can start with measurements instead of a claim.
please break it
moul/gnopm. One Go binary, no third-party dependencies, no gno toolchain needed to build it.
go install moul.io/gnopm@latest
gnopm init
If you arrive from npm and type gnopm install, it tells you why there is
nothing to install rather than "unknown command". There is no resolver and never
will be: an import path carries its version, so two versions are two paths and
there is no constraint to solve. gno skipped the entire class of problem that
produces SAT solvers, by accident of path design, and I am not reintroducing it.
What I want is an issue when it surprises you, and that counts when it behaved exactly as documented. Most of what is in there started as somebody, usually me, saying "I expected it to just do that".
Transactions
| Hash | Block | Time | Msg | Caller | Status |
|---|---|---|---|---|---|
| D275522CDC…C64DA812 | 408,742 | 12d ago | Call Set | g1manfre…dlf5 | ok |
| 7FA3FBE10E…8F7C98D1 | 406,149 | 12d ago | Call Set | g1manfre…dlf5 | ok |
| F87AED9E85…1694ECE2 | 403,367 | 12d ago | Call SetIntro | g1manfre…dlf5 | ok |
| 309AED3658…5B81E495 | 400,677 | 12d ago | EnablePkg | — | ok |
| 427251B5E8…73DED251 | 400,670 | 12d ago | Call SetIntro | g1manfre…dlf5 | ok |
| 4A96C1BA23…1430498E | 400,668 | 12d ago | AddPackage | — | ok |
| 7D22BA87AF…F2F833BB | 271,760 | 17d ago | Call Set | g1manfre…dlf5 | ok |
| AE4BBD069E…76165AA9 | 271,476 | 17d ago | Call SetIntro | g1manfre…dlf5 | ok |
| 00030B077A…CCA5FDBC | 271,243 | 17d ago | EnablePkg | — | ok |
| 72F71A363D…532552EF | 271,237 | 17d ago | AddPackage | — | ok |
Calls, deploys, and child packages in indexed history.