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
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
select
s. 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.
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, 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,
)
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
.
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 avisibility
argument - must have an
implementation
function - may not return values
- may not mutate their
args
- may not call
native.existing_rules()
unless they are specialfinalizer
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 for more details).
Visibility
TODO: Expand this section
Target visibility
At default, targets created by symbolic macros are visible to the package in
which they are created. They also accept a visibility
attribute, which can
expand that visibility to the caller of the macro (by passing the visibility
attribute directly from the macro call to the target created) and to other
packages (by explicitly specifying them in the target's visibility).
Dependency visibility
Macros must have visibility to the files and targets they refer to. They can do so in one of the following ways:
- Explicitly passed in as an
attr
value to the macro
# pkg/BUILD
my_macro(... deps = ["//other_package:my_tool"] )
- Implicit default of an
attr
value
# my_macro:macro.bzl
my_macro = macro(
attrs = {"deps" : attr.label_list(default = ["//other_package:my_tool"])} )
- Already visible to the macro definition
# other_package/BUILD
cc_binary(
name = "my_tool",
visibility = "//my_macro:\\__pkg__",
)
Selects
If an attribute is configurable
, then the macro implementation function will
always see the attribute value as select
-valued. 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"]})
.
Rule targets reverse this transformation, and store trivial select
s as their
unconditional values; in this example, if _my_macro_impl
declares a rule
target my_rule(..., deps = deps)
, that rule target's deps
will be stored as
["//a"]
.
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.