Bazel 项目布局和组织

Bazel 项目

Bazel 项目是一组源文件和构建文件,用于描述:

  • 如何构建工件(例如二进制文件或数据文件)及其依赖项。
  • 如何运行特定命令,例如脚本或可执行文件。
  • 如何测试上述 build 工件。

Bazel 项目由顶级目录进行具体化,其内容遵循特定布局惯例

Bazel 工作区

Bazel 工作区是一个目录树,其中包含顶级 WORKSPACE.bazel1 文件。这定义了一系列源代码和相关 build 文件的根目录。Bazel 项目可以使用多个工作区:

  • 项目的根目录称为 工作区,因此必须包含 WORKSPACE.bazel 文件。

  • 项目还可以引用其他工作区(称为外部 代码库),这些工作区对应于第三方项目依赖项。

例如:

/home/user/project/
    WORKSPACE.bazel
    src/
        BUILD.bazel
        extra/
            extra.cc
        lib/
            BUILD.bazel
            foo.cc
            foo.h
        main.cc

目录 /home/user/project 是根 Bazel 工作区,其中包含所有文件。

WORKSPACE.bazel 文件可以为空,但也可以包含引用其他工作区的指令,如后文所述。

Bazel 软件包

在工作区中,包含 BUILD.bazel2 文件的目录定义了一个软件包,它是 Bazel 知晓的一组源文件和项的边界。

例如,以下文件布局:

/home/user/project/
  WORKSPACE.bazel
  BUILD.bazel
  main.cc

定义一个位于 /home/user/project 的根工作区,其中包含一个顶级软件包,该软件包包含 BUILD.bazelmain.cc 文件。

BUILD.bazel 文件还可以包含用于定义命名(例如目标、配置条件等)的说明,这些项在技术上也属于该软件包。

单个工作区中可以存在多个软件包,并且每个文件只能属于一个软件包。例如,使用以下文件布局:

/home/user/project/
  WORKSPACE.bazel
  BUILD.bazel
  main.cc
  lib/
    BUILD.bazel
    foo.cc

根工作区包含两个不同的软件包:

  • 顶级软件包,其中仍包含文件 BUILD.bazelmain.cc(相对于根工作区目录)。

  • 第二个软件包,其中包含 lib/BUILD.bazellib/foo.cc 文件。

请注意,/home/user/project/lib/foo.cc 中的文件仅属于第二个软件包,而非第一个软件包。这是因为软件包 边界 绝不会 重叠

BUILD.bazelBUILD

Bazel 源自 Google 的 Blaze,后者仅适用于区分大小写的文件系统的 Linux。Blaze 仅使用文件名 BUILD 来存储 build 指令。

不过,Bazel 还需要在具有不区分大小写文件系统的 Windows 上运行,而许多 Google 或非 Google 项目已在使用名为“build”的目录,这会与此类系统上名为“BUILD”的文件冲突。

为解决此问题,Bazel 使用 BUILD.bazelWORKSPACE.bazel 作为默认文件名,同时仍支持将 BUILDWORKSPACE 用作后备。

工作区指令

WORKSPACE.bazel 可以为空,也可以包含引用其他 Bazel 工作区(称为外部代码库)的指令。这些指令始终会为代码库指定名称。例如:

local_repository(
  name = "my_ssl",
  path = "/home/user/src/openssl-bazel",
)

将名称 my_ssl 与位于 build 机上的 /home/user/src/openssl-bazel 的工作区相关联。此目录还必须包含 WORKSPACE.bazel(或 WORKSPACE)文件。

代码库只是一个带有名称的外部 Bazel 工作区。此名称是项目本地的名称,稍后可用于引用外部工作区的项目(见下文)。

Bazel 还支持其他指令,用于从网络下载代码库,甚至以编程方式生成其内容。

Bazel 标签

Bazel 标签是对源文件和 BUILD.bazel 文件中定义的项的字符串引用。其一般格式为:

@<repository_name>//<package_name>:<target_name>

其中:

  • @<repository_name>// 用于指定命名 Bazel 工作区的目录。

    为方便起见,对于当前工作区(包含当前 BUILD.bazel 文件的工作区),可以将其简写为 //。另请注意,即使在外部代码库中使用,@// 也用于指定项目的根工作区。

  • <package_name> 是相对于工作区目录的软件包目录路径。例如,在标签 //src:main.cc//src/lib:foo 中,软件包名称分别为 srcsrc/lib

    此值可以为空,例如 //:BUILD.bazel 指向当前工作区的顶级目录中的 build 文件。

  • 对于源文件,<target_name> 是相对于其父级软件包目录的文件路径,并且可能包含子目录部分。例如,对于 //src:main.cc//src:extra/extra.cc,目标名称分别为 main.ccextra/extra.cc

  • 对于其他项,<target_name> 对应于 BUILD.bazel 文件中定义的项(build 工件、build 设置、配置条件等)。

    按照惯例,除极少数情况外,其 name 属性不应包含目录分隔符,以免与来源混淆。

    对于来自其他 build 系统的开发者来说,这可能会造成困惑,因为其他 build 系统会区分 build 图中的项类型(例如,GN 使用“目标”“配置”“工具链”和“池”来指定不同的内容)。

还支持使用缩写标签表达式:

  • 如果标签以代码库名称开头且不包含冒号,则表示它是软件包路径,指向同名项。例如,//src/foo 等同于 //src/foo:foo

  • 如果标签以冒号开头,则表示相对于当前软件包的名称。例如,src/foo/BUILD.bazel 中显示的“:bar”和“:extra/bar.cc”分别等同于 //src/foo:bar//src/foo:extra/bar.cc

  • 如果标签没有代码库名称和英文冒号,则始终是相对于当前软件包的名称,即使包含目录分隔符也是如此。例如,src/foo/BUILD.bazel 中的“bar/bar.cc”始终是指 //src/foo:bar/bar.cc

    请注意,这与 //src/foo/bar:bar.cc 不同

相对标签和软件包所有权

由于每个源文件只能属于一个软件包,因此相对标签可能会无效。例如,在如下所示的项目中:

/home/user/project/
    WORKSPACE.bazel
    src/
        BUILD.bazel
        main.cc
        extra/
            extra.cc
        lib/
            BUILD.bazel
            foo.cc
            foo.h

foo.cc 文件属于软件包 src/lib,因此其标签必须//src/lib:foo.cc

src/BUILD.bazel 中使用 src:lib/foo.cc 等标签是错误的:

# From src/BUILD.bazel
cc_binary(
  name = "program",
  srcs = [
    "extra/extra.cc",
    "lib/foo.cc",       # Error: Label '//src:lib/foo.cc' is invalid because 'src/lib' is a subpackage
    "lib/foo.h"         # Error: Label '//src:lib/foo.h' is invalid because 'src/lib' is a subpackage
    "main.cc",
  ],
)

从其他软件包访问源文件

默认情况下,其他软件包无法访问给定软件包的源文件,并且相对软件包标签无效,如下所示:

# From src/BUILD.bazel
cc_binary(
  name = "program",
  srcs = [
    "extra/extra.cc",
    "lib:foo.cc",    # Error: invalid label 'lib:foo.cc': absolute label must begin with '@' or '//'
    "lib:foo.h"      # Error: invalid label 'lib:foo.h': absolute label must begin with '@' or '//'
    "main.cc",
  ],
)

即使使用正确的绝对标签,也会发生错误:

# From src/BUILD.bazel
cc_binary(
  name = "program",
  srcs = [
    "extra/extra.cc",
    "//src/lib:foo.cc",    # Error: no such target '//src/lib:foo.cc': target 'foo.h' not declared in package 'src/lib'
    "//src/lib:foo.h"      # Error: no such target '//src/lib:foo.h': target 'foo.h' not declared in package 'src/lib'
    "main.cc",
  ],
)

export_files() 可以授予对跨软件包边界的文件的直接访问权限:

# From src/lib/BUILD.bazel
export_files([
  "foo.cc" ,
  "foo.h" ,
])

# From src/BUILD.bazel
cc_binary(
  name = "program",
  srcs = [
    "extra/extra.cc",
    "//src/lib:foo.cc",    # OK
    "//src/lib:foo.h"      # OK
    "main.cc",
  ],
)

从其他软件包访问目标

BUILD.bazel 文件中定义的标记项(非源文件)无需导出,但其 visibility 属性必须允许在其自身软件包之外使用:

# From src/lib/BUILD.bazel
cc_library(
  name = " lib" ,
  srcs = [ " foo.cc"  ],
  hdrs = [ " foo.h"  ],
  visibility = [ " //visibility:public" ],  # Anyone can reference this directly!
)

# From src/BUILD.bazel
cc_binary(
  name = "program",
  srcs = [
    "extra/extra.cc",
    "main.cc",
  ],
  deps = [ "lib" ],   # OK!
)

默认情况下,项仅对同一软件包中的其他项可见。您可以使用 package() 指令更改软件包中定义的所有项的默认公开范围,从而更改此设置:

# From src/lib/BUILD.bazel

# Ensure that all items defined in this file are visible to anyone
package(default_visibility = ["//visibility:public"])

cc_library(
  name = " lib" ,
  srcs = [ "foo.cc" ],
  hdrs = [ "foo.h" ],
)

# From src/BUILD.bazel
cc_binary(
  name = "program",
  srcs = [
    "extra/extra.cc",
    "main.cc",
  ],
  deps = [ "lib" ],   # OK!
)

关于虚拟软件包的警告

避免在项目中创建以下名称的顶级目录:

  • conditions
  • command_line_option
  • external
  • visibility

这是因为 Bazel 在 BUILD.bazel 文件中的标签中使用了多个硬编码的“虚拟软件包”。例如:

  //visibility:public
  //conditions:default
  //command_line_option:copt

external 的情况略有不同:它不会显示在 BUILD.bazel 文件中,而是在内部用于管理外部代码库。这会导致 Bazel 在用作项目目录时感到困惑

规范代码库名称

从 Bazel 6.0 开始,标签中的代码库名称也可以以 @@ 开头。

启用可选的 BzlMod 功能后,这些标签会用作外部代码库的替代但唯一标签名称,这在项目中使用复杂的传递依赖项树时非常重要。

例如,@@com_acme_anvil.1.0.3 可以是工作区目录的规范名称,在项目自己的 BUILD.bazel 文件中由 @anvil 标识,在外部代码库(例如 @foo//:BUILD.bazel 中)中则由 @acme_anvil 标识。这三个标签都指同一目录的内容。

规范代码库名称不会显示在 BUILD.bazel 文件中,但会在分析阶段(执行查看标签值的 Starlark 函数时)或查看 Bazel 查询的结果时显示。

Bazel 扩展 (.bzl) 文件

扩展程序文件包含可导入到其他几个文件中的额外定义:

  • 它们的名称始终以 .bzl 文件扩展名结尾。

  • 它们必须属于 Bazel 软件包,因此可以通过标签进行标识。例如 //bazel_utils:defs.bzl

  • 它们采用 Starlark 语言编写,并且应遵循特定准则

  • 它们是唯一可定义 Starlark 函数的位置! 换句话说,您无法在 BUILD.bazel 文件中定义函数!

  • 它们始终只会评估一次,即使多次导入,它们定义的变量和函数也会被记录为常量。

  • 您可以使用 load() 语句从其他文件导入它们。

例如:

  • 以下内容来自 $PROJECT/my_definitions.bzl

    # The official release number
    release_version = "1.0.0"
    
  • 以下内容来自 $PROJECT/BUILD.bazel

    # Import the value of `release_version` from my_definitions.bzl
    load("//:my_definitions.bzl", "release_version")
    
    # Compile C++ executable, hard-coding its version number with a macro.
    cc_binary(
      name = "my_program",
      defines = [ "RELEASE_VERSION=" + release_version ],
      sources = [ … ],
    )
    

load() 语句具有特殊的语义:

  • 其第一个参数必须是 .bzl 文件的标签字符串(例如 "//src:definitions.bzl")。

  • 其他参数用于命名导入的常量或函数:

    • 如果参数是字符串,则必须是 .bzl 文件定义的导入符号的名称。例如:

      load("//src:defs.bzl", "my_var", "my_func")
      
    • 如果参数是变量赋值,则会为导入的符号定义一个本地别名。E.g.:

      load("//src:defs.bzl", "my_var", func = "my_func")
      
    • 不支持通配符:所有导入的常量和函数都必须明确命名。

    • load() 出现在 .bzl 文件中时,系统绝不会记录导入的符号。

    • 同样,名称以下划线开头的符号(例如 _foo)永远不会被记录,也无法导入。也就是说,它们仅对定义它们的 .bzl 文件可见。

有时,.bzl 文件需要从另一个文件导入符号,并使用相同的名称重新导出该符号。这需要使用别名,例如:

# From //src:utils.bzl

# Import "my_vars" from defs.bzl as '_my_var'.
load("//src:defs.bzl", _my_var = "my_var")

# Define my_var in the current scope as a copy of _my_var
# This symbol and its value will be recorded, and available for import
# to any other file that loads //src:utils.bzl.
my_var = _my_var

  1. 出于旧版原因,此文件也可以简单地称为 WORKSPACE 

  2. 此外,出于旧版原因,该文件也可以简单地称为 BUILD。