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-worldBazel 会生成如下内容: 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博客



















