规则定义了 Bazel 对输入执行的一系列操作,以生成一组输出,这些输出会在规则的实现函数返回的提供程序中引用。例如,C++ 二进制规则可能会:
- 获取一组
.cpp
源文件(输入)。 - 对源文件运行
g++
(操作)。 - 返回
DefaultInfo
提供程序以及可执行输出和在运行时可用的其他文件。 - 返回
CcInfo
提供程序,其中包含从目标及其依赖项收集的 C++ 特定信息。
从 Bazel 的角度来看,g++
和标准 C++ 库也是此规则的输入。作为规则编写器,您不仅必须考虑用户提供的规则输入,还必须考虑执行操作所需的所有工具和库。
在创建或修改任何规则之前,请确保您已熟悉 Bazel 的构建阶段。了解 build 的三个阶段(加载、分析和执行)非常重要。了解宏也有助于理解规则和宏之间的区别。首先,请查看规则教程。然后,使用此页面作为参考。
Bazel 本身内置了几项规则。这些原生规则(例如 genrule
和 filegroup
)提供了一些核心支持。通过定义自己的规则,您可以添加对 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 提供特定值,也可以引用其他目标,从而创建依赖关系图。
特定于规则的属性(例如 srcs
或 deps
)是通过将属性名称到架构的映射(使用 attr
模块创建)传递给 rule
的 attrs
参数来定义的。name
和 visibility
等常用属性会隐式添加到所有规则中。其他属性会具体地隐式添加到可执行和测试规则中。隐式添加到规则的属性无法包含在传递给 attrs
的字典中。
依赖项属性
处理源代码的规则通常会定义以下属性来处理各种类型的依赖项:
srcs
用于指定目标的操作处理的源文件。通常,属性架构会指定规则处理的源文件类型所需的文件扩展名。针对具有头文件的语言的规则通常会为目标及其使用方处理的标头指定单独的hdrs
属性。deps
用于指定目标的代码依赖项。属性架构应指定这些依赖项必须提供的 provider。(例如,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.label
或 attr.label_keyed_string_dict
定义的属性)都指定了目标与在其标签(或相应的 Label
对象)中列出目标(或相应 Label
对象)之间的特定类型的依赖关系。这些标签的代码库(也可能是路径)将相对于定义的目标进行解析。
example_library(
name = "my_target",
deps = [":other_target"],
)
example_library(
name = "other_target",
...
)
在此示例中,other_target
是 my_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.output
和 attr.output_list
)用于声明目标生成的输出文件。这些属性与依赖项属性的不同之处体现在以下两个方面:
- 它们会定义输出文件目标,而不是引用在其他位置定义的目标。
- 输出文件目标取决于实例化的规则目标,而不是相反。
通常,只有在规则需要使用不能基于目标名称的用户定义的名称创建输出时,才会用到输出属性。如果规则有一个输出属性,则通常命名为 out
或 outs
。
输出属性是创建预声明输出的首选方式,可以专门依赖于或通过命令行请求。
实现职能
每条规则都需要一个 implementation
函数。这些函数在分析阶段严格执行,并将在加载阶段生成的目标图转换为要在执行阶段执行的操作图。因此,实现函数实际上无法读取或写入文件。
规则实现函数通常是私有的(使用前导下划线命名)。按照惯例,它们的名称与规则相同,但带有 _impl
后缀。
实现函数仅接受一个参数:规则上下文,通常命名为 ctx
。它们会返回提供商列表。
目标
依赖项在分析时表示为 Target
对象。这些对象包含在执行目标的实现函数时生成的 providers。
ctx.attr
包含与每个依赖项属性的名称对应的字段,其中包含表示使用该属性的每个直接依赖项的 Target
对象。对于 label_list
属性,这是 Targets
的列表。对于 label
属性,这是单个 Target
或 None
。
目标的实现函数会返回提供程序对象列表:
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
包含依赖项属性(其规范设置为 allow_single_file = True
)的单个 File
或 None
。ctx.executable
的行为与 ctx.file
相同,但仅包含规范设为 executable = True
的依赖项属性的字段。
声明输出
在分析阶段,规则的实现函数可以创建输出。由于在加载阶段必须知道所有标签,因此这些额外的输出没有标签。可以使用 ctx.actions.declare_file
和 ctx.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.run
,用于运行可执行文件。ctx.actions.run_shell
,用于运行 shell 命令。ctx.actions.write
,用于将字符串写入文件。ctx.actions.expand_template
,用于根据模板生成文件。
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 或其他归档文件格式)中。
Action 必须列出其所有输入。允许列出未使用的输入,但效率低下。
操作必须创建其所有输出。它们可以写入其他文件,但在输出中不包含的任何内容均对使用方不可用。所有声明的输出都必须由某个操作写入。
操作与纯函数类似:它们应仅依赖于提供的输入,并避免访问计算机信息、用户名、时钟、网络或 I/O 设备(读取输入和写入输出除外)。这很重要,因为输出将被缓存并重复使用。
依赖项由 Bazel 解析,后者决定要执行哪些操作。如果依赖关系图中存在循环,则会出错。创建操作并不能保证它一定会执行,具体取决于构建是否需要其输出。
提供程序
提供程序是规则会向依赖该提供程序的其他规则公开的部分信息。这些数据可能包括输出文件、库、要在工具命令行中传递的参数或目标使用者应了解的任何其他内容。
由于规则的实现函数只能从实例化的目标的直接依赖项中读取提供程序,因此规则需要从目标的依赖项转发任何信息,而这些信息需要被目标使用方知道,通常是通过将其累加到 depset
中。
目标的提供程序由实现函数返回的提供程序对象列表指定。
旧实现函数也可以采用旧版样式编写,其中实现函数会返回 struct
而不是提供程序对象列表。强烈建议不要使用此样式,应弃用这种样式。
默认输出
目标的默认输出是指通过命令行请求构建目标时默认请求的输出。例如,java_library
目标 //pkg:foo
将 foo.jar
作为默认输出,因此将通过 bazel build //pkg:foo
命令进行构建。
默认输出由 DefaultInfo
的 files
参数指定:
def _example_library_impl(ctx):
...
return [
DefaultInfo(files = depset([output_file]), ...),
...
]
如果规则实现未返回 DefaultInfo
或未指定 files
参数,则 DefaultInfo.files
默认为所有预先声明的输出(通常是由输出属性创建的输出)。
执行操作的规则应提供默认输出,即使这些输出不应直接使用。所请求输出图表中不存在的操作会被剪除。如果输出仅供目标的使用方使用,那么当目标是单独构建时,系统不会执行这些操作。这会增加调试的难度,因为仅重新构建失败的目标不会重现失败。
Runfile
Runfile 是目标在运行时(而不是构建时)使用的一组文件。在执行阶段,Bazel 会创建一个目录树,其中包含指向 runfile 的符号链接。这会暂存二进制文件的环境,以便它可以在运行时访问运行文件。
您可以在创建规则期间手动添加 Runfile。runfiles
对象可以通过规则上下文 ctx.runfiles
的 runfiles
方法创建,并传递给 DefaultInfo
上的 runfiles
参数。可执行规则的可执行输出会隐式添加到 runfile 中。
某些规则会指定通常名为 data
的属性,其输出会添加到目标的 runfile 中。此外,还应从 data
以及为最终执行提供代码的任何属性合并运行文件,通常是 srcs
(可能包含具有关联 data
的 filegroup
目标)和 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
# 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
的调用中将相应的 executable
或 test
参数设置为 True
:
example_binary = rule(
implementation = _example_binary_impl,
executable = True,
...
)
example_test = rule(
implementation = _example_binary_impl,
test = True,
...
)
测试规则的名称必须以 _test
结尾。(按照惯例,测试目标名称通常也以 _test
结尾,但这不是必须的。)非测试规则不得具有此后缀。
这两种规则都必须生成将由 run
或 test
命令调用的可执行输出文件(不一定已预先声明)。如需告知 Bazel 将规则的哪些输出用作此可执行文件,请将其作为返回的 DefaultInfo
提供程序的 executable
参数传递。该 executable
会添加到规则的默认输出中(因此您无需将其同时传递给 executable
和 files
)。它也会隐式添加到 runfiles 中:
def _example_binary_impl(ctx):
executable = ctx.actions.declare_file(ctx.label.name)
...
return [
DefaultInfo(executable = executable, ...),
...
]
生成此文件的操作必须设置该文件的可执行位。对于 ctx.actions.run
或 ctx.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)
runfiles 目录下的 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
可以由切面和应用该切面的规则目标返回,只要它们不定义相同的输出组即可。在这种情况下,将合并生成的提供程序。
请注意,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
来使用用户定义的转换,这使得规则编写者在更改配置方面具有极大的灵活性,但其缺点是使 build 图变大且不那么易于理解。
注意:Bazel 之前并不存在执行平台的概念,而是将所有构建操作都视为在宿主机上运行。6.0 之前的 Bazel 版本创建了一个不同的“主机”配置来表示这种情况。如果您在代码或旧文档中看到对“host”的引用,则这就是所指的名称。建议您使用 Bazel 6.0 或更高版本,以避免这种额外的概念性开销。
配置 fragment
规则可以访问配置 fragment,例如 cpp
和 java
。不过,必须声明所有必需的 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 符号链接
通常,Runfiles 树中文件的相对路径与该文件在源代码树或生成的输出树中的相对路径相同。如果这些变量因某种原因需要有所不同,您可以指定 root_symlinks
或 symlinks
参数。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
如果使用 symlinks
或 root_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_info
的 dependency_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
设置为 "exec"
的非工具依赖项属性。(这并不是理想的行为,因为它将 srcs
等属性放在 dependency_attributes
中而不是 source_attributes
中,但这样就无需为依赖项链中的所有规则进行显式覆盖率配置。)
验证操作
有时,您需要验证 build 的相关信息,并且执行验证所需的信息仅在工件(源文件或生成的文件)中提供。由于这些信息位于工件中,因此规则在分析时无法执行此验证,因为规则无法读取文件。相反,操作必须在执行时执行此验证。验证失败时,操作将失败,因此构建也会失败。
可能运行的验证示例包括静态分析、lint 检查、依赖项和一致性检查,以及样式检查。
验证操作还可以将构建工件不需要的部分操作拆分为单独的操作,从而帮助您提高构建性能。例如,如果可以将执行编译和 lint 的单个操作拆分为编译操作和 lint 检查操作,那么 lint 检查操作可以作为验证操作运行,并与其他操作并行运行。
这些“验证操作”通常不会生成在构建中的其他地方使用的任何内容,因为它们只需要对输入进行断言。不过,这就带来了一个问题:如果验证操作没有生成在 build 中的其他地方使用的任何内容,那么规则如何使操作运行?过去,方法是让验证操作输出一个空文件,然后人为地将该输出添加到 build 中其他一些重要操作的输入中:
这是可行的,因为在运行编译操作时,Bazel 始终会运行验证操作,但这样做有很大的缺点:
验证操作位于构建的关键路径中。Bazel 认为需要空输出才能运行编译操作,因此它会先运行验证操作,即使编译操作会忽略输入也是如此。 这会减少并行处理并减慢构建速度。
如果可能运行 build 中的其他操作(而不是编译操作),则还需要将验证操作的空输出(例如,
java_library
的源代码 jar 输出)添加到这些操作中。如果稍后添加了可能会运行(而非编译操作)的新操作,并且意外遗漏了空的验证输出,这也是一个问题。
这些问题的解决方案是使用验证输出组。
验证输出组
验证输出组是一个输出组,旨在保存验证操作原本未使用的输出,这样就无需人为地将其添加到其他操作的输入中。
该组的特殊之处在于,无论 --output_groups
标志的值如何,也无论目标的依赖方式如何(例如,在命令行上、作为依赖项,还是通过目标的隐式输出),系统始终会请求其输出。请注意,常规缓存和增量仍然适用:如果验证操作的输入未更改,且验证操作先前成功,则不会运行验证操作。
使用此输出组仍需验证操作输出某个文件,甚至是空文件。这可能需要封装一些通常不会创建输出的工具,以便创建文件。
以下三种情况下系统不会运行目标的验证操作:
- 将目标视为工具时
- 当目标依赖于作为隐式依赖项(例如以“_”开头的属性)时
- 当目标在 exec 配置中构建时。
我们假定这些目标具有自己的单独的 build 和测试,可发现任何验证失败问题。
使用验证输出组
验证输出组名为 _validation
,其使用方式与任何其他输出组一样:
def _rule_with_validation_impl(ctx):
ctx.actions.write(ctx.outputs.main, "main output\n")
ctx.actions.write(ctx.outputs.implicit, "implicit output\n")
validation_output = ctx.actions.declare_file(ctx.attr.name + ".validation")
ctx.actions.run(
outputs = [validation_output],
executable = ctx.executable._validation_tool,
arguments = [validation_output.path],
)
return [
DefaultInfo(files = depset([ctx.outputs.main])),
OutputGroupInfo(_validation = depset([validation_output])),
]
rule_with_validation = rule(
implementation = _rule_with_validation_impl,
outputs = {
"main": "%{name}.main",
"implicit": "%{name}.implicit",
},
attrs = {
"_validation_tool": attr.label(
default = Label("//validation_actions:validation_tool"),
executable = True,
cfg = "exec"
),
}
)
请注意,验证输出文件不会添加到 DefaultInfo
或任何其他操作的输入中。如果目标依赖于标签,或者目标的任何隐式输出直接或间接依赖于该目标,则此规则种类的目标的验证操作仍会运行。
通常,重要的是,验证操作的输出只会进入验证输出组,而不会添加到其他操作的输入中,因为这可能会使并行增益失效。但请注意,Bazel 不会执行任何特殊检查来强制执行此要求。因此,您应在 Starlark 规则测试中测试验证操作输出是否不会添加到任何操作的输入中。例如:
load("@bazel_skylib//lib:unittest.bzl", "analysistest")
def _validation_outputs_test_impl(ctx):
env = analysistest.begin(ctx)
actions = analysistest.target_actions(env)
target = analysistest.target_under_test(env)
validation_outputs = target.output_groups._validation.to_list()
for action in actions:
for validation_output in validation_outputs:
if validation_output in action.inputs.to_list():
analysistest.fail(env,
"%s is a validation action output, but is an input to action %s" % (
validation_output, action))
return analysistest.end(env)
validation_outputs_test = analysistest.make(_validation_outputs_test_impl)
验证操作标志
运行验证操作由 --run_validations
命令行标志控制,其默认值为 true。
已弃用的功能
已废弃的预声明输出
您可通过以下两种方式使用预先声明的输出:
rule
的outputs
参数指定输出属性名称与用于生成预声明输出标签的字符串模板之间的映射。最好使用未预先声明的输出,并向DefaultInfo.files
明确添加输出。对于使用输出的规则,请使用规则目标的标签(而不是预先声明的输出的标签)的输入。对于可执行规则,
ctx.outputs.executable
是指与规则目标同名的预声明可执行输出。最好明确声明输出(例如使用ctx.actions.declare_file(ctx.label.name)
),并确保生成可执行文件的命令将其权限设置为允许执行。将可执行输出明确传递给DefaultInfo
的executable
参数。
应避免的 Runfiles 功能
ctx.runfiles
和 runfiles
类型具有一组复杂的特征,其中许多特征都是因遗留问题而保留的。以下建议有助于降低复杂性:
避免使用
ctx.runfiles
的collect_data
和collect_default
模式。这些模式会以令人困惑的方式跨某些硬编码依赖项边缘隐式收集 Runfile。请改为使用ctx.runfiles
的files
或transitive_files
参数添加文件,或者通过合并具有runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles)
的依赖项中的运行文件来添加文件。避免使用
DefaultInfo
构造函数的data_runfiles
和default_runfiles
。请改为指定DefaultInfo(runfiles = ...)
。 由于遗留原因,我们保留了“默认”Runfile 和“数据”Runfile 之间的区别。例如,有些规则将其默认输出放在data_runfiles
中,但不放在default_runfiles
中。规则不应使用data_runfiles
,而应同时包含默认输出,并从提供 Runfile 的属性(通常是data
)的default_runfiles
中合并。从
DefaultInfo
检索runfiles
(通常仅用于在当前规则及其依赖项之间合并运行文件)时,请使用DefaultInfo.default_runfiles
,而不是DefaultInfo.data_runfiles
。
从旧版提供商迁移
过去,Bazel 提供程序是 Target
对象上的简单字段。它们使用点运算符访问,并且是通过将字段放入由规则的实现函数返回的 struct
(而不是提供程序对象列表)中创建的:
return struct(example_info = struct(headers = depset(...)))
可以从 Target
对象的相应字段中检索此类提供程序:
transitive_headers = [hdr.example_info.headers for hdr in ctx.attr.hdrs]
此样式已弃用,不应在新代码中使用;请参阅以下内容,了解可能有助于您进行迁移的信息。新的提供程序机制避免了名称冲突。它还支持数据隐藏,方法是要求访问提供程序实例的任何代码使用提供程序符号检索它。
目前,旧版提供商仍受支持。规则可以返回旧版和新式提供程序,如下所示:
def _old_rule_impl(ctx):
...
legacy_data = struct(x = "foo", ...)
modern_data = MyInfo(y = "bar", ...)
# When any legacy providers are returned, the top-level returned value is a
# struct.
return struct(
# One key = value entry for each legacy provider.
legacy_info = legacy_data,
...
# Additional modern providers:
providers = [modern_data, ...])
如果为此规则的实例生成的 Target
对象为 dep
,则提供程序及其内容可作为 dep.legacy_info.x
和 dep[MyInfo].y
进行检索。
除了 providers
之外,返回的结构体还可以接受其他几个具有特殊含义的字段(因此不会创建相应的旧版提供程序):
字段
files
、runfiles
、data_runfiles
、default_runfiles
和executable
对应于DefaultInfo
的同名字段。不允许指定任何这些字段,同时返回DefaultInfo
提供程序。字段
output_groups
采用结构体值,并对应于OutputGroupInfo
。
在规则的 provides
声明和依赖项属性的 providers
声明中,旧版提供程序作为字符串传入,而新式提供程序由其 Info
符号传入。迁移时,请务必将字符串更改为符号。对于难以以原子方式更新所有规则的复杂或大型规则集,按以下顺序执行这些步骤可能会让您更容易解决问题:
使用上述语法修改生成旧版提供程序的规则,以同时生成旧版提供程序和新式提供程序。对于声明它们返回旧版提供程序的规则,请更新该声明以同时包含旧版提供程序和新式提供程序。
将使用旧版提供程序的规则修改为使用新型提供程序。如果任何属性声明需要旧版提供程序,也请更新它们以改为需要新式提供程序。(可选)您可以在第 1 步中交错执行此操作,方法是让使用方接受或要求其中任一提供程序:使用
hasattr(target, 'foo')
测试是否存在旧版提供程序,或使用FooInfo in target
测试新提供程序。从所有规则中完全移除旧版提供方。