本页介绍了使用宏的基础知识,包括典型用例、调试和惯例。
宏是从 BUILD
文件调用的函数,可实例化规则。宏主要用于封装现有规则和其他宏以及重复使用代码。
宏分为两种:本页介绍的符号宏和旧版宏。我们建议您尽可能使用符号宏来提高代码清晰度。
符号宏提供类型化参数(字符串转换为标签,相对于调用宏的位置),并且能够限制和指定创建的目标的可见性。它们旨在支持延迟求值(将在未来的 Bazel 版本中添加)。符号宏在 Bazel 8 中默认可用。本文档中提及的 macros
是指符号宏。
用法
在 .bzl
文件中,通过调用 macro()
函数并提供两个必需参数(attrs
和 implementation
)来定义宏。
属性
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
字典中的属性会替换同名的继承属性。您还可以使用 None
作为 attrs
字典中的值来移除继承的属性:
# 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
_my_macro_implementation(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
函数 - 可能不会返回值
- 不得更改其参数
- 不得调用
native.existing_rules()
,除非它们是特殊的finalizer
宏 - 不得调用
native.package()
- 不得调用
glob()
- 不得调用
native.environment_group()
- 必须创建名称遵循命名架构的目标
- 无法引用未声明或未作为参数传入的输入文件(如需了解详情,请参阅可见性和宏)。
可见性和宏
如需深入了解 Bazel 中的可见性,请参阅可见性。
目标公开范围
默认情况下,由符号宏创建的目标仅在包含定义宏的 .bzl 文件的软件包中可见。具体而言,它们对符号宏的调用方不可见,除非调用方恰好与宏的 .bzl 文件位于同一软件包中。
如需将目标显示给符号宏的调用方,请将 visibility = visibility
传递给规则或内部宏。您还可以通过为目标设置更广泛(甚至是公开)的公开范围,使其在其他软件包中可见。
软件包的默认公开范围(如 package()
中所声明)默认会传递给最外层宏的 visibility
参数,但之后是否将该 visibility
传递给它实例化的目标取决于该宏。
依赖项可见性
宏实现中引用的目标必须对该宏的定义可见。您可以通过以下任一方式授予公开范围:
- 如果目标通过标签、标签列表或标签键或值的字典属性(显式或隐式)传递给宏,则宏可以看到这些目标:
# pkg/BUILD
my_macro(... deps = ["//other_package:my_tool"] )
- ... 或作为属性默认值:
# my_macro:macro.bzl
my_macro = macro(
attrs = {"deps" : attr.label_list(default = ["//other_package:my_tool"])},
...
)
- 如果目标被声明为可见于包含定义宏的 .bzl 文件的软件包,则对宏也是可见的:
# other_package/BUILD
# Any macro defined in a .bzl file in //my_macro package can use this tool.
cc_binary(
name = "my_tool",
visibility = "//my_macro:\\__pkg__",
)
选择
如果某个属性为 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
,则会导致 _my_macro_impl
被调用,其 deps
参数设置为 select({"//conditions:default":
["//a"]})
。如果这导致实现函数失败(例如,因为代码尝试按 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()
类型和任何继承的非必需属性可以采用。
终结器
规则终结器是一种特殊的符号宏,无论其在 BUILD 文件中的词法位置如何,都会在加载软件包的最后阶段(即定义所有非终结器目标之后)进行评估。与普通符号宏不同,终结器可以调用 native.existing_rules()
,在这种情况下,它的行为与旧版宏略有不同:它只会返回一组非终结器规则目标。终结器可以断言该集合的状态或定义新的目标。
如需声明终结器,请使用 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
类型。
将尽可能多的逻辑提取到嵌套的符号宏中,但将顶级宏保留为旧版宏。
- 旧版宏调用会创建违反命名架构的目标的规则
没关系,只要不依赖于“违规”目标即可。系统会静默忽略命名检查。