Bazel 教程:构建 C++ 项目

报告问题 查看源代码

简介

刚开始接触 Bazel?您来对地方了。遵循此第一构建教程,简要了解如何使用 Bazel。本教程定义了 Bazel 上下文中使用的关键术语,并详细介绍了 Bazel 工作流的基础知识。从您需要的工具开始,您将构建和运行三个越来越复杂的项目,了解它们如何以及为什么变得更加复杂。

虽然 Bazel 是一个支持多语言构建的构建系统,但本教程以 C++ 项目为例进行说明,提供了适用于大多数语言的一般准则和流程。

预计所需时长:30 分钟。

前提条件

请先安装 Bazel(如果尚未安装)。本教程使用 Git 进行源代码控制,因此为获得最佳效果,请安装 Git

接下来,在您选择的命令行工具中运行以下命令,从 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(和 Git)并克隆本教程的代码库,您已经为使用 Bazel 构建首次构建奠定了基础。继续阅读下一部分以定义一些术语并设置您的工作区

使用入门

设置工作区

您需要先设置项目工作区,然后才能构建项目。工作区是一种目录,用于存放项目的源文件和 Bazel 的构建输出。它还包含这些重要文件:

  • WORKSPACE file ,用于将目录及其内容识别为 Bazel 工作区,它位于项目目录结构的根目录中。
  • 一个或多个 BUILD files ,告诉 Bazel 如何构建项目的不同部分。工作区中包含一个 BUILD 文件的目录就是一个软件包。(本教程后面部分详细介绍了软件包。)

在以后的项目中,如需将某一目录指定为 Bazel 工作区,请在该目录中创建一个名为 WORKSPACE 的空文件。在本教程中,每个阶段都存在一个 WORKSPACE 文件。

注意:在 Bazel 构建项目时,所有输入都必须位于同一工作区中。位于不同工作区中的文件彼此独立,除非已关联。如需详细了解工作区规则,请参阅此指南

了解 BUILD 文件

BUILD 文件包含 Bazel 的几种不同类型的指令。每个 BUILD 文件都需要至少一条规则作为一组指令,告诉 Bazel 如何构建所需的输出,例如可执行文件或库。BUILD 文件中的 build 规则的每个实例都称为一个目标,并指向一组特定的源文件和依赖项。 目标还可以指向其他目标。

请查看 cpp-tutorial/stage1/main 目录中的 BUILD 文件:

cc_binary(
    name = "hello-world",
    srcs = ["hello-world.cc"],
)

在此示例中,hello-world 目标会实例化 Bazel 的内置 cc_binary rule。该规则指示 Bazel 从没有依赖项的 hello-world.cc 源文件构建独立的可执行文件。

摘要:使用入门

现在您已经熟悉了一些关键术语,以及它们在此项目和 Bazel 中的一般含义。在下一部分中,您将构建和测试项目的第 1 阶段。

第 1 阶段:单个目标,单个软件包

现在,您可以开始构建项目的第一部分了。作为视觉参考,此项目第一阶段部分的结构如下:

examples
└── cpp-tutorial
    └──stage1
       ├── main
       │   ├── BUILD
       │   └── hello-world.cc
       └── WORKSPACE

运行以下命令以移至 cpp-tutorial/stage1 目录:

cd cpp-tutorial/stage1

然后运行:

bazel build //main:hello-world

在目标标签中,//main: 部分是 BUILD 文件相对于工作区根目录的位置,hello-worldBUILD 文件中的目标名称。

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-bin/main/hello-world

这会导致输出“Hello world”消息。

下面是第 1 阶段的依赖关系图:

hello-world 的依赖关系图显示了包含单个源文件的单个目标。

摘要:第 1 阶段

现在,您已经完成了第一个构建,您已经基本了解了构建的结构。在下一阶段,您将通过添加其他目标来增加复杂性。

第 2 阶段:多个 build 目标

虽然单个目标足以满足小型项目的需求,但您可能希望将较大的项目拆分为多个目标和软件包。这样可以实现快速的增量构建(即 Bazel 仅重新构建更改的应用),并通过一次构建项目的多个部分来加快构建速度。在教程的这一阶段,您将添加一个目标,并在下一步中添加软件包。

以下是您要用于第 2 阶段的目录:

    ├──stage2
    │  ├── main
    │  │   ├── BUILD
    │  │   ├── hello-world.cc
    │  │   ├── hello-greet.cc
    │  │   └── hello-greet.h
    │  └── WORKSPACE

请查看 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 rule),然后再构建 hello-world 二进制文件。hello-world 目标中的 deps 属性会告知 Bazel 需要 hello-greet 库才能构建 hello-world 二进制文件。

您需要先更改目录(通过运行以下命令切换到 cpp-tutorial/stage2 目录),然后才能构建项目的这个新版本:

cd ../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

现在,您可以测试新构建的二进制文件,这会返回另一个“Hello world”:

bazel-bin/main/hello-world

如果您现在修改 hello-greet.cc 并重新构建项目,Bazel 只会重新编译该文件。

查看依赖关系图,您可以看到 hello-world 依赖于名为 hello-greet 的额外输入:

“hello-world”的依赖关系图会在修改文件后显示依赖项更改。

摘要:第 2 阶段

现在,您已构建了具有两个目标的项目。hello-world 目标会构建一个源文件,并依赖于另一个目标 (//main:hello-greet),后者再构建两个源文件。在下一部分中,更进一步,添加其他软件包。

第 3 阶段:多个软件包

下一阶段会添加另一层复杂功能,并构建包含多个软件包的项目。让我们来看看 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,工作区现在包含两个软件包:libmain

查看 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 属性知道这一点。依赖项关系表中反映了这一点:

“hello-world”的依赖关系图显示了主软件包中的目标如何依赖于“lib”软件包中的目标。

为使构建成功,您可以使用可见性属性让 lib/BUILD 中的 //lib:hello-time 目标明确显示给 main/BUILD 中的目标。这是因为默认情况下,目标仅对同一 BUILD 文件中的其他目标可见。Bazel 使用目标可见性来防止出现包含有实现细节的库泄露到公共 API 等问题。

现在,为此项目构建此最终版本。运行以下命令以切换到 cpp-tutorial/stage3 目录:

cd  ../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

现在,测试本教程的最后一个二进制文件以获得最终的 Hello world 消息:

bazel-bin/main/hello-world

摘要:第 3 阶段

现在,您已经将项目构建为两个具有三个目标的软件包,并了解它们之间的依赖关系,这有助于您继续进行构建,并使用 Bazel 构建未来的项目。在下一部分中,我们来看看如何继续 Bazel 之旅。

后续步骤

您已完成使用 Bazel 构建的第一个基本构建,但这仅仅只是开始。下面列出了一些可继续通过 Bazel 学习的资源:

祝您制作愉快!