Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 require line in go.mod
  • a replace directive 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 get or go mod tidy, and the “lockfile is stale” failure cannot happen.
  • A change that only touches go.mod and go.sum builds as it is — a Dependabot or Renovate update, a contributor who does not use go2nix. With a lockfile such a change fails until someone runs go2nix generate.
  • go.sum stays the only pin: there is no second file to fall behind and no merge conflicts in go2nix.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.mod files 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.mod against it at build time (mvscheck, see below) and the standalone go2nix check, which have nothing to compare with — module versions are read from go.sum on every evaluation instead;
  • buildGoApplicationExperimental, which requires a lockfile;
  • a go.sum has to exist next to go.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

WhenWhatApplies toHow
GenerationThe module setAll modesgenerate 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 evalPackage graph, and the modules it usesDefault onlybuiltins.resolveGoPackages runs go list at eval time; a module the graph uses and the lockfile lacks fails with attribute '"<module>@<version>"' missing
Build timeLockfile consistencyDefault, with lockfilelink-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.