本页介绍了使用宏的基础知识,包括典型用例、调试和惯例。

宏是从 BUILD 文件调用的函数,可实例化规则。宏主要用于封装现有规则和其他宏以及重复使用代码。

宏分为两种:本页介绍的符号宏和旧版宏。我们建议您尽可能使用符号宏来提高代码清晰度。

符号宏提供类型化参数(字符串转换为标签,相对于调用宏的位置),并且能够限制和指定创建的目标的可见性。它们旨在支持延迟求值(将在未来的 Bazel 版本中添加)。符号宏在 Bazel 8 中默认可用。本文档中提及的 macros 是指符号宏

您可以在示例代码库中找到符号宏的可执行示例。

用法

.bzl 文件中,通过调用 macro() 函数并提供两个必需参数(attrsimplementation)来定义宏。

属性

attrs 接受一个将属性名称与属性类型对应的字典,该字典表示宏的实参。系统会隐式将两个常见属性(namevisibility)添加到所有宏中,这些属性不会包含在传递给 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,
)

属性类型声明接受参数 mandatorydefaultdoc。大多数属性类型还接受 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()
  • 必须创建名称遵循命名架构的目标
  • 无法引用未声明或未作为参数传入的输入文件(如需了解详情,请参阅可见性和宏)。

可见性和宏

可见性系统有助于保护(符号化)宏及其调用方的实现细节。

默认情况下,在符号宏中创建的目标在宏本身内是可见的,但不一定对宏的调用方可见。该宏可以通过转发其自身 visibility 属性的值(如 some_rule(..., visibility = visibility) 中所示)将目标“导出”为公共 API。

宏可见性的关键要点如下:

  1. 可见性检查取决于声明目标的宏,而不是调用宏的软件包。

    • 换句话说,仅仅位于同一软件包中并不会使一个目标对另一个目标可见。这可防止宏的内部目标成为软件包中其他宏或顶级目标的依赖项。
  2. 规则和宏中的所有 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 设置为该值,则 //other_pkg/BUILD//other_pkg:defs.bzl 中定义的宏中也可以使用 //pkg:foo_exported,但 //pkg:foo_helper 将保持受保护状态。

宏可以通过传递 visibility = ["//some_friend:__pkg__"](对于内部目标)或 visibility = visibility + ["//some_friend:__pkg__"](对于导出的目标)来声明目标对友好软件包可见。请注意,使用宏声明公开可见的目标 (visibility = ["//visibility:public"]) 是一种反模式。这是因为,这样会使目标对每个软件包都无条件可见,即使调用方指定了更受限的可见性也是如此。

所有可见性检查都针对当前正在运行的最内层符号宏进行。不过,有一个可见性委托机制:如果一个宏将标签作为属性值传递给内部宏,则系统会针对外部宏检查内部宏中标签的任何用法。如需了解详情,请参阅“公开范围”页面。

请注意,旧版宏对可见性系统完全透明,并且其行为就像其位置是从中调用的 BUILD 文件或符号宏一样。

选择

如果某个属性为 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 类型。

将尽可能多的逻辑提取到嵌套的符号宏中,但将顶级宏保留为旧版宏。

  • 旧版宏调用会创建违反命名架构的目标的规则

没关系,只要不依赖于“违规”目标即可。系统会静默忽略命名检查。