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

宏是从 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"})。如需了解详情,请参阅 select

属性继承

宏通常用于封装规则(或其他宏),并且宏的 作者通常希望使用 **kwargs 将封装符号的大部分属性 不变地转发到宏的主要目标(或主要内部宏)。

为了支持此模式,宏可以通过将 规则宏符号 传递给 macro()'s 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 文件中加载和调用其定义来声明。 ```starlark

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 设置为该值,则 //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 中声明的目标除了遵循 通常的符号宏可见性规则之外,还可以看到对 finalizer 目标软件包可见的所有目标。

这意味着,如果您将基于 native.existing_rules() 的旧版宏迁移到 finalizer,则 finalizer 声明的目标仍将能够看到 其旧依赖项。

不过,请注意,您可以在符号宏中声明目标,以便 finalizer 的目标在可见性系统下无法看到该目标,即使 finalizer 可以自检其属性也是如此native.existing_rules()

Select

如果属性是 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

规则 finalizer 是一种特殊的符号宏,无论其在 BUILD 文件中的词法 位置如何,都会在加载软件包的最后阶段进行求值, 在定义所有非 finalizer 目标之后。与普通符号 宏不同,finalizer 可以调用 native.existing_rules(),其行为 与旧版宏略有不同:它仅返回 非 finalizer 规则目标的集合。finalizer 可以断言该集合的状态或 定义新目标。

如需声明 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 文件后立即进行求值,这 可能会对软件包中也包含成本高昂的无关宏的目标产生负面影响。将来,只有在构建需要非 finalizer 符号宏时,才会对其进行 求值。前缀命名架构有助于 Bazel 根据请求的目标确定要展开哪个宏。

迁移问题排查

以下列出了一些常见的迁移难题以及解决方法。

  • 旧版宏调用 glob()

glob() 调用移至 BUILD 文件(或从 BUILD 文件调用的旧版宏),并使用 标签列表属性将 glob() 值传递给符号宏:

# BUILD file
my_macro(
    ...,
    deps = glob(...),
)
  • 旧版宏具有一个形参,该形参不是有效的 Starlark attr 类型。

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

  • 旧版宏调用一个规则,该规则创建的目标违反了命名架构

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