本教程将向您介绍 Bazel 的基础知识,并向您展示如何构建 Go (Golang) 项目。您将学习如何设置工作区、构建小型程序、导入库以及运行其测试。在此过程中,您将学习 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 get 和 go mod tidy 进行依赖项管理。Bazel Go 规则集可以从 go.mod 导入依赖项,但我们将在另一篇教程中介绍这一点。
我们的 MODULE.bazel 文件包含对
rules_go(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 的脚本语言编写,Starlark 是 Python 的有限子集。
BUILD 文件包含目标列表。目标是 Bazel 可以构建的内容,例如二进制文件、库或测试。
目标使用
属性列表调用规则函数,以描述应构建的内容。我们的示例有两个属性:name 用于在命令行中标识目标,srcs 是源文件路径列表(以斜线分隔,相对于包含 BUILD 文件的目录)。
规则告知 Bazel 如何构建
目标。在我们的示例中,我们使用了
go_binary
规则。每条规则都定义了操作
(命令),这些操作生成一组输出文件。例如,go_binary 定义了生成可执行输出文件的 Go 编译和链接操作。
Bazel 具有针对 Java 和 C++ 等几种语言的内置规则。您可以在构建 百科全书中找到 相关文档。您可以在 Bazel Central Registry (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 使用在
importpath 属性中声明的同一字符串导入软件包。fortune
我们还需要向 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 软件包之前引入的。相反,rules_go
使用 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 概念。