规则

报告问题 查看来源 每晚 · 7.2。 · 7.1敬上 · 7.0。 · 6.5 · 6.4

规则定义了 Bazel 对其执行的一系列操作 这些输入可生成一组输出, providers - 规则的 实现函数。例如,C++ 二元规则可能会:

  1. 获取一组 .cpp 源文件(输入)。
  2. 对源文件运行 g++(操作)。
  3. DefaultInfo 提供程序连同可执行输出和其他文件一起返回 在运行时可用
  4. 返回 CcInfo 提供程序,以及从 目标及其依赖项。

从 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 对文件进行评估以实例化目标。

属性

属性是规则参数。属性可以为 目标的实现,也可以引用其他 目标,创建依赖关系图。

特定于规则的属性(例如 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_list, attr.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。它们会返回 providers

目标

依赖项在分析时表示为 Target 对象的操作。这些对象包含在调用 providers 时生成的 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]

对于从 struct 返回的旧版样式, target 的实现函数,而不是 provider 对象列表:

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 的依赖项属性的字段。

声明输出

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

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

对于预先声明的输出,例如 输出属性,可以检索 File 对象 来自 ctx.outputs 的相应字段。

操作

操作描述如何根据一组输入生成一组输出, 例如“在 hello.c 上运行 gcc 并获取 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 命令 必须指定您希望进行膨胀的文件(在运行 unzip 之前)。 在内部创建可变数量文件的操作可以将这些文件封装在 单个文件(例如 zip、tar 或其他归档文件格式)。

操作必须列出其所有输入。列出未使用的输入 但效率低下

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

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

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

提供商

提供程序是规则向其他规则公开的信息, 一切都依赖于它这些数据可能包括输出文件、库、要传递的参数 还是目标消费者应该知道的任何其他信息 。

由于规则的实施函数只能从 实例化目标的直接依赖项,规则需要转发任何 来自目标依赖项且目标的 消费者,通常通过将其累加到 depset 中。

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

旧的实现函数也可以采用旧版样式编写,其中 实现函数会返回 struct,而不是 provider 对象。强烈建议不要使用这种样式, 从该区域迁走了。

默认输出

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

默认输出由以下函数的 files 参数指定: DefaultInfo

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

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

执行操作的规则应提供默认输出,即便是这些输出 预计不会直接使用。图表中未包含的操作 删除请求的输出如果输出仅供目标使用方使用, 如果目标是单独构建的,系统将不会执行这些操作。这个 会导致调试变得更加困难,因为仅重新构建失败的目标 重现故障。

Runfile

Runfile 是目标在运行时使用的一组文件(与 build 不同 )。在执行阶段,Bazel 会创建 包含指向 runfile 的符号链接的目录树。这一阶段 以便能够在运行时访问 runfile。

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

有些规则会指定属性,通常命名为 data:其输出会添加到 目标的runfiles。还应从 data 合并 Runfile,以及 从任何可能提供代码用于最终执行的属性中, 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
              ],
          ),
      )
  ]
自定义提供程序的初始化

您可以使用自定义 预处理和验证逻辑。这可用于确保 provider 实例遵守某些不变量,或者为用户提供更简洁的 API, 获取实例。

为此,请将 init 回调传递给 provider 函数。如果存在此回调,则 provider() 的返回值类型更改为包含两个值的元组:提供程序 符号,这是未使用 init 时的普通返回值,而“raw 构造函数”。

在这种情况下,调用提供程序符号时,而不是直接返回 新实例时,它会将参数转发给 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 命令调用。可执行规则和测试规则 设置相应的 executable 或 在对 rule 的调用中将 test 参数添加到 True

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

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

测试规则的名称必须以 _test 结尾。(也经常测试目标名称 按惯例以 _test 结尾,但这并非强制性要求。)非测试规则不得 都存在此后缀

这两种规则都必须生成可执行的输出文件(不一定会生成 )将由 runtest 命令调用。为了说明 Bazel 将规则的哪些输出用作此可执行文件,将其作为 返回的 DefaultInfoexecutable 参数 提供商。该 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 调用的二进制文件无法 相同的假设。为了缓解这一问题,每个二进制文件都应提供一种方式 使用环境或命令行接受其 runfiles 根目录作为参数 参数/标志。这样,二进制文件就可以传递正确的规范 runfile 根目录 传递给它调用的二进制文件如果未设置该属性,二进制文件可能会猜测这是 调用第一个二进制文件,并查找相邻的 runfiles 目录。

高级主题

请求输出文件

一个目标可以有多个输出文件。当 bazel build 命令被触发时, 指定给该命令的目标的部分输出会被视为 必须被请求。Bazel 只会构建这些请求的文件 直接或间接依赖的客户机(对于操作图,只有 Bazel 执行可作为 请求的文件。)

默认输出外,任何预先声明的输出 。规则可以指定预先声明的 通过 output 属性输出。在这种情况下,用户 在实例化规则时明确为输出选择标签。要获取 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 以及该切面所应用到的规则目标,例如 但前提是它们没有定义相同的输出组。在这种情况下, provider 合并。

请注意,OutputGroupInfo 通常不应用于表示特定排序 从目标文件转移到其使用方的操作。定义 针对具体规则的提供商

配置

假设您要为不同的架构构建 C++ 二进制文件。通过 构建过程可能比较复杂,涉及多个步骤。一些中级概念 二进制文件(例如编译器和代码生成器)必须在 执行平台(可以是您的主机、 或远程执行器)。有些二进制文件(例如最终输出)必须为 目标架构。

因此,Bazel 有个概念和过渡。通过 最顶层的目标(命令行中请求的目标)内置于 “目标”配置,而应在执行平台上运行的工具 都是在“exec”中构建的配置。规则可能会根据 配置,例如,更改通过系统传递的 传递给编译器。在某些情况下,不同的应用可能需要 配置。如果发生这种情况,系统会对其进行分析,并且有可能会构建 。

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

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

一般来说,在 可以使用相同的配置。

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

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

cfg="target" 实际上不会执行任何操作:它只是一种便捷值,用于 有助于规则设计者明确自己的意图。当 executable=False 时, 这意味着 cfg 是可选的,请仅在确实有助于提高可读性时才设置此属性。

您还可以使用 cfg=my_transition 来使用 用户定义的过渡 为更改配置提供了极大的灵活性, 缺点 使构建图变大且更易于理解

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

配置 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
    ...
)

通常,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_infoinstrumented_files_infodependency_attributes 参数应列出 所有运行时依赖项属性,包括 deps 和 数据依赖项,例如 datasource_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 属性 cfg 到属性架构中的 "host""exec"dependency_attributes。(这不是理想的行为,因为它将属性 比如 dependency_attributes(而非 source_attributes)中的 srcs, 可避免对 依赖项链。)

验证操作

有时,您需要验证有关 build 的一些信息, 进行验证所需的信息仅在工件中提供 (源文件或生成的文件)。由于这些信息位于工件中 规则无法在分析时执行此项验证,因为规则无法读取 文件。相反,操作必须在执行时进行此验证。时间 验证失败,操作会失败,构建也会失败。

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

验证操作还可以通过移动部件来提高构建性能 将工件构建为单独的操作时不需要的操作。 例如,如果一个执行编译和执行 lint 请求的操作 分为编译操作和执行 lint 请求的操作,然后执行 lint 请求 操作可以作为验证操作运行,并与其他操作并行运行。

这些“验证操作”通常不会生成 因为他们只需要断言有关输入的内容。这个 但是,如果验证操作没有产生 在 build 中的其他地方使用时,规则如何使操作执行? 过去,采用的方法是让验证操作输出空白 然后将该输出人为地添加到 操作:

这是可行的,因为 Bazel 始终会在编译 操作,但这样做存在明显的缺点:

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

  2. 如果构建中可能会运行其他操作,而不是 编译操作,则需要将验证操作的空输出添加到 执行这些操作(例如 java_library 的源 jar 输出)。这是 如果可能运行的新操作(而非编译操作) 因此空白验证输出结果被意外遗漏。

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

验证输出组

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

该组的独特之处在于,无论其内容为何,系统始终会请求其输出 --output_groups 标志的值,无论目标是怎样 (例如,在命令行中、作为依赖项或通过 目标的隐式输出)。请注意,常规缓存和增量实验 仍然适用:如果验证操作的输入未更改,且 则验证操作将不会 运行。

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

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

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

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

使用验证输出组

验证输出组名为 _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 类型具有一组复杂的功能,其中许多功能由于旧式原因而保留。 以下建议有助于降低复杂性:

  • 避免使用 collect_datacollect_default 模式(在 ctx.runfiles。这些模式会隐式收集 以令人困惑的方式跨某些硬编码依赖项边缘运行文件。 请改用 filestransitive_files 参数来添加文件: ctx.runfiles,或者通过将依赖项的 runfile 与 runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles)

  • 避免使用 data_runfilesdefault_runfiles DefaultInfo 构造函数。请改为指定 DefaultInfo(runfiles = ...)。 “默认”与“默认”之间的区别和“data”runfile 遗留原因。例如,有些规则会将默认输出 data_runfiles,但不是 default_runfiles。不使用 data_runfiles,规则应包含默认输出并合并到 default_runfiles(通常用于从提供 runfile 的属性中) data)。

  • DefaultInfo 检索 runfiles 时(通常仅用于合并) 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 对象,则 provider 及其内容可以作为 dep.legacy_info.x 进行检索, dep[MyInfo].y

除了 providers 之外,返回的结构体还可能需要 具有特殊含义(因此不会创建相应的旧版 提供商):

  • filesrunfilesdata_runfilesdefault_runfilesexecutable 对应于 DefaultInfo。不允许指定任何 这些字段,同时返回 DefaultInfo 提供程序。

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

在规则的 provides 声明中,以及 providers 声明依赖项 属性,传统提供程序以字符串的形式传入,而现代提供程序则 *Info 符号传递。请务必从字符串更改为符号 。适用于难以更新且复杂或大型的规则集 所有规则都以原子方式进行,如果您遵循此顺序, 步骤:

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

  2. 修改使用旧版提供程序的规则,以改为使用 现代化提供商。如果任何属性声明需要旧版提供程序, 也将其更新为需要现代提供程序。您还可以选择 通过让消费者接受/要求任一项,将这项工作与第 1 步交错进行 provider:使用以下代码测试是否存在旧版提供程序 hasattr(target, 'foo') 或使用 FooInfo in target 的新提供程序。

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