Bazel 构建输出

Bazel 使用非常主观的架构来存储 build 工件,这让开发者经常会感到困惑。本页面尝试阐明其工作原理。

Bazel output_base

根据设计,bazel build 命令绝不会将文件写入项目的源目录(或其某个子目录)。Bazel 使用特定于用户的 并行目录来存储所有输出,该目录称为user_output_root,默认情况下为:

  • ~/.cache/bazel/_bazel_$USER(在 Linux 上)。
  • /private/var/tmp/_bazel_$USER(在 macOS 上)。
  • %HOME%\_bazel_%USERNAME%(在 Windows 上)。

对于运行 bazel 的每个工作区目录,系统会在 user_output_root 下创建一个名为 output_base 的单独目录,如下所示:

  ${user_output_root}/<WORKSPACE_HASH>`

其中,<WORKSPACE_HASH> 是一个长十六进制哈希(根据目录的绝对路径计算得出)。

由于此路径完全不可预测,因此在 Bazel 项目中使用时,bazel info output_base 命令会显示此路径。例如:

$ mkdir -p /tmp/project1 && cd /tmp/project1 && touch WORKSPACE.bazel
$ bazel info output_base
Starting local Bazel server and connecting to it...
/usr/local/google/home/digit/.cache/bazel/_bazel_digit/6c7b78994da78136b5cb6b7607361ad3

$ mkdir -p /tmp/project2 && cd /tmp/project2 && touch WORKSPACE.bazel
$ bazel info output_base
Starting local Bazel server and connecting to it...
/usr/local/google/home/digit/.cache/bazel/_bazel_digit/c37b9d68308ee5abe2f781dd38b733b9

$ mkdir -p /tmp/not-a-project && cd /tmp/not-a-project
$ bazel info output_base
WARNING: Invoking Bazel in batch mode since it is not invoked from within a workspace (below a directory having a WORKSPACE file).
ERROR: The 'info' command is only supported from within a workspace (below a directory having a WORKSPACE file).
See documentation at https://bazel.build/concepts/build-ref#workspace

该方案比较灵活,但并不完美:

  • 优点:同一台计算机上的多个用户可以共享同一个只读项目目录。

  • 优点:同一用户的多个项目目录将始终使用独立的输出路径。

  • 缺点:直接通过命令行(甚至是图形浏览器)查看生成的文件非常困难。

  • 缺点:移除项目目录(例如使用 rm -rf .../my-project)并不会移除其输出(造成大量浪费)。

  • 缺点:移动项目目录(例如,使用 mv my-project my-project2)不会重复使用之前的 output_base 内容(并会保留旧的内容,现在无法访问)。

  • 缺点user_output_root 的默认位置,因此 output_base 通常与项目不在同一个文件系统 / 分区中。这可能会在性能 / 磁盘使用率方面产生意外后果。

调用 bazel clean 可从当前的 output_base 中移除构建输出。此操作必须在移除源项目目录之前完成。

在实践中,使用从未得到适当清理的过时 Bazel 项目的构建工件,很容易导致 user_output_root 的内容膨胀。更糟糕的是,尝试直接手动移除 user_output_root 可能不起作用,因为 Bazel 会默认创建只读构建工件,这会导致 rm -rf ~/.cache/bazel 等命令无法正常运行!

Bazel output_base 内容:

很多内容实际存储在 output_base 下:

  • 外部代码库的工作区目录

    它们对应于外部项目依赖项。这些文件通常不是项目源代码树的一部分,而是从网络下载或以编程方式生成的。

    它们的内容存储在 ${output_base}/external/<repository_name> 下,其中 external 部分采用硬编码,<repository_name> 与外部代码库的规范名称匹配。

  • 构建工件

    通过运行 bazel build 生成的文件。这些文件存储在以下位置:

    ${output_base}/execroot/<workspace_name>/bazel-out/<config_dir>/bin/
    

    其中:

    • execrootbazel-outbin 部分是硬编码的,无法更改。

    • 对于在项目自己的 BUILD.bazel 文件中定义的目标,<workspace_name> 默认为 __main__,除非它在项目的 WORKSPACE.bazel 文件中使用如下指令进行设置:

      workspace(
        name = "my_project",
      )
      ```
    
    - For targets defined in external repositories, `<workspace_name>` matches
    the repository's canonical name.
    
    - The `<config_dir>` value is a name derived from the build configuration used
    to configure the target that generated the build artifact. This allows
    rebuilding the same target in different ways, each time using a different
    `<config_dir>` value.
    
    Note: The `<config_dir>` value is **generally unpredictable**. More on this [here][bazel-config-dirs]
    
  • 测试结果

    调用 bazel test 时生成的日志文件,这些文件存储在 ${output_base}/execroot/<workspace_name>/bazel-out/<config_dir>/testlogs/ 下。

  • 内部缓存和配置文件

    供远程构建和远程缓存功能使用。开发者可以忽略这些文件。

Bazel execroot 目录:

execroot 用于运行生成构建工件的 Bazel 命令,但具体操作方式取决于是否针对特定操作启用了沙盒。

  • 在 Linux 和 MacOS 上,系统会默认为所有操作启用沙盒。Windows 不再支持沙盒(从 Bazel 7 开始)。

  • Bazel 操作可以在定义中使用 no-sandbox 标记有意停用沙盒。

  • 在调用 bazel 时,可以使用 --spawn_strategy=local 等选项全局停用沙盒。

不使用沙盒时:

停用沙盒后,为指定工作区生成工件的所有构建操作都会将输出文件放在 ${output_base}/execroot/<workspace_name> 下。

因此,该操作的命令中出现的源文件和构建工件的所有路径都将相对于该路径。

Bazel 可确保在 execroot 下创建指向该命令所用输入源的符号链接,然后再启动该命令。

例如,可编译包含 #include "foo.h"(对应于 //src/foo/foo.h)的 //src/foo/foo.cc 文件的操作可能如下所示:

gcc -c -o bazel-out/k8-fastbuild/bin/src/foo/foo.o src/foo/foo.cc -Isrc/foo

此方法可行,原因如下:

  • 在运行该命令之前,Bazel 会创建指向 $PROJECT/src 的符号链接 ${output_base}/execroot/__main__/src,以使 src/foo/foo.ccsrc/foo/foo.h 按预期解析为 $PROJECT/src/foo/foo.cc$PROJECT/src/foo/foo.h

  • bazel-out/k8-fastbuild/bin/src/foo/foo.o 位置是通过在此命令使用的 build 配置中编译 foo.cc 而创建的对象文件的最终输出路径。

使用沙盒时:

启用沙盒后,Bazel 会为每个命令创建一个临时目录(例如 ${output_base}/sandbox/linux-sandbox/<random-number>),并创建一个符号链接树来模拟其下的 execroot 布局,但仅限于其知道的输入。在这种情况下,代码将如下所示:

  • 输入 ${sandbox}/execroot/__main__/src/foo/foo.cc${sandbox}/execroot/__main__/src/foo/foo.h 的符号链接,分别指向 $PROJECT/src/foo/foo.cc$PROJECT/src/foo/foo.h

  • ${sandbox}/execroot/__main__ 下运行完全相同的命令。 而不是 ${output_base}/execroot/__main__

  • 命令完成后,将沙盒路径 (${sandbox}/execroot/__main__/bazel-out/k8-fastbuild/bin/src/foo/foo.o) 中的已知输出复制到其最终位置 (${output_base}/execroot/__main__/bazel-out/k8-fastbuild/bin/src/foo/foo.o)。

  • 最后,沙盒目录及其所有内容都会被移除。这也意味着忽略未声明的输出。