Bazel官方教程 – 构建C++工程基础知识
需求:因项目需要,将Cmake编译方式改为Bazel。
官方参考:https://docs.bazel.build/versions/main/tutorial/cpp.html
Bazel教程:构建C++工程
本教程涵盖了使用 Bazel 构建 C++ 应用程序的基础知识。 您将设置您的工作区并构建一个简单的 C++ 项目,该项目说明了关键的 Bazel 概念,例如 目标 和 BUILD 文件。 完成本教程后,请查看 Common C++ Build Use Cases 以获取有关更高级内容的信息,例如编写和运行 C++ 测试。
预计完成时间:30 分钟。
0. 你将学习到的内容
在本教程中,你将学会怎样:
- 构建一个目标
- 可视化工程依赖
- 将项目分成多个目标和包
- 通过包控制目标可视化
- 通过标签引用目标
1. 开始之前
为了教程做准备,如果您还没有安装Bazel,请先安装它。 然后,从 Bazel 的 GitHub 存储库中检索示例项目:
git clone https://github.com/bazelbuild/examples
本教程的示例项目位于 examples/cpp-tutorial 目录中,结构如下:
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
如您所见,共有三组文件,每组代表本教程中的一个阶段。 在第一阶段,您将构建单个包中的单个目标。 在第二阶段,您将项目拆分为多个目标,但将其保存在一个包中。 在第三个也是最后一个阶段,您将项目拆分为多个包并使用多个目标构建它。
2. 使用Bazel进行构建
2.1 建立工作空间
在构建项目之前,您需要设置其工作空间。 工作空间是一个包含项目源文件和 Bazel 构建后文件输出的目录。 它还包含 Bazel 认为特殊的文件:
- WORKSPACE 文件,它将目录及其中的内容标记为 Bazel 工作区,并位于项目目录结构的根目录中,
- 可以使用一个或多个 BUILD 文件,它们告诉 Bazel 如何构建项目的不同部分。 (工作空间中包含 BUILD 文件的目录称为一个包(package)。您将在本教程的后面部分了解包相关知识)
2.2 理解Build文件
一个 BUILD 文件包含几种不同类型的 Bazel 指令。 最重要的类型是构建规则,它告诉 Bazel 如何构建所需的输出,例如可执行的二进制文件或库。 BUILD 文件中构建规则的每个实例称为目标。一个目标可以指向一组特定的源文件和依赖项, 也可以指向其他目标。
看一下cpp-tutorial/stage1/main目录下的BUILD文件:
cc_binary(
name = "hello-world",
srcs = ["hello-world.cc"],
)
在我们的示例中,hello-world 目标实例化了 Bazel 的内置 cc_binary 规则。 该规则告诉 Bazel 从 hello-world.cc 源文件构建一个独立的,没有依赖关系的可执行二进制文件。
目标中的属性明确声明其依赖项和选项。 虽然 name 属性是强制性的,但许多是可选的。 例如,在 hello-world 目标中,name 是必需的且不言自明,而 srcs 是可选的,意味着指定 Bazel 从中构建目标的源文件。
2.3 构建工程
为了构建您的示例项目,请跳转到 cpp-tutorial/stage1 目录并运行:
cd examples/cpp-tutorial/stage1/
bazel build //main:hello-world
在目标标签中,//main: 是 BUILD 文件在工作空间根目录的位置,hello-world 是BUILD 文件中的目标名称(name)。 (您将在本教程末尾更详细地了解目标标签。)
Bazel 产生类似于以下内容的输出:
Starting local Bazel server and connecting to it...
INFO: Analyzed target //main:hello-world (37 packages loaded, 161 targets configured).
INFO: Found 1 target...
Target //main:hello-world up-to-date:
bazel-bin/main/hello-world
INFO: Elapsed time: 7.820s, Critical Path: 0.59s
INFO: 6 processes: 4 internal, 2 linux-sandbox.
INFO: Build completed successfully, 6 total actions
恭喜,您刚刚构建了您的第一个 Bazel 目标! Bazel 将构建的输出文件放在工作空间根目录的 bazel-bin 目录中。 浏览其内容以了解 Bazel 的输出结构。
现在测试您新构建的二进制文件:
bazel-bin/main/hello-world
2.4 查看依赖关系图
一次成功的构建所包含的依赖项都会在 BUILD 文件中明确说明。 Bazel 使用这些语句来创建项目的依赖关系图,从而实现准确的增量构建。
要可视化示例项目的依赖关系,您可以通过在工作区根目录运行如下命令,来生成依赖关系图的文本表示:
bazel query --notool_deps --noimplicit_deps "deps(//main:hello-world)" --output graph
结果如下:
digraph mygraph {
node [shape=box];
"//main:hello-world"
"//main:hello-world" -> "//main:hello-world.cc"
"//main:hello-world.cc"
}
上面的命令告诉 Bazel 查找目标 //main:hello-world 的所有依赖项(不包括主机和隐式依赖项)并将输出格式化为图形。
然后,将文本粘贴到 GraphViz 中。
在 Ubuntu 上,您可以通过安装 GraphViz 和 xdot Dot Viewer 在本地查看图形:
sudo apt update && sudo apt install graphviz xdot
然后,您可以通过将上面的文本输出直接传送到 xdot 来生成和查看图形:
xdot <(bazel query --notool_deps --noimplicit_deps "deps(//main:hello-world)" --output graph)
如您所见,示例项目的第一阶段,只有一个单一的目标,它构建一个没有额外依赖项的单一源文件:
在设置好工作区、构建项目并检查其依赖项之后,您可以添加一些复杂的东西。
3. 完善你的Bazel构建项目
虽然单个目标对于小型项目就足够了,但您可能希望将较大的项目拆分为多个目标和包,以允许快速增量构建(即仅重建已更改的内容)并通过构建项目的多个部分来加速构建。
3.1 指定多个构建目标
您可以将示例项目构建拆分为两个目标,看一下cpp-tutorial/stage2/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",
],
)
使用这个 BUILD 文件,Bazel 首先构建 hello-greet 库(使用 Bazel 的内置 cc_library 规则),然后是 hello-world 二进制文件。 hello-world 目标中的 deps 属性告诉 Bazel,构建 hello-world 二进制文件需要 hello-greet 库。
接下来,构建这个新版本的项目。 切换到 cpp-tutorial/stage2 目录并运行以下命令:
cd ../stage2
bazel build //main:hello-world
Bazel 产生类似于以下内容的输出:
Starting local Bazel server and connecting to it...
INFO: Analyzed target //main:hello-world (37 packages loaded, 164 targets configured).
INFO: Found 1 target...
Target //main:hello-world up-to-date:
bazel-bin/main/hello-world
INFO: Elapsed time: 8.504s, Critical Path: 0.54s
INFO: 7 processes: 4 internal, 3 linux-sandbox.
INFO: Build completed successfully, 7 total actions
现在你可以再次测试你构建的二进制文件:
bazel-bin/main/hello-world
如果您现在修改 hello-greet.cc 并重新构建项目,Bazel 只会重新编译该文件。
查看依赖图,您可以看到 hello-world 依赖于与之前相同的输入,但构建的结构不同:
您现在已经使用两个目标构建了项目。 hello-world 目标构建一个源文件并依赖于另一个目标 (//main:hello-greet),该库是由两个额外的源文件构建的。
4. 使用多个包
您可以将项目拆分为多个包,看一下cpp-tutorial/stage3目录的内容:
└──stage3
├── main
│ ├── BUILD
│ ├── hello-world.cc
│ ├── hello-greet.cc
│ └── hello-greet.h
├── lib
│ ├── BUILD
│ ├── hello-time.cc
│ └── hello-time.h
└── WORKSPACE
请注意,现在有两个子目录,每个子目录都包含一个 BUILD 文件。 因此,对于 Bazel,工作区现在包含两个包: lib 和 main。
看一下 lib/BUILD 文件:
cc_library(
name = "hello-time",
srcs = ["hello-time.cc"],
hdrs = ["hello-time.h"],
visibility = ["//main:__pkg__"],
)
再看一下 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 目标依赖于 lib 包中的 hello-time 目标(因此目标标签为 //lib:hello-time) - Bazel 通过 deps 属性知道这一点。 看一下依赖图:
请注意,为使构建成功,您使用 visibility 属性使 lib/BUILD 中的 //lib:hello-time 目标对 main/BUILD 中的目标显式可见。 这是因为默认情况下,目标仅对同一 BUILD 文件中的其他目标可见。 (Bazel 使用目标可见性来防止诸如包含实现细节的库泄漏到公共 API 中的问题。)
接下来,您可以构建该项目的最终版本。 切换到 cpp-tutorial/stage3 目录并运行以下命令:
cd ../stage3/
bazel build //main:hello-world
Bazel 产生类似于以下内容的输出:
Starting local Bazel server and connecting to it...
INFO: Analyzed target //main:hello-world (38 packages loaded, 167 targets configured).
INFO: Found 1 target...
Target //main:hello-world up-to-date:
bazel-bin/main/hello-world
INFO: Elapsed time: 2.594s, Critical Path: 0.27s
INFO: 8 processes: 4 internal, 4 linux-sandbox.
INFO: Build completed successfully, 8 total actions
现在你可以再次测试你构建的二进制文件:
bazel-bin/main/hello-world
您现在已经将项目构建为具有三个目标的两个包,并了解它们之间的依赖关系。
5. 使用标签引用目标
在 BUILD 文件和命令行中,Bazel 使用标签来引用目标 - 例如,//main:hello-world 或 //lib:hello-time。 它们的语法是:
//path/to/package:target-name
如果目标是规则目标,那么 path/to/package 是++从工作空间根目录(包含 WORKSPACE 文件的目录)到包含 BUILD 文件的目录的路径++,target-name 是您在 BUILD 中命名的目标文件(++名称属性++)。 如果目标是文件目标,那么 path/to/package 是包所在++根目录的路径++,target-name 是目标文件的名称,需要包括它相对于根目录的完整路径( BUILD 文件所在目录)。
在存储库根目录引用目标时,包路径为空,只需使用 //:target-name。 在同一个 BUILD 文件中引用目标时,您甚至可以跳过 // ++工作区根标识符++,而只需使用 :target-name。
6. 深入阅读
恭喜! 您现在了解了使用 Bazel 构建 C++ 项目的基础知识。 接下来,阅读最常见的 C++ 构建用例。
有关更多详细信息,请参阅:
- 外部依赖项以了解有关使用本地和远程存储库的更多信息。
- 了解更多关于 Bazel 的其他规则。
- Java 构建教程开始使用 Bazel 构建 Java 应用程序。
- 开始使用 Bazel 为 Android 构建移动应用程序的 Android 应用程序教程。
- iOS 应用程序教程开始使用 Bazel 为 iOS 构建移动应用程序。
构建愉快!