Bazel 教程:构建 C++ 项目

报告问题 查看源代码 每夜 build · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

简介

刚开始接触 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 文件都需要至少一个 rule 作为一组指令,用于告知 Bazel 如何构建所需的输出(例如可执行二进制文件或库)。BUILD 文件中的每个 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 阶段

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

第 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-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
   └── 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` 软件包中的目标。

为确保构建成功,您可以使用可见性属性让 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 完成了第一个基本 build,但这只是开始。以下是一些可帮助您继续学习 Bazel 的资源:

祝您构建顺利!