规则

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

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

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

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

Bazel 本身中内置了一些规则。这些原生规则(例如 cc_libraryjava_binary)为特定语言提供一些核心支持。通过定义自己的规则,您可以为 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"],
    ...
)

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

您也可以通过 Starlark 函数调用规则,并在 .bzl 文件中加载规则。调用规则的 Starlark 函数称为 Starlark 宏。Starlark 宏最终必须从 BUILD 文件调用,并且只能在加载阶段(当 BUILD 文件被评估以实例化目标时)调用。

属性

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

特定于规则的属性(例如 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 对象的形式表示。这些对象包含在执行目标的实现函数时生成的 providers

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]

对于从目标的实现函数(而不是提供程序对象列表)返回 struct 的旧版样式:

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

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

transitive_headers = [hdr.example_info.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=Truectx.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 对象。

Action

操作描述如何从一组输入生成一组输出,例如“run gcc on hello.c and get hello.o”。创建操作后,Bazel 不会立即运行该命令,因为一项操作可以依赖于另一项操作的输出,所以它会在依赖关系图中注册该操作。例如,在 C 语言中,必须在编译器之后调用链接器。

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

ctx.actions.args 可用于高效累积操作的参数。它可避免在执行时间之前展平依赖项:

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

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

操作必须列出其所有输入。系统允许列出不使用的输入,但这种方式会导致效率低下。

Action 必须创建其所有输出。它们可以写入其他文件,但输出中不包含的任何内容都无法供使用者使用。所有声明的输出都必须通过某个操作写入。

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

依赖项由 Bazel 解析,后者将决定执行哪些操作。如果依赖关系图中存在循环,则会出错。创建操作并不能保证它一定会执行,具体取决于构建是否需要其输出。

提供方

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

由于规则的实现函数只能从实例化的目标的直接依赖项中读取提供程序,因此规则需要从目标的依赖项转发所有需要被目标使用方知道的信息,具体做法通常是将其累积到 depset 中。

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

旧的实现函数也可以采用旧版样式编写,其中实现函数会返回 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 默认为所有预先声明的输出(通常是由输出属性创建的输出)。

执行操作的规则应提供默认输出,即使这些输出不应直接使用。所请求输出图中未包含的操作会被剪除。如果输出仅供目标的使用方使用,则在隔离构建目标时将不会执行这些操作。这会使调试变得更加困难,因为仅重新构建失败的目标不会重现失败。

Runfiles

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

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

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

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

# It's possible to define an init accepting positional arguments, but
# 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(
    ...
    init = _exampleinfo_init)

export ExampleInfo

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

    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 executable_file and runfile_file:
runfiles_root = executable_file.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 调用的二进制文件不能做出相同的假设。为了缓解此问题,每个二进制文件都应提供一种方法,以使用环境或命令行参数/标志接受其 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 以及应用该切面的规则目标返回,前提是它们不定义相同的输出组。在这种情况下,系统会合并生成的提供程序。

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

配置

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

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

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

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

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

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

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

cfg="target" 实际上不会执行任何操作:它只是一个辅助值,用于帮助规则设计者明确他们的意图。如果为 executable=False(这意味着 cfg 是可选的),请仅在确实有助于提高可读性时才设置此字段。

您还可以使用 cfg=my_transition 来使用用户定义的转换,这样规则作者在更改配置时具有极大的灵活性,但其缺点是使构建图变大且不那么易于理解

注意:Bazel 过去没有执行平台的概念,而是将所有构建操作都视为在宿主机上运行。因此,只有一个“主机”配置和一个可用于在主机配置中构建依赖项的“主机”转换。许多规则仍然对其工具使用“主机”转换,但这种转换目前已弃用,并将尽可能迁移到使用“exec”转换。

“host”和“exec”配置之间存在许多差异:

  • “host”是终端状态,“exec”不是:一旦依赖项进入“host”配置,就不允许再进行转换。进入“exec”配置后,您可以继续进行进一步的配置转换。
  • “host”是单体式的,“exec”不是:只有一个“host”配置,但每个执行平台可以有不同的“exec”配置。
  • “host”假设您在与 Bazel 相同的机器上运行工具,或在非常类似的机器上运行工具。现在已不然了:您可以在本地机器或远程执行器上运行构建操作,但无法保证远程执行器与本地机器具有相同的 CPU 和操作系统。

“exec”和“host”配置都会应用相同的选项更改(例如,从 --host_compilation_mode 设置 --compilation_mode,从 --host_cpu 设置 --cpu,等等)。不同之处在于,“host”配置以所有其他标志的 default 值开头,而“exec”配置以目标配置的当前标志值开头。

配置 fragment

规则可以访问配置 fragment,如 cppjavajvm。不过,必须声明所有必需的 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
    host_fragments = ["java"], # Required fragments of the host configuration
    ...
)

ctx.fragments 仅为目标配置提供配置 fragment。如果您要访问宿主配置的 fragment,请改用 ctx.host_fragments

通常,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 可能需要为特定目标添加覆盖率插桩。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,则系统会为 dependency_attributes 中的每个未将 cfg 设置为 "host""exec" 的非工具依赖项属性创建一个默认属性。(这并非理想行为,因为它将 srcs 等属性放在 dependency_attributes 中而非 source_attributes 中,但这样就无需为依赖项链中的所有规则进行显式覆盖率配置。)

验证操作

有时,您需要对 build 进行验证,并且验证所需的信息仅在工件(源文件或生成的文件)中可用。由于这些信息位于工件中,因此规则在分析时无法执行此验证,因为规则无法读取文件。相反,操作必须在执行时执行此验证。如果验证失败,操作将失败,构建也将失败。

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

此外,验证操作还可以将构建工件不需要的部分操作拆分为单独的操作,从而帮助您提高构建性能。例如,如果可以将执行编译和 lint 的单个操作拆分为编译操作和 lint 检查操作,那么 lint 检查操作可以作为验证操作运行,并与其他操作并行运行。

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

这种做法行之有效,因为在运行编译操作时,Bazel 始终会运行验证操作,但这样做有很大的缺点:

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

  2. 如果可能运行构建中的其他操作(而不是编译操作),那么也需要将验证操作的空输出(例如,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 参数。

应避免的 Runfiles 功能

ctx.runfilesrunfiles 类型具有一组复杂的特征,其中许多特征因遗留问题而被保留。以下建议有助于降低复杂性:

  • 避免使用 ctx.runfilescollect_datacollect_default 模式。这些模式会以令人困惑的方式在某些硬编码的依赖项边缘隐式收集 Runfile。而是使用 ctx.runfilesfilestransitive_files 参数添加文件,或者通过合并具有 runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles) 的依赖项的运行文件来添加文件。

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

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

从旧版提供商迁移

过去,Bazel 提供程序是 Target 对象上的简单字段。它们使用点运算符进行访问,并且是通过将字段放入由规则的实现函数返回的结构体来创建的。

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

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

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. 从所有规则中完全移除旧版提供方。