规则定义了 Bazel 对输入执行的一组操作,以生成一组输出,这些输出在规则的实现函数返回的提供程序中引用。例如,C++ 二进制文件规则可以:
- 获取一组
.cpp
源文件(输入)。 - 对源文件运行
g++
(操作)。 - 返回
DefaultInfo
提供程序及其可在运行时使用的输出和其他文件。 - 返回
CcInfo
提供程序,其中包含从目标及其依赖项收集的 C++ 专用信息。
从 Bazel 的角度来看,g++
和标准 C++ 库也是此规则的输入。作为规则编写者,您不仅必须考虑用户提供的规则输入,还必须考虑执行相应操作所需的所有工具和库。
在创建或修改任何规则之前,请确保您熟悉 Bazel 的构建阶段。请务必了解构建的三个阶段(加载、分析和执行)。了解宏也有助于您了解规则和宏之间的区别。首先,请查看规则教程。然后,使用本页面作为参考。
Bazel 本身内置了一些规则。这些原生规则(例如 cc_library
和 java_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
文件以实例化目标)调用。
属性
属性是规则参数。属性可以为目标的实现提供特定值,也可以引用其他目标,从而创建依赖项图。
规则专用属性(例如 srcs
或 deps
)的定义方法是将从属性名称到架构(使用 attr
模块创建)的映射传递给 rule
的 attrs
参数。常见属性(例如 name
和 visibility
)会隐式添加到所有规则中。其他属性会隐式隐式添加到可执行文件和测试规则中。隐式添加到规则的属性不能包含在传递给 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.label
或 attr.label_keyed_string_dict
定义的属性)都会在定义目标时,在该属性中列出标签(或相应的 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]
对于从目标的实现函数(而不是提供程序对象列表)返回的 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
...
对于规范设置了 allow_single_file=True
的依赖项属性,ctx.file
包含单个 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
对象。
操作
操作描述了如何根据一组输入生成一组输出,例如“对 hello.c 运行 gcc 并获取 hello.o”。创建操作后,Bazel 不会立即运行该命令。它会在依赖项图中注册该操作,因为某个操作可能会依赖于另一个操作的输出。例如,在 C 中,必须在编译器之后调用链接器。
用于创建操作的通用函数在 ctx.actions
中定义:
ctx.actions.run
,用于运行可执行文件。ctx.actions.run_shell
,用于运行 shell 命令。ctx.actions.write
:用于将字符串写入文件。ctx.actions.expand_template
,用于根据模板生成文件。
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],
)
...
操作会对输入文件执行列表或部署操作,并生成输出文件(非空)列表。在分析阶段,输入和输出文件集必须是已知的。它可能取决于属性的值(包括依赖项中的提供程序),但不能取决于执行结果。例如,如果您的操作运行 unzip 命令,则必须指定您希望膨胀的文件(在运行 unzip 之前)。在内部创建数量可变的文件的操作可以将这些文件封装在单个文件(例如 zip、tar 或其他归档文件格式)中。
操作必须列出其所有输入。允许列出未使用的输入,但效率低下。
操作必须创建其所有输出。它们可以写入其他文件,但任何不在输出中的内容对使用方来说将不可用。所有声明的输出都必须由某个操作写入。
操作与纯函数类似:它们应仅依赖于所提供的输入,并避免访问计算机信息、用户名、时钟、网络或 I/O 设备(读取输入和写入输出除外)。这一点很重要,因为输出将被缓存和重复使用。
依赖项由 Bazel 解析,Bazel 会决定要执行哪些操作。如果依赖关系图中存在循环,则会发生错误。创建操作并不能保证该操作会被执行,具体取决于构建是否需要其输出。
提供商
提供程序是规则向依赖于它的其他规则公开的信息。这些数据可以包括输出文件、库、要通过工具的命令行传递的参数,或目标的使用方应了解的任何其他内容。
由于规则的实现函数只能从实例化目标的直接依赖项中读取提供程序,因此规则需要从目标的依赖项转发目标的使用方需要知道的任何信息,通常是通过将这些信息累积到 depset
中来实现的。
目标的提供程序由实现函数返回的 Provider
对象列表指定。
旧版实现函数也可以采用旧版样式编写,其中实现函数会返回 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。
您可以在创建规则时手动添加 runfile。您可以通过规则上下文 ctx.runfiles
上的 runfiles
方法创建 runfiles
对象,并将其传递给 DefaultInfo
上的 runfiles
形参。可执行规则的可执行输出会隐式添加到 runfile 中。
某些规则会指定属性(通常命名为 data
),其输出会添加到目标的 runfile 中。还应从 data
以及任何可能提供最终执行代码的属性合并 Runfile,通常为 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
# 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
的调用中将相应的 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(
...
)
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 root 作为参数。这样,二进制文件就能将正确的规范 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 没有执行平台的概念,而是将所有构建操作都视为在主机上运行。6.0 之前的 Bazel 版本创建了一个不同的“主机”配置来表示这种情况。如果您在代码或旧文档中看到对“host”的引用,那就是它所指的内容。我们建议使用 Bazel 6.0 或更高版本,以避免这种额外的概念开销。
配置 fragment
规则可以访问配置 fragment,如 cpp
、java
和 jvm
。不过,必须声明所有必需的 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_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 还会收集插桩的源文件列表。所考虑的目标子集由 --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
设置为 "host"
或 "exec"
)创建一个默认依赖项。(这并不是理想的行为,因为它会将 srcs
等属性放入 dependency_attributes
而不是 source_attributes
,但这样可以避免为依赖项链中的所有规则进行显式覆盖率配置。)
验证操作
有时,您需要验证 build 的某些方面,而执行此验证所需的信息仅在工件(源文件或生成的文件)中提供。由于这些信息位于工件中,因此规则无法在分析时执行此验证,因为规则无法读取文件。相反,操作必须在执行时执行此验证。如果验证失败,相应操作将失败,构建也将失败。
可能要运行的验证示例包括静态分析、lint 检查、依赖项和一致性检查以及样式检查。
此外,验证操作还可以将构建工件时不需要的部分操作移到单独的操作中,从而帮助提升构建性能。例如,如果执行编译和执行 lint 检查的单个操作可以拆分为编译操作和执行 lint 检查操作,那么执行 lint 请求的操作可以作为验证操作运行,并与其他操作并行运行。
这些“验证操作”通常不会生成在 build 中其他位置使用的任何内容,因为它们只需要断言有关其输入的信息。不过,这会带来一个问题:如果验证操作未生成任何在 build 的其他位置使用的内容,规则如何让该操作运行?过去,此方法是让验证操作输出一个空文件,然后人为地将该输出添加到 build 中某些其他重要操作的输入中:
之所以能这样做,是因为 Bazel 在运行编译操作时始终会运行验证操作,但这有明显的缺点:
验证操作位于构建的关键路径中。由于 Bazel 认为运行编译操作需要空输出,因此它会先运行验证操作,即使编译操作会忽略输入也是如此。这会减少并行性并减慢构建速度。
如果 build 中的其他操作可能会运行而非编译操作,则还需要将验证操作的空输出(例如
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。
已弃用的功能
已废弃的预声明输出
使用预声明的输出有两种已废弃的方法:
rule
的outputs
参数用于指定输出属性名称与字符串模板之间的映射,以生成预声明的输出标签。最好使用未预声明的输出,并将输出明确添加到DefaultInfo.files
中。将规则目标的标签用作使用输出的规则的输入,而不是使用预声明的输出的标签。对于可执行规则,
ctx.outputs.executable
是指与规则目标同名的预声明可执行输出。最好明确声明输出(例如使用ctx.actions.declare_file(ctx.label.name)
),并确保用于生成可执行文件的命令将其权限设置为允许执行。将可执行文件输出显式传递给DefaultInfo
的executable
参数。
Runfile
ctx.runfiles
和 runfiles
类型具有一组复杂的功能,其中许多功能由于旧式原因而保留。以下建议有助于降低复杂性:
避免使用
ctx.runfiles
的collect_data
和collect_default
模式。这些模式会以令人困惑的方式跨某些硬编码依赖项边缘隐式收集运行文件。请改用ctx.runfiles
的files
或transitive_files
参数添加文件,或者使用runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles)
合并依赖项中的 runfile。避免使用
DefaultInfo
构造函数的data_runfiles
和default_runfiles
。请改为指定DefaultInfo(runfiles = ...)
。 出于历史原因,我们保留了“默认”和“数据”运行文件之间的区别。例如,有些规则会将其默认输出放入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.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
测试是否存在新的提供方。从所有规则中彻底移除旧版提供程序。