规则

报告问题 查看来源 每晚 · 7.3。 · 7.2 条 · 7.1。 · 7.0。 · 6.5

规则定义了 Bazel 对输入执行的一组操作,以生成一组输出,这些输出在规则的实现函数返回的提供程序中引用。例如,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 对象。这些对象包含在执行目标的实现函数时生成的提供程序

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

声明输出

在分析阶段,规则的实现函数可以创建输出。 由于所有标签在加载阶段都必须已知,因此这些额外的 输出没有标签。您可以使用 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")
  ...

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

操作

操作描述如何根据一组输入生成一组输出, 例如“在 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 解析,Bazel 会决定要执行哪些操作。如果依赖关系图中存在循环,则会发生错误。正在创建 操作并不能保证一定会执行,具体取决于 构建时需要其输出。

提供商

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

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

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

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

默认输出

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

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

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

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

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

Runfiles

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

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

有些规则会指定属性,通常命名为 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

# 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(
 ...
)

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 可以由 aspect 以及该切面所应用到的规则目标,例如 但前提是它们没有定义相同的输出组。在这种情况下,系统会合并生成的提供程序。

请注意,OutputGroupInfo 通常不应用于将特定类型的文件从目标传递到其使用方的操作。定义 针对具体规则的提供商

配置

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

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

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

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

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

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

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

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

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

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

“托管应用”与和“exec”配置:

  • “host”是终端,“exec”不是:一旦依赖项位于“host”配置中,便不允许再进行转换。进入“exec”配置后,您可以继续进行进一步的配置转换。
  • “host”是单体,“exec”不是:只有一个“host”配置,但每个执行平台可以有不同的“exec”配置。
  • “主机”假定您在与 Bazel 相同的机器上或在与之非常相似的机器上运行工具。现在情况不再是这样:您可以运行 或远程执行器上执行什么操作 确保远程执行器与本地应用具有相同的 CPU 和操作系统 虚拟机。

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

配置 fragment

规则可以访问 cppjavajvm配置 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
    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 属性 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 配置中构建目标时。

假设这些目标具有自己的单独 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 模式。这些模式会以令人困惑的方式隐式收集特定硬编码依赖项边缘的运行文件。请改用 filestransitive_files 参数来添加文件: ctx.runfiles,或者通过将依赖项的 runfile 与 runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles)

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

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