规则

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

规则定义了 Bazel 对输入执行的一组操作,以生成一组输出,这些输出在规则的实现函数返回的提供程序中引用。例如,C++ 二进制文件规则可以:

  1. 获取一组 .cpp 源文件(输入)。
  2. 对源文件运行 g++(操作)。
  3. 返回 DefaultInfo 提供程序,其中包含可在运行时提供的可执行输出和其他文件。
  4. 返回 CcInfo 提供程序,其中包含从目标及其依赖项收集的 C++ 专用信息。

从 Bazel 的角度来看,g++ 和标准 C++ 库也是此规则的输入。作为规则编写者,您不仅要考虑用户为规则提供的输入,还要考虑执行操作所需的所有工具和库。

在创建或修改任何规则之前,请确保您熟悉 Bazel 的构建阶段。请务必了解构建的三个阶段(加载、分析和执行)。了解也有助于您了解规则和宏之间的区别。首先,请查看规则教程。然后,使用本页面作为参考。

Bazel 本身内置了一些规则。这些原生规则(例如 genrulefilegroup)提供了一些核心支持。通过定义自己的规则,您可以添加对 Bazel 不原生支持的语言和工具的支持。

Bazel 提供了一种可扩展性模型,用于使用 Starlark 语言编写规则。这些规则以 .bzl 文件的形式编写,可直接从 BUILD 文件加载。

在定义自己的规则时,您可以决定它支持哪些属性以及如何生成输出。

规则的 implementation 函数定义了其在分析阶段中的确切行为。此函数不会运行任何外部命令。而是注册操作,以便在执行阶段稍后根据需要使用这些操作来构建规则的输出。

创建规则

.bzl 文件中,使用 rule 函数定义新规则,并将结果存储在全局变量中。对 rule 的调用指定了属性实现函数

example_library = rule(
    implementation = _example_library_impl,
    attrs = {
        "deps": attr.label_list(),
        ...
    },
)

这定义了一个名为 example_library规则类型

rule 的调用还必须指定规则是创建executable输出(使用 executable = True),还是专门创建测试可执行文件(使用 test = True)。如果是后者,则该规则是测试规则,并且规则的名称必须以 _test 结尾。

目标实例化

您可以在 BUILD 文件中加载和调用规则:

load('//some/pkg:rules.bzl', 'example_library')

example_library(
    name = "example_target",
    deps = [":another_target"],
    ...
)

对构建规则的每次调用都不会返回任何值,但会产生定义目标的副作用。这称为实例化规则。这会为新目标指定名称,并为目标的属性指定值。

您还可以从 Starlark 函数调用规则,并将其加载到 .bzl 文件中。调用规则的 Starlark 函数称为 Starlark 宏。Starlark 宏最终必须从 BUILD 文件调用,并且只能在加载阶段(系统评估 BUILD 文件以实例化目标)调用。

属性

属性是规则参数。属性可以为目标的实现提供特定值,也可以引用其他目标,从而创建依赖项图。

规则专用属性(例如 srcsdeps)的定义方法是将从属性名称到架构(使用 attr 模块创建)的映射传递给 ruleattrs 参数。常见属性(例如 namevisibility)会隐式添加到所有规则中。系统会将其他属性隐式添加到可执行文件和测试规则。隐式添加到规则中的属性无法包含在传递给 attrs 的字典中。

依赖项属性

处理源代码的规则通常会定义以下属性来处理各种依赖项类型

  • srcs 用于指定由目标操作处理的源文件。通常,属性架构会指定规则处理的源文件类型应具有哪些文件扩展名。适用于具有头文件的语言的规则通常会为目标及其使用方处理的头文件指定单独的 hdrs 属性。
  • deps 用于指定目标的代码依赖项。属性架构应指定这些依赖项必须提供哪些提供程序。(例如,cc_library 提供 CcInfo。)
  • data 用于指定要在运行时提供给依赖于目标的任何可执行文件的文件。这样应该就可以指定任意文件了。
example_library = rule(
    implementation = _example_library_impl,
    attrs = {
        "srcs": attr.label_list(allow_files = [".example"]),
        "hdrs": attr.label_list(allow_files = [".header"]),
        "deps": attr.label_list(providers = [ExampleInfo]),
        "data": attr.label_list(allow_files = True),
        ...
    },
)

以下是依赖项属性示例。用于指定输入标签的任何属性(使用 attr.label_listattr.labelattr.label_keyed_string_dict 定义的属性)都会在定义目标时,在该属性中列出标签(或相应的 Label 对象)的目标之间指定某种类型的依赖项。这些标签的代码库(可能还有路径)会相对于定义的目标进行解析。

example_library(
    name = "my_target",
    deps = [":other_target"],
)

example_library(
    name = "other_target",
    ...
)

在此示例中,other_targetmy_target 的依赖项,因此系统会先分析 other_target。如果目标的依赖项图表中存在循环,则会出错。

不公开属性和隐式依赖项

具有默认值的依赖项属性会创建隐式依赖项。之所以是隐式依赖项,是因为它是目标图的一部分,用户未在 BUILD 文件中指定它。隐式依赖项非常适合对规则与工具(构建时依赖项,例如编译器)之间的关系进行硬编码,因为大多数情况下,用户不想指定规则使用的工具。在规则的实现函数中,此依赖项的处理方式与其他依赖项相同。

如果您想提供隐式依赖项,但不允许用户替换该值,则可以通过为该属性指定以下划线 (_) 开头的名称,将其设为私有。私有属性必须具有默认值。通常,只有在使用隐式依赖项时才有必要使用私有属性。

example_library = rule(
    implementation = _example_library_impl,
    attrs = {
        ...
        "_compiler": attr.label(
            default = Label("//tools:example_compiler"),
            allow_single_file = True,
            executable = True,
            cfg = "exec",
        ),
    },
)

在此示例中,每个类型为 example_library 的目标都隐式依赖于编译器 //tools:example_compiler。这样一来,即使用户未将其标签作为输入传递,example_library 的实现函数也可以生成调用编译器的操作。由于 _compiler 是私有属性,因此在这种规则类型的所有目标中,ctx.attr._compiler 始终会指向 //tools:example_compiler。或者,您也可以将属性命名为 compiler(不带下划线),并保留默认值。这样,用户就可以根据需要替换其他编译器,而无需了解编译器的标签。

隐式依赖项通常用于与规则实现位于同一代码库中的工具。如果该工具来自执行平台或其他代码库,则规则应从工具链获取该工具。

输出属性

输出属性(例如 attr.outputattr.output_list)用于声明目标生成的输出文件。这些属性与依赖项属性有以下两点不同:

  • 它们用于定义输出文件目标,而不是引用其他位置定义的目标。
  • 输出文件目标取决于实例化的规则目标,而不是相反。

通常,只有当规则需要创建无法基于目标名称的用户定义名称的输出时,才会使用输出属性。如果规则只有一个输出属性,则该属性通常命名为 outouts

输出属性是创建预声明的输出的首选方式,您可以专门依赖于这些输出,也可以在命令行中请求这些输出。

实现函数

每条规则都需要一个 implementation 函数。这些函数会严格在分析阶段执行,并将在加载阶段生成的目标图转换为在执行阶段执行的操作图。因此,实现函数实际上无法读取或写入文件。

规则实现函数通常是私有的(以英文下划线开头)。通常,它们的命名与规则相同,但带有 _impl 后缀。

实现函数只接受一个参数:一个规则上下文,通常命名为 ctx。它们会返回提供商列表。

目标

依赖项在分析时表示为 Target 对象。这些对象包含在执行目标的实现函数时生成的提供程序

ctx.attr 具有与每个依赖项属性的名称对应的字段,其中包含 Target 对象,这些对象代表使用该属性的每个直接依赖项。对于 label_list 属性,此列表为 Targets。对于 label 属性,此值为单个 TargetNone

目标的实现函数会返回一个提供程序对象列表:

return [ExampleInfo(headers = depset(...))]

您可以使用索引表示法 ([]) 访问这些提供程序,其中提供程序的类型用作键。这些可以是 Starlark 中定义的自定义提供程序,也可以是可用作 Starlark 全局变量的原生规则提供程序

例如,如果规则使用 hdrs 属性获取头文件,并将其提供给目标及其使用方的编译操作,则可以按如下方式收集它们:

def _example_library_impl(ctx):
    ...
    transitive_headers = [hdr[ExampleInfo].headers for hdr in ctx.attr.hdrs]

有一个旧版结构体样式,强烈建议不要使用,并且应从中迁移规则

文件

文件由 File 对象表示。由于 Bazel 不会在分析阶段执行文件 I/O,因此这些对象无法用于直接读取或写入文件内容。而是会传递给发出操作的函数(请参阅 ctx.actions),以构建操作图的各个部分。

File 可以是源文件,也可以是生成的文件。每个生成的文件都必须是仅由一项操作生成的输出。源文件不能是任何操作的输出。

对于每个依赖项属性,ctx.files 的对应字段包含使用该属性的所有依赖项的默认输出的列表:

def _example_library_impl(ctx):
    ...
    headers = depset(ctx.files.hdrs, transitive = transitive_headers)
    srcs = ctx.files.srcs
    ...

对于规范设置了 allow_single_file = True 的依赖项属性,ctx.file 包含单个 FileNonectx.executable 的行为与 ctx.file 相同,但仅包含规范设置了 executable = True 的依赖项属性的字段。

声明输出

在分析阶段,规则的实现函数可以创建输出。由于在加载阶段必须知道所有标签,因此这些额外输出没有标签。您可以使用 ctx.actions.declare_filectx.actions.declare_directory 创建输出的 File 对象。通常,输出的名称基于目标的名称 ctx.label.name

def _example_library_impl(ctx):
  ...
  output_file = ctx.actions.declare_file(ctx.label.name + ".output")
  ...

对于预声明的输出(例如为输出属性创建的输出),您可以改为从 ctx.outputs 的对应字段检索 File 对象。

操作

操作描述了如何根据一组输入生成一组输出,例如“对 hello.c 运行 gcc 并获取 hello.o”。创建操作后,Bazel 不会立即运行该命令。它会在依赖项图中注册该操作,因为某个操作可能会依赖于另一个操作的输出。例如,在 C 中,必须在编译器之后调用链接器。

用于创建操作的通用函数在 ctx.actions 中定义:

ctx.actions.args 可用于高效累积操作的参数。它会避免在执行时扁平化 depset:

def _example_library_impl(ctx):
    ...

    transitive_headers = [dep[ExampleInfo].headers for dep in ctx.attr.deps]
    headers = depset(ctx.files.hdrs, transitive = transitive_headers)
    srcs = ctx.files.srcs
    inputs = depset(srcs, transitive = [headers])
    output_file = ctx.actions.declare_file(ctx.label.name + ".output")

    args = ctx.actions.args()
    args.add_joined("-h", headers, join_with = ",")
    args.add_joined("-s", srcs, join_with = ",")
    args.add("-o", output_file)

    ctx.actions.run(
        mnemonic = "ExampleCompile",
        executable = ctx.executable._compiler,
        arguments = [args],
        inputs = inputs,
        outputs = [output_file],
    )
    ...

操作会接受输入文件的列表或依赖项集,并生成输出文件的(非空)列表。在分析阶段,必须知道一组输入和输出文件。它可能取决于属性的值(包括依赖项中的提供程序),但不能取决于执行结果。例如,如果您的操作运行解压缩命令,您必须指定预期要膨胀的文件(在运行解压缩命令之前)。在内部创建可变数量文件的操作可以将这些文件封装在单个文件(例如 ZIP、tar 或其他归档格式)中。

操作必须列出其所有输入。可以列出未使用的输入,但效率不高。

操作必须创建其所有输出。它们可能会写入其他文件,但任何不在输出中的内容都不会提供给使用方。所有声明的输出都必须由某个操作写入。

操作与纯函数类似:它们应仅依赖于提供的输入,并避免访问计算机信息、用户名、时钟、网络或 I/O 设备(读取输入和写入输出除外)。这一点很重要,因为输出将被缓存和重复使用。

Bazel 会解析依赖项,并决定要执行哪些操作。如果依赖项图表中存在循环,则会出错。创建操作并不保证系统会执行该操作,具体取决于 build 是否需要其输出。

提供商

提供程序是规则向依赖于它的其他规则公开的信息。这些数据可以包括输出文件、库、要通过工具的命令行传递的参数,或目标的使用方应了解的任何其他内容。

由于规则的实现函数只能读取实例化目标的直接依赖项中的提供程序,因此规则需要转发目标的依赖项中需要由目标的使用方知道的任何信息,通常是通过将这些信息累积到 depset 中。

目标的提供程序由实现函数返回的提供程序对象列表指定。

旧版实现函数也可以采用旧版样式编写,其中实现函数会返回 struct,而不是提供程序对象列表。强烈建议不要采用这种样式,应将规则从这种样式迁移出去

默认输出

目标的默认输出是指在命令行中请求构建目标时默认请求的输出。例如,java_library 目标 //pkg:foofoo.jar 作为默认输出,因此该输出将由命令 bazel build //pkg:foo 构建。

默认输出由 DefaultInfofiles 参数指定:

def _example_library_impl(ctx):
    ...
    return [
        DefaultInfo(files = depset([output_file]), ...),
        ...
    ]

如果规则实现未返回 DefaultInfo 或未指定 files 参数,则 DefaultInfo.files 默认为所有预声明的输出(通常是通过输出属性创建的输出)。

执行操作的规则应提供默认输出,即使这些输出预计不会直接使用也是如此。系统会修剪不在请求输出的图表中的操作。如果输出仅供目标的使用方使用,则在单独构建目标时,系统不会执行这些操作。这会增加调试难度,因为仅重新构建失败的目标无法重现失败问题。

runfile

运行文件是一组目标在运行时(而不是构建时)使用的文件。在执行阶段,Bazel 会创建一个目录树,其中包含指向 runfile 的符号链接。这会为二进制文件设置环境,以便其在运行时访问 runfile。

您可以在创建规则时手动添加 runfile。您可以通过规则上下文 ctx.runfiles 上的 runfiles 方法创建 runfiles 对象,并将其传递给 DefaultInfo 上的 runfiles 形参。可执行规则的可执行输出会隐式添加到 runfile 中。

某些规则会指定属性(通常命名为 data),其输出会添加到目标的 runfile 中。还应从 data 以及任何可能提供最终执行代码的属性(通常是 srcs,其中可能包含具有关联 datafilegroup 目标)和 deps 中合并 runfile。

def _example_library_impl(ctx):
    ...
    runfiles = ctx.runfiles(files = ctx.files.data)
    transitive_runfiles = []
    for runfiles_attr in (
        ctx.attr.srcs,
        ctx.attr.hdrs,
        ctx.attr.deps,
        ctx.attr.data,
    ):
        for target in runfiles_attr:
            transitive_runfiles.append(target[DefaultInfo].default_runfiles)
    runfiles = runfiles.merge_all(transitive_runfiles)
    return [
        DefaultInfo(..., runfiles = runfiles),
        ...
    ]

自定义提供程序

您可以使用 provider 函数定义提供程序,以传达特定于规则的信息:

ExampleInfo = provider(
    "Info needed to compile/link Example code.",
    fields = {
        "headers": "depset of header Files from transitive dependencies.",
        "files_to_link": "depset of Files from compilation.",
    },
)

然后,规则实现函数可以构建和返回提供程序实例:

def _example_library_impl(ctx):
  ...
  return [
      ...
      ExampleInfo(
          headers = headers,
          files_to_link = depset(
              [output_file],
              transitive = [
                  dep[ExampleInfo].files_to_link for dep in ctx.attr.deps
              ],
          ),
      )
  ]
自定义初始化提供程序

您可以使用自定义预处理和验证逻辑来保护提供方的实例化。这可用于确保所有提供程序实例都满足特定不变性,或为用户提供更简洁的 API 来获取实例。

为此,请将 init 回调传递给 provider 函数。如果提供此回调,provider() 的返回类型将更改为两个值的元组:提供程序符号(即不使用 init 时的普通返回值)和“原始构造函数”。

在这种情况下,调用提供程序符号时,它会将参数转发给 init 回调,而不是直接返回新实例。回调的返回值必须是将字段名称(字符串)映射到值的字典;这用于初始化新实例的字段。请注意,回调可以具有任何签名,如果参数与签名不匹配,系统会报告错误,就像直接调用回调一样。

与之相反,原始构造函数会绕过 init 回调。

以下示例使用 init 对其参数进行预处理和验证:

# //pkg:exampleinfo.bzl

_core_headers = [...]  # private constant representing standard library files

# Keyword-only arguments are preferred.
def _exampleinfo_init(*, files_to_link, headers = None, allow_empty_files_to_link = False):
    if not files_to_link and not allow_empty_files_to_link:
        fail("files_to_link may not be empty")
    all_headers = depset(_core_headers, transitive = headers)
    return {"files_to_link": files_to_link, "headers": all_headers}

ExampleInfo, _new_exampleinfo = provider(
    fields = ["files_to_link", "headers"],
    init = _exampleinfo_init,
)

然后,规则实现可以按如下方式实例化提供程序:

ExampleInfo(
    files_to_link = my_files_to_link,  # may not be empty
    headers = my_headers,  # will automatically include the core headers
)

原始构造函数可用于定义不通过 init 逻辑的替代公开工厂函数。例如,exampleinfo.bzl 可以定义:

def make_barebones_exampleinfo(headers):
    """Returns an ExampleInfo with no files_to_link and only the specified headers."""
    return _new_exampleinfo(files_to_link = depset(), headers = all_headers)

通常,原始构造函数会绑定到名称以下划线开头的变量(上文中的 _new_exampleinfo),以便用户代码无法加载该变量并生成任意提供程序实例。

init 的另一种用途是阻止用户调用提供程序符号,并强制他们改用工厂函数:

def _exampleinfo_init_banned(*args, **kwargs):
    fail("Do not call ExampleInfo(). Use make_exampleinfo() instead.")

ExampleInfo, _new_exampleinfo = provider(
    ...
    init = _exampleinfo_init_banned)

def make_exampleinfo(...):
    ...
    return _new_exampleinfo(...)

可执行规则和测试规则

可执行规则用于定义可由 bazel run 命令调用的目标。测试规则是一种特殊的可执行规则,其目标还可以通过 bazel test 命令调用。如需创建可执行文件和测试规则,请在对 rule 的调用中将相应的 executabletest 参数设置为 True

example_binary = rule(
   implementation = _example_binary_impl,
   executable = True,
   ...
)

example_test = rule(
   implementation = _example_binary_impl,
   test = True,
   ...
)

测试规则的名称必须以 _test 结尾。(按照惯例,测试目标名称通常也以 _test 结尾,但这不是必需的。)非测试规则不得带有此后缀。

这两种规则都必须生成一个可执行的输出文件(可以是预声明的,也可以不是),该文件将由 runtest 命令调用。如需告知 Bazel 要将规则的哪个输出用作此可执行文件,请将其作为返回的 DefaultInfo 提供程序的 executable 参数传递。该 executable 会添加到规则的默认输出中(因此您无需将其同时传递给 executablefiles)。它还会隐式添加到 runfiles 中:

def _example_binary_impl(ctx):
    executable = ctx.actions.declare_file(ctx.label.name)
    ...
    return [
        DefaultInfo(executable = executable, ...),
        ...
    ]

生成此文件的操作必须为该文件设置可执行位。对于 ctx.actions.runctx.actions.run_shell 操作,应由操作调用的底层工具执行此操作。对于 ctx.actions.write 操作,请传递 is_executable = True

作为旧版行为,可执行规则具有特殊的 ctx.outputs.executable 预声明输出。如果您未使用 DefaultInfo 指定默认可执行文件,则此文件将用作默认可执行文件;否则,不得使用此文件。此输出机制已废弃,因为它不支持在分析时自定义可执行文件的名称。

请参阅可执行规则测试规则示例。

除了为所有规则添加的属性之外,可执行规则测试规则还隐式定义了其他属性。无法更改隐式添加的属性的默认值,但可以通过将私有规则封装在用于更改默认值的 Starlark 宏中来解决此问题:

def example_test(size = "small", **kwargs):
  _example_test(size = size, **kwargs)

_example_test = rule(
 ...
)

runfile 位置

使用 bazel run(或 test)运行可执行目标时,runfiles 目录的根目录与可执行文件相邻。路径如下所示:

# Given launcher_path and runfile_file:
runfiles_root = launcher_path.path + ".runfiles"
workspace_name = ctx.workspace_name
runfile_path = runfile_file.short_path
execution_root_relative_path = "%s/%s/%s" % (
    runfiles_root, workspace_name, runfile_path)

runfiles 目录下 File 的路径对应于 File.short_path

bazel 直接执行的二进制文件位于 runfiles 目录的根目录旁边。不过, runfile 调用的二进制文件无法做出同样的假设。为缓解此问题,每个二进制文件都应提供一种方法,以便使用环境、命令行参数或标志接受其 runfile 根目录作为参数。这样,二进制文件就可以将正确的规范化 runfile 根目录传递给其调用的二进制文件。如果未设置该值,二进制文件可以推断它是被调用的第一个二进制文件,并查找相邻的 runfiles 目录。

高级主题

请求输出文件

单个目标可以有多个输出文件。运行 bazel build 命令时,系统会将提供给该命令的一些目标的输出视为请求。Bazel 只会构建这些请求的文件以及它们直接或间接依赖的文件。(从操作图的角度来看,Bazel 仅执行可作为请求文件的传递依赖项访问的操作。)

除了默认输出之外,您还可以在命令行中明确请求任何预声明的输出。规则可以使用输出属性指定预声明的输出。在这种情况下,用户在实例化规则时会明确选择输出标签。如需获取输出属性的 File 对象,请使用 ctx.outputs 的相应属性。规则还可以根据目标名称隐式定义预声明的输出,但此功能已废弃。

除了默认输出之外,还有输出组,即可以一起请求的输出文件集合。您可以使用 --output_groups 请求这些权限。例如,如果目标 //pkg:mytarget 的规则类型具有 debug_files 输出组,则可以通过运行 bazel build //pkg:mytarget --output_groups=debug_files 来构建这些文件。由于非预声明输出没有标签,因此只能通过在默认输出或输出组中显示来请求它们。

您可以使用 OutputGroupInfo 提供程序指定输出组。请注意,与许多内置提供程序不同,OutputGroupInfo 可以接受具有任意名称的参数,以便使用该名称定义输出组:

def _example_library_impl(ctx):
    ...
    debug_file = ctx.actions.declare_file(name + ".pdb")
    ...
    return [
        DefaultInfo(files = depset([output_file]), ...),
        OutputGroupInfo(
            debug_files = depset([debug_file]),
            all_files = depset([output_file, debug_file]),
        ),
        ...
    ]

此外,与大多数提供程序不同,OutputGroupInfo在这种情况下,系统会合并生成的提供程序。

请注意,OutputGroupInfo 通常不应用于将特定类型的文件从目标传递到其使用者的操作。请改为为此定义特定于规则的提供程序

配置

假设您要为其他架构构建 C++ 二进制文件。构建过程可能很复杂,涉及多个步骤。某些中间二进制文件(例如编译器和代码生成器)必须在执行平台(可能是您的主机或远程执行器)上运行。某些二进制文件(例如最终输出)必须针对目标架构进行构建。

因此,Bazel 提供了“配置”和转换的概念。最顶层的目标(命令行中请求的目标)内置于“target”配置中,而应在执行平台上运行的工具内置于“exec”配置中。规则可能会根据配置生成不同的操作,例如更改传递给编译器的 CPU 架构。在某些情况下,不同的配置可能需要使用相同的库。如果发生这种情况,系统会分析该模块并可能多次构建该模块。

默认情况下,Bazel 会使用与目标本身相同的配置构建目标的依赖项,也就是说,不会进行转换。如果依赖项是构建目标所需的工具,则相应的属性应指定向 exec 配置的转换。这会导致该工具及其所有依赖项针对执行平台进行构建。

对于每个依赖项属性,您可以使用 cfg 来决定依赖项是否应在相同配置中构建,还是应转换为 exec 配置。如果依赖项属性具有标志 executable = True,则必须显式设置 cfg。这是为了防止意外为错误的配置构建工具。查看示例

通常,运行时所需的源代码、依赖项库和可执行文件可以使用相同的配置。

应针对 exec 配置构建在构建过程中执行的工具(例如编译器或代码生成器)。在这种情况下,请在属性中指定 cfg = "exec"

否则,应针对目标配置构建在运行时使用的可执行文件(例如测试的一部分)。在这种情况下,请在属性中指定 cfg = "target"

cfg = "target" 实际上不会执行任何操作:它只是一个方便值,可帮助规则设计师明确说明其意图。如果为 executable = False(即 cfg 为可选),请仅在真正有助于提高可读性时设置此项。

您还可以使用 cfg = my_transition 使用用户定义的转换,这样规则作者就可以非常灵活地更改配置,但缺点是会使 build 图更大且更难理解

注意:历史上,Bazel 没有执行平台的概念,而是将所有构建操作都视为在主机上运行。6.0 之前的 Bazel 版本会创建一个独特的“主机”配置来表示这一点。如果您在代码或旧文档中看到对“主机”的引用,这就是指的“主机”。我们建议使用 Bazel 6.0 或更高版本,以避免这种额外的概念开销。

配置 fragment

规则可以访问 cppjava配置 fragment。不过,必须声明所有必需的 fragment,以免出现访问错误:

def _impl(ctx):
    # Using ctx.fragments.cpp leads to an error since it was not declared.
    x = ctx.fragments.java
    ...

my_rule = rule(
    implementation = _impl,
    fragments = ["java"],      # Required fragments of the target configuration
    ...
)

通常,runfiles 树中文件的相对路径与源代码树或生成的输出树中该文件的相对路径相同。如果出于某种原因,这些值需要不同,您可以指定 root_symlinkssymlinks 参数。root_symlinks 是一个字典,用于将路径映射到文件,其中路径相对于 runfiles 目录的根目录。symlinks 字典保持不变,但路径会隐式附加主工作区的名称(而非包含当前目标的代码库的名称)。

    ...
    runfiles = ctx.runfiles(
        root_symlinks = {"some/path/here.foo": ctx.file.some_data_file2}
        symlinks = {"some/path/here.bar": ctx.file.some_data_file3}
    )
    # Creates something like:
    # sometarget.runfiles/
    #     some/
    #         path/
    #             here.foo -> some_data_file2
    #     <workspace_name>/
    #         some/
    #             path/
    #                 here.bar -> some_data_file3

如果使用 symlinksroot_symlinks,请务必避免将两个不同的文件映射到 runfiles 树中的同一路径。这会导致构建失败,并显示描述冲突的错误。如需解决此问题,您需要修改 ctx.runfiles 参数以移除冲突。系统会对使用该规则的所有目标以及依赖于这些目标的任何类型的目标执行此检查。如果您的工具可能会被其他工具间接使用,这种做法尤其危险;在工具及其所有依赖项的 runfile 中,符号链接名称必须是唯一的。

代码覆盖率

运行 coverage 命令时,构建可能需要为特定目标添加覆盖率插桩。该 build 还会收集插桩的源文件列表。要考虑的目标子集由标志 --instrumentation_filter 控制。除非指定了 --instrument_test_targets,否则测试目标会被排除。

如果规则实现在构建时添加覆盖检测,则需要 在其实施功能中考虑这一点。 ctx.coverage_instrumented 返回 如果应检测目标源,则在覆盖模式下True

# Are this rule's sources instrumented?
if ctx.coverage_instrumented():
  # Do something to turn on coverage for this compile action

在覆盖率模式下始终需要开启的逻辑(无论目标的源代码是否具体插桩)可以基于 ctx.configuration.coverage_enabled 进行条件控制。

如果规则在编译之前直接包含其依赖项中的源代码(例如头文件),则可能还需要开启编译时插桩,以便插桩依赖项的源代码:

# Are this rule's sources or any of the sources for its direct dependencies
# in deps instrumented?
if (ctx.configuration.coverage_enabled and
    (ctx.coverage_instrumented() or
     any([ctx.coverage_instrumented(dep) for dep in ctx.attr.deps]))):
    # Do something to turn on coverage for this compile action

规则还应提供有关哪些属性与 InstrumentedFilesInfo 提供商的覆盖率相关的信息,这些信息应使用 coverage_common.instrumented_files_info 构建。instrumented_files_infodependency_attributes 参数应列出所有运行时依赖项属性,包括 deps 等代码依赖项和 data 等数据依赖项。如果可能会添加覆盖率插桩,source_attributes 参数应列出规则的源文件属性:

def _example_library_impl(ctx):
    ...
    return [
        ...
        coverage_common.instrumented_files_info(
            ctx,
            dependency_attributes = ["deps", "data"],
            # Omitted if coverage is not supported for this rule:
            source_attributes = ["srcs", "hdrs"],
        )
        ...
    ]

如果未返回 InstrumentedFilesInfo,系统会为每个未在属性架构中将 cfg 设置为 "exec" 的非工具依赖项属性创建一个默认依赖项。dependency_attributes(这并不是理想的行为,因为它会将 srcs 等属性放入 dependency_attributes 而不是 source_attributes,但这样可以避免为依赖项链中的所有规则进行显式覆盖率配置。)

验证操作

有时,您需要验证 build 的某些方面,而执行此验证所需的信息仅在工件(源文件或生成的文件)中提供。由于此类信息位于工件中,规则无法在分析时执行此验证,因为规则无法读取文件。相反,操作必须在执行时执行此验证。如果验证失败,相应操作将失败,构建也将失败。

可能要运行的验证示例包括静态分析、lint 检查、依赖项和一致性检查以及样式检查。

验证操作还可以通过将构建工件不需要的操作部分移至单独的操作来帮助提升构建性能。例如,如果一个执行编译和 lint 的操作可以拆分为编译操作和 lint 操作,那么 lint 操作可以作为验证操作运行,并与其他操作并行运行。

这些“验证操作”通常不会生成在 build 中的其他位置使用的任何内容,因为它们只需断言其输入的内容即可。不过,这会带来一个问题:如果验证操作未生成任何在 build 的其他位置使用的内容,规则如何让该操作运行?过去,此方法是让验证操作输出一个空文件,然后人为地将该输出添加到 build 中某些其他重要操作的输入中:

之所以能这样做,是因为 Bazel 在运行编译操作时始终会运行验证操作,但这有明显的缺点:

  1. 验证操作位于构建的关键路径中。由于 Bazel 认为运行编译操作需要空输出,因此它会先运行验证操作,即使编译操作会忽略输入也是如此。这会降低并行性并减慢构建速度。

  2. 如果 build 中的其他操作可能会运行,而不是编译操作,则还需要将验证操作的空输出添加到这些操作(例如 java_library 的源 jar 输出)。如果稍后添加了可能会代替编译操作的新操作,并且意外遗漏了空的验证输出,也会出现此问题。

解决这些问题的方法是使用“验证输出组”。

验证输出组

验证输出组是一种输出组,用于存储验证操作的其他未使用的输出,以免这些输出需要人为地添加到其他操作的输入中。

此组具有特殊性,因为无论 --output_groups 标志的值如何,无论目标的依赖项如何(例如,在命令行上作为依赖项,或通过目标的隐式输出),系统始终会请求其输出。请注意,系统仍会应用正常的缓存和增量功能:如果验证操作的输入未发生变化,并且验证操作之前已成功,则系统不会运行验证操作。

使用此输出组仍然需要验证操作输出某个文件,即使是空文件也是如此。这可能需要封装一些通常不会创建输出的工具,以便创建文件。

在以下三种情况下,系统不会运行目标的验证操作:

  • 将目标作为工具进行依赖时
  • 当目标作为隐式依赖项(例如以“_”开头的属性)被依赖时
  • 在 exec 配置中构建目标时。

假设这些目标具有自己的单独 build 和测试,可发现任何验证失败情况。

使用“验证输出”组

验证输出组的名称为 _validation,其使用方式与任何其他输出组相同:

def _rule_with_validation_impl(ctx):

  ctx.actions.write(ctx.outputs.main, "main output\n")
  ctx.actions.write(ctx.outputs.implicit, "implicit output\n")

  validation_output = ctx.actions.declare_file(ctx.attr.name + ".validation")
  ctx.actions.run(
    outputs = [validation_output],
    executable = ctx.executable._validation_tool,
    arguments = [validation_output.path],
  )

  return [
    DefaultInfo(files = depset([ctx.outputs.main])),
    OutputGroupInfo(_validation = depset([validation_output])),
  ]


rule_with_validation = rule(
  implementation = _rule_with_validation_impl,
  outputs = {
    "main": "%{name}.main",
    "implicit": "%{name}.implicit",
  },
  attrs = {
    "_validation_tool": attr.label(
        default = Label("//validation_actions:validation_tool"),
        executable = True,
        cfg = "exec"
    ),
  }
)

请注意,验证输出文件不会添加到 DefaultInfo 或任何其他操作的输入中。如果目标由标签依赖,或者目标的任何隐式输出被直接或间接依赖,则此类规则的目标的验证操作仍会运行。

通常,验证操作的输出应仅进入验证输出组,而不要添加到其他操作的输入中,因为这可能会降低并行处理的效果。不过请注意,Bazel 没有任何特殊检查来强制执行此操作。因此,您应测试验证操作输出是否未添加到 Starlark 规则测试中的任何操作的输入。例如:

load("@bazel_skylib//lib:unittest.bzl", "analysistest")

def _validation_outputs_test_impl(ctx):
  env = analysistest.begin(ctx)

  actions = analysistest.target_actions(env)
  target = analysistest.target_under_test(env)
  validation_outputs = target.output_groups._validation.to_list()
  for action in actions:
    for validation_output in validation_outputs:
      if validation_output in action.inputs.to_list():
        analysistest.fail(env,
            "%s is a validation action output, but is an input to action %s" % (
                validation_output, action))

  return analysistest.end(env)

validation_outputs_test = analysistest.make(_validation_outputs_test_impl)

验证操作标志

运行验证操作由 --run_validations 命令行标志控制,该标志的默认值为 true。

已弃用的功能

已废弃的预声明输出

使用预声明的输出有两种已废弃的方法:

  • ruleoutputs 参数用于指定输出属性名称与字符串模板之间的映射,以生成预声明的输出标签。请优先使用非预声明输出,并将输出显式添加到 DefaultInfo.files。将规则目标的标签用作使用输出的规则的输入,而不是使用预声明的输出的标签。

  • 对于可执行规则ctx.outputs.executable 是指与规则目标同名的预声明可执行输出。最好明确声明输出(例如使用 ctx.actions.declare_file(ctx.label.name)),并确保用于生成可执行文件的命令将其权限设置为允许执行。将可执行文件输出显式传递给 DefaultInfoexecutable 参数。

应避免使用 runfile 功能

ctx.runfilesrunfiles 类型具有一组复杂的功能,其中许多功能出于历史原因而保留。以下建议有助于降低复杂性:

  • 避免使用 ctx.runfilescollect_datacollect_default 模式。这些模式会以令人困惑的方式隐式收集特定硬编码依赖项边缘的运行文件。请改用 ctx.runfilesfilestransitive_files 参数添加文件,或使用 runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles) 合并依赖项中的 runfile。

  • 避免使用 DefaultInfo 构造函数的 data_runfilesdefault_runfiles。请改为指定 DefaultInfo(runfiles = ...)。 出于历史原因,我们保留了“默认”和“数据”运行文件之间的区别。例如,某些规则会将其默认输出放入 data_runfiles,但不会放入 default_runfiles。规则应同时包含默认输出,并从提供 runfile 的属性(通常是 data)中合并 default_runfilesdata_runfiles

  • DefaultInfo 检索 runfiles 时(通常仅用于合并当前规则及其依赖项之间的运行文件),请使用 DefaultInfo.default_runfiles而不是 DefaultInfo.data_runfiles

从旧版提供商迁移

过去,Bazel 提供程序是 Target 对象上的简单字段。这些字段是使用点运算符访问的,并且是通过将字段放入规则实现函数返回的 struct(而不是提供程序对象列表)中创建的:

return struct(example_info = struct(headers = depset(...)))

您可以从 Target 对象的相应字段中检索此类提供程序:

transitive_headers = [hdr.example_info.headers for hdr in ctx.attr.hdrs]

此样式已废弃,不应在新代码中使用;请参阅以下信息,了解可能对您迁移有帮助的信息。新提供程序机制可避免名称冲突。它还支持数据隐藏,方法是要求访问提供程序实例的任何代码都必须使用提供程序符号检索该实例。

目前,旧版提供程序仍受支持。规则可以同时返回旧版和新版提供程序,如下所示:

def _old_rule_impl(ctx):
  ...
  legacy_data = struct(x = "foo", ...)
  modern_data = MyInfo(y = "bar", ...)
  # When any legacy providers are returned, the top-level returned value is a
  # struct.
  return struct(
      # One key = value entry for each legacy provider.
      legacy_info = legacy_data,
      ...
      # Additional modern providers:
      providers = [modern_data, ...])

如果 dep 是此规则实例的生成 Target 对象,则可以将提供程序及其内容检索为 dep.legacy_info.xdep[MyInfo].y

除了 providers 之外,返回的结构体还可以采用几个具有特殊含义的其他字段(因此不会创建相应的旧版提供程序):

  • filesrunfilesdata_runfilesdefault_runfilesexecutable 字段与 DefaultInfo 中的同名字段相对应。不允许同时指定这些字段并返回 DefaultInfo 提供程序。

  • 字段 output_groups 采用结构体值,对应于 OutputGroupInfo

在规则的 provides 声明和依赖项属性的 providers 声明中,旧版提供程序会作为字符串传入,而新版提供程序会通过其 Info 符号传入。迁移时,请务必从字符串更改为符号。对于难以原子化更新所有规则的复杂或大型规则集,您可以按照以下步骤顺序操作,以便更轻松地完成更新:

  1. 使用上述语法修改用于生成旧版提供程序的规则,以便同时生成旧版和新版提供程序。对于声明会返回旧版提供程序的规则,请更新该声明,使其同时包含旧版和新版提供程序。

  2. 修改使用旧版提供程序的规则,改为使用新版提供程序。如果任何属性声明都需要旧版提供程序,请将其更新为需要新版提供程序。(可选)您可以让使用方接受或要求使用任一提供程序,从而将此工作与第 1 步交错执行:使用 hasattr(target, 'foo') 测试是否存在旧版提供程序,或使用 FooInfo in target 测试是否存在新版提供程序。

  3. 从所有规则中彻底移除旧版提供程序。