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

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 build fails with error: 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

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.

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 calls go tool compile and go tool link directly instead of go 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 running go list against 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.

ModeHow it worksLockfileCachingRequires
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 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 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 (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.

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: .drv paths are computed in-process (microseconds each) and registered concurrently over the nix-daemon socket; the per-derivation nix derivation add subprocess 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 path
  • goVersion — the main module’s go directive (e.g. "1.25"); the dag builder threads this as -lang to local-package compiles
  • replacementsreplace directives from go.mod
  • testPackages — test-only third-party packages (when doCheck = true)
  • moduleHashes — module NAR hashes (when resolveHashes = 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 wraps go 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

LayerOne derivation perCache key (informally)Rebuilds when
stdlibGo toolchainGo version + GOOS/GOARCH + tagsGo is bumped
module FODgo.sum linemodule path@version + NAR hashthat module is bumped in go.sum
third-party packageimported packagemodule FOD + import deps + gcflagsthe module or any of its transitive deps change
local packagelocal Go packagefiltered package directory + import depsa .go file in that directory or a dep changes
importcfg bundleappthe set of compiled package outputsany package output changes
appappimportcfg bundle + main package sourceanything 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:

  1. The edited package recompiles.
  2. Each package that imports it (directly or transitively) recompiles.
  3. 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 .a resolves to the same store path and short-circuits downstream rebuilds.
  • iface output split. Each per-package derivation gains a second iface output containing only the export data (the .x file produced by go tool compile -linkobj). Downstream compiles depend on iface instead of the full .a, so changes to private symbols that don’t alter the package’s exported API don’t cascade. This mirrors the .x model used by Bazel’s rules_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-derivations experimental 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 .x file, 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:

  1. Calls builtins.resolveGoPackages (the Nix plugin), which runs go list -json -deps against your source tree.
  2. 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.

AttributeTypeModesDescription
srcpathbothSee above.
pnamestringbothPackage name for the output derivation.
versionstringdefault onlyPackage version. The experimental builder does not accept this attribute (its wrapper produces a CA .drv whose name is derived from pname alone).

Optional attributes

AttributeTypeDefaultModesDescription
goLockpath or nullnull (default) / required (experimental)bothPath to go2nix.toml. In default mode, null enables lockfile-free builds. The experimental builder requires a lockfile.
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. 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.
nativeBuildInputslist[]bothExtra build inputs for the final derivation.
packageOverridesattrset{}bothPer-package customization (see below).
doCheckbooltruedefault onlyRun tests. Matches buildGoModule’s default. See Test Support.
checkFlagslist of strings[]default onlyFlags passed to the compiled test binary (e.g., -v, -count=1). See Test Support.
extraMainSrcFileslist of strings[]default onlyExtra 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.
goProxystring or nullnulldefault onlyCustom GOPROXY URL.
allowGoReferenceboolfalsedefault onlyAllow the output to reference the Go toolchain.
metaattrset{}default onlyNix meta attributes.
contentAddressedboolfalsedefault onlyMake 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
};
AttributeTypeDefaultDescription
goderivationrequiredGo toolchain.
go2nixderivationrequiredgo2nix CLI binary.
callPackagefunctionrequiredpkgs.callPackage.
tagslist of strings[]Build tags applied to all builds in this scope.
goEnvattrset{}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.
netrcFilepath or nullnull.netrc file for private module authentication (see below).
nixPackagederivation or nullnullNix 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 DefaultGODEBUGgo 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:

  1. The package’s exact import path (e.g. "github.com/diamondburned/gotk4/pkg/core/glib").
  2. The package’s module path (e.g. "github.com/diamondburned/gotk4/pkg").
  3. 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

KeyTypeDefault modeExperimental modeNotes
nativeBuildInputslist of derivationscgo packages onlyyesAdded to the per-package derivation’s nativeBuildInputs
envattrsetyesnoExtra environment variables on the per-package derivation
srcOverlayderivation or pathyesnoContents 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 PATHnativeBuildInputs 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:

  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 (otherwise the xtest would link two copies of the package — one with test helpers, one without).
  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 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.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.

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 require line in go.mod
  • a replace directive changes which remote module a path resolves to
  • go.sum gains 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 when goLock = null — there is no lockfile for go.mod to drift from. Module versions are read live from go.sum via the plugin on every evaluation, so a go get is reflected on the next nix build with nothing to regenerate. The standalone go2nix check subcommand 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

WhenWhatApplies toHow
GenerationMVS (Minimal Version Selection) consistencyAll modesgo list -json -deps resolves actual versions
Nix evalPackage graphDefault onlybuiltins.resolveGoPackages runs go list at eval time
Build timeLockfile consistencyDefault, with lockfilelink-binary re-reads go.mod and checks every required module is present in the lockfile at the right version; skipped when goLock = null

In default mode, 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...]
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 — 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]
FlagDefaultDescription
--lockfilego2nix.tomlPath 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]
FlagRequiredDescription
--manifestYesPath to compile-manifest.json
--import-pathYesGo import path for the package
--src-dirYesDirectory containing source files
--outputYesOutput .a archive path
--iface-outputNoWrite export-data-only interface (.x) to this path; --output then receives the link object via -linkobj
--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=...] [-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]
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 derivation registrations
--daemon-socketNonix-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>
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/)

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

FlagDefaultMeaning
-runs N3Runs per scenario; the median is reported
-scenario SallOne of no_change, leaf, mid, deep, all
-touch-mode Mprivateprivate edits an unexported symbol; exported edits an exported one
-tools Lnix-nocgo,nix-ca-nocgoComma-separated tool variants: nix, nix-ca, nix-nocgo, nix-ca-nocgo
-fixture Flightlight or torture (see below)
-json PATHWrite raw results as JSON
-assert-cascade NFail (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/:

FixtureShapeleaf editsmid editsdeep edits
lightsmall app, a few internal packagesapp/cmd/app/main.gointernal/handler/handler.gointernal/core/core.go
torturelarge app, hundreds of modulesapp-full/cmd/app-full/main.gointernal/aws/aws.gointernal/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 pure nix eval of 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_ENABLED isn’t forced to 0 and that the cgo files aren’t excluded by build tags on your target platform.
  • If you need to influence a pure-Go compile, use env instead.
  • If you only need the inputs at link time, put them in the top-level nativeBuildInputs of buildGoApplication rather than in packageOverrides.

See Package Overrides.

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.