Profpatsch/nix/buildGo
- example/ 1.6 KiB · 3 files
- external/ 11.1 KiB · 2 files
- mkembedcfg/main.go 2.7 KiB
- CLAUDE.md 706 B
- buildGo.7 10.6 KiB
- default.nix 15.2 KiB
buildGo(7)
NAME
buildGo - build Go libraries and programs by invoking the compiler directly
DESCRIPTION
buildGo
is a Nix build system for Go that calls
go(1)'s
underlying tools
(go tool compile, go tool link)
directly, rather than delegating to
go build
and its module resolution.
Composition of packages and programs therefore stays inside Nix, in the style
of Bazel's
rules_go.
Every dependency is an explicit Nix value; there is no
go.mod
resolution, no network access and no vendor directory at build time.
buildGo is a function, not a package: it is imported by //nix/packages.nix and reached as depot.nix.buildGo. It produces no bin/ output of its own and cannot be built or installed.
Any go.mod in the tree exists only for editor tooling and go test; it has no effect on the Nix build. Dependency versions come from go-deps.nix files, and the two can disagree without any warning – see CAVEATS.
FUNCTIONS
program({ name, srcs, deps ? [], x_defs ? {} })
Build an executable.
name
Name of the program and of the resulting executable. Required.
srcs
List of paths to source files. Required. All sources are compiled into package
main.
deps
List of Go libraries to link against.
x_defs
Attribute set of linker variable definitions, passed as -X flags. Used to stamp values such as a version into the binary at link time.
package({ name, srcs, deps ? [], path ? name, sfiles ? [] })
Build an importable library. The result carries both the compiled archive and the sources, as downstream consumers need both.
name
Name of the library. Required.
srcs
List of paths to source files. Required.
deps
List of Go libraries to link against.
path
Go import path of the result. Defaults to name.
sfiles
List of Go assembly (*.s) files, assembled and packed into the archive. Required by several packages under
golang.org/x.
external({ path, src, deps ? [], tags ? [] })
Build a Go repository that was not written for buildGo, laid out in the standard Go tooling style.
path
Go import path of the repository, e.g.
github.com/emersion/go-imap. Required.
src
Path to the source directory, typically a fetchFromGitHub() result. Required.
deps
List of other external() results this repository imports.
tags
Build tags to set while analysing the repository. See BUILD TAGS.
EXTERNAL PACKAGES
external() analyses the repository and returns a nested attribute set mirroring its directory structure. Each directory that contains a Go package gains a gopkg attribute holding the compiled library. A dependency must always be referred to by an attribute that has a gopkg, which is not necessarily the root. Three shapes occur:
Single-package repository
gopkg sits at the root, so the result is used directly:
deps = [ goDeps.creack-pty ];
Repository with subpackages
Navigate to the ones actually imported:
deps = [ goDeps.golang-x-net.html ];
Repository with no root package
golang.org/x/syshas code only in subdirectories:
deps = [ goDeps.golang-x-sys.unix ];
Passing an attribute without a gopkg is an error, and the resulting message enumerates the valid paths within that dependency.
Dependencies between packages inside one repository are resolved automatically from the import statements. Only imports of other repositories need to be listed in deps, where they are matched by their goImportPath.
BUILD TAGS
tags
sets the build tags used when the analyser decides which files a package
consists of.
It selects
sources,
not compiler behaviour: a file excluded by a tag is never handed to
go tool compile
at all.
The case that motivates it is assembly.
Several cryptographic repositories ship hand-written amd64 assembly next to a
pure-Go implementation of the same routines, chosen by the
purego
tag.
buildGo
stages each package into a flat directory and copies only
*.go
and
*.s
files, so an assembly file that
#includes
a header fails to assemble:
fp_amd64.s:6: #include: open .../pkg/include/fp_amd64.h:
no such file or directory
Selecting the pure-Go sources avoids the problem at its root rather than patching around it:
circl = depot.nix.buildGo.external {
path = "github.com/cloudflare/circl";
src = pkgs.fetchFromGitHub { /* ... */ };
tags = [ "purego" ];
};
Copying the headers alongside the assembly would not be enough in general,
because such headers are routinely included by
relative
path across package boundaries –
circl/dh/x25519
includes
../../math/fp25519/fp_amd64.h
– which presumes the original repository tree that the flat staging has
already flattened away.
The cost is performance: the pure-Go path is slower than the assembly it replaces. For work measured in a handful of operations per request this is not observable, but a tag should not be reached for merely to silence a build error in a hot path.
EMBEDDING FILES
buildGo
supports
//go:embed.
The compiler cannot glob the filesystem itself, so
buildGo
runs
go list
over the source directory to discover which files each pattern matches, and
converts the result into the
-embedcfg
JSON the compiler expects.
This is where the one genuinely surprising constraint of buildGo lives.
The staging requirement
The source directory handed to go list is computed as the directory of the first entry of srcs. When sources are given as ordinary path literals:
srcs = [ ./main.go ./serve.go ];
each path is copied into the store individually, becoming its own /nix/store/<hash>-main.go. Their common directory is therefore /nix/store itself, which contains no embeddable files. go list finds nothing, the embed configuration comes out empty, and the embedded variables are silently empty at runtime. The build does not fail.
The remedy is to stage the sources and the files they embed into a single directory first, and pass strings pointing into it rather than path literals:
let
src = pkgs.runCommandLocal "myprog-src" {} ''
mkdir -p $out/templates
cp ${./main.go} $out/main.go
cp ${./templates}/*.html $out/templates/
'';
in depot.nix.buildGo.program {
name = "myprog";
srcs = [ "${src}/main.go" ];
}
A path literal
(./main.go)
is copied to the store on its own; a string
("${src}/main.go")
names a file
inside
an existing store directory and keeps its siblings reachable.
The distinction is invisible at the call site and matters only here.
Note that the staging derivation copies a fixed set of files.
Adding a template with a new extension means widening the
cp
glob as well as the
//go:embed
pattern; forgetting the former leaves the file out of the build entirely.
Pattern matching
Patterns are matched with
filepath.Match(),
so
templates/*.html
and plain paths both work, and a pattern naming a directory matches the files
directly below it.
A pattern that matches nothing falls back to embedding every file go list reported, rather than failing. A typo in a pattern thus yields a working build with the wrong contents.
EXAMPLES
A program with one local library:
let
lib = depot.nix.buildGo.package {
name = "somelib";
srcs = [ ./lib/foo.go ./lib/bar.go ];
};
in depot.nix.buildGo.program {
name = "my-program";
srcs = [ ./main.go ];
deps = [ lib ];
}
External dependencies are conventionally pinned in a go-deps.nix beside the package, or taken from the shared one:
{ depot, pkgs, ... }:
{
creack-pty = depot.nix.buildGo.external {
path = "github.com/creack/pty";
src = pkgs.fetchFromGitHub {
owner = "creack";
repo = "pty";
rev = "edfbf75025b0ba4ee17c19f52d9b600fad80a787";
sha256 = "0yy4zhfb7vrrbwd13rcw0zzcq0ami3zv3hp0x7g7il6mrbadcf25";
};
};
}
Rather than reproducing every pattern here, the following files in this repository are worth reading as worked examples:
//nix/buildGo/example
A minimal package() plus program(), including x_defs.
//users/Profpatsch/link-check/default.nix
The smallest realistic case: one source file, one external subpackage dependency.
//users/Profpatsch/go-deps.nix
The shared dependency pin set, and the idiom of navigating to subpackages.
//users/Profpatsch/mailweb/default.nix
Embed staging with a subdirectory of assets, as described above.
//users/Profpatsch/source-forge/default.nix
A program with a separate man output, via symlinkJoin().
//users/Profpatsch/maildir-varlink/default.nix
Wrapping the result with wrapProgram() to put runtime dependencies on
PATH.
SEE ALSO
go(1)
//README.md for the default.nix versus packages.nix rules that govern how a package is named and found.
nix-1p, a brief introduction to the Nix language.
HISTORY
buildGo
originates from the
TVL depot,
where it was written by Vincent Ambo, and is licensed Apache 2.0.
It has since gained
//go:embed
support, assembly support and static linking.
CAVEATS
Tests are never run
buildGo has no test support. *_test.go files are simply left out of srcs, where they are inert. Run them with the ordinary Go tooling instead, which the go.work workspace at the repository root makes possible:
$ go test ./users/Profpatsch/mailweb/
Consequently a package can build under Nix while its tests do not compile.
Versions can drift
The Nix build ignores go.mod entirely, so the version pinned in go-deps.nix is what is actually compiled. When the two disagree, editor tooling and go test see one version and the built artefact another, with no diagnostic.
cgo is unavailable
The standard library is built with
CGO_ENABLED=0
so that everything links statically and no
libc
or dynamic loader becomes a runtime dependency.
Packages that require cgo cannot be built with
buildGo;
use
pkgs.buildGoModule()
for those.
documentation building
Manual pages and other documentation are installed by hand, typically by wrapping the result in symlinkJoin() with an added man output.