SEANK.H.LIAO

bazel and go, 2023

notes and bazel

bazel and go

Occasionally, I think "it would be nice if everything could be built together in one tool", and I inevitable think of bazel. Well this is my 2023 attempt to try and use it.

getting started

We start with not bazel itself, but bazelisk, a wrapper around bazel that will handle downloading and running the correct version of bazel for you. Obtain bazelisk through magic (like OS repositories), install it as bazel, and we're good to go. We'll want to choose a version of bazel though and record that in .bazelversion for bazelisk to pick up:

1$ echo 6.4.0 > .bazelversion

Next we need to define a bazel workspace, the root of everything that will be built by bazel. The previous link used WORKSPACE or WORKSPACE.bazel, which is also where you'd put all the directives to download external dependencies, but bazel is in the process of moving to modules where dependencies come from central registries like registry.bazel.build, so we can replace that with a MODULE.bazel file. We declare a module using module:

1# MODULE.bazel
2
3module(name = "example_repo1", version = "0.0.1")

Bazel modules also have to be enabled with a flag right now, which can be passed to every command via .bazelrc:

1$ echo "common --enable_bzlmod" > .bazelrc

basic tools

There are some tools that might be considered almost mandatory:

From registry.bazel.build we can find buildifier_prebuilt and gazelle. We can add them to MODULE.bazel using bazel_dep:

1# MODULE.bazel
2
3bazel_dep(name = "gazelle", version = "0.33.0", dev_dependency = True)
4bazel_dep(name = "buildifier_prebuilt", version = "6.3.3", dev_dependency = True)

Finally, we can load extensions and register targets in BUILD.bazel.

1# BUILD.bazel
2
3load("@gazelle//:def.bzl", "gazelle")
4gazelle(name = "gazelle")
5
6load("@buildifier_prebuilt//:rules.bzl", "buildifier")
7buildifier(name = "buildifier")

This should mean we can run commands like:

1$ bazel run //:buildifier # formats files

go go go

Time to write some Go code. We'll still try to be compatible with vanilla go (and it's nicer to manage deps this way) so:

1$ go mod init repo1.example

and let's write some very simple code:

 1$ mkdir helloworld
 2$ cat << EOF > helloworld/main.go
 3package main
 4
 5import "fmt"
 6
 7func main() {
 8	fmt.Println("hello world")
 9}
10EOF

We can run it with go:

1$ go run ./helloworld

But to run it with bazel we'll need some rules for building go, specifically rules_go:

1# MODULE.bazel
2
3bazel_dep(name = "rules_go", version = "0.42.0")

Then we can generate the BUILD.bazel files with gazelle:

 1$ bazel run //:gazelle
 2
 3# results
 4$ cat helloworld/BUILD.bazel
 5load("@rules_go//go:def.bzl", "go_binary", "go_library")
 6
 7go_library(
 8    name = "helloworld_lib",
 9    srcs = ["main.go"],
10    importpath = "repo1.example/helloworld",
11    visibility = ["//visibility:private"],
12)
13
14go_binary(
15    name = "helloworld",
16    embed = [":helloworld_lib"],
17    visibility = ["//visibility:public"],
18)
19
20# build and run
21$ bazel run //helloworld
22
23# or without build logs
24$ bazel run --ui_event_filters=-info,-stdout,-stderr --noshow_progress //helloworld

Just run bazel run //:gazelle every time new packages are created or imports change.

external go dependency

Now for external dependencies. We start with a simple piece of code borrowed from the Go tutorial

 1$ mkdir helloworld2
 2$ cat << EOF > helloworld2/main.go
 3package main
 4
 5import (
 6	"fmt"
 7
 8	"rsc.io/quote"
 9)
10
11func main() {
12	fmt.Println(quote.Go())
13}
14EOF

We'll want to run go mod tidy to get the dep in go.mod:

1$ bazel run @rules_go//go -- mod tidy

We'll also want to use gazelle to sync deps from go.mod into the bazel module system:

1# MODULE.bazel
2go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
3go_deps.from_file(go_mod = "//:go.mod")

Then we can run gazelle, which should give us a warning about needing to actually register the module

1$ bazel run //:gazelle
2WARNING: /home/arccy/tmp/testrepo0416/MODULE.bazel:10:24: The module extension go_deps defined in @gazelle//:extensions.bzl reported incorrect imports of repositories via use_repo():
3
4Not imported, but reported as direct dependencies by the extension (may cause the build to fail):
5    io_rsc_quote
6
7 ** You can use the following buildozer command to fix these issues:
8
9buildozer 'use_repo_add @gazelle//:extensions.bzl go_deps io_rsc_quote' //MODULE.bazel:all

Run the suggested command to add the dep to MODULE.bazel which should now have use_repo(go_deps, "io_rsc_quote"). And we should finally be able to run:

1$ bazel run //helloworld2

Note that it appears we can't run buildozer via bazel itself because we can't escape the sandbox, and I don't understand enough of bazel to make it work. (apparently it's not too hard: aspect blog)