Lockfile Format
go2nix uses TOML lockfiles to pin module hashes. Both builder modes share
the same lockfile format, generated by go2nix generate.
Format
# go2nix lockfile v2. Generated by go2nix. Do not edit.
[mod]
"github.com/foo/bar@v1.2.3" = "sha256-abc..."
"golang.org/x/sys@v0.20.0" = "sha256-def..."
[replace]
"github.com/foo/bar@v1.2.3" = "github.com/fork/bar"
Versioning
The header comment carries a format version (go2nix lockfile vN). The
current version is v2. go2nix generate always writes the current
version; older lockfiles should be regenerated. Nothing reads the version
back, but the Go side rejects sections it does not know
(parsing go2nix.toml: unknown keys: [...]), and generate reads the
existing output file first to reuse its hashes — so delete an old-format lockfile, or write
to a new path with -o, before regenerating.
Sections
[mod] — Module hashes. Each key is a composite "path@version" string,
each value is a sha256-... SRI NAR hash of the module’s extracted source
tree, i.e. $GOMODCACHE/<path>@<version>/ after go mod download and
nothing else from the cache (the same tree the FOD fetcher produces).
[replace] — Module replacements (from go.mod replace directives).
Maps a composite key — the original module path with the replacement’s
version — to the replacement module path; the [mod] hash under that same
key is the hash of the replacement’s tree. Only remote replacements are
recorded; local replace directives (filesystem paths) are not included, and
the modules they replace are not fetched. This section is read by the
experimental builder and by go2nix build-modinfo; the default builder gets
its replacements from the plugin and only reads [mod].
Composite keys
Module keys use "path@version" format (e.g., "golang.org/x/sys@v0.20.0").
This keeps each module uniquely identified and avoids collisions across
versions.
Package graph resolution
The lockfile stores only module NAR hashes; the package graph is discovered
separately (eval-time plugin in default mode, build-time go list in
experimental mode — see Builder Modes).
Monorepo support
When go2nix generate is given multiple directories, all modules are merged
into a single lockfile. Modules from different go.mod files coexist without
conflict since each is uniquely keyed by "path@version".
When to regenerate
Regenerate the lockfile when — and only when — the module set changes:
- you add, remove, or bump a
requireline ingo.mod - a
replacedirective changes which remote module a path resolves to
generate and the staleness check look at go.mod only, so keep it tidy
(go mod tidy); a change to go.sum alone neither needs nor triggers
anything.
You do not need to regenerate after changing which packages import
which other packages, adding a new local package, or editing .go files.
The lockfile pins module hashes; the package graph is rediscovered on every
evaluation (see Package graph resolution).
go2nix generate . # rewrite go2nix.toml
go2nix check . # verify go2nix.toml still matches go.mod, no rewrite
Lockfile-free builds
The lockfile is optional in default mode: leave goLock out (or set it to
null).
goEnv.buildGoApplication {
src = ./.;
pname = "my-app";
version = "0.1.0";
}
The Nix plugin is then invoked with resolveHashes = true
and computes a NAR hash for each module from go.sum and GOMODCACHE,
returning a moduleHashes attrset that fills the role of the [mod] section.
The hashes are cached under $XDG_CACHE_HOME/go2nix/nar/, so only the first
evaluation pays for them. Module fetches are keyed on those hashes, and they
are the hashes go2nix generate would have written: every fetch and every
compile is the same derivation with or without a lockfile, and a binary cache
filled one way serves the other. Only the final derivation differs, because
with a lockfile it also checks go.mod against it; switching costs one link.
What building without a lockfile gives you:
- Nothing to regenerate after
go getorgo mod tidy, and the “lockfile is stale” failure cannot happen. - A change that only touches
go.modandgo.sumbuilds as it is — a Dependabot or Renovate update, a contributor who does not use go2nix. With a lockfile such a change fails until someone runsgo2nix generate. go.sumstays the only pin: there is no second file to fall behind and no merge conflicts ingo2nix.toml.- Nothing to install to start: an existing Go module builds with
src = ./.and no other change to the repository. - Packaging code you do not own needs no generated file kept next to your expression, and none to regenerate when you bump its revision.
- A repository with many
go.modfiles has nothing extra to maintain per module.
What you give up:
- the list of hashes that lives in the repository and is read in review;
- the check of
go.modagainst it at build time (mvscheck, see below) and the standalonego2nix check, which have nothing to compare with — module versions are read fromgo.sumon every evaluation instead; buildGoApplicationExperimental, which requires a lockfile;- a
go.sumhas to exist next togo.mod.
Commit a lockfile for what you ship or build in CI, where a hash that changes
should show up in a diff. Build without one for a new project, an experiment,
upstream code, or a repository that already treats go.sum as the pin.
Staleness detection
| When | What | Applies to | How |
|---|---|---|---|
| Generation | The module set | All modes | generate takes every require line of go.mod, with replace applied, and hashes each module; it runs no go list and relies on go.mod being tidy |
| Nix eval | Package graph, and the modules it uses | Default only | builtins.resolveGoPackages runs go list at eval time; a module the graph uses and the lockfile lacks fails with attribute '"<module>@<version>"' missing |
| Build time | Lockfile consistency | Default, with lockfile | link-binary re-reads go.mod and checks every required module is present in the lockfile at the right version; skipped when goLock = null |
In default mode a module that a package in the build imports but the
lockfile does not have stops evaluation; requirements the package graph did
not reach (another platform’s, or a test’s when doCheck is off) are caught
at build time when link-binary validates the lockfile. There is no stale
package graph: builtins.resolveGoPackages runs go list on every
evaluation. In experimental mode go2nix resolve reports
lockfile missing module <module>@<version> — regenerate with go2nix generate
inside the recursive-nix sandbox, or go list or a module fetch fails there.
Run go2nix check <dir> or go2nix check --lockfile <path> <dir> to verify
a lockfile without building.