Macros

This page covers the basics of using macros and includes typical use cases, debugging, and conventions.

A macro is a function called from the BUILD file that can instantiate rules. Macros are mainly used for encapsulation and code reuse of existing rules and other macros.

Macros come in two flavors: symbolic macros, which are described on this page, and legacy macros. Where possible, we recommend using symbolic macros for code clarity.

Symbolic macros offer typed arguments (string to label conversion, relative to where the macro was called) and the ability to restrict and specify the visibility of targets created. They are designed to be amenable to lazy evaluation (which will be added in a future Bazel release). Symbolic macros are available by default in Bazel 8. Where this document mentions macros, it's referring to symbolic macros.

Usage

Macros are defined in .bzl files by calling the macro() function with two required parameters: attrs and implementation.

Attributes

attrs accepts a dictionary of attribute name to attribute types, which represents the arguments to the macro. Two common attributes – name and visibility – are implicitly added to all macros and are not included in the dictionary passed to 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,
)

Attribute type declarations accept the parameters, mandatory, default, and doc. Most attribute types also accept the configurable parameter, which determines wheher the attribute accepts selects. If an attribute is configurable, it will parse non-select values as an unconfigurable select"foo" will become select({"//conditions:default": "foo"}). Learn more in selects.

Attribute inheritance

IMPORTANT: Attribute inheritance is an experimental feature enabled by the --experimental_enable_macro_inherit_attrs flag. Some of the behaviors described in this section may change before the feature is enabled by default.

Macros are often intended to wrap a rule (or another macro), and the macro's author often wants to forward the bulk of the wrapped symbol's attributes unchanged, using **kwargs, to the macro's main target (or main inner macro).

To support this pattern, a macro can inherit attributes from a rule or another macro by passing the rule or macro symbol to macro()'s inherit_attrs argument. (You can also use the special string "common" instead of a rule or macro symbol to inherit the common attributes defined for all Starlark build rules.) Only public attributes get inherited, and the attributes in the macro's own attrs dictionary override inherited attributes with the same name. You can also remove inherited attributes by using None as a value in the attrs dictionary:

# 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,
    },
    ...
)

The default value of non-mandatory inherited attributes is always overridden to be None, regardless of the original attribute definition's default value. If you need to examine or modify an inherited non-mandatory attribute – for example, if you want to add a tag to an inherited tags attribute – you must make sure to handle the None case in your macro's implementation function:

# 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

implementation accepts a function which contains the logic of the macro. Implementation functions often create targets by calling one or more rules, and they are are usually private (named with a leading underscore). Conventionally, they are named the same as their macro, but prefixed with _ and suffixed with _impl.

Unlike rule implementation functions, which take a single argument (ctx) that contains a reference to the attributes, macro implementation functions accept a parameter for each argument.

# 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,
        )

If a macro inherits attributes, its implementation function must have a **kwargs residual keyword parameter, which can be forwarded to the call that invokes the inherited rule or submacro. (This helps ensure that your macro won't be broken if the rule or macro which from which you are inheriting adds a new attribute.)

Declaration

Macros are declared by loading and calling their definition in a BUILD file.


# pkg/BUILD

my_macro(
    name = "macro_instance",
    deps = ["src.cc"] + select(
        {
            "//config_setting:special": ["special_source.cc"],
            "//conditions:default": [],
        },
    ),
    create_tests = True,
)

This would create targets //pkg:macro_instance_cc_lib and//pkg:macro_instance_test.

Just like in rule calls, if an attribute value in a macro call is set to None, that attribute is treated as if it was omitted by the macro's caller. For example, the following two macro calls are equivalent:

# pkg/BUILD
my_macro(name = "abc", srcs = ["src.cc"], deps = None)
my_macro(name = "abc", srcs = ["src.cc"])

This is generally not useful in BUILD files, but is helpful when programmatically wrapping a macro inside another macro.

Details

Naming conventions for targets created

The names of any targets or submacros created by a symbolic macro must either match the macro's name parameter or must be prefixed by name followed by _ (preferred), . or -. For example, my_macro(name = "foo") may only create files or targets named foo, or prefixed by foo_, foo- or foo., for example, foo_bar.

Targets or files that violate macro naming convention can be declared, but cannot be built and cannot be used as dependencies.

Non-macro files and targets within the same package as a macro instance should not have names that conflict with potential macro target names, though this exclusivity is not enforced. We are in the progress of implementing lazy evaluation as a performance improvement for Symbolic macros, which will be impaired in packages that violate the naming schema.

Restrictions

Symbolic macros have some additional restrictions compared to legacy macros.

Symbolic macros

  • must take a name argument and a visibility argument
  • must have an implementation function
  • may not return values
  • may not mutate their arguments
  • may not call native.existing_rules() unless they are special finalizer macros
  • may not call native.package()
  • may not call glob()
  • may not call native.environment_group()
  • must create targets whose names adhere to the naming schema
  • can't refer to input files that weren't declared or passed in as an argument (see visibility and macros for more details).

Visibility and macros

See Visibility for an in-depth discussion of visibility in Bazel.

Target visibility

By default, targets created by a symbolic macro are visible only in the package containing the .bzl file defining the macro. In particular, they are not visible to the caller of the symbolic macro unless the caller happens to be in the same package as the macro's .bzl file.

To make a target visible to the caller of the symbolic macro, pass visibility = visibility to the rule or inner macro. You may also make the target visible in additional packages by giving it a wider (or even public) visibility.

A package's default visibility (as declared in package()) is by default passed to the outermost macro's visibility parameter, but it is up to the macro to then pass (or not pass!) that visibility to the targets it instantiates.

Dependency visibility

Targets that are referred to in a macro's implementation must be visible to that macro's definition. Visibility can be given in one of the following ways:

  • Targets are visible to a macro if they are passed to the macro through label, label list, or label-keyed or -valued dict attributes, either explicitly:

# pkg/BUILD
my_macro(... deps = ["//other_package:my_tool"] )
  • ... or as attribute default values:
# my_macro:macro.bzl
my_macro = macro(
  attrs = {"deps" : attr.label_list(default = ["//other_package:my_tool"])},
  ...
)
  • Targets are also visible to a macro if they are declared visible to the package containing the .bzl file defining the macro:
# 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__",
)

Selects

If an attribute is configurable (the default) and its value is not None, then the macro implementation function will see the attribute value as wrapped in a trivial select. This makes it easier for the macro author to catch bugs where they did not anticipate that the attribute value could be a select.

For example, consider the following macro:

my_macro = macro(
    attrs = {"deps": attr.label_list()},  # configurable unless specified otherwise
    implementation = _my_macro_impl,
)

If my_macro is invoked with deps = ["//a"], that will cause _my_macro_impl to be invoked with its deps parameter set to select({"//conditions:default": ["//a"]}). If this causes the implementation function to fail (say, because the code tried to index into the value as in deps[0], which is not allowed for selects), the macro author can then make a choice: either they can rewrite their macro to only use operations compatible with select, or they can mark the attribute as nonconfigurable (attr.label_list(configurable = False)). The latter ensures that users are not permitted to pass a select value in.

Rule targets reverse this transformation, and store trivial selects as their unconditional values; in the above example, if _my_macro_impl declares a rule target my_rule(..., deps = deps), that rule target's deps will be stored as ["//a"]. This ensures that select-wrapping does not cause trivial select values to be stored in all targets instantiated by macros.

If the value of a configurable attribute is None, it does not get wrapped in a select. This ensures that tests like my_attr == None still work, and that when the attribute is forwarded to a rule with a computed default, the rule behaves properly (that is, as if the attribute were not passed in at all). It is not always possible for an attribute to take on a None value, but it can happen for the attr.label() type, and for any inherited non-mandatory attribute.

Finalizers

A rule finalizer is a special symbolic macro which – regardless of its lexical position in a BUILD file – is evaluated in the final stage of loading a package, after all non-finalizer targets have been defined. Unlike ordinary symbolic macros, a finalizer can call native.existing_rules(), where it behaves slightly differently than in legacy macros: it only returns the set of non-finalizer rule targets. The finalizer may assert on the state of that set or define new targets.

To declare a finalizer, call macro() with finalizer = True:

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,
)

Laziness

IMPORTANT: We are in the process of implementing lazy macro expansion and evaluation. This feature is not available yet.

Currently, all macros are evaluated as soon as the BUILD file is loaded, which can negatively impact performance for targets in packages that also have costly unrelated macros. In the future, non-finalizer symbolic macros will only be evaluated if they're required for the build. The prefix naming schema helps Bazel determine which macro to expand given a requested target.

Migration troubleshooting

Here are some common migration headaches and how to fix them.

  • Legacy macro calls glob()

Move the glob() call to your BUILD file (or to a legacy macro called from the BUILD file), and pass the glob() value to the symbolic macro using a label-list attribute:

# BUILD file
my_macro(
    ...,
    deps = glob(...),
)
  • Legacy macro has a parameter that isn't a valid starlark attr type.

Pull as much logic as possible into a nested symbolic macro, but keep the top level macro a legacy macro.

  • Legacy macro calls a rule that creates a target that breaks the naming schema

That's okay, just don't depend on the "offending" target. The naming check will be quietly ignored.