本教程将向您介绍 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 声明此依赖项。以下是 BUILD 文件在
stage2 目录中。
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 概念。