Bazel 快速入门与核心知识
Bazel 简介
Bazel 是一款与 Make、Maven 和 Gradle 类似的开源构建和测试工具。 它使用人类可读的高级构建语言。Bazel 支持多种语言的项目 (C/C++, Java, Python, …),可为多个平台构建输出。Bazel 支持跨多个代码库和大量用户的大型代码库。
本文是作者结合 Bazel 官方文档以及一些其他博客总结的学习笔记,凝炼了个人认为最核心的一些 Bazel 知识。通过此文,希望能让大家不仅看懂并编译一个通过 Bazel 构建的项目,同时还能够使用 Bazel 对自己的项目完成构建。
使用 Bazel 的基本流程
如需使用 Bazel 构建或测试项目,您通常要执行以下操作:
-
设置 Bazel。下载并安装 Bazel。
-
设置项目工作区,这是 Bazel 在其中查找 build 输入和
BUILD
文件以及用于存储 build 输出的目录。 -
编写
BUILD
文件,告知 Bazel 要构建什么以及如何构建它。如需编写
BUILD
文件,您可以使用领域特定语言 Starlark 声明构建目标。(请查看此处的示例。)构建目标指定了一组 Bazel 将要构建的输入工件及其依赖项,Bazel 将用于构建它的构建规则,以及用于配置构建规则的选项。
build 规则用于指定 Bazel 将使用的构建工具,例如编译器和链接器。Bazel 附带多条构建规则,这些规则涵盖受支持平台上以支持的语言显示的最常见工件类型。
-
通过命令行运行 Bazel。Bazel 会将您的输出内容放在工作区中。
Bazel 构建流程
运行构建或测试时,Bazel 会执行以下操作:
- 加载与目标相关的
BUILD
文件。 - 分析输入及其依赖项,应用指定的构建规则,并生成操作图表。
- 对输入执行构建操作,直到生成最终构建输出。
由于之前的所有构建工作都已缓存,因此 Bazel 可以识别并重复使用缓存的 artifacts,并且只会重新构建或重新测试发生更改的内容。为了进一步强制执行正确性,您可以设置 Bazel,以通过沙盒化的方式运行构建和测试,从而最大限度地减少偏差并最大限度地提高可重现性。
Bazel C++ demo
目录组织结构如下。下面尝试用 bazel 构建 stage3/main
中以 hello-world.cc
为入口的 hello-world 程序,该程序依赖于同路径下的 hello-greet 以及 stage3/lib
路径下的 hello-time
。
examples
└── cpp-tutorial
├──stage1
│ ├── main
│ │ ├── BUILD
│ │ └── hello-world.cc
│ └── WORKSPACE
├──stage2
│ ├── main
│ │ ├── BUILD
│ │ ├── hello-world.cc
│ │ ├── hello-greet.cc
│ │ └── hello-greet.h
│ └── WORKSPACE
└──stage3
├── main
│ ├── BUILD
│ ├── hello-world.cc
│ ├── hello-greet.cc
│ └── hello-greet.h
├── lib
│ ├── BUILD
│ ├── hello-time.cc
│ └── hello-time.h
└── WORKSPACE
-
在
stage3
目录下创建了名为WORKSPACE
的空文件,标记了这是一个 bazel 的工作区 -
在
stage3/main
和stage3/lib
目录下创建名为 BUILD 的文件,用于指示 bazel 构建工作,一个拥有 BUILD 文件的目录就是一个包 (软件包)# lib/BUILD 文件 # 定义了名为"hello-time"的一个目标(target),这个目标是cc_library规则(rule)的一个实例,cc_library规则定义的是构建C/C++库(library)的规则 cc_library( name = "hello-time", # 构建此库目标所需的C/C++文件列表,路径相对于BUILD文件所处目录 srcs = ["hello-time.cc"], # 描述此库目标的C/C++头文件列表,路径相对于BUILD文件所处目录 hdrs = ["hello-time.h"], # 使用可见性属性让 lib/BUILD 中的 //lib:hello-time 目标对 main/BUILD 中的目标显式可见。这是因为,默认情况下,只有同一 BUILD 文件中的其他目标才会看到这些目标。 visibility = ["//main:__pkg__"], )
# main/BUILD 文件 # 定义了名为"hello-greet"的一个目标(target),这个目标是cc_library规则(rule)的一个实例,cc_library规则定义的是构建C/C++库(library)的规则 cc_library( name = "hello-greet", # 构建此库目标所需的C/C++文件列表,路径相对于BUILD文件所处目录 srcs = ["hello-greet.cc"], # 描述此库目标的C/C++头文件列表,路径相对于BUILD文件所处目录 hdrs = ["hello-greet.h"], ) # 定义了名为"hello-world"的一个目标(target),这个目标是cc_binary规则(rule)的一个实例,cc_binary规则定义的是构建C/C++二进制程序(binary)的规则 cc_binary( name = "hello-world", # 构建此二进制目标所需的C/C++文件列表,路径相对于BUILD文件所处目录 srcs = ["hello-world.cc"], # 要链接到二进制目标的其他库的列表 deps = [ ":hello-greet", # 同一包下可省略包路径和// "//lib:hello-time", # 不同包下必须严格按照 //包路径:目标名 的标签写法 ], )
目标间的依赖关系如下:
-
执行构建:在
stage3
目录下,执行:bazel build //main:hello-world
Bazel 会生成如下内容:
INFO: Found 1 target... Target //main:hello-world up-to-date: bazel-bin/main/hello-world INFO: Elapsed time: 0.167s, Critical Path: 0.00s
现在已经构建完成了,继续执行
bazel-bin/main/hello-world
即可运行 hello-world 程序
Bazel 核心知识
工作区 (workspace)
- 一个 Workspace 就可以认为就是一个独立的 C/C++ Project。譬如上面
cpp-tutorial
目录下分别由stage1
、stage2
和stage3
三个项目,每个项目的根目录下有一个WORKSPACE
文件(空的就行)。 - Bazel 会将包含一个
WORKSPACE
或WORKSPACE.bazel
文件的目录识别为一个项目,每个项目之间互不干扰是完全独立的。- 可以同时包含
WORKSPACE
和WORKSPACE.bazel
,此时 .bazel 那个优先级更高。
- 可以同时包含
- 一个 Workspace 里可以包含多个 Packages (包),每个 Package 中包含一组相关的源文件和一个
BUILD
文件。BUILD
文件指定可以从源代码构建哪些输出。例如,stage3
下就包含了两个 Package:main
和lib
。 - 工作区有时也叫代码库。
BUILD
& 包
-
软件包 (包) 指的是包含名为
BUILD
或BUILD.bazel
的 BUILD 文件的目录。- 可以同时包含
BUILD
和BUILD.bazel
,此时 .bazel 那个优先级更高。
- 可以同时包含
-
软件包包含其目录中的所有文件,以及其下的所有子目录,但那些本身包含 BUILD 文件的子目录除外。根据此定义,任何文件或目录都不能包含在两个不同的软件包中。
例如,以下目录树中有两个软件包:
my/app
和子软件包my/app/tests
。请注意,my/app/data
不是软件包,而是属于软件包my/app
的目录。src/ └─ my └─ app ├─ BUILD ├─ app.cc ├─ data │ └─ input.txt └─ tests ├─ BUILD └─ test.cc
-
BUILD 文件采用 Starlark 语言对模块构建进行描述,语法类似于 Python
- 每个 BUILD 文件都需要至少一条规则 (rule) 作为一组指令,告诉 Bazel 如何构建所需的输出,例如可执行文件或库。
- BUILD 文件中定义的规则 (rule) 的实例都称为一个目标 (target),并指向一组特定的源文件和依赖项。 目标还可以指向其他目标。从逻辑上来说即每个 package 可以包含多个 targets,而具体的 target 则采用 Starlark 语法定义在一个 BUILD 文件中。
BUILD
文件核心语法
规则 (rule)
-
规则用于在
BUILD
文件(例如cc_library
)中定义如何生成一个目标 (target)。从BUILD
文件作者的角度来看,规则由一组属性和黑盒逻辑组成。 -
在简单的
BUILD
文件中,规则声明可以随意重新排序,而不改变行为。 -
bazel 定义了很多原生规则,可以直接在
BUILD
文件中使用,而无需load
语句引入-
可以在
.bzl
文件中自定义规则,并在 BUILD 中用load
语句引入。 -
原生规则可以在
.bzl
文件中需要使用native
模块来引用(如native.cc_binary
),但在 BUILD 文件中原生规则可以直接使用。 -
详细的各项原生规则及其API见文档:Bazel 构建函数百科全书 (google.cn)。
-
我们常用的原生规则包括
cc_binary
和cc_library
等,分别用来构建二进制可执行程序和库(静态库/动态库)。# 例子:在BUILD中使用bazel内置的原生规则: cc_binary cc_binary( name = "hello-world", srcs = ["hello-world.cc"], deps = [ ":hello-greet", "//lib:hello-time", ], )
-
-
大多数规则都具有类似的命名方案。例如,
cc_binary
、cc_library
和cc_test
分别是 C++ 二进制文件、库和测试的构建规则。其他语言会采用相同的命名方案,但采用不同的前缀,例如适用于 Java 的java_*
。*_binary
规则可用于构建给定语言的可执行程序。*_test
规则是*_binary
规则的专用规则,用于自动测试。测试只是在成功时返回零的程序。*_library
规则以指定给定的编程语言指定单独编译的模块。库可以依赖于其他库,二进制文件和测试可以依赖于库,并且具有预期的单独编译行为。
-
一个规则一般具有很多属性(见后面的小节)。
目标 (target)
-
规则和目标是定义和实现的关系。也就是说,目标是规则的一个实例。
-
一个 Rule 由很多 attribute 构成,这点采用面向对象的概念来看,Rule 就好比是 class,而 attribute 就好比是 class 的 member。
-
下面这段代码实际上就是定义了一个 target,每个实例必须要有一个名字在同一个 package 中和其他 target 实例进行区分。所以 name 这个 attribute 是必须有的,其他 attribute 是可选的,不写则按默认值定义。
# 例子:定义一个name为"hello-world"的target,它是cc_binary规则的一个实例 cc_binary( name = "hello-world", srcs = ["hello-world.cc"], deps = [ ":hello-greet", "//lib:hello-time", ], )
-
-
可以使用标签来唯一标识一个目标(详下节)。
标签 (label)
-
标签是目标的标识符。简单来说,标签就是唯一标识一个 target 的 ID。
-
大部分情况下,我们引用的都是同一个 workspace 中的 target,此时标签的语法如下:
//path/to/package:target-name
-
以
//
开始,接下来的path/to/package
也就是这个 target 所在 package 在 workspace 中的相对路径。然后是一个:
后面跟着一个target-name
即上面说的一个 target 中的 name 那个属性的字符串值。 -
如果要引用不同 workspace 中的 target,就必须使用标签的完整语法,见:标签 | Bazel (google.cn)
-
如果是引用同一个包中的 target,那么标签语法可进一步简化,以下两种方式均可:
//:target-name :target-name
-
-
特别地,可以使用
//path/to/package:__pkg__
来表示一个包下地所有 target。
属性 (attribute) 及依赖
-
属性是规则的参数,用于表示每个目标的 build 信息。如果在
BUILD
中实例化规则时没有显式指定某个属性的值,则该属性会使用默认值。 -
大多数规则常见的属性包括
names
(必需)、srcs
、deps
、data
、visibility
、includes
和copts
等,它们分别声明目标的源文件、依赖项和自定义编译器选项。给定目标可用的特定属性取决于其规则类型。 -
原生规则的具体属性需要参见文档:Bazel 构建函数百科全书 (google.cn)。以
cc_library
规则为例,说明它的一些常用属性:names
: 目标的唯一名称。deps
: 此库所依赖的其他库的列表(可以通过标签来引用)。srcs
: 为创建库目标而处理的 C 和 C++ 文件的列表,包括源文件和头文件。data
: 此库在运行时所需的文件列表。hdrs
: 伴随此库发布的头文件,并且可以被其他依赖这个库的目标(如其他cc_library
或cc_binary
)使用,Bazel 在构建过程中会确保这些头文件能够被正确找到和使用。cc_binary
等规则是没有此属性的。visibility
: 指定此库在其他库中的可见性(可以通过标签来引用)。默认情况下,一个目标只对相同库中的其他目标显式可见。includes
: 要添加到编译行的 include 目录列表。copts
: 将这些选项添加到 C++ 编译命令中。比如这里可以写-Imy_libpath
来将 my_libpath 加入编译时的头文件搜索路径,可以写-pthread
来表明使用了多线程库。includes
和copts
中设置-I
都可以指定头文件位置。但是,前者会为该规则即依赖该规则的所有规则都设置头文件位置,而后者只会为该规则设置头文件位置(因为本身只是一次编译命令的选项)。
注意到这里并没有指定生成
.a
静态库还是.so
动态库,实际上静态库还是动态库由引用此库的cc_binary
规则决定,具体来说,cc_binary
可以指定linkshared
或linkstatic
为 True 还是 False 来决定链接时使用动态库还是静态库。默认情况下,linkshared
是 False,linkstatic
是 True。此外需要说明的是,
srcs
、data
、hdrs
、includes
等属性中设置的地址都是相对于当前包路径而言的,即相对于当前BUILD
文件所处的目录。以下给出一个示例:最终目标是构建可执行程序 foo,它依赖于源文件
foo.cc
和头文件foo.h
,同时还依赖于库bar
。库bar
则依赖于源文件bar.cc
和头文件bar-impl.h
,它同时还依赖于另一个库baz
,库bar
中的接口由bar.h
这个头文件所定义。另一个库baz
依赖于源文件baz.cc
和baz-impl.h
,库baz
中的接口由baz.h
这个头文件所定义。cc_binary( name = "foo", srcs = [ "foo.cc", "foo.h", ], deps = [":bar"], ) cc_library( name = "bar", srcs = [ "bar.cc", "bar-impl.h", ], hdrs = ["bar.h"], deps = [":baz"], ) cc_library( name = "baz", srcs = [ "baz.cc", "baz-impl.h", ], hdrs = ["baz.h"], )
-
在 srcs, deps 等依赖属性中,可以使用 bazel 提供的
glob
函数来查找与特定路径模式匹配的所有文件,详细语法见:glob
构建命令
详细的各项参数见:使用 Bazel 构建程序 (google.cn)
使用 bazel build
来完成对目标的构建:
# 以 // 开头的所有目标模式都是相对于当前工作区而言的。
bazel build //path/to/package:target-name
# 以 // 开头的目标模式会根据当前的工作目录进行解析。
bazel build path/to/package:target-name
案例:
# 构建workspace下的foo/bar包中的wiz目标
bazel build //foo/bar:wiz
# 构建workspace下的foo/bar包中的bar目标,等同于 //foo/bar:bar
bazel build //foo/bar
# 构建workspace下的foo/bar包中的全部目标
bazel build //foo/bar:all
# 构建当前目录下定义的foo目标
bazel build :foo
# 构建当前目录下的bar子目录下定义的foo目标
bazel build bar:wiz
参考文献
- Google Bazel 官方教程
- bazel工程介绍和demo构建_bazel构建方式图-CSDN博客