A repository that builds images can also build a Go project it has checked out, without packaging it first.
The project's committed go.mod and go.sum pin everything: one online action fetches the modules with go mod download inside a box, where go checks them against the go.sum. The compile then runs
offline against that fetched cache.
Go embeds the module list in every binary it links (runtime/debug.BuildInfo), so the image's SBOM reports
those modules.
examples/image-go-project builds the Go projects checked out beside it in a box and copies two of their
binaries into an image.
load("@tine//go:defs.bzl", "go")
go.package(
name = "hello",
box = ":go.box",
packages = {"hello-cli": "./cmd/hello-cli"},
)srcis one source directory and defaults to"<name>": the checkout is expected in a directory named after the target. Passsrc = "checkout"when it lives elsewhere, or a target such assrc = ":hello.git"for a directory artifact produced bygit.fetch(). Individual files and lists of sources are not accepted. Nothing needs to be added inside the checkout, so a pristine project clone works.- The source directory must contain a
go.mod. The directory of the selectedgo.modis the module root the build runs in. An action finds it once the source directory has been built, and the fetch and the build are declared from what it reports. - Without
module_root, the build selects the outermostgo.mod. Every othergo.modmust be in a directory below it, such as a tools or testdata helper module. go leaves nested modules out of a./...build, and so doesgo.package(). A checkout with ago.modat its root builds that module, even when the program is in a module below it. A checkout whose modules are side by side, with nogo.modabove them, fails. Thego.sumbeside the selectedgo.modpins what the fetch may download, and ago.sumelsewhere insrcis not read. A module that resolves nothing has nogo.sum; then nothing is fetched and the build runs withGOPROXY=off, so anything go would want to resolve fails. module_rootselects the module to build in a checkout with several modules. It is the path of the directory that contains the module'sgo.mod, relative tosrc, such as"server". Areplacedirective in the selectedgo.modcan point at another module insrc.packagesare relative to the selected module.packagesmaps output names to Go main packages, such as{"etcd": "."}or{"server": "./cmd/server"}. Values are paths relative to the module root or full import paths; keys set the binary's filename and sub-target name. Together these are the target's default outputs.packagesmay be left out when the module holds exactly one main package. The binary is then named after the target, not after the package's directory as a barego buildwould:helloabove would yield:hello[hello]rather thanhello-cli. Every main package under the module root counts, including ones undertools/orexamples/. With none or several, the build fails and lists what it found.boxis the build environment, declared by the consumer because only the consumer knows what its projects need.tagsare Go build tags: a//go:build <tag>line decides whether a file compiles at all. Nothing declares which tags a project defines, and one tag set applies to every module in the build, so this list has to come from the project's own build recipe.linker_flagsare passed to the Go linker as-ldflags, the same list the Buck prelude'sgo_binarytakes. Nothing is passed by default.["-s", "-w"]drops the symbol table and DWARF, which is what a distribution's packaging does to a binary before shipping it and takes roughly a third off a Go binary; the SBOM is unaffected, because the module list lives in.go.buildinfoand survives.-Xstamps a variable at link time, and-extldflagsreaches the C linker of a cgo build.cgo_cflagsadds C compiler flags for a cgo build, on top of the-O2 -gthe driver always passes. A project needs one when a dependency's headers do not compile as the box's compiler defaults.cgoforces cgo on or off. Unset, the toolchain's own default decides, as it would for ago buildrun in the checkout by hand; that default is on.cgo = Falsebuilds static binaries and needs no C compiler, taking Go's own resolver and user lookup instead of the system's.cgo = Trueturns a missing C compiler into a build failure, rather than that same silent switch.build_modesets go's-buildmodeto"exe"or"pie"."pie"builds position-independent executables, which the kernel loads at a random address on each run. Unset, the toolchain's default applies, which is"exe"on x86-64 and AArch64 Linux.toolchainis a Go toolchain that builds the project instead of the box'sgo. See Building with your own Go toolchain.
The binaries are ordinary artifacts, so an image installs one with an image.ops.copy() operation; see the
example.
For example, etcd's build scripts build the server in server/, which contains the server module. The
server's go.mod replaces other etcd modules with their directories in the checkout:
replace (
go.etcd.io/etcd/api/v3 => ../api
go.etcd.io/etcd/client/pkg/v3 => ../client/pkg
go.etcd.io/etcd/client/v3 => ../client/v3
go.etcd.io/etcd/pkg/v3 => ../pkg
)
The etcd target below selects the server module and builds its root package (.) as the binary
etcd-server, available through :etcd[etcd-server]. The fetch and the build find the replaced modules in
the checkout:
go.package(
name = "etcd",
box = ":go.box",
module_root = "server",
packages = {"etcd-server": "."},
)Set test_packages on the go.package() to compile the project's tests in its build, and declare a
go.test() that runs them:
go.package(
name = "hello",
box = ":go.box",
packages = {"hello-cli": "./cmd/hello-cli"},
test_packages = ["./..."],
)
go.test(
name = "hello-test",
package = ":hello",
)buck test :hello-test runs the tests in the package's box, without network.
test_packageslists Go package patterns relative to the module root, such as["./..."]or["./pkg/...", "./internal/store"]. The build compiles one test binary for each matched package that has_test.gofiles, with the same tags, cgo setting,build_modeandlinker_flagsas the binaries. Building the binaries also compiles the tests.- A test that does not compile fails the build of the
go.package(), and with it the binaries. go's vet checks do not run, so a vet finding fails neither the build nor the tests. - A pattern that matches a package outside
src, such as a dependency's package, fails the build. - Each test binary runs in its package's directory, as with
go test, so a test finds itstestdata/. The sources are read-only while the tests run, so a test that writes into its package directory fails. argspasses arguments to every test binary. A test binary takes the-test.form of thego testflags, such as["-test.short", "-test.skip=TestNetwork"]. Each binary has a timeout of 10 minutes, which-test.timeoutoverrides.labelsare passed to the test runner, sobuck test --exclude <label>can leave the test out.
box.new(
name = "go.box",
packages = ["ca-certificates", "golang", "python3"],
release = "tine//catalog:fedora.rawhide.release",
)golangis the toolchain,python3runs the in-box drivers, andca-certificatesgives the module fetch its TLS trust forproxy.golang.org. A module that proxy does not serve is fetched directly, withgit, which then has to be there too.- The fetch and the build set
GOTOOLCHAIN=local, so go never downloads another toolchain. Ago.modthat requires a newer Go than the box'sgo, or than thetoolchain, fails the build. - A project using cgo needs
gccand the-develpackages it binds, pluspkgconf-pkg-configandrpmfor a#cgo pkg-config:directive, because Fedora's/usr/bin/pkg-configshells out torpm --eval. Its binaries then link dynamically, so the image needs the matching runtime packages. This is not limited to projects that write C: with cgo enabled, importingnetoros/useris enough.cgo = Falseuses Go's own implementations of those instead, and needs no C compiler.
The box is a build environment. It never becomes part of an image, and the toolchain is not shipped.
Set toolchain to build with a Go release other than the one in the box, such as an older release that a
project requires. An upstream Go release from go.dev works as a toolchain. Pin it in the project's
tine.lock.json and download it with http.tool().
Add a pin for the Go version to tine.lock.json, without digests:
{
"go": {
"url": "https://go.dev/dl",
"release": "go1.27",
"releases": {
"url": "https://go.dev/dl/?mode=json&include=all",
"pattern": "go1\\.27\\.[0-9]+"
},
"platforms": {
"arm64": {"artifact": "go1.27.linux-arm64.tar.gz", "strip_prefix": "go"},
"x86_64": {"artifact": "go1.27.linux-amd64.tar.gz", "strip_prefix": "go"}
}
}
}The pattern matches the Go 1.27 releases in the release list on go.dev. Run tine bump to move the pin to
the newest of them and fill in the digests and sizes:
tine bump --tool gogo: updated go1.27 -> go1.27.2
Declare the toolchain and pass it to go.package():
load("//:tine.lock.json", pins = "value")
load("@tine//box:defs.bzl", "box")
load("@tine//go:defs.bzl", "go")
load("@tine//http:defs.bzl", "http")
http.tool(
name = "go",
directory = True,
path = "bin/go",
spec = pins["go"],
)
box.new(
name = "go-toolchain.box",
packages = ["ca-certificates"],
release = "tine//catalog:fedora.rawhide.release",
)
go.package(
name = "hello",
box = ":go-toolchain.box",
packages = {"hello-cli": "./cmd/hello-cli"},
toolchain = ":go",
)- The toolchain's
gofetches the modules and builds the binaries and the tests. The box does not need thegolangpackage. toolchaintakes any target whose output is a Go root directory, the directory that containsbin/go.- The toolchain runs inside the box, so Buck downloads the release for the box's architecture. go then
cross-compiles for the target architecture, as the box's
godoes. buck run :go -- versionruns the pinnedgo.bumpkeeps the pin on the releases that the pattern matches. Change the pattern, such as to"go1\\.28\\.[0-9]+", and runbumpagain to move to another minor version.
How the modules are pinned, fetched and verified is described under "Go source builds" in architecture.md.
- A project is one cache unit, and reruns are incremental for a
tine mount. Any change to its sources reruns the build action for the whole project, but go's build cache survives between runs, so it recompiles only what changed. Only changes togo.sumand to thego.modfiles the fetch reads rerun the fetch. Its module cache is kept too, so a dependency bump downloads only what is missing. The fetch reads the project'sgo.modand thego.modof each module that it replaces with a directory.buck cleanis what forces a build from scratch. - A fetched or committed project keeps neither cache between runs. Its build cache never reaches the shared cache, only the binaries; the module cache does, refetched whole whenever the pins move.
go.modandgo.summust agree. The build runs-mod=readonly, so a stalego.sumfails the build instead of quietly resolving something else;go mod tidyand commit the result.- The fetch is a network action, one of the few build steps that reach out at all. It declares its own
GOPROXY(proxy.golang.org, thendirect) and checksum database rather than taking the box's, which a distribution patches. - A local
go buildin the checkout wants-o. A bare one writes its binary into the current directory. Sincesrcis an action input, use-oto place that binary outside the source directory. - A checkout is not the only way in. The module is found in the built sources rather than at parse
time, so
git.fetch()can supply the complete project tree without committing it to the consuming repository. - A committed
vendor/tree is ignored, because the build passes-mod=readonly, and an explicit-modis what turns off go's habit of preferring a vendor directory. Every module comes from thego.sumeither way. - A
replacedirective must point insidesrc. Ago.modthat replaces a module with a directory outsidesrcfails the build, because the build would read files that are not among its inputs. module_rootmust be a directory insrcthat contains ago.mod. An empty path, an absolute path, a path with..and a path through a symlink to a directory fail the build.- A committed
go.workis ignored, because the build setsGOWORK=off. The module builds against its owngo.sum. - The build reads the source directory as it is on disk, including the files that Buck ignores. An
ignored
.gofile, such as generated code, is compiled into the binary. See Building from a local checkout instead.