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
    │  └── MODULE.bazel
    ├──stage2
    │  ├── main
    │  │   ├── BUILD
    │  │   ├── hello-world.cc
    │  │   ├── hello-greet.cc
    │  │   └── hello-greet.h
    │  └── MODULE.bazel
    └──stage3
       ├── main
       │   ├── BUILD
       │   ├── hello-world.cc
       │   ├── hello-greet.cc
       │   └── hello-greet.h
       ├── lib
       │   ├── BUILD
       │   ├── hello-time.cc
       │   └── hello-time.h
       └── MODULE.bazel

共有三组文件,每组文件代表本教程中的一个阶段。 在第一阶段,您将构建位于单个软件包中的单个目标。在第二阶段,您将从单个软件包构建二进制文件和库。在第三个也是最后一个阶段,您将构建一个包含多个软件包的项目,并使用多个目标进行构建。

摘要:简介

通过安装 Bazel(和 Git)并克隆本教程的代码库,您已为使用 Bazel 进行首次构建奠定了基础。请继续阅读下一部分,了解一些术语并设置工作区

使用入门

在构建项目之前,您需要设置其工作区。工作区是一个目录,用于存放项目的源文件和 Bazel 的构建输出。 它还包含以下重要文件:

  • MODULE.bazel 文件,用于将目录及其内容标识为 Bazel 工作区,并位于项目目录结构的根目录中。您还可以在其中指定外部依赖项。
  • 一个或多个 BUILD 文件,用于告知 Bazel 如何构建项目的不同部分。工作区内包含 BUILD 文件的目录是一个 软件包。(本教程稍后会详细介绍软件包。)

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

了解 BUILD 文件

BUILD 文件包含针对 Bazel 的几种不同类型的指令。每个 BUILD文件至少需要一条 规则作为一组指令, 用于告知 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> 源文件构建一个不含依赖项的自包含可执行二进制文件。

摘要:使用入门

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

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

现在可以构建项目的第一部分了。如需查看视觉参考,请参阅项目第 1 阶段部分的结构:

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

运行以下命令以移至 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 阶段:多个构建目标

虽然单个目标对于小型项目来说已足够,但您可能需要将大型项目拆分为多个目标和软件包。这样可以实现快速增量构建(即 Bazel 仅重新构建已更改的内容),并通过一次构建项目的多个部分来加快构建速度。本教程的这一阶段添加了一个目标,下一阶段添加了一个软件包。

这是您在第 2 阶段中使用的目录:

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

查看 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 再次生成的内容如下所示:

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
   └── MODULE.bazel

您可以看到,现在有两个子目录,每个子目录都包含一个 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` 软件包中的目标。

为了使构建成功,您可以使用 visibility 属性,使 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:

祝您构建愉快!