参考官方示例项目:git clone https://github.com/bazelbuild/examples
项目结构
使用Bazel管理的项目一般包含以下几种Bazel相关的文件:WORKSPACE(同WORKSPACE.bazel),BUILD(同BUILD.bazel),.bzl 和 .bazelrc 等。
具体结构如下:
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
例子分三个 stage,由简单到复杂介绍了一些 Bazel 构建的基本概念,我这里就直接根据第三个 stage3 例子来总结一下:
WORKSPACE文件--项目(工作区)
一个 workspace 可以认为就是一个 project。譬如上面 cpp-tutorial 目录下分别由 stage1、stage2 和 stage3 三个项目,每个项目的根目录下有一个 WORKSPACE 文件,空的就行,Bazel 就会将包含一个 WORKSPACE 文件的目录识别为一个项目。每个项目之间互不干扰是完全独立的。一个 workspace 里可以包含多个 packages。
WORKSPACE文件也可能定义加载 Bazel 工具和 rules 集,以及包含对构建输出所需的外部依赖项的引用。
BUILD文件--软件包
软件包可以认为是一个软件模块,譬如一个库等,指包含名为BUILD或BUILD.bazel的ef="https://bazel.build/concepts/build-files?hl=zh-cn">BUILD文件的目录。代码库中的主要代码组织单元是软件包。软件包是相关文件的集合,以及如何使用这些文件生成输出工件的规范。软件包中包含其目录中的所有文件及其下的所有子目录,但包含BUILD文件的文件除外。根据该定义,任何文件或目录都不得是两个不同软件包的一部分。
BUILD 文件采用 Starlark 语言对模块构建进行描述,包含 Bazel 的几种不同类型的指令。每个 BUILD 文件都需要至少一条规则作为一组指令,告诉 Bazel 如何构建所需的输出,例如可执行文件或库。BUILD 文件中的 build 规则的每个实例都称为一个目标target,并指向一组特定的源文件和依赖项。 目标还可以指向其他目标。从逻辑上来说即每个 package 可以包含多个 Targets,而具体的 target 则采用 Starlark 语法定义在一个 BUILD 文件中。
查看 stage3/lib/BUILD 文件:
cc_library(
name = "hello-time",
srcs = ["hello-time.cc"],
hdrs = ["hello-time.h"],
visibility = ["//main:__pkg__"],
)
在 stage3/main/BUILD 文件:
cc_library(
name = "hello-greet",
srcs = ["hello-greet.cc"],
hdrs = ["hello-greet.h"],
)
cc_binary(
name = "hello-world",
srcs = ["hello-world.cc"],
deps = [
":hello-greet",
"//lib:hello-time",
],
)
在此示例中,hello-world 目标会实例化 Bazel 的内置 cc_binary rule。该规则指示 Bazel 从hello-world.cc 源文件和两个依赖项hello-greet和//lib:hello-time构建独立的可执行文件。
为使构建成功,您可以使用可见性属性让 lib/BUILD 中的 //lib:hello-time 目标明确显示给 main/BUILD 中的目标。这是因为默认情况下,目标仅对同一 BUILD 文件中的其他目标可见。Bazel 使用目标可见性来防止出现包含有实现细节的库泄露到公共 API 等问题。
target定义--目标
软件包是目标软件包的容器,在软件包的BUILD文件中定义。大多数目标是两种主要类型之一:文件和规则。如示例中的hello-world和hello-greet等。
文件进一步分为两种。源文件通常由用户编写并签入代码库。生成的文件(有时称为派生文件或输出文件)不会被签入,但是从源文件生成的。
第二种目标使用规则声明。每个规则实例都用于指定一组输入文件与一组输出文件之间的关系。规则的输入可以是源文件,但也可以是其他规则的输出。
target是某个 rule 的一个实例。那么 Rule 又是什么?示例中cc_library
就是一个 rule,它规定了 一类构建规则。从 cc_library
这个规则名称上我们很容易猜测出来这 一类规则 描述了如何构建一个采用 C/C++ 编程语言编写的库(library,可以是静态库也可能是动态库)。一个 Rule 由很多 attribute 构成,这点采用面向对象的概念来看,Rule 就好比是 class,而 attribute 就好比是 class 的 member。定义 target 就是实例化了这个 rule,这样大家就理解了上面这段代码实际上就是定义了一个 target,每个实例必须要有一个名字在同一个 package 中和其他 target 实例进行区分。所以 name 这个 attribute 是必须有的,其他 attribute 是可选的,不写则按默认值定义。
标签 --label
所谓 label 可以认为是在一个 Bazel 的 workspace 范围中唯一标识一个 target 的 ID。我们可以用这个 label 来引用一个 target。label 的语法如下:
//path/to/package:target-name
以 //
开始,接下来的 path/to/package
也就是这个 target 所在 package 在 workspace 中的相对路径。然后是一个 :
后面跟着一个 target-name
即上面说的一个 target 中的 name 那个属性的字符串值。
譬如我们要构建 examples/cpp-tutorial/stage3
这个 workspace 下的 main 这个 package 中的 "hello-greet" 这个 target。那么我们要做的就是先 cd 到 stage3 这个 workspace 下然后用 label 引用这个 target 执行构建。具体命令如下:
$ cd examples/cpp-tutorial/stage3 $ bazel build //main:hello-greet
就会生成 libhello-greet.a
和 libhello-greet.so
依赖--dependency
各个 target 之间存在依赖关系,这是在所有构建系统中都存在的概念,同样在 Bazel 中也缺少不了。譬如在 stage3 这个例子中,target //main:hello-world
就依赖于 target //main:hello-greet
,背后的含义就是我们要构建最终的可执行程序 hello-world,则首先要构建成功 hello-greet
这个规则的 obj 文件,这种依赖关系在 BUILD 文件中体现为 deps
这个 attribute 的描述。
注意以下两点:
":hello-greet"
这里也是一个 label 描述,由于 hello-greet 和 hello-world 这两个 target 在一个 package 中,所以前面的path/to/package
可以省略。- 这里如果直接执行
bazel build //main:hello-world
并不会生成libhello-greet.a
和libhello-greet.so
,原因是目前例子中上面的描述并没有给出构建 hello-world 需要依赖 so 或者 .a。所以默认只是依赖于 hello-greet 的 obj 文件。
.bzl文件
如果你的项目有一些复杂构造逻辑、或者一些需要复用的构造逻辑,那么可以将这些逻辑以函数形式保存在.bzl文件,供WORKSPACE或者BUILD文件调用。其语法跟Python类似。
.bazelrc文件
对于Bazel来说,如果某些构建动作都需要某个参数,就可以将其写在此配置中,从而省去每次敲命令都重复输入该参数。Bazel 会按以下顺序读取可选的 bazelrc 文件:
-
系统级文件,位于 etc/bazel.bazelrc。
-
位于 $workspace/tools/bazel.rc 的 Workspace rc 文件。
-
主目录文件位于 $HOME/.bazelrc 中
此处列出的每个 bazelrc 文件都有一个对应的标志,可用于停用这些标志(例如 --nosystem_rc、--noworkspace_rc 和 --nohome_rc)。您还可以通过传递 --ignore_all_rc_files 启动选项让 Bazel 忽略所有 Bazelrcs。
如何工作
当运行构建或者测试时,Bazel会:
-
加载和目标相关的BUILD文件
-
分析输入及其依赖,应用指定的构建规则,产生一个Action图。这个图表示需要构建的目标、目标之间的关系,以及为了构建目标需要执行的动作。Bazel依据此图来跟踪文件变动,并确定哪些目标需要重新构建
-
针对输入执行构建动作,直到最终的构建输出产生出来
构建
运行以下命令以切换到 stage3 目录:
cd stage3
运行以下命令:
bazel build //main:hello-world
在目标标签中,//main:部分是BUILD文件相对于工作区根目录的位置,hello-world是BUILD文件中的目标名称。
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 clean
运行
执行二进制文件以获得最终的 Hello world 消息:
bazel-bin/main/hello-world
依赖图
-
生成依赖图:
# 查找target为 //main:hello-world 的所有依赖
# --nohost_deps 表示不包括host依赖
# --noimplicit_deps 表示不包括隐式依赖 e.g: @bazel_tools//tools/cpp:stl
bazel query --nohost_deps --noimplicit_deps 'deps(//main:hello-world)' --output graph
-
将生成的输出图文字描述, 粘贴到 GraphViz, 生成的依赖图如下
本文属于如下文章中的子章节
bazel学习系列章节汇总_m0_74043383的博客-CSDN博客