本页介绍了使用宏的基础知识,包括典型用例、调试和惯例。
宏是从 BUILD
文件调用的函数,可实例化规则。宏主要用于封装现有规则和其他宏以及重复使用代码。
宏分为两种类型:符号宏(本页介绍)和旧版宏。我们建议您尽可能使用符号宏来提高代码清晰度。
符号宏提供类型化参数(字符串转换为标签,相对于调用宏的位置),并且能够限制和指定创建的目标的可见性。它们旨在支持延迟求值(将在未来的 Bazel 版本中添加)。符号宏在 Bazel 8 中默认可用。本文档中提及的 macros
是指符号宏。
用法
您可以在 .bzl
文件中调用带有以下两个参数(attrs
和 implementation
)的 macro()
函数,从而定义宏。
属性
attrs
接受将属性名称字典转换为属性类型,该属性表示宏的参数。系统会隐式将两个常见属性(名称和可见性)添加到所有宏,并且这些属性不会包含在传递给 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"})
。如需了解详情,请参阅选择。
实现
implementation
接受包含宏逻辑的函数。实现函数通常通过调用一个或多个规则来创建目标,并且通常是私有的(以下划线开头)。通常,它们的命名与宏相同,但前缀为 _
,后缀为 _impl
。
与规则实现函数不同,规则实现函数接受包含对属性的引用的单个参数 (ctx
),而宏实现函数接受每个参数的参数。
# macro/macro.bzl
def _my_macro_impl(name, 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,
)
声明
通过在 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
。
详细信息
创建目标的命名惯例
符号宏创建的任何目标或子宏的名称必须与宏的 name
参数匹配,或者必须带有前缀 name
,后跟 _
(首选)、.
或 -
。例如,my_macro(name = "foo")
只能创建名为 foo
的文件或目标,或者前缀为 foo_
、foo-
或 foo.
的文件或目标,例如 foo_bar
。
您可以声明违反宏命名惯例的目标或文件,但无法构建这些目标或文件,也无法将其用作依赖项。
与宏实例位于同一软件包中的非宏文件和目标的名称不得与潜在的宏目标名称冲突,但系统不会强制执行此独占性。我们正在实现延迟求值,以改进符号宏的性能,因为违反命名架构的软件包会降低符号宏的性能。
限制
与旧版宏相比,符号宏有一些额外的限制。
符号宏
- 必须有一个
name
参数和一个visibility
参数 - 必须具有
implementation
函数 - 可能不会返回值
- 不得改变其
args
- 不能调用
native.existing_rules()
,除非它们是特殊的finalizer
宏 - 不得调用
native.package()
- 无法呼叫
glob()
- 不得调用
native.environment_group()
- 必须创建名称遵循命名架构的目标
- 不能引用未声明或作为参数传入的输入文件(如需了解详情,请参阅 visibility)。
公开范围
待办事项:展开此部分
目标可见性
默认情况下,由符号宏创建的目标对其创建的软件包可见。它们还接受 visibility
属性,该属性可将可见性扩展到宏的调用方(通过将 visibility
属性直接从宏调用传递到创建的目标)和其他软件包(通过在目标的可见性中明确指定它们)。
依赖项可见性
宏必须能看到其引用的文件和目标。他们可以通过以下方式之一来实现此目的:
- 以
attr
值的形式显式传递给宏
# pkg/BUILD
my_macro(... deps = ["//other_package:my_tool"] )
attr
值的隐式默认值
# my_macro:macro.bzl
my_macro = macro(
attrs = {"deps" : attr.label_list(default = ["//other_package:my_tool"])} )
- 已向宏定义显示
# other_package/BUILD
cc_binary(
name = "my_tool",
visibility = "//my_macro:\\__pkg__",
)
选择
如果某个属性为 configurable
,则宏实现函数将始终将该属性值视为 select
值。例如,请考虑以下宏:
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"]})
。
规则目标会反转此转换,并将较小的 select
存储为其无条件值;在此示例中,如果 _my_macro_impl
声明规则目标 my_rule(..., deps = deps)
,则该规则目标的 deps
将存储为 ["//a"]
。
定稿
规则终结器是一种特殊的符号宏,无论其在 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(...),
)
- 旧版宏的 1 个参数不是有效的 starlark
attr
类型。
将尽可能多的逻辑提取到嵌套的符号宏中,但将顶级宏保留为旧版宏。
- 旧版宏调用会创建违反命名架构的目标的规则
没关系,只要不依赖于“违规”目标即可。系统会静默忽略命名检查。