Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. A Go CLI (go2nix) that generates lockfiles, discovers packages and files, compiles packages, and validates lockfile consistency.
  2. 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

ToolGranularityKey difference from go2nix
buildGoModuleApp-level (one fetch + one build derivation)Nix doesn’t model the Go package graph; any change rebuilds the whole app
gomod2nixModule-level (lockfile-driven offline builds)Focuses on locking and fetching modules, not per-package compilation
gobuild.nixModule-level (GOCACHEPROG-backed cache reuse)Per-module derivations, not per-package; different caching layer
nix-gocacheprogImpure shared cacheOptimization for local iteration speed, not a pure builder
go2nixPackage-level (per-package derivations)Discovers the import graph and compiles each package as its own derivation

Builder modes

ModeHow it worksLockfileCachingNix features
Defaultgo tool compile/link per-package[mod] + optional [replace]Per-packagego2nix-nix-plugin
ExperimentalRecursive-nix, per-package at build time[mod] + optional [replace]Per-packagedynamic-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

WhenWhatApplies toHow
GenerationMVS consistencyAll modesgo list -json -deps resolves actual versions
Nix evalPackage graphDefault onlybuiltins.resolveGoPackages runs go list at eval time
Build timeLockfile consistencyDefault onlylink-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

AttributeTypeModesDescription
srcpathbothSource tree. For monorepos with modRoot, this should be the repository root.
goLockpathbothPath to go2nix.toml lockfile.
pnamestringbothPackage name for the output derivation.
versionstringdefault onlyPackage version. The experimental builder does not accept this attribute.

Optional attributes

AttributeTypeDefaultModesDescription
subPackageslist of strings[ "." ]bothPackages to build, relative to modRoot. A ./ prefix is auto-added if missing.
modRootstring"."bothSubdirectory within src containing go.mod.
tagslist of strings[]bothGo build tags.
ldflagslist of strings[]bothFlags passed to go tool link (-s, -w, -X, etc.).
gcflagslist of strings[]bothExtra flags passed to go tool compile.
CGO_ENABLED0, 1, or nullnull (auto)bothOverride CGO detection. When null, CGO is enabled per-package based on the presence of C/C++ files.
pgoProfilepath or nullnullbothPath to a pprof CPU profile for profile-guided optimization.
nativeBuildInputslist[]bothExtra build inputs for the final derivation.
packageOverridesattrset{}bothPer-package customization (see below).
doCheckboolmodRoot == "."default onlyRun tests. Defaults to false when modRoot is set, because test discovery may not find local replace targets outside the module root.
checkFlagslist of strings[]default onlyFlags passed to the compiled test binary (e.g., -v, -count=1).
goProxystring or nullnulldefault onlyCustom GOPROXY URL.
allowGoReferenceboolfalsedefault onlyAllow the output to reference the Go toolchain.
metaattrset{}default onlyNix 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

AttributeDefault modeExperimental mode
nativeBuildInputsyesyes
envyesno

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
};
AttributeTypeDefaultDescription
goderivationrequiredGo toolchain.
go2nixderivationrequiredgo2nix CLI binary.
callPackagefunctionrequiredpkgs.callPackage.
tagslist of strings[]Build tags applied to all builds in this scope.
netrcFilepath or nullnull.netrc file for private module authentication (see below).
nixPackagederivation or nullnullNix 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...]
FlagDefaultDescription
-ogo2nix.tomlOutput lockfile path
-jNumCPUMax 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]
FlagDefaultDescription
--lockfilego2nix.tomlPath 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]
FlagRequiredDescription
--manifestYesPath to compile-manifest.json
--import-pathYesGo import path for the package
--src-dirYesDirectory containing source files
--outputYesOutput .a archive path
--importcfg-outputNoWrite importcfg entry for consumers to this path
--trim-pathNoPath prefix to trim
--pNoOverride -p flag (default: import-path)
--go-versionNoGo 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]
FlagRequiredDescription
--srcYesStore path to Go source
--mod-rootNoSubdirectory within src containing go.mod
--lockfileYesPath to go2nix.toml lockfile
--systemYesNix system (e.g., x86_64-linux)
--goYesPath to go binary
--nixYesPath to nix binary
--pnameYesOutput binary name
--outputYes$out path
--stdlibYesPath to pre-compiled Go stdlib
--go2nixNoPath to go2nix binary (defaults to self)
--bashNoPath to bash binary
--coreutilsNoPath to a coreutils binary (e.g., coreutils/bin/mkdir)
--sub-packagesNoComma-separated sub-packages
--tagsNoComma-separated build tags
--ldflagsNoLinker flags
--cgo-enabledNoOverride CGO_ENABLED (0 or 1)
--gcflagsNoExtra flags for go tool compile
--pgo-profileNoStore path to pprof CPU profile for PGO
--overridesNoJSON-encoded packageOverrides
--cacertNoPath to CA certificate bundle
--netrc-fileNoPath to .netrc for private modules
--nix-jobsNoMax 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>
FlagRequiredDescription
--lockfileYesPath to go2nix.toml lockfile
--goNoPath 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]
FlagRequiredDescription
--import-pathYesImport path of the package under test
--test-filesNoComma-separated absolute paths to internal _test.go files
--xtest-filesNoComma-separated absolute paths to external _test.go files
--outputNoOutput 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
FlagRequiredDescription
--manifestYesPath 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 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
FlagRequiredDescription
--manifestYesPath to link-manifest.json
--outputYesOutput directory (binaries written to <output>/bin/)

Lockfile Format

go2nix uses TOML lockfiles to pin module hashes. Both builder modes use the same lockfile format.

BuilderCommandSections
Defaultgo2nix generate[mod] + [replace]
Experimentalgo2nix 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 runs go list against the source tree.
  • Experimental mode discovers the package graph at build time via go list -json -deps inside 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

WhenWhatApplies toHow
GenerationMVS consistencyAll modesgo list -json -deps resolves actual versions
Nix evalPackage graphDefault onlybuiltins.resolveGoPackages runs go list at eval time
Build timeLockfile consistencyDefault onlylink-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:

  1. Internal test compilation — library source files + _test.go files in the same package are compiled together into a single archive that replaces the library archive.
  2. 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).
  3. External test compilation_test.go files in the *_test package (xtests) are compiled as a separate package that imports the internal test archive.
  4. Test main generation — a _testmain.go is generated that registers all Test*, Benchmark*, Fuzz*, and Example* functions.
  5. 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.go files 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.go files in the _test package. These can only access exported identifiers and test the public API. They are compiled as a separate package (foo_test) that imports foo.

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.go files) 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.go files) 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.

ModeHow it worksLockfileCachingNix features
Defaultgo tool compile/link per-package[mod] + optional [replace]Per-packagego2nix-nix-plugin
ExperimentalRecursive-nix at build time[mod] + optional [replace]Per-packagedynamic-derivations, ca-derivations, recursive-nix
  • Default (buildGoApplication): every package (not just every module) gets its own derivation. go2nix calls go tool compile and go tool link directly, bypassing go 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.mod replace directives
  • 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 packages
  • testDepsImportcfg: adds test-only third-party packages when doCheck = 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:

  1. Build phase — writes linkManifestJSON to a file and calls go2nix link-binary --manifest, which validates the lockfile, generates modinfo, compiles main packages, and invokes the linker.
  2. Check phase — writes testManifestJSON to a file and calls go2nix 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.

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 add calls (~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.