Bazel 项目布局和组织

Bazel 项目

Bazel 项目是源文件和构建文件的集合,这些文件用于描述以下各项:

  • 如何构建工件(例如二进制文件或数据文件)及其依赖项。
  • 如何运行特定命令,即脚本或可执行文件。run
  • 如何测试上述 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 来存储构建指令。

不过,Bazel 还需要在 Windows 和 MacOS 上运行,因为 Windows 和 MacOS 的文件系统不区分大小写,并且许多 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 与构建机器上位于 /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 属性不应包含目录分隔符,除非在极少数情况下,以避免与源混淆。

    对于来自其他构建系统的开发者,这可能会使开发者感到困惑,这些系统可区分其构建图中的项目类型(例如,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 混淆

规范代码库名称

从 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