Profpatsch/nix/buildGo
- example/ 1.6 KiB · 3 files
- external/ 12.7 KiB · 2 files
- mkembedcfg/ 7.9 KiB · 2 files
- CLAUDE.md 986 B
- buildGo.7 12.6 KiB
- default.nix 15.4 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, which follow the versions the go.work workspace resolves; see Versions can drift under CAVEATS.
FUNCTIONS
program({ name, srcs, deps ? [], x_defs ? {}, embeds ? [] })
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.
embeds
List of files and directories the sources'
//go:embeddirectives draw from. See EMBEDDING FILES.
package({ name, srcs, deps ? [], path ? name, sfiles ? [], embeds ? [] })
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.
embeds
As for program().
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,
but only for files named in
embeds:
depot.nix.buildGo.program {
name = "myprog";
srcs = [ ./main.go ./serve.go ];
embeds = [ ./templates ./static/style.css ];
}
Each entry is a file or a directory
inside
the package directory, which is the directory of the first entry of
srcs.
Its name as a
//go:embed
pattern sees it is its path relative to that directory:
./templates
above is
templates,
so
//go:embed templates/*.html
finds it.
A directory is taken whole and the patterns choose from it, so listing
./templates
once covers every template added later.
An entry outside the package directory is an evaluation error.
Two mistakes that used to build fine now fail the build:
- a source with a
//go:embedline and an empty embeds, which would otherwise compile to an empty variable; - a pattern that matches none of the listed files, with the error go build itself gives.
How it works
The compiler cannot glob the filesystem, so
go build
normally resolves patterns and hands the result to
go tool compile
as a
-embedcfg
JSON file.
buildGo
does the same in a separate derivation: it stages
srcs
and
embeds
into one directory under their package-relative names, reads the patterns with
go list,
and resolves each one with
mkembedcfg,
which follows
go build's
rules: a pattern is a
path.Match()
glob, a matched directory contributes every regular file below it, names
beginning with
'.'
or
'_'
are skipped unless the pattern has the
all:
prefix, and nested modules are not entered.
The compile step reads the embedded files from that derivation and nothing
else.
A package with no
embeds
gets no such derivation at all.
external() lists embeds itself: its analyser records what each package's patterns match in the fetched source tree.
Why embeds are listed
Earlier versions took the directory of the first source,
dirOf (head srcs),
and put it into the embed derivation.
For path-literal sources that is a
path
to the working directory, and putting a path into a derivation copies it into
the store at evaluation time - the whole directory, including build output
and anything else untracked, for every package, whether it embedded anything
or not.
For one package that was 235MB, hashed on every evaluation of the system that
used it, and every change to an unrelated file in the directory rebuilt the
package.
The same versions documented the opposite: that path-literal sources left the directory as /nix/store and embeds silently empty, and that sources had to be staged into a directory by hand and passed as strings. The first half was never true; the staging worked, but only by accident of the copy. Listing embeds makes what is embedded explicit and copies nothing it does not name.
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
*embeds*
with two directories of assets.
//users/Profpatsch/asciinema-server/default.nix
*embeds*
listing single files, where a directory holds more than the patterns should
take.
//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), go-deps-check(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, and the build itself gives no diagnostic.
The rule that keeps them together is that go.work decides and go-deps.nix follows. A workspace resolves exactly one version per module across all of its modules, which is the same shape as a set of Nix pins, so each pin is simply that version. To add or upgrade a dependency:
$ go -C users/Profpatsch/foo get example.com/bar@v1.3.0
$ go work sync
$ go-deps-check
go work sync writes the workspace's choice back into every module's go.mod, so each module still resolves on its own, as pkgs.buildGoModule() sees it; commit those go.mod and go.sum changes on their own. go-deps-check(1) then lists every pin that differs. Bump those, update their hashes, and rebuild what uses them - a new version may import packages its deps do not list yet. A buildGoModule() package's vendorHash changes with it.
A project outside go.work keeps pins of its own and is not checked.
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.