规则定义了 Bazel 对其执行的一系列操作 这些输入可生成一组输出, providers - 规则的 实现函数。例如,C++ 二元规则可能会:
- 获取一组
.cpp
源文件(输入)。 - 对源文件运行
g++
(操作)。 - 将
DefaultInfo
提供程序连同可执行输出和其他文件一起返回 在运行时可用 - 返回
CcInfo
提供程序,以及从 目标及其依赖项。
从 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输出(包含 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 时生成的
target 的实现函数已执行。
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
返回的旧版样式,
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
包含一个 File
或 None
,
依赖项属性,其规范设置了 allow_single_file=True
。
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")
...
对于预先声明的输出,例如
输出属性,可以检索 File
对象
来自 ctx.outputs
的相应字段。
操作
操作描述了如何根据一组输入生成一组输出,例如“对 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],
)
...
操作会对输入文件执行列表或弃用,并生成一个(非空)列表, 输出文件。在分析阶段,必须知道一组输入和输出文件。这取决于 属性,包括来自依赖项的提供程序,但不能依赖于 执行结果。例如,如果您的操作运行解压缩命令,您必须指定预期要膨胀的文件(在运行解压缩命令之前)。在内部创建可变数量文件的操作可以将这些文件封装在单个文件(例如 ZIP、tar 或其他归档格式)中。
操作必须列出其所有输入。列出未使用的输入 但效率低下
操作必须创建其所有输出。它们可能会写入其他文件,但使用方无法使用输出中未包含的任何内容。所有声明的输出都必须由某个操作写入。
操作类似于纯函数:它们应仅依赖于提供的输入,并避免访问计算机信息、用户名、时钟、网络或 I/O 设备(读取输入和写入输出除外)。这一点很重要,因为输出将被缓存和重复使用。
依赖项由 Bazel 解析,后者将决定哪些操作 。如果依赖项图表中存在循环,则表示存在错误。正在创建 操作并不能保证一定会执行,具体取决于 构建时需要其输出。
提供商
提供程序是规则向依赖于它的其他规则公开的信息。这些数据可以包括输出文件、库、要通过工具的命令行传递的参数,或目标的使用方应了解的任何其他内容。
由于规则的实施函数只能从
实例化目标的直接依赖项,规则需要转发任何
来自目标依赖项且目标的
消费者,通常通过将其累加到 depset
中。
目标的提供程序由实现函数返回的 Provider
对象列表指定。
旧的实现函数也可以采用旧版样式编写,其中
实现函数会返回 struct
,而不是
provider 对象。强烈建议不要采用这种样式,应将规则从这种样式迁移出去。
默认输出
目标的默认输出是指在命令行中请求构建目标时默认请求的输出。例如,
java_library
目标 //pkg:foo
的默认输出为 foo.jar
,
将由命令 bazel build //pkg:foo
构建。
默认输出由 DefaultInfo
的 files
参数指定:
def _example_library_impl(ctx):
...
return [
DefaultInfo(files = depset([output_file]), ...),
...
]
如果规则实现或 files
未返回 DefaultInfo
参数未指定,DefaultInfo.files
默认为全部
预先声明的输出(通常是由输出
属性)。
执行操作的规则应提供默认输出,即使这些输出预计不会直接使用也是如此。图表中未包含的操作 删除请求的输出如果输出仅供目标的使用方使用,则在单独构建目标时,系统不会执行这些操作。这会增加调试难度,因为仅重新构建失败的目标无法重现失败问题。
runfile
Runfile 是目标在运行时使用的一组文件(与 build 不同 )。在执行阶段,Bazel 会创建 包含指向 runfile 的符号链接的目录树。这会为二进制文件设置环境,以便其在运行时访问 runfile。
您可以在创建规则时手动添加 runfile。您可以通过规则上下文 ctx.runfiles
上的 runfiles
方法创建 runfiles
对象,并将其传递给 DefaultInfo
上的 runfiles
形参。此 API 的
可执行规则会隐式添加到 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
时的普通返回值,而“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
结尾,但这不是必需的。)非测试规则不得带有此后缀。
这两种规则都必须生成一个可执行的输出文件(可以是预声明的,也可以不是),该文件将由 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 调用的二进制文件无法做出同样的假设。为缓解此问题,每个二进制文件都应提供一种方法,以使用环境或命令行参数/标志接受其 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 有个概念和过渡。通过 最顶层的目标(命令行中请求的目标)内置于 "target"配置,而应在执行平台上运行的工具 都是在“exec”中构建的配置。规则可能会根据 配置,例如,更改通过系统传递的 传递给编译器。在某些情况下,不同的配置可能需要使用相同的库。如果出现这种情况,系统会分析该文件,并有可能构建 。
默认情况下,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
规则可以访问 cpp
、java
和 jvm
等配置 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
...
)
运行文件符号链接
通常,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 属性
cfg
到属性架构中的 "host"
或 "exec"
)
dependency_attributes
。(这并不是理想的行为,因为它会将 srcs
等属性放入 dependency_attributes
而不是 source_attributes
,但这样可以避免为依赖项链中的所有规则进行显式覆盖率配置。)
验证操作
有时,您需要验证有关 build 的一些信息, 进行验证所需的信息仅在工件中提供 (源文件或生成的文件)。由于这些信息是在工件中 规则无法在分析时执行此项验证,因为规则无法读取 文件。相反,操作必须在执行时执行此验证。如果验证失败,相应操作将失败,构建也将失败。
可能会运行的验证示例包括静态分析、lint 检查、 依赖项检查和一致性检查,以及样式检查
验证操作还可以通过将构建工件不需要的操作部分移至单独的操作来帮助提升构建性能。例如,如果一个执行编译和 lint 的操作可以拆分为编译操作和 lint 操作,那么 lint 操作可以作为验证操作运行,并与其他操作并行运行。
这些“验证操作”通常不会生成在 build 的其他位置使用的任何内容,因为它们只需断言其输入的内容即可。不过,这会带来一个问题:如果验证操作未生成任何在 build 的其他位置使用的内容,规则如何让该操作运行?过去,此方法是让验证操作输出一个空文件,然后人为地将该输出添加到 build 中某些其他重要操作的输入中:
之所以能这样做,是因为 Bazel 在运行编译操作时始终会运行验证操作,但这有明显的缺点:
验证操作位于构建的关键路径中。由于 Bazel 认为运行编译操作需要空输出,因此它会先运行验证操作,即使编译操作会忽略输入也是如此。这会减少并行性并减慢构建速度。
如果构建中可能会运行其他操作,而不是 编译操作,则需要将验证操作的空输出添加到 执行这些操作(例如
java_library
的源 jar 输出)。如果稍后添加了可能会代替编译操作而运行的新操作,并且意外遗漏了空的验证输出,也会出现此问题。
这些问题的解决方法是使用验证输出组。
验证输出组
验证输出组是一个输出组,旨在存放 验证操作的未使用输出,这样就不必人为地 添加到其他操作的输入中。
此组具有特殊性,因为无论 --output_groups
标志的值如何,无论目标的依赖项如何(例如,在命令行上、作为依赖项或通过目标的隐式输出),系统始终会请求其输出。请注意,常规缓存和增量实验
仍然适用:如果验证操作的输入未更改,且
则验证操作将不会
运行。
使用此输出组仍然要求验证操作输出某个文件, 即使是空的,也无妨。这可能需要封装一些通常不需要的工具 创建输出,以便创建一个文件。
在以下三种情况下,系统不会运行目标的验证操作:
- 将目标作为工具进行依赖时
- 当目标被依赖为隐式依赖项(例如, 以“_”)开头的属性
- 在主机或执行配置中构建目标时。
假设这些目标有自己的 单独的构建和测试,以发现任何验证失败的情况。
使用验证输出组
验证输出组名为 _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
模式。这些模式会隐式收集 以令人困惑的方式跨某些硬编码依赖项边缘运行文件。 请改用files
或transitive_files
参数来添加文件:ctx.runfiles
,或者通过将依赖项的 runfile 与runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles)
。避免使用
DefaultInfo
构造函数的data_runfiles
和default_runfiles
。请改为指定DefaultInfo(runfiles = ...)
。 出于历史原因,我们保留了“默认”和“数据”运行文件之间的区别。例如,有些规则会将默认输出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
之外,返回的结构体还可以采用几个具有特殊含义的其他字段(因此不会创建相应的旧版提供程序):
files
、runfiles
、data_runfiles
、default_runfiles
和executable
字段与DefaultInfo
中的同名字段相对应。不允许指定任何 这些字段,同时返回DefaultInfo
提供程序。字段
output_groups
采用结构体值,对应于OutputGroupInfo
。
在规则的 provides
声明和依赖项属性的 providers
声明中,旧版提供程序会作为字符串传入,而新版提供程序会通过其 *Info
符号传入。请务必从字符串更改为符号
。适用于难以更新且复杂或大型的规则集
所有规则都以原子方式进行,如果您遵循此顺序,
步骤:
修改生成旧版提供程序的规则,以同时生成旧版提供程序 和现代提供程序,并使用上述语法。对于声明 返回旧版提供程序,请更新该声明以包含 传统提供商和现代提供商。
修改使用旧版提供程序的规则,以改为使用 现代化提供商。如果任何属性声明都需要旧版提供程序,请将其更新为需要新版提供程序。您还可以选择 通过让消费者接受/要求任一项,将这项工作与第 1 步交错进行 provider:使用以下代码测试是否存在旧版提供程序
hasattr(target, 'foo')
或使用FooInfo in target
的新提供程序。从所有规则中完全移除旧版提供程序。