Bazel简介:构建C ++项目
在本教程中,您将学习使用Bazel构建C ++应用程序的基础知识。您将设置工作区并构建一个简单的C ++项目,该项目说明了Bazel的关键概念,例如目标和BUILD
文件。完成本教程后,请查看 Common C ++ Build Use Cases,以获取有关更高级概念(如编写和运行C ++测试)的信息。
预计完成时间:30分钟。
您将学到什么
在本教程中,您将学习如何:
- 建立目标
- 可视化项目的依赖关系
- 将项目分为多个目标和程序包
- 控制整个程序包的目标可见性
- 通过标签参考目标
内容
- 您将学到什么
- 在你开始之前
- 用Bazel构建
- 设置工作区
- 了解BUILD文件
- 建立项目
- 查看依赖关系图
- 优化您的Bazel构建
- 指定多个构建目标
- 使用多个包
- 使用标签参考目标
- 进一步阅读
在你开始之前
要开始本教程,请先安装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
如您所见,共有三组文件,每组文件代表本教程中的一个阶段。在第一阶段,您将构建一个驻留在单个程序包中的单个目标。在第二阶段,您将把您的项目分成多个目标,但将其保存在一个程序包中。在第三阶段(也是最后一个阶段)中,您将把您的项目分成多个包,并使用多个目标进行构建。
用Bazel构建
设置工作区
在构建项目之前,您需要设置其工作区。工作区是一个目录,其中包含项目的源文件和Bazel的构建输出。它还包含Bazel认为特殊的文件:
该
WORKSPACE
文件将目录及其内容标识为Bazel工作区,并位于项目目录结构的根目录中,一个或多个
BUILD
文件,这些文件告诉Bazel如何构建项目的不同部分。(工作空间中包含BUILD
文件的目录是一个包。您将在本教程的后面部分中学习有关包的信息。)
要将目录指定为Bazel工作区,请WORKSPACE
在该目录中创建一个空文件 。
Bazel构建项目时,所有输入和依赖项必须位于同一工作空间中。除非链接,否则位于不同工作空间中的文件彼此独立,这超出了本教程的范围。
了解BUILD文件
一个BUILD
文件包含几种不同类型的用于Bazel指令。最重要的类型是构建规则(build rule),该规则(rule)告诉Bazel如何构建所需的输出,例如可执行二进制文件或库。BUILD
文件中构建规则的每个实例都称为目标(target)并指向一组特定的源文件和依赖项。一个目标也可以指向其他目标。
看一下目录BUILD
中的cpp-tutorial/stage1/main
文件:
cc_binary(
name = "hello-world",
srcs = ["hello-world.cc"],
)
在我们的示例中,hello-world
目标实例化了Bazel内置的 cc_binary
rule。该规则告诉Bazel从hello-world.cc
源文件构建一个自包含的可执行二进制文件,而没有任何依赖关系。
目标中的属性明确声明其依赖项和选项。虽然该name
属性是强制性的,但许多是可选的。例如,在 hello-world
目标中name
是不言自明的, 其srcs
属性指定Bazel从中构建目标的源文件。
建立项目
让我们构建您的示例项目。切换到cpp-tutorial/stage1
目录并运行以下命令:
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: 2.267s, Critical Path: 0.25s
恭喜,您刚刚建立了第一个Bazel目标!Bazel将构建输出放置bazel-bin
在工作空间根目录中的目录中。浏览其内容以了解Bazel的输出结构。
现在测试您新构建的二进制文件:
bazel-bin/main/hello-world
查看依赖关系图
成功的构建具有在BUILD
文件中明确声明的所有依赖项。Bazel使用这些语句来创建项目的依赖关系图,从而实现准确的增量构建。
让我们可视化示例项目的依赖关系。首先,生成依赖关系图的文本表示(在工作区根目录运行命令):
bazel query --notool_deps --noimplicit_deps 'deps(//main:hello-world)' \
--output graph
上面的命令告诉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)
如您所见,示例项目的第一阶段有一个目标,该目标构建一个没有附加依赖项的源文件:
既然您已经设置了工作区,构建了项目并检查了其依赖性,那么让我们增加一些复杂性。
优化您的Bazel构建
尽管单个目标足以满足小型项目的需要,但您可能希望将较大的项目拆分为多个目标和程序包,以实现快速增量构建(即,仅重建更改的内容)并通过同时构建项目的多个部分来加快构建速度。
指定多个构建目标
让我们将示例项目构建分为两个目标。看一下 目录BUILD
中的cpp-tutorial/stage2/main
文件:
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
二进制文件。目标中的deps
属性hello-world
告诉Bazel,该hello-greet
库是构建hello-world
二进制文件所必需的。
让我们构建这个项目的新版本。切换到 cpp-tutorial/stage2
目录并运行以下命令:
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: 2.399s, Critical Path: 0.30s
现在测试您新构建的二进制文件:
bazel-bin/main/hello-world
如果现在修改hello-greet.cc
并重建项目,Bazel将仅重新编译该文件。
查看依赖关系图,您可以看到它hello-world
依赖与以前相同的输入,但是构建的结构不同:
您现在已经用两个目标构建了该项目。的hello-world
目标建立一个源文件,并依赖于目标(//main:hello-greet
),它建立两个附加的源文件。
使用多个包
现在让我们将项目分成多个包。看一下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
在目标main
包依赖于 hello-time
目标lib
包(因此目标标签 //lib:hello-time
) -Bazel通过知道这个deps
属性。看一下依赖图:
注意,为使构建成功,我们使用 属性使//lib:hello-time
目标对于目标 lib/BUILD
明确可见。这是因为默认情况下,目标仅对同一文件中的其他目标可见。(Bazel使用目标可见性来防止诸如包含实现详细信息的库之类的问题泄漏到公共API中。)main/BUILD``visibility``BUILD
让我们构建项目的最终版本。切换到 cpp-tutorial/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
现在,您已经将项目构建为带有三个目标的两个程序包,并了解了它们之间的依赖性。
使用标签参考目标
在BUILD
文件中和命令行中,Bazel使用标签来引用目标(例如//main:hello-world
或)//lib:hello-time
。它们的语法是:
//path/to/package:target-name
如果目标是规则目标,则path/to/package
是包含BUILD
文件的目录的路径,并且target-name
是您在BUILD
文件中命名目标(name
属性)的名称。如果目标是文件目标,则path/to/package
是包根目录的路径,并且 target-name
是目标文件的名称,包括其完整路径。
在存储库根目录引用目标时,包路径为空,只需使用即可//:target-name
。在同一BUILD
文件中引用目标时,您甚至可以跳过//
工作空间的根标识符,而只需使用 :target-name
。
进一步阅读
恭喜你!您现在知道了使用Bazel构建C ++项目的基础知识。接下来,阅读最常见的C ++构建用例。然后,检查以下内容:
外部依赖关系,以了解有关使用本地和远程存储库的更多信息。
在其他规则以了解更多有关巴泽尔。
在Java构建教程上手与Bazel构建Java应用程序。
该Android应用教程上手构建移动应用程序为Android与Bazel。
在iOS版应用教程上手构建移动应用程序适用于iOS与Bazel。
构建愉快!