Use Go build and module caching inside Nix builds
Go build and module caching is great, but it doesn't work in Nix builds because of the sandbox.
nix-gocacheprog
pokes a hole in the sandbox to make it work. Iterating on Go
builds in Nix is fast again!
Note: Only use this if you trust that Go's build cache is accurate (this seems pretty well-accepted). Maybe don't do it on your release builds.
This is just single-machine caching, nothing over the network yet.
nix-gocacheprog
also caches module downloads, by routing them through the
build cache with a module proxy.
This isn't as big of a win as caching builds, since buildGoModule
and
gomod2nix
already cache modules by using a separate derivation, but it does
help if you're using buildGoModule
with vendorHash
and add a single
dependency: all the others won't be downloaded again.
Only for NixOS for now, but it shouldn't be that hard to make it work on other systems that Nix runs on (contributions welcome).
- Import module.nix in your NixOS config.
- Add overlay.nix to your project's overlays.
(You'll probably want to use flakes or niv or npins or whatever, but this should give you the idea.)
configuration.nix
{
imports = [
"${fetchTarball "https://github.com/dnr/nix-gocacheprog/archive/main.tar.gz"}/module.nix"
];
}
myproject/default.nix
{ pkgs ? import <nixpkgs> { config = {}; overlays = [
(import "${fetchTarball "https://github.com/dnr/nix-gocacheprog/archive/main.tar.gz"}/overlay.nix")
]; } }:
buildGoModule {
name = "myproject-1.2.3";
vendorHash = "...";
src = pkgs.lib.cleanSource ./.;
}
GOCACHEPROG
is an experimental Go feature that will be promoted to stable with Go 1.24.
It lets you plug in an external program to handle the Go build cache.
There's a "reference implemention" of the protocol here: https://github.com/bradfitz/go-tool-cache
We can plug in a program but how do we share the cache across builds?
We poke a hole using Nix's pre-build-hook
:
Nix runs a program of our choice before setting up the sandbox, and it can add
specific paths to the sandbox (with bind mounts).
So we just add the whole Go build cache, right?
Well, no. That would work but it would be a big mess, with multiple uids writing to one directory, that could read each other's files…
We add:
- Our
GOCACHEPROG
binary. - A unix socket, for our
GOCACHEPROG
to communicate over. - A build-specific directory to put the cached files in (this is why it needs to be a pre-build-hook and not just extra-sandbox-paths).
The other end of the socket is a daemon that runs on the host, that speaks the
GOCACHEPROG
protocol with extensions for setting up build-specific
directories. The cache itself is shared among all builds on the system, but each
build can only see its own files (files that it knows the input hash of).
The module:
- Sets
nix.settings.pre-build-hook
. - Sets up the daemon, including socket activation.
The overlay sets up:
- Build
go_1_21
,go_1_22
, andgo_1_23
withGOEXPERIMENT=cacheprog
. - Add
nixGocacheprogHook
to set upGOCACHEPROG
. - Make
buildGoModule
addnixGocacheprogHook
tonativeBuildInputs
.
nixGocacheprogHook
does:
- Checks if it looks like the pre-build-hook has run. If not, do nothing.
- Set
GOCACHEPROG
to our binary (in client mode).
The above sets up caching for builds. What about the module cache?
The module cache uses a separate mechanism and protocol.
To intercept it, we can use a module proxy.
The proxy mostly passes things through to an upstream proxy
(https://proxy.golang.org
by default, but it respects GOPROXY
),
but for .mod
and .zip
files (the immutable ones),
it uses the build cache.
This works in the "module derivation" of buildGoModule
, which is a FOD.
The "main derivation" sets GOPROXY=off
so this doesn't apply.
To support this, nixGocacheprogHook
also does:
- If
GOPROXY
isoff
orfile:...
, do nothing. - Runs the binary in goproxy mode in the background, listening on a localhost port.
- Sets
GOPROXY
to point to that proxy.
- Cache size limit
- Flake (contributions welcome)
- Extend over the network
- Make it work with
sandbox = false
(see this issue) - Make it work with non-NixOS systems
- Make it work with
gomod2nix
and other builders (should just be able to addnixGocacheprogHook
)
This main difference between this and other Go building alternatives is that this is a quick and dirty solution to speed up Go incremental builds inside of Nix during development. It's impure and relies on a system-level cache daemon. That said, it's easy to drop into existing projects.
More details:
- pre-builds dependencies and puts them in a separate derivation that is a dependency of your build
- requires an extra step to list external packages and an extra file in the repo
- requires some changes to project Go build, but is compatible with
buildGoModule
- module granularity
- only caches dependency builds
- is pure
- works anywhere
- also uses
GOCACHEPROG
but in a purer way, with separate derivations per module that contain subsets of the cache - replacement for the entire Nix Go build system
- module granularity
- only caches dependency builds
- is pure
- works anywhere
nix-gocacheprog
(this project)
- punches hole in sandbox for cache inputs/outputs
- caches at file granularity
- also caches build results of the main module, test runs, and module downloads
- easy to drop-in to any project, only overlay required (or build hook)
- but requires system-level changes (
pre-build-hook
innix.conf
) - is impure, but we generally trust the Go build tool
- is only set up for NixOS right now, but should work elsewhere with a little effort
Parts of this project were forked from https://github.com/bradfitz/go-tool-cache and so it inherits its 3-clause BSD license.