Bazel官方教程 -- 构建C++工程基础知识

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)

如您所见,示例项目的第一阶段,只有一个单一的目标,它构建一个没有额外依赖项的单一源文件:
Bazel官方教程 -- 构建C++工程基础知识_第1张图片
在设置好工作区、构建项目并检查其依赖项之后,您可以添加一些复杂的东西。

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 依赖于与之前相同的输入,但构建的结构不同:
Bazel官方教程 -- 构建C++工程基础知识_第2张图片
您现在已经使用两个目标构建了项目。 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 属性知道这一点。 看一下依赖图:
Bazel官方教程 -- 构建C++工程基础知识_第3张图片
请注意,为使构建成功,您使用 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 构建移动应用程序。

构建愉快!

你可能感兴趣的:(编译,c++,开发语言,visual,studio,linux)