本页面介绍了使用宏的基础知识,包括典型用例、调试和惯例。
宏是从 BUILD
文件中调用的函数,可以实例化规则。
宏主要用于封装和重用现有规则和其他宏的代码。
宏分为两种:符号宏(本页介绍)和旧版宏。如果可能,我们建议使用符号宏来提高代码清晰度。
符号宏提供类型化实参(字符串到标签的转换,相对于调用宏的位置)以及限制和指定所创建目标的可视性的功能。它们旨在适应延迟评估(将在未来的 Bazel 版本中添加)。在 Bazel 8 中,符号宏默认可用。本文档中提及的 macros
是指符号宏。
您可以在示例代码库中找到符号宏的可执行示例。
用法
通过调用 macro()
函数(带有两个必需的形参:attrs
和 implementation
)在 .bzl
文件中定义宏。
属性
attrs
接受一个从属性名称到属性类型的字典,该字典表示宏的实参。两个常见属性(name
和 visibility
)会隐式添加到所有宏,并且不会包含在传递给 attrs
的字典中。
# macro/macro.bzl
my_macro = macro(
attrs = {
"deps": attr.label_list(mandatory = True, doc = "The dependencies passed to the inner cc_binary and cc_test targets"),
"create_test": attr.bool(default = False, configurable = False, doc = "If true, creates a test target"),
},
implementation = _my_macro_impl,
)
属性类型声明接受参数 mandatory
、default
和 doc
。大多数属性类型还接受 configurable
参数,该参数用于确定属性是否接受 select
。如果属性为 configurable
,它会将非 select
值解析为不可配置的 select
,即 "foo"
将变为 select({"//conditions:default": "foo"})
。如需了解详情,请参阅选择器。
属性继承
宏通常用于封装规则(或其他宏),并且宏的作者通常希望使用 **kwargs
将封装的符号的大部分属性不变地转发到宏的主要目标(或主要内部宏)。
为了支持此模式,宏可以通过将规则或另一个宏的规则或宏符号传递给 macro()
的 inherit_attrs
实参,来继承属性。(您也可以使用特殊字符串 "common"
代替规则或宏符号,以继承为所有 Starlark build 规则定义的通用属性。)
只有公共属性会被继承,并且宏自身 attrs
字典中的属性会覆盖具有相同名称的继承属性。您还可以通过在 attrs
字典中使用 None
作为值来移除继承的属性:
# macro/macro.bzl
my_macro = macro(
inherit_attrs = native.cc_library,
attrs = {
# override native.cc_library's `local_defines` attribute
"local_defines": attr.string_list(default = ["FOO"]),
# do not inherit native.cc_library's `defines` attribute
"defines": None,
},
...
)
无论原始属性定义的默认值是什么,非必需的继承属性的默认值始终会被覆盖为 None
。如果您需要检查或修改继承的非必需属性(例如,如果您想向继承的 tags
属性添加标记),则必须确保在宏的实现函数中处理 None
这种情况:
# macro/macro.bzl
def _my_macro_impl(name, visibility, tags, **kwargs):
# Append a tag; tags attr is an inherited non-mandatory attribute, and
# therefore is None unless explicitly set by the caller of our macro.
my_tags = (tags or []) + ["another_tag"]
native.cc_library(
...
tags = my_tags,
**kwargs,
)
...
实现
implementation
接受一个包含宏逻辑的函数。实现函数通常通过调用一个或多个规则来创建目标,并且通常是私有函数(名称以英文下划线开头)。按照惯例,它们的名称与宏相同,但以 _
为前缀,以 _impl
为后缀。
与规则实现函数(采用包含属性引用的单个实参 [ctx
])不同,宏实现函数会为每个实参接受一个形参。
# macro/macro.bzl
def _my_macro_impl(name, visibility, deps, create_test):
cc_library(
name = name + "_cc_lib",
deps = deps,
)
if create_test:
cc_test(
name = name + "_test",
srcs = ["my_test.cc"],
deps = deps,
)
如果宏继承了属性,则其实现函数必须具有 **kwargs
残留关键字参数,该参数可以转发给调用继承的规则或子宏的调用。(这有助于确保,如果继承的规则或宏添加了新属性,您的宏不会中断。)
声明
通过在 BUILD
文件中加载并调用宏定义来声明宏。
# pkg/BUILD
my_macro(
name = "macro_instance",
deps = ["src.cc"] + select(
{
"//config_setting:special": ["special_source.cc"],
"//conditions:default": [],
},
),
create_tests = True,
)
这将创建目标 //pkg:macro_instance_cc_lib
和 //pkg:macro_instance_test
。
与规则调用中一样,如果宏调用中的属性值设置为 None
,则该属性会被视为宏的调用者省略了该属性。例如,以下两个宏调用是等效的:
# pkg/BUILD
my_macro(name = "abc", srcs = ["src.cc"], deps = None)
my_macro(name = "abc", srcs = ["src.cc"])
这在 BUILD
文件中通常没什么用,但在以编程方式将宏封装在另一个宏中时很有用。
详细信息
所创建定位条件的命名惯例
由符号宏创建的任何目标或子宏的名称必须与宏的 name
参数匹配,或者必须以 name
为前缀,后跟 _
(首选)、.
或 -
。例如,my_macro(name = "foo")
可能只能创建名为 foo
或以 foo_
、foo-
或 foo.
为前缀的文件或目标,例如 foo_bar
。
可以声明违反宏命名惯例的目标或文件,但无法构建这些目标或文件,也无法将其用作依赖项。
与宏实例位于同一软件包中的非宏文件和目标不应具有与潜在宏目标名称冲突的名称,但此排他性并非强制执行。我们正在实施延迟评估,以提高符号宏的性能,但违反命名架构的软件包会受到影响。
限制
与旧版宏相比,符号宏还有一些额外的限制。
符号宏
- 必须接受
name
实参和visibility
实参 - 必须具有
implementation
函数 - 可能不返回值
- 不得改变其实参
- 除非是特殊的
finalizer
宏,否则不得调用native.existing_rules()
- 可能无法调用
native.package()
- 可能无法调用
glob()
- 可能无法调用
native.environment_group()
- 必须创建名称遵循命名架构的目标
- 无法引用未声明或未作为实参传入的输入文件
- 无法引用其调用方的私有目标(如需了解详情,请参阅可见性和宏)。
可见性和宏
可见性系统有助于保护(符号)宏及其调用者的实现细节。
默认情况下,在符号宏中创建的目标在宏本身中可见,但不一定对宏的调用者可见。宏可以通过转发其自身的 visibility
属性值(如 some_rule(..., visibility = visibility)
中所示)将目标“导出”为公共 API。
宏可见性的关键理念如下:
系统会根据声明目标的宏(而非调用宏的软件包)来检查可见性。
- 换句话说,即使两个目标位于同一软件包中,一个目标本身也不会对另一个目标可见。这可防止宏的内部目标成为软件包中其他宏或顶级目标的依赖项。
规则和宏上的所有
visibility
属性都会自动包含调用规则或宏的位置。- 因此,目标会无条件地对同一宏(或
BUILD
文件,如果不在宏中)中声明的其他目标可见。
- 因此,目标会无条件地对同一宏(或
实际上,这意味着当宏声明目标而不设置其 visibility
时,目标默认在宏内部。(软件包的默认公开范围不适用于宏。)导出目标意味着,目标对宏的调用者在宏的 visibility
属性中指定的任何内容、宏的调用者本身的软件包以及宏自身的代码都是可见的。另一种理解方式是,宏的可见性决定了谁(除了宏本身)可以查看宏的导出目标。
# tool/BUILD
...
some_rule(
name = "some_tool",
visibility = ["//macro:__pkg__"],
)
# macro/macro.bzl
def _impl(name, visibility):
cc_library(
name = name + "_helper",
...
# No visibility passed in. Same as passing `visibility = None` or
# `visibility = ["//visibility:private"]`. Visible to the //macro
# package only.
)
cc_binary(
name = name + "_exported",
deps = [
# Allowed because we're also in //macro. (Targets in any other
# instance of this macro, or any other macro in //macro, can see it
# too.)
name + "_helper",
# Allowed by some_tool's visibility, regardless of what BUILD file
# we're called from.
"//tool:some_tool",
],
...
visibility = visibility,
)
my_macro = macro(implementation = _impl, ...)
# pkg/BUILD
load("//macro:macro.bzl", "my_macro")
...
my_macro(
name = "foo",
...
)
some_rule(
...
deps = [
# Allowed, its visibility is ["//pkg:__pkg__", "//macro:__pkg__"].
":foo_exported",
# Disallowed, its visibility is ["//macro:__pkg__"] and
# we are not in //macro.
":foo_helper",
]
)
如果使用 visibility = ["//other_pkg:__pkg__"]
调用了 my_macro
,或者 //pkg
软件包已将其 default_visibility
设置为该值,则 //pkg:foo_exported
也可在 //other_pkg/BUILD
内或在 //other_pkg:defs.bzl
中定义的宏内使用,但 //pkg:foo_helper
仍会受到保护。
宏可以通过传递 visibility = ["//some_friend:__pkg__"]
(对于内部目标)或 visibility = visibility + ["//some_friend:__pkg__"]
(对于导出的目标)来声明目标对好友软件包可见。请注意,宏声明具有公开可见性 (visibility = ["//visibility:public"]
) 的目标是一种反模式。这是因为,即使调用方指定了更受限的可见性,该目标也会无条件地对每个软件包可见。
所有可见性检查都是针对当前正在运行的最内层符号宏完成的。不过,存在一种可见性委托机制:如果宏将标签作为属性值传递给内部宏,则内部宏中对该标签的所有使用情况都会根据外部宏进行检查。如需了解详情,请参阅公开范围页面。
请注意,旧版宏对可见性系统完全透明,其行为就好像其位置是调用它们的任何 BUILD 文件或符号宏。
终结器和可见性
在规则 finalizer 中声明的目标除了遵循常规的符号宏可见性规则之外,还可以查看对 finalizer 目标软件包可见的所有目标。
这意味着,如果您将基于 native.existing_rules()
的旧版宏迁移到 finalizer,则由 finalizer 声明的目标仍将能够看到其旧依赖项。
不过,请注意,可以在符号宏中声明目标,这样一来,即使 finalizer 可以使用 native.existing_rules()
内省其属性,finalizer 的目标也无法在可见性系统下看到该目标。
选择
如果属性为 configurable
(默认值)且其值不是 None
,则宏实现函数会将属性值视为封装在简单的 select
中。这样一来,宏作者就可以更轻松地发现他们未预料到属性值可能是 select
的 bug。
例如,请看以下宏:
my_macro = macro(
attrs = {"deps": attr.label_list()}, # configurable unless specified otherwise
implementation = _my_macro_impl,
)
如果使用 deps = ["//a"]
调用 my_macro
,则会导致使用 deps
参数(设置为 select({"//conditions:default":
["//a"]})
)调用 _my_macro_impl
。如果这导致实现函数失败(例如,因为代码尝试对值进行索引,如 deps[0]
中所示,这对于 select
是不允许的),宏作者可以选择:要么重写宏以仅使用与 select
兼容的操作,要么将属性标记为不可配置 (attr.label_list(configurable = False)
)。后者可确保不允许用户传入 select
值。
规则目标会反转此转换,并将微不足道的 select
存储为无条件值;在上述示例中,如果 _my_macro_impl
声明了规则目标 my_rule(..., deps = deps)
,则该规则目标的 deps
将存储为 ["//a"]
。这可确保 select
封装不会导致由宏实例化的所有目标中存储微不足道的 select
值。
如果可配置属性的值为 None
,则不会将其封装在 select
中。这可确保 my_attr == None
等测试仍能正常运行,并且当属性转发到具有计算默认值的规则时,该规则的行为正常(即,就像根本没有传入属性一样)。属性并不总是可以采用 None
值,但对于 attr.label()
类型和任何继承的非必需属性,这种情况可能会发生。
最终确定者
规则 finalizer 是一种特殊的符号宏,无论其在 BUILD 文件中的词法位置如何,都会在加载软件包的最后阶段(在定义了所有非 finalizer 目标后)进行评估。与普通的符号宏不同,finalizer 可以调用 native.existing_rules()
,但其行为与旧版宏略有不同:它仅返回一组非 finalizer 规则目标。最终确定器可能会断言该集合的状态或定义新目标。
如需声明终结器,请使用 finalizer = True
调用 macro()
:
def _my_finalizer_impl(name, visibility, tags_filter):
for r in native.existing_rules().values():
for tag in r.get("tags", []):
if tag in tags_filter:
my_test(
name = name + "_" + r["name"] + "_finalizer_test",
deps = [r["name"]],
data = r["srcs"],
...
)
continue
my_finalizer = macro(
attrs = {"tags_filter": attr.string_list(configurable = False)},
implementation = _impl,
finalizer = True,
)
慵懒
重要提示:我们正在实施延迟宏扩展和评估。此功能尚无法使用。
目前,所有宏在加载 BUILD 文件后立即进行评估,这可能会对软件包中还包含代价高昂的无关宏的目标产生负面性能影响。未来,只有在构建需要时,才会评估非终结器符号宏。前缀命名架构可帮助 Bazel 根据请求的目标确定要展开哪个宏。
迁移问题排查
下面列出了一些常见的迁移难题以及相应的解决方法。
- 旧版宏调用
glob()
将 glob()
调用移至您的 BUILD 文件(或从 BUILD 文件调用的旧版宏),并使用标签列表属性将 glob()
值传递给符号宏:
# BUILD file
my_macro(
...,
deps = glob(...),
)
- 旧版宏的参数不是有效的 Starlark
attr
类型。
尽可能将更多逻辑拉入嵌套的符号宏中,但将顶级宏保留为旧版宏。
- 旧版宏调用了创建违反命名架构的目标的规则
没关系,只要不依赖于“违规”目标即可。系统会默默忽略命名检查。