规则

报告问题 查看来源 Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

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

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

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

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

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 = True),或者专门创建测试可执行文件(使用 test = True)。如果是后者,则该规则为测试规则,并且规则的名称必须以 _test 结尾。

目标实例化

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

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

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

对 build 规则的每次调用都不返回值,但具有定义目标的副作用。这称为规则的实例化。此元素用于指定新目标的名称以及目标属性的值。

规则还可以从 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 文件中指定它。隐式依赖项对于硬编码规则与工具(例如编译器等 build-time 依赖项)之间的关系非常有用,因为大多数情况下,用户并不关心规则使用什么工具。在规则的实现函数中,此属性与其他依赖项的处理方式相同。

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

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
    ...

ctx.file 包含一个 FileNone,用于规范设置了 allow_single_file = True 的依赖项属性。ctx.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 可用于高效地累积操作的实参。在执行时之前,避免扁平化 depsets:

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],
    )
    ...

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

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

操作必须创建其所有输出。它们可能会写入其他文件,但输出中未包含的任何内容都无法供消费者使用。所有声明的输出都必须由某个操作写入。

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

依赖项由 Bazel 解析,Bazel 会决定要执行哪些操作。如果依赖关系图中存在环路,则会出错。创建操作并不保证该操作一定会执行,这取决于构建是否需要该操作的输出。

提供商

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

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

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

旧的实现函数也可以采用旧版样式编写,其中实现函数返回 struct 而不是提供程序对象的列表。强烈建议不要使用此样式,规则应迁移到其他样式

默认输出

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

默认输出由 DefaultInfofiles 参数指定:

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

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

执行操作的规则应提供默认输出,即使这些输出预计不会直接使用。不在所请求输出的图中的操作会被剪除。如果输出仅供目标的消费者使用,那么在单独构建目标时,系统不会执行这些操作。这会增加调试难度,因为仅重新构建失败的目标无法重现失败。

Runfiles

runfile 是目标在运行时(而非构建时)使用的一组文件。在执行阶段,Bazel 会创建一个包含指向 runfiles 的符号链接的目录树。此命令会为二进制文件准备环境,以便二进制文件在运行时可以访问 runfiles。

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

有些规则指定了属性(通常命名为 data),这些属性的输出会添加到目标的 runfiles 中。还应从 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(
 ...
)

Runfiles 位置

当可执行目标通过 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)

运行文件目录下的 File 的路径对应于 File.short_path

bazel 直接执行的二进制文件与 runfiles 目录的根目录相邻。不过,从 runfiles 调用的二进制文件不能做出相同的假设。为了缓解此问题,每个二进制文件都应提供一种方法来接受其 runfiles 根作为参数,方法是使用环境变量、命令行实参或标志。这样,二进制文件就可以将正确的规范运行文件根传递给其调用的二进制文件。如果未设置,二进制文件可以猜测它是第一个被调用的二进制文件,并查找相邻的 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 可由 aspect 和应用该 aspect 的规则目标返回,前提是它们未定义相同的输出组。在这种情况下,系统会合并生成的提供方。

请注意,OutputGroupInfo 通常不应用于传达从目标到其消费者行为的特定类型的文件。请改为定义特定于规则的提供商

配置

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

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

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

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

一般来说,运行时所需的源代码、依赖库和可执行文件可以使用相同的配置。

作为 build 的一部分执行的工具(例如编译器或代码生成器)应针对 exec 配置进行 build。在这种情况下,请在属性中指定 cfg = "exec"

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

cfg = "target" 实际上不会执行任何操作:它纯粹是一个便利值,可帮助规则设计者明确表达自己的意图。当 executable = False 时,表示 cfg 是可选的,只有在确实有助于提高可读性的情况下才设置此项。

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

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

配置片段

规则可以访问配置 fragment,例如 cppjava。不过,必须声明所有必需的 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 树中的同一路径。这会导致 build 失败,并显示描述冲突的错误。如需修复此问题,您需要修改 ctx.runfiles 实参以消除冲突。系统将针对使用您的规则的任何目标以及依赖于这些目标的任何类型的目标执行此检查。如果您的工具可能会被另一个工具以传递方式使用,那么这种情况尤其危险;符号链接名称在工具及其所有依赖项的 runfiles 中必须是唯一的。

代码覆盖率

运行 coverage 命令时,构建可能需要为某些目标添加覆盖率插桩。构建还会收集已插桩的源文件列表。所考虑的目标子集由标志 --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.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,则会创建一个默认的 InstrumentedFilesInfo,其中每个未在属性架构中将 cfg 设置为 "exec" 的非工具依赖项属性都位于 dependency_attributes 中。(这不是理想的行为,因为它会将 srcs 等属性放在 dependency_attributes 中,而不是 source_attributes 中,但它避免了为依赖链中的所有规则进行显式覆盖配置的需要。)

测试规则

测试规则需要进行额外设置才能生成覆盖率报告。规则本身必须添加以下隐式属性:

my_test = rule(
    ...,
    attrs = {
        ...,
        # Implicit dependencies used by Bazel to generate coverage reports.
        "_lcov_merger": attr.label(
            default = configuration_field(fragment = "coverage", name = "output_generator"),
            executable = True,
            cfg = config.exec(exec_group = "test"),
        ),
        "_collect_cc_coverage": attr.label(
            default = "@bazel_tools//tools/test:collect_cc_coverage",
            executable = True,
            cfg = config.exec(exec_group = "test"),
        )
    },
    test = True,
)

通过使用 configuration_field,只要不请求覆盖率,就可以避免对 Java LCOV 合并工具的依赖。

运行测试时,它应以一个或多个具有唯一名称的 LCOV 文件的形式,将覆盖率信息输出到 COVERAGE_DIR 环境变量指定的目录中。然后,Bazel 将使用 _lcov_merger 工具将这些文件合并为单个 LCOV 文件。如果存在,它还会使用 _collect_cc_coverage 工具收集 C/C++ 代码覆盖率。

基准覆盖率

由于覆盖率仅针对最终位于测试依赖树中的代码进行收集,因此覆盖率报告可能会产生误导,因为它们不一定涵盖 --instrumentation_filter 标志匹配的所有代码。

为此,Bazel 允许规则使用 ctx.instrumented_files_infobaseline_coverage_files 属性指定基准覆盖率文件。这些文件必须由用户定义的操作以 LCOV 格式生成,并且应列出目标源文件中的所有行、分支、函数和/或块(根据 sources_attributesextensions 参数)。对于已检测覆盖率的目标中的源文件,Bazel 会将其基准覆盖率合并到使用 --combined_report 生成的合并覆盖率报告中,从而确保未测试的文件仍显示为未覆盖。

如果规则未提供任何基准覆盖率文件,Bazel 会生成仅提及源文件路径但不包含任何有关其内容的合成覆盖率信息。

验证操作

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

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

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

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

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

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

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

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

验证输出组

验证输出组是一种输出组,旨在保存验证操作中未使用的输出,这样就不需要人为地将其添加到其他操作的输入中。

此组的特殊之处在于,无论 --output_groups 标志的值为何,也无论目标依赖项的依赖方式如何(例如,在命令行中、作为依赖项或通过目标的隐式输出),系统始终会请求此组的输出。请注意,正常的缓存和增量仍适用:如果验证操作的输入未发生变化,且验证操作之前已成功完成,则不会再次运行验证操作。

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

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

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

假设这些目标具有自己的单独 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。

已弃用的功能

已弃用的预声明输出

使用预先声明的输出有 2 种已弃用的方式:

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

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

需避免使用的 runfiles 功能

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

  • 避免使用 ctx.runfilescollect_datacollect_default 模式。这些模式会以令人困惑的方式隐式收集某些硬编码依赖关系边缘上的 runfile。请改为使用 ctx.runfilesfilestransitive_files 参数添加文件,或者通过使用 runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles) 合并来自依赖项的 runfile。

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

  • DefaultInfo 中检索 runfiles 时(通常仅用于合并当前规则及其依赖项之间的 runfile),请使用 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. 从所有规则中完全移除旧版提供商。