Introduction
⚠️ Experimental — APIs and lockfile formats may change without notice.
go2nix is a Nix-native Go builder with per-package derivations and
fine-grained caching. It is an alternative to nixpkgs buildGoModule for
projects that want more visibility and reuse than the usual “fetch all
modules, then build everything in one derivation” model.
In Go, a module is the versioned unit you depend on (one go.mod, one
entry in go.sum); a package is a single importable directory of .go
files. One module typically contains many packages. go2nix locks modules but
builds packages:
- the lockfile pins modules, not the package graph
- the builder discovers the package graph and compiles it at package granularity
- Nix can cache and rebuild individual Go packages, not just the whole app
This works especially well for monorepos and multi-package repositories that want to maximize Nix store reuse. When only part of the Go package graph changes, go2nix reuses the rest of the graph instead of rebuilding the whole application derivation.
If you just want the simplest way to package a Go program in nixpkgs,
buildGoModule is still the default choice. go2nix is aimed at cases where
per-package reuse and explicit graph handling are worth the extra machinery.
Quick start
Heads up: the default builder requires the go2nix Nix plugin to be loaded into your evaluator. Without it,
nix buildfails witherror: attribute 'resolveGoPackages' missing.
1. Generate a lockfile
go2nix generate .
This writes a go2nix.toml next to your go.mod — one NAR hash per module.
See Lockfile Format.
2. Add go2nix to your flake
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
go2nix = {
url = "github:numtide/go2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { nixpkgs, go2nix, ... }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
goEnv = go2nix.lib.mkGoEnv {
inherit (pkgs) go callPackage;
go2nix = go2nix.packages.${system}.go2nix;
};
in {
packages.${system}.default = goEnv.buildGoApplication {
src = ./.;
goLock = ./go2nix.toml;
pname = "my-app";
version = "0.1.0";
};
};
}
3. Build
Default mode needs the Nix plugin loaded in the evaluator:
nix build \
--option plugin-files \
"$(nix build --no-link --print-out-paths github:numtide/go2nix#go2nix-nix-plugin)/lib/nix/plugins/libgo2nix_plugin.so"
For permanent setup, see Nix Plugin → Loading the plugin.
Where to next
- Architecture — how the builder works
- Builder Modes — default vs experimental
- Incremental Builds — what gets cached
- Builder API — full attribute reference
- Troubleshooting — when something doesn’t work
go2nix Architecture
Technical reference for the go2nix build system.
Overview
go2nix builds Go applications in Nix with two modes that share the same Go CLI and lockfile infrastructure but differ in how they create derivations.
The system has two components:
- A Go CLI (
go2nix) that generates lockfiles, discovers packages and files, compiles packages, and validates lockfile consistency. - A Nix library that reads lockfiles and builds Go applications using one of two modes.
Design context
go2nix builds Go applications at package granularity rather than treating
go build as a single opaque step. The approach is architecturally inspired
by Bazel’s rules_go — both systems work from an explicit package graph —
but go2nix has a much narrower scope: bring package-graph-aware Go builds to
Nix derivations and lockfiles, not replicate a full Bazel rule ecosystem.
For how go2nix compares to buildGoModule, gomod2nix, gobuild.nix, and
nix-gocacheprog, see the
comparison table in the README.
Builder modes
go2nix ships two builders that share the same lockfile and CLI but differ in when the package graph is discovered:
-
Default mode (
buildGoApplication) turns each Go package into its own Nix derivation. go2nix callsgo tool compileandgo tool linkdirectly instead ofgo build, giving Nix full control of the dependency graph at package granularity. The go2nix-nix-plugin (builtins.resolveGoPackages) discovers the package graph at eval time by runninggo listagainst the source tree, so when a dependency changes only the affected packages rebuild. -
Experimental mode (
buildGoApplicationExperimental) provides the same per-package granularity, but discovers the package graph at build time using recursive-nix and content-addressed derivations. Dependency discovery is deferred to the build, so no plugin is required.
See Builder Modes for the full comparison, requirements, and how to choose between them.
Nix directory layout
nix/
├── mk-go-env.nix # Entry point: creates Go toolchain scope
├── scope.nix # Self-referential package set (lib.makeScope)
├── stdlib.nix # Shared: compiled Go standard library
├── helpers.nix # Shared: sanitizeName, escapeModPath, etc.
├── dag/ # Default mode (eval-time DAG)
│ ├── default.nix # buildGoApplication
│ ├── fetch-go-module.nix # FOD fetcher (GOMODCACHE layout)
│ └── hooks/ # Setup hooks (compile, link, env)
└── dynamic/ # Experimental mode (recursive-nix)
└── default.nix # buildGoApplicationExperimental
Entry point: mk-go-env.nix
goEnv = go2nix.lib.mkGoEnv { # == import ./nix/mk-go-env.nix inside this repo
inherit go go2nix;
inherit (pkgs) callPackage;
tags = [ "nethttpomithttp2" ]; # optional
nixPackage = pkgs.nix_234; # optional, enables experimental mode
};
Creates a scope via scope.nix containing both builders plus shared
toolchain.
Package scope: scope.nix
Uses lib.makeScope newScope to create a self-referential package set.
Everything within the scope shares the same Go version, build tags, and
go2nix binary.
Exposes:
buildGoApplication— default mode (eval-time per-package DAG)buildGoApplicationExperimental— experimental mode (recursive-nix)go,go2nix,stdlib,hooks,fetchers,helpers
Shared: stdlib.nix
Compiles the entire Go standard library:
GODEBUG=installgoroot=all GOROOT=. go install -v --trimpath std
Output: $out/<pkg>.a for each stdlib package + $out/importcfg. Shared by
both modes.
Shared: helpers.nix
Pure Nix utility functions:
sanitizeName— Whitelist[a-zA-Z0-9+-._?=],/→-,~→_,@→_at_for derivation names.removePrefix— Substring after a known prefix.escapeModPath— Go module case-escaping (A→!a).
Staleness detection
The lockfile is validated at generation, eval, and build time — see
Lockfile Format → Staleness detection
for the full table. The go2nix check subcommand can also be used standalone
to verify a lockfile without building.
Further reading
Builder Modes
A Go application is made up of modules (downloaded units, each with a
go.mod) and packages (individual directories of .go files within a
module). A single module can contain dozens of packages. Both builder modes
turn each package into its own Nix derivation; they differ in when the
package graph is discovered and what that requires of your Nix setup.
| Mode | How it works | Lockfile | Caching | Requires |
|---|---|---|---|---|
| Default | go tool compile/link per-package | [mod] + optional [replace] | Per-package | go2nix-nix-plugin |
| Experimental | Recursive-nix at build time | [mod] + optional [replace] | Per-package | dynamic-derivations, ca-derivations, recursive-nix |
-
Default (
buildGoApplication): every package (not just every module) gets its own derivation. go2nix callsgo tool compileandgo tool linkdirectly, bypassinggo build. The import graph is discovered at eval time by the go2nix-nix-plugin (builtins.resolveGoPackages), so the lockfile stays small ([mod]hashes plus optional[replace]). When one package changes, only it and its reverse dependencies rebuild. -
Experimental (
buildGoApplicationExperimental): same per-package granularity as the default mode, but discovers the import graph at build time inside a recursive-nix wrapper instead of at eval time. The lockfile stays small ([mod]plus optional[replace]) and only changes when module resolution changes. Requires Nix >= 2.34 with experimental features enabled.
Choosing a mode
Use buildGoApplication (the default) for the best balance of caching and
simplicity — the lockfile is small (just module hashes), and the
go2nix-nix-plugin resolves the package graph at eval time.
Use buildGoApplicationExperimental only if you have Nix >= 2.34 with
dynamic-derivations, ca-derivations, and recursive-nix enabled, and
want per-package caching without requiring the plugin.
# Default (recommended):
goEnv.buildGoApplication { ... }
# Experimental (requires nix experimental features):
goEnv.buildGoApplicationExperimental { ... }
Default Mode
Per-package Nix derivations at eval time, with fine-grained caching.
Overview
The default mode creates per-package derivations from an eval-time package
graph. The go2nix-nix-plugin runs builtins.resolveGoPackages to discover
third-party packages, local packages, local replaces, module metadata, and
optional test-only third-party packages when checks are enabled. Module hashes
are read from the lockfile’s [mod] section, with optional [replace] entries
applied to module fetch paths. When a single dependency changes, only it and
its reverse dependencies rebuild.
Lockfile requirements
The default mode requires a lockfile with [mod] (and optionally [replace])
sections:
go2nix generate .
The lockfile contains only module hashes — the package graph is resolved at eval time by the plugin, so the lockfile does not need to be regenerated when import relationships change (only when modules are added or removed).
Nix evaluation flow
1. Lockfile parsing (builtins.fromTOML)
The lockfile is parsed at eval time with builtins.fromTOML. Module metadata
(path, version, hash) is read from the [mod] section, with [replace]
entries applied to module fetch paths.
2. Package graph discovery (builtins.resolveGoPackages)
The go2nix-nix-plugin runs go list -json -deps against the source tree at
eval time and returns the package graph (third-party, local, test-only, and
replacement metadata) — see Nix Plugin
for the full return shape.
Replace directives are applied to module fetchPath and dirSuffix fields
so that FODs download from the correct path.
3. Module fetching (fetch-go-module.nix)
Each module is a fixed-output derivation (FOD) that downloads via go mod download and produces a GOMODCACHE directory layout:
$out/<escaped-path>@<version>/
The netrcFile option supports private module authentication.
4. Package derivations (default.nix)
For each third-party package in goPackagesResult.packages, a derivation is
created:
Each becomes a stdenv derivation whose builder is the compile-go-pkg.sh
setup hook (from nix/dag/hooks/). The hook writes a JSON compile manifest
(importcfg parts, build tags, gcflags, PGO profile) and passes it to
go2nix compile-package --manifest.
CGO packages (where pkg.isCgo is true) automatically get stdenv.cc added
to nativeBuildInputs.
Dependencies (deps) are resolved lazily via Nix’s laziness — each package
references other packages from the same packages attrset.
5. Local package derivations
Each local package in goPackagesResult.localPackages also gets its own
derivation with a source tree filtered down to that package directory plus its
parents. Local package dependencies can point to other local packages and to
third-party packages.
6. Importcfg bundles
Instead of passing every compiled package as a direct dependency of the final
application derivation, the default mode builds bundled importcfg derivations:
depsImportcfg: stdlib + third-party + local packagestestDepsImportcfg: adds test-only third-party packages whendoCheck = true
This keeps the final derivation’s input fan-in small while preserving fine-grained package caching.
7. Application derivation
The final derivation receives typed JSON manifests via environment variables
and uses goAppHook (link-go-binary.sh) to invoke the Go CLI:
- Build phase — writes
linkManifestJSONto a file and callsgo2nix link-binary --manifest, which validates the lockfile, generates modinfo, compiles main packages, and invokes the linker. - Check phase — writes
testManifestJSONto a file and callsgo2nix test-packages --manifest, which discovers testable local packages, compiles test archives, and runs them.
Package overrides
Per-package customization (nativeBuildInputs, env) for cgo libraries is
supported via packageOverrides — see
Package Overrides for lookup rules and recipes.
Directory layout
nix/dag/
├── default.nix # buildGoApplication
├── fetch-go-module.nix # FOD fetcher
└── hooks/
├── default.nix # Hook definitions
├── setup-go-env.sh # GOPROXY=off, GOSUMDB=off
├── compile-go-pkg.sh # Compile one package
└── link-go-binary.sh # Link binary and run checks
Trade-offs
Pros:
- Fine-grained caching — changing one dependency doesn’t rebuild everything
- No experimental Nix features required
- Small lockfile (
[mod]plus optional[replace], no[pkg]section) - Lockfile only changes when modules are added/removed, not when imports change
- Automatic CGO detection and compiler injection
Cons:
- Requires the go2nix-nix-plugin (provides
builtins.resolveGoPackages) - Many small derivations can slow Nix evaluation on very large projects
Compilation and linking are handled by the builder hooks and direct
go tool compile / go tool link invocations described above.
Experimental Mode
Per-package CA derivations at build time, via recursive-nix.
Overview
The experimental mode moves package graph discovery from Nix eval time to build
time. A single recursive-nix wrapper derivation runs go2nix resolve, which
calls go list -json -deps to discover the import graph, then registers one
content-addressed (CA) derivation per package via nix derivation add. The
wrapper’s output is a .drv file; builtins.outputOf resolves it to the
final binary at eval time.
Because derivations are content-addressed, a change that doesn’t affect the compiled output (e.g., editing a comment) won’t propagate rebuilds — Nix deduplicates by content hash.
Requirements
The experimental mode requires Nix >= 2.34 with these experimental features enabled:
extra-experimental-features = recursive-nix ca-derivations dynamic-derivations
Lockfile requirements
The experimental mode uses the same lockfile format as the default mode:
go2nix generate .
The package graph is discovered at build time, so the lockfile does not store
package-level dependency data. It contains [mod] hashes and optional
[replace] entries. See lockfile-format.md for
details.
Build flow
1. Wrapper derivation (eval time)
Nix evaluates a text-mode CA derivation (${pname}.drv) that will run
go2nix resolve at build time. All inputs (Go toolchain, go2nix, Nix binary,
source, lockfile) are captured as derivation inputs.
2. Module FODs (build time)
go2nix resolve reads [mod] from the lockfile and creates fixed-output
derivations for each module, then builds them inside the recursive-nix
sandbox. Each FOD runs go mod download and produces a GOMODCACHE directory.
The netrcFile option supports private module authentication.
3. Package graph discovery (build time)
With all modules available, go list -json -deps discovers the full import
graph. The default mode performs this step at eval time via the
go2nix-nix-plugin — the experimental mode defers it to build time inside the
recursive-nix sandbox.
4. CA derivation registration (build time)
For each package, go2nix resolve calls nix derivation add to register a
content-addressed derivation that compiles one Go package to an archive
(.a file). Dependencies between packages are expressed as derivation inputs.
Local packages are also individual CA derivations.
5. Link derivation (build time)
A final CA derivation links all compiled packages into the output binary. For multi-binary projects, a collector derivation aggregates multiple link outputs.
6. Output resolution (eval time)
The wrapper’s output is the .drv file path. builtins.outputOf tells Nix
to build that derivation and use its output, connecting eval time to the
build-time-generated derivation graph.
Package overrides
packageOverrides is serialized to JSON and passed to go2nix resolve,
which adds the extra inputs to the appropriate CA derivations. Only
nativeBuildInputs is forwarded; env (and any other key) is rejected at
eval time because derivations are synthesized at build time and only store
paths can cross that boundary. See
Package Overrides for lookup rules and recipes.
Usage
goEnv.buildGoApplicationExperimental {
src = ./.;
goLock = ./go2nix.toml;
pname = "my-app";
subPackages = [ "cmd/server" ];
tags = [ "nethttpomithttp2" ];
ldflags = [ "-s" "-w" ];
}
The result has a target passthru attribute containing the final binary,
resolved via builtins.outputOf.
Directory layout
nix/dynamic/
└── default.nix # buildGoApplicationExperimental (wrapper derivation)
The build-time logic lives in the go2nix resolve command
(see cli-reference.md).
Trade-offs (vs default mode)
Pros:
- No Nix plugin required — package graph discovery happens in a hermetic
build, not via an impure eval-time
go list - CA deduplication is always on — comment-only edits don’t trigger rebuilds
- Same small lockfile and per-package caching as default mode
Cons:
- Requires Nix >= 2.34 with experimental features (
recursive-nix,ca-derivations,dynamic-derivations) - Build-time overhead from derivation registration:
.drvpaths are computed in-process (microseconds each) and registered concurrently over the nix-daemon socket; the per-derivationnix derivation addsubprocess is only used as a fallback when no daemon is reachable
Performance and scaling characteristics depend on recursive-nix support, content-addressed derivations, and daemon round-trip latency.
Nix Plugin (resolveGoPackages)
Default mode needs to know the Go package graph at eval time so it can
turn each package into a separate derivation. Nix has no builtin that can
run go list, so go2nix ships a Nix plugin that adds one:
builtins.resolveGoPackages.
If you only use experimental mode (buildGoApplicationExperimental), you
do not need the plugin — that mode discovers the graph at build time inside
a recursive-nix sandbox.
What it provides
The plugin registers a single primop:
builtins.resolveGoPackages {
src = ./.; # source tree
subPackages = [ "./..." ]; # optional, default ["./..."]
modRoot = "."; # optional
tags = [ ]; # optional build tags
goos = null; # optional cross target
goarch = null; # optional cross target
goProxy = null; # optional GOPROXY override
cgoEnabled = null; # optional CGO_ENABLED value
doCheck = false; # also resolve test-only deps
resolveHashes = false; # also compute module NAR hashes
}
go is intentionally omitted: the plugin defaults to the Go toolchain baked
in at its own build time, so the call carries no derivation context and the
default-mode evaluation stays IFD-free. You may pass
go = "/path/to/bin/go" to override the toolchain (e.g. a nix-shell Go).
A derivation-backed value like "${pkgs.go}/bin/go" is accepted but emits a
warning and forces the plugin to realise the derivation at eval time — i.e.
opt-in IFD, still gated by allow-import-from-derivation.
It runs go list -json -deps against src and returns:
packages— third-party package metadata (modKey,subdir,imports,drvName,isCgo)localPackages— local package metadata (dir,localImports,thirdPartyImports,isCgo)modulePath— the main module’s import pathgoVersion— the main module’sgodirective (e.g."1.25"); the dag builder threads this as-langto local-package compilesreplacements—replacedirectives fromgo.modtestPackages— test-only third-party packages (whendoCheck = true)moduleHashes— module NAR hashes (whenresolveHashes = true; see Lockfile-free builds)
You normally never call this directly — buildGoApplication does.
Architecture
The plugin lives under packages/go2nix-nix-plugin/ and is built in two
halves:
- a Rust core (
rust/) that wrapsgo list, parses its JSON output, classifies packages and computes module hashes; - a C++ shim (
plugin/resolveGoPackages.cc) that registers the primop with the Nix evaluator and marshals the Rust output back into Nix values.
Nix’s plugin ABI is unstable across releases, so the plugin must be built
against the same Nix version you evaluate with. The package defaults to
nixVersions.nix_2_34; override nixComponents in
packages/go2nix-nix-plugin/default.nix to match your Nix.
Loading the plugin
Build it from this flake:
nix build github:numtide/go2nix#go2nix-nix-plugin
Then make the evaluator load it. Either set it globally in nix.conf:
plugin-files = /nix/store/.../lib/nix/plugins/libgo2nix_plugin.so
or pass it per-invocation:
nix build --option plugin-files /nix/store/.../lib/nix/plugins/libgo2nix_plugin.so .#my-app
The latter is what the bench-incremental harness does internally.
Loading the plugin from a flake
Rather than hand-pasting a store path, derive it from the flake input.
On NixOS:
{ inputs, pkgs, ... }: {
nix.settings.plugin-files = [
"${inputs.go2nix.packages.${pkgs.system}.go2nix-nix-plugin}/lib/nix/plugins/libgo2nix_plugin.so"
];
}
Ad-hoc on the command line:
nix build .#my-app \
--option plugin-files \
"$(nix build --no-link --print-out-paths github:numtide/go2nix#go2nix-nix-plugin)/lib/nix/plugins/libgo2nix_plugin.so"
If the plugin is not loaded, evaluating buildGoApplication fails with:
error: attribute 'resolveGoPackages' missing
Purity
builtins.resolveGoPackages is impure: it shells out to go list, which
reads GOMODCACHE for module sources (and may consult GOPROXY for module
metadata). The Nix evaluator does not cache its result across
invocations, so the plugin runs once per evaluation.
This is the dominant per-eval cost of default mode; see Incremental Builds for timings.
Incremental Builds
This page explains what go2nix actually puts in the Nix store, what gets
reused on rebuilds, and how that differs from buildGoModule.
If you only want the API surface, see Builder API. If you want the step-by-step eval flow, see Default Mode.
The shape of a build
buildGoModule (nixpkgs)
┌──────────────────────────┐ ┌──────────────────────────┐
│ vendor FOD │ ──▶ │ app derivation │
│ (all modules, one hash) │ │ (go build ./..., 1 drv) │
└──────────────────────────┘ └──────────────────────────┘
Two derivations total. Any change to any .go file rebuilds the whole app
derivation; any go.sum change re-downloads the entire vendor tree.
go2nix (default mode)
┌────────────┐ ┌────────────┐ ┌────────────┐
│ module FOD │ │ module FOD │ ... │ module FOD │ one fixed-output derivation
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘ (FOD) per go.sum entry
│ │ │
┌─────▼──────┐ ┌─────▼──────┐ ┌─────▼──────┐
│ pkg drv │ │ pkg drv │ ... │ pkg drv │ one per Go package
│ (.a/.x) │ │ (.a/.x) │ │ (.a/.x) │ (third-party + local)
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
└───────────────┴──┬───────────────────┘
┌─────▼─────┐ ┌───────────┐
│ importcfg │ │ stdlib │ stdlib is independent
│ bundle │ │ drv │ (Go toolchain only)
└─────┬─────┘ └─────┬─────┘
└────────┬───────┘
┌─────▼─────┐
│ app drv │ link only
│ (link) │
└───────────┘
.a = compiled package archive; .x = export-data interface (see
Early cutoff below). The
importcfg is a file that maps each import path to its compiled .a
archive in the store — go tool compile and go tool link read it instead
of searching GOPATH.
For a non-trivial application this is hundreds to thousands of derivations instead of two — but almost all of them are reusable across rebuilds.
What gets cached
| Layer | One derivation per | Cache key (informally) | Rebuilds when |
|---|---|---|---|
| stdlib | Go toolchain | Go version + GOOS/GOARCH + tags | Go is bumped |
| module FOD | go.sum line | module path@version + NAR hash | that module is bumped in go.sum |
| third-party package | imported package | module FOD + import deps + gcflags | the module or any of its transitive deps change |
| local package | local Go package | filtered package directory + import deps | a .go file in that directory or a dep changes |
| importcfg bundle | app | the set of compiled package outputs | any package output changes |
| app | app | importcfg bundle + main package source | anything above changes |
Third-party module FODs and third-party package derivations are shared between every application in the flake (and across flakes, via the binary cache). Bumping a single module re-fetches one FOD and recompiles only the packages that transitively import it.
Local package derivations use a builtins.path-filtered source: only the
package’s own directory (plus its parent go.mod/go.sum, and any files
matched by //go:embed patterns in that package) is hashed, so editing
pkg/a/a.go does not change the input hash of the pkg/b derivation unless
b imports a. Embedded assets therefore participate in the per-package
cache key.
Rebuild propagation
When you edit a single local package, only the reverse-dependency cone of that package rebuilds:
- The edited package recompiles.
- Each package that imports it (directly or transitively) recompiles.
- The importcfg bundle and the final link derivation rebuild.
Packages outside the cone keep their existing store paths and are not rebuilt.
To get a feel for how big the cone is in your project, see Benchmarking.
Early cutoff with contentAddressed = true
By default, per-package derivations are input-addressed: if a package’s inputs change, every downstream derivation gets a new store path even if the compiled output happens to be byte-identical.
Setting contentAddressed = true opts into two coupled mechanisms:
- Floating-CA outputs. Per-package and importcfg derivations become
content-addressed, so a rebuild that produces a byte-identical
.aresolves to the same store path and short-circuits downstream rebuilds. ifaceoutput split. Each per-package derivation gains a secondifaceoutput containing only the export data (the.xfile produced bygo tool compile -linkobj). Downstream compiles depend onifaceinstead of the full.a, so changes to private symbols that don’t alter the package’s exported API don’t cascade. This mirrors the.xmodel used by Bazel’srules_go.
The two are coupled by design: CA without iface only short-circuits
comment-only edits, and iface without CA can’t cut off anything because
the input-addressed .x path still changes whenever src changes.
Requires the
ca-derivationsexperimental feature in Nix. The final binary stays input-addressed.Known limitation: adding the first package-level initializer to a previously init-free package still flips a bit in the
.xfile, so that particular edit cascades even though the API didn’t change. This is rare in practice.
The cost: eval time
go2nix trades build time for eval time. Every nix build evaluation:
- Calls
builtins.resolveGoPackages(the Nix plugin), which runsgo list -json -depsagainst your source tree. - Instantiates one derivation per package in the resulting graph.
For a large application (~3,500 packages) the warm-cache go list step
takes on the order of a few hundred milliseconds, and instantiation
adds a similar amount on top. This is the floor on every rebuild — even a
no-change rebuild — and is the main reason go2nix is overkill for small
single-binary projects.
The plugin call is impure (it reads GOMODCACHE), so the result is not
cached by the Nix evaluator across invocations. See
Nix Plugin for details.
Builder API Reference
Both builders accept a shared set of attributes. Differences are noted below.
buildGoApplication (default mode)
goEnv.buildGoApplication {
src = ./.;
goLock = ./go2nix.toml;
pname = "my-app";
version = "0.1.0";
}
buildGoApplicationExperimental (experimental mode)
goEnv.buildGoApplicationExperimental {
src = ./.;
goLock = ./go2nix.toml;
pname = "my-app";
}
Requires nixPackage to be set in mkGoEnv and Nix >= 2.34 with
recursive-nix, ca-derivations, and dynamic-derivations enabled.
Required attributes
src
Source tree. For monorepos with modRoot, this should be the repository
root.
In default mode, src is passed to builtins.resolveGoPackages, which
shells out to go list at eval time. To keep that call IFD-free, prefer a
source path (./.) or an eval-time fetcher (builtins.fetchTarball,
builtins.fetchGit). A derivation-backed value such as
pkgs.fetchFromGitHub is accepted, but the plugin must realise it at eval
time and emits an IFD warning.
| Attribute | Type | Modes | Description |
|---|---|---|---|
src | path | both | See above. |
pname | string | both | Package name for the output derivation. |
version | string | default only | Package version. The experimental builder does not accept this attribute (its wrapper produces a CA .drv whose name is derived from pname alone). |
Optional attributes
| Attribute | Type | Default | Modes | Description |
|---|---|---|---|---|
goLock | path or null | null (default) / required (experimental) | both | Path to go2nix.toml. In default mode, null enables lockfile-free builds. The experimental builder requires a lockfile. |
subPackages | list of strings | [ "." ] | both | Packages to build, relative to modRoot. A ./ prefix is auto-added if missing. |
modRoot | string | "." | both | Subdirectory within src containing go.mod. |
tags | list of strings | [] | both | Go build tags. |
ldflags | list of strings | [] | both | Flags passed to go tool link (-s, -w, -X, etc.). |
gcflags | list of strings | [] | both | Extra flags passed to go tool compile. |
CGO_ENABLED | 0, 1, or null | null (auto) | both | Override CGO detection. When null, CGO is enabled per-package based on the presence of C/C++ files. |
pgoProfile | path or null | null | both | Path to a pprof CPU profile for profile-guided optimization. The profile is passed to every go tool compile invocation, so changing it invalidates all package derivations. See Go’s PGO docs for producing a profile. |
nativeBuildInputs | list | [] | both | Extra build inputs for the final derivation. |
packageOverrides | attrset | {} | both | Per-package customization (see below). |
doCheck | bool | true | default only | Run tests. Matches buildGoModule’s default. See Test Support. |
checkFlags | list of strings | [] | default only | Flags passed to the compiled test binary (e.g., -v, -count=1). See Test Support. |
extraMainSrcFiles | list of strings | [] | default only | Extra src-relative paths (files or directories) kept in the filtered test source tree. Escape hatch for tests that read runtime files outside testdata/ and //go:embed. See Test Support. |
goProxy | string or null | null | default only | Custom GOPROXY URL. |
allowGoReference | bool | false | default only | Allow the output to reference the Go toolchain. |
meta | attrset | {} | default only | Nix meta attributes. |
contentAddressed | bool | false | default only | Make per-package and importcfg derivations floating-CA and add an iface (export-data) output so private-symbol-only edits don’t cascade. Requires the ca-derivations experimental feature; the final binary stays input-addressed. See Incremental Builds → Early cutoff for details and limitations. |
modRoot
When building one module inside a larger source tree (e.g., a monorepo), set
src to the repository root and modRoot to the subdirectory containing
go.mod:
goEnv.buildGoApplication {
src = ./.;
goLock = ./app/go2nix.toml;
pname = "my-app";
version = "0.1.0";
modRoot = "app";
subPackages = [ "cmd/server" ];
}
This is necessary when the module uses replace directives pointing to sibling
directories outside modRoot. The builder needs access to the full src tree,
with modRoot telling it where go.mod lives. The filtered mainSrc for the
final derivation unions in those sibling replace directories, so doCheck
works regardless of modRoot.
subPackages
List of packages to build, relative to modRoot. Each entry is a Go package
path like "cmd/server" or "." (the module root package).
A ./ prefix is added automatically if missing, so "cmd/server" and
"./cmd/server" are equivalent.
The default [ "." ] builds the package at modRoot.
packageOverrides
Per-package customization keyed by Go import path or module path:
packageOverrides = {
"github.com/mattn/go-sqlite3" = {
nativeBuildInputs = [ pkg-config sqlite ];
};
};
See Package Overrides for the lookup rules, supported keys, cgo recipes, and mode differences.
mkGoEnv
Both builders are accessed through a scope created by mkGoEnv:
goEnv = go2nix.lib.mkGoEnv {
inherit (pkgs) go callPackage;
go2nix = go2nix.packages.${system}.go2nix;
# Optional:
tags = [ "nethttpomithttp2" ];
netrcFile = ./my-netrc;
nixPackage = pkgs.nixVersions.nix_2_34; # required for experimental mode
};
| Attribute | Type | Default | Description |
|---|---|---|---|
go | derivation | required | Go toolchain. |
go2nix | derivation | required | go2nix CLI binary. |
callPackage | function | required | pkgs.callPackage. |
tags | list of strings | [] | Build tags applied to all builds in this scope. |
goEnv | attrset | {} | Environment variables applied to stdlib compilation and every go tool invocation in this scope (e.g. GOEXPERIMENT, GOFIPS140). Scope-level because the stdlib derivation is shared by every build in the scope. |
netrcFile | path or null | null | .netrc file for private module authentication (see below). |
nixPackage | derivation or null | null | Nix binary. Required for buildGoApplicationExperimental. |
Cross-compilation
GOOS / GOARCH are read from stdenv.hostPlatform.go, so cross builds are
driven the standard nixpkgs way — pass a cross pkgs (e.g.
pkgsCross.aarch64-multiplatform) into mkGoEnv via callPackage, and the
resulting scope produces binaries for that target. The
Nix plugin is told the target goos/goarch so build-tag
evaluation matches the host platform.
FIPS 140 mode (GOFIPS140)
Set GOFIPS140 via the scope-level goEnv to build against the Go FIPS 140
crypto module, equivalent to GOFIPS140=latest go build:
goEnv = go2nix.lib.mkGoEnv {
inherit (pkgs) go callPackage;
go2nix = go2nix.packages.${system}.go2nix;
goEnv = { GOFIPS140 = "latest"; };
};
The variable reaches both go install std (so the FIPS-aware stdlib is
compiled) and the link step, where go2nix emits the matching
build GOFIPS140= modinfo line and folds fips140=on into
DefaultGODEBUG — go version -m output is identical to a vanilla
GOFIPS140=latest go build -trimpath.
Private modules (netrcFile)
Go modules hosted behind authentication (private Git repos, private proxies)
require credentials. Set netrcFile in mkGoEnv to a .netrc file:
goEnv = go2nix.lib.mkGoEnv {
inherit (pkgs) go callPackage;
go2nix = go2nix.packages.${system}.go2nix;
netrcFile = ./secrets/netrc;
};
The file uses standard .netrc format:
machine github.com
login x-access-token
password ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
machine proxy.example.com
login myuser
password mytoken
The file is copied to $HOME/.netrc inside each module fetch derivation.
Go’s default GOPROXY (https://proxy.golang.org,direct) falls back to
direct VCS access when the proxy returns 404, so netrcFile covers both
proxy-authenticated and direct-access private module setups.
In experimental mode, the file is passed as --netrc-file to
go2nix resolve, which forwards it to the module FODs built inside
the recursive-nix sandbox.
Note: Any value passed to netrcFile reaches a fixed-output derivation
and is therefore world-readable in /nix/store. There is currently no
mechanism to keep the credential out of the store entirely; use a
low-privilege, repository-scoped token (and rotate it) rather than a
personal credential.
Package Overrides
packageOverrides lets you customize the per-package derivation for
specific Go packages — typically to give a cgo package the C toolchain
inputs it needs.
This page covers the lookup rules, the supported keys, and worked examples. For the one-line summary, see the Builder API table.
Lookup order
packageOverrides is keyed by import path or module path. When
compiling a package, the builder looks up an override in this order:
- The package’s exact import path
(e.g.
"github.com/diamondburned/gotk4/pkg/core/glib"). - The package’s module path (e.g.
"github.com/diamondburned/gotk4/pkg"). - Otherwise, no override.
Only the first match is used; entries are not merged. Module-path keys are convenient when one module ships many cgo packages that all need the same system libraries.
The module-path fallback applies to third-party (and test) packages only. Local packages from your own main module are matched by exact import path; a key equal to your main module’s path will not be applied to every local package.
Supported keys
| Key | Type | Default mode | Experimental mode | Notes |
|---|---|---|---|---|
nativeBuildInputs | list of derivations | cgo packages only | yes | Added to the per-package derivation’s nativeBuildInputs |
env | attrset | yes | no | Extra environment variables on the per-package derivation |
srcOverlay | derivation or path | yes | no | Contents are layered onto the package’s source directory at build time |
In default mode, nativeBuildInputs from every entry in packageOverrides
(regardless of whether the key matched a package in the graph) are also
collected and added to the final application derivation, so headers and
libraries are available at link time as well.
nativeBuildInputs is cgo-only
Non-cgo packages are compiled with a raw builder (rawGoCompile) that
bypasses stdenv entirely and hardcodes PATH — nativeBuildInputs would
silently do nothing, so the builder rejects it instead. For the error
message and fix list, see
Troubleshooting.
Example: single cgo package
dotool has one local cgo package wrapping libxkbcommon via pkg-config:
goEnv.buildGoApplication {
pname = "dotool";
version = "1.6";
src = ./.;
goLock = ./go2nix.toml;
packageOverrides = {
"git.sr.ht/~geb/dotool/xkb" = {
nativeBuildInputs = [
pkgs.pkg-config
pkgs.libxkbcommon
];
};
};
}
Example: many cgo packages from one module
gotk4 ships dozens of cgo packages under one module. Key the override by
the module path so it applies to every package in that module:
let
gtkDeps = {
nativeBuildInputs = [
pkgs.pkg-config
pkgs.glib
pkgs.cairo
pkgs.gobject-introspection
pkgs.gdk-pixbuf
pkgs.pango
pkgs.gtk3
pkgs.at-spi2-core
pkgs.gtk-layer-shell
];
};
in
goEnv.buildGoApplication {
pname = "nwg-drawer";
version = "0.7.4";
src = ./.;
goLock = ./go2nix.toml;
packageOverrides = {
"github.com/diamondburned/gotk4/pkg" = gtkDeps;
"github.com/diamondburned/gotk4-layer-shell/pkg" = gtkDeps;
};
}
Example: env
packageOverrides = {
"github.com/example/pkg" = {
env = {
CGO_CFLAGS = "-I${libfoo.dev}/include";
};
};
};
env is default-mode only. The experimental builder synthesizes
derivations at build time inside go2nix resolve and can only forward
store paths, so it rejects env (and any other unknown key) at eval time.
Example: srcOverlay for build-time-generated //go:embed content
When a package embeds files that are themselves build outputs — a bundled
SPA under //go:embed all:dist, generated protobuf descriptors, etc. —
srcOverlay lets you supply them as a derivation that is layered onto the
package’s source directory at build time:
packageOverrides = {
"example.com/app/ui" = {
srcOverlay = pkgs.runCommand "ui-dist" { } ''
mkdir -p $out/dist
cp -r ${uiBundle}/* $out/dist/
'';
};
};
The overlay is cp -rL’d onto a writable copy of the package’s source
before go2nix compile-package runs, so ResolveEmbedCfg sees the
generated files. This is a build-time input of that one compile derivation
only — it does not flow into src at evaluation time, so it does not
trigger IFD and does not invalidate sibling packages.
Keep a placeholder file in the source tree (e.g. ui/dist/.gitkeep) so
the eval-time go list doesn’t error on a //go:embed pattern with zero
matches; the overlay then replaces or augments it at build time.
The doCheck testrunner applies the same overlay before resolving
//go:embed patterns and recompiling for tests, so tests see the
overlaid content too.
Test Support
go2nix runs Go tests during the check phase of default mode builds. Tests are
compiled and executed per-package, approximating go test semantics for
supported cases (see Limitations below).
Enabling tests
Tests are controlled by doCheck:
goEnv.buildGoApplication {
src = ./.;
goLock = ./go2nix.toml;
pname = "my-app";
version = "0.1.0";
doCheck = true;
}
doCheck defaults to true (matching buildGoModule). The filtered
mainSrc for the final derivation includes local replace targets outside
modRoot, so test discovery works for sibling-replace layouts without
overrides. See the Builder API table for the other
buildGoApplication defaults.
What gets tested
The test runner discovers all local packages (under modRoot) that contain
_test.go files and runs their tests. Third-party packages are not tested.
Each testable package goes through these steps:
- Internal test compilation — library source files +
_test.gofiles in the same package are compiled together into a single archive that replaces the library archive. - Dependent recompilation — local packages that transitively depend on the package under test are recompiled against the test archive so the dependency graph stays consistent (otherwise the xtest would link two copies of the package — one with test helpers, one without).
- External test compilation —
_test.gofiles in the*_testpackage (xtests) are compiled as a separate package that imports the internal test archive. - Test main generation — a
_testmain.gois generated that registers allTest*,Benchmark*,Fuzz*, andExample*functions. - Link and run — the test binary is linked and executed in the package’s source directory.
Internal tests vs external tests (xtests)
Go has two kinds of test files, both supported:
-
Internal tests (
package foo):_test.gofiles in the same package. These can access unexported identifiers. They are compiled together with the package’s regular source files into a single archive. -
External tests (
package foo_test):_test.gofiles in the_testpackage. These can only access exported identifiers and test the public API. They are compiled as a separate package (foo_test) that importsfoo.
When a package has both, the internal test archive replaces the original library archive, and any local dependents reachable from the xtest’s import graph are recompiled to see the replacement.
Test-only dependencies
When doCheck = true, the Nix plugin runs a second
go list -deps -test pass
to discover third-party packages that are only reachable through test
imports (e.g., github.com/stretchr/testify). These are built as separate
testPackages derivations and included in a testDepsImportcfg bundle
that is a superset of the build importcfg.
This means test-only dependencies don’t affect the build derivation or its cache key — they only appear in the check phase.
//go:embed in tests
Embed directives in test files are supported:
-
TestEmbedPatterns(from internal_test.gofiles) are resolved and their files are symlinked into the internal test source directory alongside the package’s regular embed files. The embed configs are merged. -
XTestEmbedPatterns(from external_test.gofiles) are resolved and symlinked into the xtest source directory with their own embed config.
extraMainSrcFiles
Tests run against a filtered copy of src that keeps only what the build
needs: .go sources, resolved //go:embed targets, and every testdata/
directory under a tested package. Anything else is dropped so unrelated
edits don’t invalidate the test derivation.
A test that reads a file at runtime without //go:embed and outside
testdata/ — e.g. os.ReadFile("../config.yaml") — will not find it.
Prefer moving such fixtures under testdata/. When that isn’t practical,
list the paths in extraMainSrcFiles:
goEnv.buildGoApplication {
src = ./.;
goLock = ./go2nix.toml;
pname = "my-app";
version = "0.1.0";
extraMainSrcFiles = [ "config.yaml" "internal/svc/fixtures" ];
}
Each entry is relative to src (not modRoot). A directory entry includes
its full subtree, and a trailing / is tolerated. An entry that does not
exist under src fails evaluation.
checkFlags
Extra flags passed to the test binary (not to go test, since go2nix
compiles and runs tests directly):
goEnv.buildGoApplication {
src = ./.;
goLock = ./go2nix.toml;
pname = "my-app";
version = "0.1.0";
checkFlags = [ "-v" "-count=1" ];
}
These map to the standard testing package flags (-v, -run, -count,
-bench, -timeout, etc.).
Limitations
- Default mode only. The experimental builder does not run tests.
- No per-package test caching. All local tests re-run whenever the final
app derivation rebuilds; go2nix does not skip individual test packages
whose inputs are unchanged (unlike
go test’s cache). - Third-party tests are not run. Only local packages under the module root are tested.
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.
Sections
[mod] — Module hashes. Each key is a composite "path@version" string,
each value is a sha256-... SRI NAR hash of the module as laid out by
go mod download under $GOMODCACHE (the same tree the FOD fetcher
produces).
[replace] — Module replacements (from go.mod replace directives).
Maps the original composite key to the replacement module path. Only
remote replacements are recorded; local replace directives (filesystem
paths) are not included.
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 go.sumgains or loses entries
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
Default mode can build without a lockfile by setting goLock = null:
goEnv.buildGoApplication {
src = ./.;
goLock = null;
pname = "my-app";
version = "0.1.0";
}
When no lockfile is present, the Nix plugin is invoked
with resolveHashes = true and computes a NAR hash for each module from
go.sum + GOMODCACHE, returning a moduleHashes attrset that fills the
role of the [mod] section. Module FODs are then keyed on those hashes.
This trades a checked-in pin file for zero lockfile maintenance. The build
is still reproducible as long as go.sum is unchanged, but you lose the
explicit, reviewable hash list.
Note: the build-time staleness check (
mvscheck, see below) is skipped whengoLock = null— there is no lockfile forgo.modto drift from. Module versions are read live fromgo.sumvia the plugin on every evaluation, so ago getis reflected on the nextnix buildwith nothing to regenerate. The standalonego2nix checksubcommand still requires a lockfile path and is not applicable in this mode.
Prefer a committed lockfile for anything you ship; lockfile-free is useful for ad-hoc builds and during early development.
Staleness detection
| When | What | Applies to | How |
|---|---|---|---|
| Generation | MVS (Minimal Version Selection) consistency | All modes | go list -json -deps resolves actual versions |
| Nix eval | Package graph | Default only | builtins.resolveGoPackages runs go list at eval time |
| 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, missing or mismatched modules are caught at build time when
link-binary validates the lockfile. Stale package graph information is
caught at eval time when builtins.resolveGoPackages runs go list. In
experimental mode, module mismatches surface at build time when go list or
module fetching fails inside the recursive-nix sandbox.
Run go2nix check <dir> or go2nix check --lockfile <path> <dir> to verify
a lockfile without building.
CLI Reference
All commands are subcommands of go2nix. Set GO2NIX_DEBUG=1 for verbose
output.
Commands you run
generate
Generate a lockfile from one or more Go module directories.
go2nix generate [flags] [dir...]
| Flag | Default | Description |
|---|---|---|
-o | go2nix.toml | Output lockfile path |
-j | NumCPU | Max parallel hash invocations |
When no directory is given, defaults to .. Multiple directories produce a
merged lockfile (monorepo support).
The generated lockfile is shared by both builder modes. Use
buildGoApplication (default) or buildGoApplicationExperimental in Nix.
Examples:
go2nix generate . # write go2nix.toml in the current module
go2nix # same — generate is the default when no subcommand is given
go2nix generate -o lock.toml ./a ./b # merged lockfile for two modules
See Lockfile Format for the output schema.
check
Validate a lockfile against go.mod.
go2nix check [flags] [dir]
| Flag | Default | Description |
|---|---|---|
--lockfile | go2nix.toml | Path to lockfile for consistency check |
Verifies that all go.mod requirements are present in the lockfile with
correct versions.
Internal commands (invoked by the Nix builders)
You won’t run these directly; they are documented for debugging build failures.
compile-package
Compile a single Go package to an archive (.a file). Used internally by
the default mode’s setup hooks.
go2nix compile-package --manifest FILE --import-path PATH --src-dir DIR --output FILE [flags]
| Flag | Required | Description |
|---|---|---|
--manifest | Yes | Path to compile-manifest.json |
--import-path | Yes | Go import path for the package |
--src-dir | Yes | Directory containing source files |
--output | Yes | Output .a archive path |
--iface-output | No | Write export-data-only interface (.x) to this path; --output then receives the link object via -linkobj |
--importcfg-output | No | Write importcfg entry for consumers to this path |
--trim-path | No | Path prefix to trim |
--p | No | Override -p flag (default: import-path) |
--go-version | No | Go language version for -lang |
list-files
List Go source files for a package directory, respecting build tags and constraints.
go2nix list-files [-tags=...] [-go-version=...] <package-dir>
Outputs JSON with categorized file lists (Go files, C files, assembly, etc.).
-go-version sets the target Go toolchain version (e.g. 1.25) used to
evaluate //go:build go1.N constraints; defaults to go env GOVERSION.
list-packages
List all local packages in a Go module with their import dependencies.
go2nix list-packages [-tags=...] [-go-version=...] <module-root>
Outputs JSON with each package’s import path and dependencies.
resolve
Build-time command for experimental mode (the nix/dynamic/ builder).
Discovers the package graph, computes CA .drv paths in-process, registers
them with the nix-daemon (falling back to nix derivation add if no daemon
socket is reachable), and produces a .drv file as output. See
Experimental Mode.
go2nix resolve [flags]
| Flag | Required | Description |
|---|---|---|
--src | Yes | Store path to Go source |
--mod-root | No | Subdirectory within src containing go.mod |
--lockfile | Yes | Path to go2nix.toml lockfile |
--system | Yes | Nix system (e.g., x86_64-linux) |
--go | Yes | Path to go binary |
--nix | Yes | Path to nix binary |
--pname | Yes | Output binary name |
--output | Yes | $out path |
--stdlib | Yes | Path to pre-compiled Go stdlib |
--go2nix | No | Path to go2nix binary (defaults to self) |
--bash | No | Path to bash binary |
--coreutils | No | Path to a coreutils binary (e.g., coreutils/bin/mkdir) |
--sub-packages | No | Comma-separated sub-packages |
--tags | No | Comma-separated build tags |
--ldflags | No | Linker flags |
--cgo-enabled | No | Override CGO_ENABLED (0 or 1) |
--gcflags | No | Extra flags for go tool compile |
--pgo-profile | No | Store path to pprof CPU profile for PGO |
--overrides | No | JSON-encoded packageOverrides |
--cacert | No | Path to CA certificate bundle |
--netrc-file | No | Path to .netrc for private modules |
--nix-jobs | No | Max concurrent derivation registrations |
--daemon-socket | No | nix-daemon Unix socket; default $NIX_DAEMON_SOCKET_PATH. When reachable, derivations are registered over the socket instead of via nix CLI subprocesses |
This command is not intended for direct use — it is invoked by the experimental-mode Nix builder inside a recursive-nix build.
build-modinfo
Generate a modinfo linker directive for embedding debug/buildinfo
metadata into the final binary. This is a standalone utility; the default
mode’s link-binary command generates modinfo internally.
go2nix build-modinfo [flags] <module-root>
| Flag | Required | Description |
|---|---|---|
--lockfile | Yes | Path to go2nix.toml lockfile |
--go | No | Path to go binary (default: from PATH) |
Outputs a modinfo directive for the linker’s importcfg (embedding
debug/buildinfo metadata), and optionally a godebug line with the
default GODEBUG value parsed from the module’s go.mod (used for
-X=runtime.godebugDefault=...).
generate-test-main
Generate a _testmain.go file that registers test, benchmark, fuzz, and
example functions. Used internally by the test runner.
go2nix generate-test-main [flags]
| Flag | Required | Description |
|---|---|---|
--import-path | Yes | Import path of the package under test |
--test-files | No | Comma-separated absolute paths to internal _test.go files |
--xtest-files | No | Comma-separated absolute paths to external _test.go files |
--output | No | Output file path (default: stdout) |
test-packages
Compile and run tests for all testable local packages in a module. Used internally by the default mode’s check phase.
go2nix test-packages --manifest FILE
| Flag | Required | Description |
|---|---|---|
--manifest | Yes | Path to test-manifest.json |
Discovers local packages with _test.go files, compiles internal and
external test archives, generates test mains, links test binaries, and
runs them. See test-support.md for details on the
test pipeline.
link-binary
Link Go application binaries. Reads a link manifest that declares all inputs (importcfg parts, local archives, ldflags, etc.), validates the lockfile, generates modinfo, compiles main packages, and invokes the linker. Used internally by the default mode’s build phase.
go2nix link-binary --manifest FILE --output DIR
| Flag | Required | Description |
|---|---|---|
--manifest | Yes | Path to link-manifest.json |
--output | Yes | Output directory (binaries written to <output>/bin/) |
Benchmarking
bench-incremental measures rebuild time for go2nix’s default-mode builder
after touching a single file at different depths in the dependency graph.
Use it to see how contentAddressed and the iface early-cutoff behave on
a representative project before adopting them.
Running it
nix run github:numtide/go2nix#bench-incremental -- -fixture light
(or nix run .#bench-incremental -- ... from a checkout).
The harness spins up a rooted local store
(NIX_REMOTE=local?root=$TMPDIR/...), loads the
Nix plugin via --option plugin-files, does a full warm
build, then repeatedly edits one file and times the rebuild. It needs
network access (the warm build fetches modules from substituters), so it
cannot run inside a nix build sandbox — nix flake check only
verifies that the binary links.
Flags
| Flag | Default | Meaning |
|---|---|---|
-runs N | 3 | Runs per scenario; the median is reported |
-scenario S | all | One of no_change, leaf, mid, deep, all |
-touch-mode M | private | private edits an unexported symbol; exported edits an exported one |
-tools L | nix-nocgo,nix-ca-nocgo | Comma-separated tool variants: nix, nix-ca, nix-nocgo, nix-ca-nocgo |
-fixture F | light | light or torture (see below) |
-json PATH | — | Write raw results as JSON |
-assert-cascade N | — | Fail (non-zero exit) if any tool builds more than N derivations on a touch scenario |
The nix-ca* variants set contentAddressed = true; the *-nocgo
variants set CGO_ENABLED = 0. Comparing nix-nocgo against
nix-ca-nocgo with -touch-mode private shows the iface early-cutoff in
action.
Fixtures and scenarios
Two synthetic projects under tests/fixtures/:
| Fixture | Shape | leaf edits | mid edits | deep edits |
|---|---|---|---|---|
light | small app, a few internal packages | app/cmd/app/main.go | internal/handler/handler.go | internal/core/core.go |
torture | large app, hundreds of modules | app-full/cmd/app-full/main.go | internal/aws/aws.go | internal/common/common.go |
leaf touches the entrypoint (no reverse dependents — only the link
rebuilds). mid touches a package roughly halfway up the graph with a
moderate reverse-dependency cone. deep touches a package near the bottom
of the graph that fans out to most of the app. no_change measures the
eval + no-op-build floor.
Using -assert-cascade in CI
nix run .#bench-incremental -- \
-fixture light -scenario mid -touch-mode private \
-tools nix-ca-nocgo -assert-cascade 5
This fails if a private-symbol edit to a mid-graph package causes more than five derivations to rebuild — a regression check for the early-cutoff machinery.
Other benchmarks
The flake also exposes coarser-grained harnesses:
benchmark-build— wall-clock time for a full cold build of a fixture.benchmark-eval— wall-clock time for a purenix evalof the package graph (plugin + instantiation cost).benchmark-build-cross-app-isolation— verifies that two apps sharing third-party packages reuse each other’s per-package store paths.
Troubleshooting
error: attribute 'resolveGoPackages' missing
The default-mode builder calls builtins.resolveGoPackages, which is
provided by the go2nix Nix plugin. This error means the evaluating Nix
hasn’t loaded it.
Load the plugin via nix.conf plugin-files = ... or
--option plugin-files <path>. See Nix Plugin.
If the plugin is configured and you still see this, your Nix and the
plugin were built against different libnixexpr versions — rebuild the
plugin against the Nix you’re evaluating with.
packageOverrides.<path>: unknown attributes ["nativeBuildInputs"]
Full message:
packageOverrides.<path>: unknown attributes ["nativeBuildInputs"].
Valid: env (nativeBuildInputs is cgo-only — rawGoCompile hardcodes PATH)
You set nativeBuildInputs for a package that go2nix classified as
non-cgo. Non-cgo packages use a raw builder that bypasses stdenv, so
nativeBuildInputs would have no effect; the builder rejects it instead of
silently ignoring it.
Fixes:
- If the package really is cgo, make sure
CGO_ENABLEDisn’t forced to0and that the cgo files aren’t excluded by build tags on your target platform. - If you need to influence a pure-Go compile, use
envinstead. - If you only need the inputs at link time, put them in the top-level
nativeBuildInputsofbuildGoApplicationrather than inpackageOverrides.
See Package Overrides.
“lockfile is stale” / link-binary fails validating go.mod
Default mode validates the lockfile against go.mod at link time
(mvscheck). If you’ve added, removed, or bumped a module since the last
go2nix generate, regenerate:
go2nix generate .
You do not need to regenerate after editing imports between packages that already exist — the lockfile pins modules, not the package graph. See When to regenerate.
Run go2nix check . to validate without building.
Private modules: 404 or auth failures in module FODs
Module fetch derivations run go mod download in a sandbox with no ambient
credentials. Pass a .netrc via mkGoEnv { netrcFile = ...; }. See
Private modules for the format
and the store-path-visibility caveat.
Evaluation feels slow on large graphs
Every evaluation runs go list -json -deps (via the plugin) and
instantiates one derivation per package. On a few-thousand-package graph
this is a few hundred milliseconds of floor on every nix build, even when
nothing changed. That’s expected; see
Incremental Builds.
If builds (not eval) cascade further than you expect after small edits,
turn on contentAddressed = true so private-symbol changes don’t rebuild
reverse dependents. Use bench-incremental to measure.
Inspecting the package graph
The default-mode app derivation exposes the graph through passthru:
# All third-party package derivations
nix eval .#my-app.passthru.packages --apply builtins.attrNames
# All local package derivations
nix eval .#my-app.passthru.localPackages --apply builtins.attrNames
# Build a single package in isolation
nix build '.#my-app.passthru.packages."github.com/foo/bar"'
# The bundled importcfg used at link time
nix build .#my-app.passthru.depsImportcfg
Also available: passthru.go, passthru.go2nix, passthru.goLock,
passthru.mainSrc, passthru.modulePath, and (when doCheck = true)
passthru.testPackages / passthru.testDepsImportcfg.