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.
Comparison with other Nix Go builders
| Tool | Granularity | Key difference from go2nix |
|---|---|---|
buildGoModule | App-level (one fetch + one build derivation) | Nix doesn’t model the Go package graph; any change rebuilds the whole app |
gomod2nix | Module-level (lockfile-driven offline builds) | Focuses on locking and fetching modules, not per-package compilation |
gobuild.nix | Module-level (GOCACHEPROG-backed cache reuse) | Per-module derivations, not per-package; different caching layer |
nix-gocacheprog | Impure shared cache | Optimization for local iteration speed, not a pure builder |
| go2nix | Package-level (per-package derivations) | Discovers the import graph and compiles each package as its own derivation |
Builder modes
| Mode | How it works | Lockfile | Caching | Nix features |
|---|---|---|---|---|
| Default | go tool compile/link per-package | [mod] + optional [replace] | Per-package | go2nix-nix-plugin |
| Experimental | Recursive-nix, per-package at build time | [mod] + optional [replace] | Per-package | dynamic-derivations, ca-derivations, recursive-nix |
Default mode
Go packages are compiled as Nix derivations at eval time: third-party
packages, local packages, and optionally test-only third-party packages when
checks are enabled. go2nix calls go tool compile and go tool link
directly, bypassing go build. This gives Nix full control over the
dependency graph at package granularity. The package graph is discovered at
eval time by the go2nix-nix-plugin (builtins.resolveGoPackages), which runs
go list against the source tree. When a dependency changes, only affected
packages rebuild.
See default-mode.md for details.
Experimental mode
Same per-package granularity as the default mode, but the package graph is discovered at build time using recursive-nix and content-addressed (CA) derivations. The lockfile stays package-graph-free because dependency discovery is deferred to the build.
See experimental-mode.md for details.
Choosing a mode
buildGoApplication uses the default mode. Use buildGoApplicationExperimental
only if you have Nix >= 2.34 with the required experimental features enabled:
# Default (recommended):
goEnv.buildGoApplication { ... }
# Experimental (requires nix experimental features):
goEnv.buildGoApplicationExperimental { ... }
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 = import ./nix/mk-go-env.nix {
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
| When | What | Applies to | How |
|---|---|---|---|
| Generation | MVS 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 only | link-binary validates lockfile against go.mod via mvscheck.CheckLockfile |
The go2nix check subcommand can also be used standalone to verify a
lockfile without building.
Further reading
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
| Attribute | Type | Modes | Description |
|---|---|---|---|
src | path | both | Source tree. For monorepos with modRoot, this should be the repository root. |
goLock | path | both | Path to go2nix.toml lockfile. |
pname | string | both | Package name for the output derivation. |
version | string | default only | Package version. The experimental builder does not accept this attribute. |
Optional attributes
| Attribute | Type | Default | Modes | Description |
|---|---|---|---|---|
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. |
nativeBuildInputs | list | [] | both | Extra build inputs for the final derivation. |
packageOverrides | attrset | {} | both | Per-package customization (see below). |
doCheck | bool | modRoot == "." | default only | Run tests. Defaults to false when modRoot is set, because test discovery may not find local replace targets outside the module root. |
checkFlags | list of strings | [] | default only | Flags passed to the compiled test binary (e.g., -v, -count=1). |
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. |
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.
When modRoot != ".", doCheck defaults to false because the filtered
source tree for tests may not include out-of-tree replace targets. Override
with doCheck = true if your layout doesn’t use out-of-tree replaces.
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 ];
};
};
Override lookup: exact import path first, then module path.
Supported override attributes
| Attribute | Default mode | Experimental mode |
|---|---|---|
nativeBuildInputs | yes | yes |
env | yes | no |
The env attribute sets environment variables on the per-package derivation:
packageOverrides = {
"github.com/example/pkg" = {
env = {
CGO_CFLAGS = "-I${libfoo.dev}/include";
};
};
};
The experimental builder rejects unknown attributes (including env) at eval
time. Derivations are synthesized at build time by go2nix resolve, so only
nativeBuildInputs (store paths) can be forwarded.
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. |
netrcFile | path or null | null | .netrc file for private module authentication (see below). |
nixPackage | derivation or null | null | Nix binary. Required for buildGoApplicationExperimental. |
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: The netrc file becomes a Nix store path, so its contents are
world-readable in /nix/store. For sensitive credentials, consider using
a secrets manager or a file reference outside the store (e.g., via
builtins.readFile from a non-tracked path).
CLI Reference
All commands are subcommands of go2nix. Set GO2NIX_DEBUG=1 for verbose
output.
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 as generate: default command
go2nix generate -o lock.toml ./a ./b
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.
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 |
--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=...] <package-dir>
Outputs JSON with categorized file lists (Go files, C files, assembly, etc.).
list-packages
List all local packages in a Go module with their import dependencies.
go2nix list-packages [-tags=...] <module-root>
Outputs JSON with each package’s import path and dependencies.
resolve
Build-time command for dynamic mode. Discovers the package graph, creates
CA derivations via nix derivation add, and produces a .drv file as
output.
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 nix derivation add calls |
This command is not intended for direct use — it is invoked by the dynamic 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/) |
Lockfile Format
go2nix uses TOML lockfiles to pin module hashes. Both builder modes use the same lockfile format.
| Builder | Command | Sections |
|---|---|---|
| Default | go2nix generate | [mod] + [replace] |
| Experimental | go2nix generate | [mod] + [replace] |
Format
Generated by go2nix generate. Both modes use the same lockfile 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"
Sections
[mod] — Module hashes. Each key is a composite "path@version" string,
each value is a sha256-... SRI hash of the module’s GOMODCACHE directory.
[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 does not contain a package dependency graph. Instead:
- Default mode discovers the package graph at eval time via the
go2nix-nix-plugin (
builtins.resolveGoPackages), which runsgo listagainst the source tree. - Experimental mode discovers the package graph at build time via
go list -json -depsinside a recursive-nix wrapper.
Only module NAR hashes are required in the lockfile (for fixed-output derivations that fetch each module).
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".
Staleness detection
| When | What | Applies to | How |
|---|---|---|---|
| Generation | MVS 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 only | link-binary validates lockfile against go.mod via mvscheck.CheckLockfile |
In the 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 the 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.
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 when modRoot == "." and false otherwise.
When modRoot points to a subdirectory, the source tree filtered for the
final derivation may not include local replace targets outside the module
root, causing test discovery to fail. Override with doCheck = true if your
layout doesn’t use out-of-tree replaces.
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 (Go’s “recompileForTest” logic).
- 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 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.
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.
modRoot != "."disables tests by default. The source filter for the final derivation may exclude sibling modules needed by tests.- No test caching. Tests run fresh on every build (there is no persistent test cache across derivations).
- Third-party tests are not run. Only local packages under the module root are tested.
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. The two builder
modes differ in what unit of work becomes a Nix derivation, which determines
rebuild granularity when a dependency changes.
| Mode | How it works | Lockfile | Caching | Nix features |
|---|---|---|---|---|
| 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 a package graph:
- packages: Third-party package metadata (modKey, subdir, imports, drvName, isCgo)
- localPackages: Local package metadata (dir, localImports, thirdPartyImports, isCgo)
- modulePath: Main module import path
- replacements: Module replacement mappings from
go.modreplacedirectives - testPackages: Test-only third-party package metadata when
doCheck = true
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:
stdenv.mkDerivation {
name = pkg.drvName; # "gopkg-github.com-foo-bar"
nativeBuildInputs = [ hooks.goModuleHook ] # compile-go-pkg.sh
++ cgoBuildInputs; # stdenv.cc for CGO packages
buildInputs = deps; # dependency package derivations
env = {
goPackagePath = importPath;
goPackageSrcDir = srcDir;
compileManifestJSON = mkCompileManifestJSON deps;
};
}
The compile manifest is a JSON string declaring importcfg parts, build tags,
gcflags, and PGO profile. The shell hook writes it to a file 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 (e.g., for cgo libraries):
goEnv.buildGoApplication {
src = ./.;
goLock = ./go2nix.toml;
pname = "my-app";
version = "0.1.0";
tags = [ "netgo" ];
packageOverrides = {
"github.com/mattn/go-sqlite3" = {
nativeBuildInputs = [ pkg-config sqlite ];
};
};
}
Overrides apply to both the per-package derivation and are collected for the final application derivation.
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
Per-package customization (e.g., for cgo libraries):
goEnv.buildGoApplicationExperimental {
src = ./.;
goLock = ./go2nix.toml;
pname = "my-app";
packageOverrides = {
"github.com/mattn/go-sqlite3" = {
nativeBuildInputs = [ pkg-config sqlite ];
};
};
}
Overrides are serialized to JSON and passed to go2nix resolve, which adds
the extra inputs to the appropriate CA derivations.
Note: The experimental builder only supports nativeBuildInputs in
packageOverrides. The env attribute supported by the default builder is
not available here because derivations are synthesized at build time by
go2nix resolve. Unknown attributes are rejected at eval time.
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
Pros:
- Small lockfile — only
[mod]hashes (same as default mode) - No lockfile regeneration when import graph changes (only when modules change)
- Per-package caching via CA derivations
- CA deduplication — comment-only edits don’t trigger rebuilds
Cons:
- Requires Nix >= 2.34 with experimental features (
recursive-nix,ca-derivations,dynamic-derivations) - Build-time overhead from
nix derivation addcalls (~32ms each) - Parallelism limited by SQLite write lock (saturates at ~4 concurrent adds)
Performance and scaling characteristics depend on recursive-nix support,
content-addressed derivations, and the overhead of nix derivation add.