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

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.