本教程将通过介绍如何构建 Go (Golang) 项目,向您介绍 Bazel 的基础知识。您将学习如何设置工作区、构建小程序、导入库以及运行其测试。在此过程中,您将学习一些重要的 Bazel 概念,例如目标和 BUILD 文件。
预计完成时间:30 分钟
准备工作
安装 Bazel
在开始之前,如果您尚未安装 bazel,请先执行此操作。
您可以在任何目录中运行 bazel version,以检查 Bazel 是否已安装。
安装 Go(可选)
您无需安装 Go 即可使用 Bazel 构建 Go 项目。Bazel Go 规则集会自动下载并使用 Go 工具链,而不是使用您机器上安装的工具链。这样可以确保项目中的所有开发者使用相同版本的 Go 进行构建。
不过,您可能仍需要安装 Go 工具链,以运行 go
get 和 go mod tidy 等命令。
您可以在任何目录中运行 go version 来检查是否已安装 Go。
获取示例项目
Bazel 示例存储在 Git 代码库中,因此如果您尚未安装 Git,则需要先安装。如需下载示例代码库,请运行以下命令:
git clone https://github.com/bazelbuild/examples本教程的示例项目位于 examples/go-tutorial 目录中。查看该文件包含的内容:
go-tutorial/
└── stage1
└── stage2
└── stage3
其中包含三个子目录(stage1、stage2 和 stage3),分别对应本教程的不同部分。每个阶段都基于上一个阶段。
使用 Bazel 构建
从 stage1 目录开始,我们将在其中找到一个程序。我们可以使用 bazel build 构建它,然后运行它:
$ cd go-tutorial/stage1/
$ bazel build //:hello
INFO: Analyzed target //:hello (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:hello up-to-date:
bazel-bin/hello_/hello
INFO: Elapsed time: 0.473s, Critical Path: 0.25s
INFO: 3 processes: 1 internal, 2 darwin-sandbox.
INFO: Build completed successfully, 3 total actions
$ bazel-bin/hello_/hello
Hello, Bazel! 💚
我们还可以使用单个 bazel run 命令构建和运行该程序:
$ bazel run //:hello
bazel run //:hello
INFO: Analyzed target //:hello (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:hello up-to-date:
bazel-bin/hello_/hello
INFO: Elapsed time: 0.128s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/hello_/hello
Hello, Bazel! 💚
了解项目结构
查看我们刚刚构建的项目。
hello.go 包含该程序的 Go 源代码。
package main
import "fmt"
func main() {
fmt.Println("Hello, Bazel! 💚")
}
BUILD 包含一些面向 Bazel 的指令,告知 Bazel 我们要构建什么。通常,您会在每个目录中写入这样的文件。对于此项目,我们有一个 go_binary 目标,用于从 hello.go 构建程序。
load("@rules_go//go:def.bzl", "go_binary")
go_binary(
name = "hello",
srcs = ["hello.go"],
)
MODULE.bazel 会跟踪项目的依赖项。它还会标记项目的根目录,因此您每个项目只会写入一个 MODULE.bazel 文件。它的用途与 Go 的 go.mod 文件类似。您实际上不需要在 Bazel 项目中创建 go.mod 文件,但创建一个 go.mod 文件可能仍然很有用,这样您就可以继续使用 go get 和 go mod tidy 进行依赖项管理。Bazel Go 规则集可以从 go.mod 导入依赖项,但我们将在另一个教程中介绍这一点。
我们的 MODULE.bazel 文件包含对 Go 规则集 rules_go 的单个依赖项。我们需要此依赖项,因为 Bazel 不支持 Go。
bazel_dep(
name = "rules_go",
version = "0.50.1",
)
最后,MODULE.bazel.lock 是 Bazel 生成的文件,其中包含有关依赖项的哈希值和其他元数据。它包含 Bazel 本身添加的隐式依赖项,因此非常长,我们不会在此处显示它。与 go.sum 一样,您应将 MODULE.bazel.lock 文件提交到源代码控制系统,以确保项目中的每个人都能获得每个依赖项的相同版本。您无需手动修改 MODULE.bazel.lock。
了解 BUILD 文件
您与 Bazel 的大部分交互都将通过 BUILD 文件(或等效的 BUILD.bazel 文件)进行,因此了解这些文件的用途非常重要。
BUILD 文件使用名为 Starlark 的脚本语言编写,该语言是 Python 的一部分。
BUILD 文件包含目标列表。目标是 Bazel 可以构建的内容,例如二进制文件、库或测试。
目标会使用属性列表调用规则函数,以描述应构建的内容。我们的示例有两个属性:name 用于在命令行上标识目标,srcs 是源文件路径的列表(以斜线分隔,相对于包含 BUILD 文件的目录)。
规则用于告知 Bazel 如何构建目标。在我们的示例中,我们使用了 go_binary 规则。每条规则都定义了用于生成一组输出文件的操作(命令)。例如,go_binary 定义了用于生成可执行输出文件的 Go 编译和链接操作。
Bazel 针对 Java 和 C++ 等几种语言提供了内置规则。您可以在构建百科全书中找到这些规则的文档。您可以在 Bazel 中央注册库 (BCR) 上找到适用于许多其他语言和工具的规则集。
添加库
进入 stage2 目录,我们将在其中构建一个用于输出运势的新程序。此程序使用单独的 Go 软件包作为库,从预定义的消息列表中选择一条吉祥话。
go-tutorial/stage2
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│ ├── BUILD
│ └── fortune.go
└── print_fortune.go
fortune.go 是库的源文件。fortune 库是一个单独的 Go 软件包,因此其源文件位于单独的目录中。Bazel 不要求您将 Go 软件包放在单独的目录中,但这是 Go 生态系统中的一个强制性惯例,遵循此惯例有助于您与其他 Go 工具保持兼容性。
package fortune
import "math/rand"
var fortunes = []string{
"Your build will complete quickly.",
"Your dependencies will be free of bugs.",
"Your tests will pass.",
}
func Get() string {
return fortunes[rand.Intn(len(fortunes))]
}
fortune 目录有自己的 BUILD 文件,用于告知 Bazel 如何构建此软件包。我们在此处使用 go_library,而不是 go_binary。
我们还需要将 importpath 属性设置为一个字符串,以便将库导入其他 Go 源文件。此名称应为与仓库目录串联的仓库路径(或模块路径)。
最后,我们需要将 visibility 属性设置为 ["//visibility:public"]。visibility 可在任何目标上设置。它用于确定哪些 Bazel 软件包可能会依赖于此目标。在本例中,我们希望任何目标都可以依赖此库,因此我们使用特殊值 //visibility:public。
load("@rules_go//go:def.bzl", "go_library")
go_library(
name = "fortune",
srcs = ["fortune.go"],
importpath = "github.com/bazelbuild/examples/go-tutorial/stage2/fortune",
visibility = ["//visibility:public"],
)
您可以使用以下工具构建此库:
$ bazel build //fortune
接下来,了解 print_fortune.go 如何使用此软件包。
package main
import (
"fmt"
"github.com/bazelbuild/examples/go-tutorial/stage2/fortune"
)
func main() {
fmt.Println(fortune.Get())
}
print_fortune.go 使用 fortune 库的 importpath 属性中声明的相同字符串导入软件包。
我们还需要向 Bazel 声明此依赖项。下面是 stage2 目录中的 BUILD 文件。
load("@rules_go//go:def.bzl", "go_binary")
go_binary(
name = "print_fortune",
srcs = ["print_fortune.go"],
deps = ["//fortune"],
)
您可以使用以下命令运行此脚本。
bazel run //:print_fortune
print_fortune 目标具有 deps 属性,即它依赖的其他目标的列表。它包含 "//fortune",这是一个标签字符串,用于引用名为 fortune 的 fortune 目录中的目标。
Bazel 要求所有目标都使用 deps 等属性明确声明其依赖项。这可能看起来很麻烦,因为依赖项也在源文件中指定,但 Bazel 的明确性使其具有优势。Bazel 会在运行任何命令之前构建一个包含所有命令、输入和输出的操作图,而无需读取任何源文件。然后,Bazel 可以缓存操作结果或发送操作以进行远程执行,而无需内置特定于语言的逻辑。
了解标签
标签是 Bazel 用来标识目标或文件的字符串。标签用于命令行参数和 BUILD 文件属性(如 deps)。我们已经看到了一些,例如 //fortune、//:print-fortune 和 @rules_go//go:def.bzl。
标签由三个部分组成:代码库名称、软件包名称和目标(或文件)名称。
代码库名称写在 @ 和 // 之间,用于引用其他 Bazel 模块中的目标(由于历史原因,模块和代码库有时可互换使用)。在标签 @rules_go//go:def.bzl 中,代码库名称为 rules_go。引用同一代码库中的目标时,可以省略代码库名称。
软件包名称写在 // 和 : 之间,用于引用其他 Bazel 软件包中的目标。在标签 @rules_go//go:def.bzl 中,软件包名称为 go。Bazel 软件包是指由其顶级目录中的 BUILD 或 BUILD.bazel 文件定义的一组文件和目标。其软件包名称是从模块根目录(包含 MODULE.bazel)到包含 BUILD 文件的目录的路径,以斜线分隔。软件包可以包含子目录,但前提是这些子目录不包含用于定义自己的软件包的 BUILD 文件。
大多数 Go 项目每个目录有一个 BUILD 文件,每个 BUILD 文件有一个 Go 软件包。引用同一目录中的目标时,可以省略标签中的软件包名称。
目标名称写在 : 后面,表示软件包中的目标。如果目标名称与软件包名称的最后一个组件相同,则可以省略该名称(因此 //a/b/c:c 与 //a/b/c 相同;//fortune:fortune 与 //fortune 相同)。
在命令行中,您可以使用 ... 作为通配符来引用软件包中的所有目标。这对于构建或测试代码库中的所有目标非常有用。
# Build everything
$ bazel build //...
测试您的项目
接下来,前往 stage3 目录,我们将在其中添加一个测试。
go-tutorial/stage3
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│ ├── BUILD
│ ├── fortune.go
│ └── fortune_test.go
└── print-fortune.go
fortune/fortune_test.go 是我们的新测试源文件。
package fortune
import (
"slices"
"testing"
)
// TestGet checks that Get returns one of the strings from fortunes.
func TestGet(t *testing.T) {
msg := Get()
if i := slices.Index(fortunes, msg); i < 0 {
t.Errorf("Get returned %q, not one the expected messages", msg)
}
}
此文件使用未导出的 fortunes 变量,因此需要编译为与 fortune.go 相同的 Go 软件包。查看 BUILD 文件,了解其运作方式:
load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "fortune",
srcs = ["fortune.go"],
importpath = "github.com/bazelbuild/examples/go-tutorial/stage3/fortune",
visibility = ["//visibility:public"],
)
go_test(
name = "fortune_test",
srcs = ["fortune_test.go"],
embed = [":fortune"],
)
我们有一个新的 fortune_test 目标,它使用 go_test 规则来编译和链接测试可执行文件。go_test 需要使用同一命令同时编译 fortune.go 和 fortune_test.go,因此我们在此处使用 embed 属性将 fortune 目标的属性纳入 fortune_test。embed 最常与 go_test 和 go_binary 搭配使用,但也适用于 go_library,这对生成的代码有时很有用。
您可能想知道 embed 属性是否与 Go 的 embed 软件包相关,该软件包用于访问复制到可执行文件中的数据文件。这是一个不幸的名称冲突:rules_go 的 embed 属性在 Go 的 embed 软件包之前引入。而是使用 embedsrcs 列出可通过 embed 软件包加载的文件。
尝试使用 bazel test 运行我们的测试:
$ bazel test //fortune:fortune_test
INFO: Analyzed target //fortune:fortune_test (0 packages loaded, 0 targets configured).
INFO: Found 1 test target...
Target //fortune:fortune_test up-to-date:
bazel-bin/fortune/fortune_test_/fortune_test
INFO: Elapsed time: 0.168s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
//fortune:fortune_test PASSED in 0.3s
Executed 0 out of 1 test: 1 test passes.
There were tests whose specified size is too big. Use the --test_verbose_timeout_warnings command line option to see which ones these are.
您可以使用 ... 通配符运行所有测试。Bazel 还会构建非测试目标,因此即使在没有测试的软件包中,也能捕获编译错误。
$ bazel test //...
结语和拓展阅读
在本教程中,我们使用 Bazel 构建并测试了一个小型 Go 项目,并在此过程中学习了一些核心 Bazel 概念。