Intro #
Bazel’s evaluation model includes three phases:
- Loading phase:
evaluates and processes the Bazel files (
WORKSPACE
,BUILD
,*.bzl
) to create a target graph. - Analysis phase: processes the target graph to identify actions, determines their order and builds the action graph.
- Execution phase: runs the action graph to produce artifacts.
Macros are special StarkLark functions used exclusively in the loading
phase that we can use to make rules more convenient for users. As a
contrived but simple example, let’s create a macro that will provide a
reasonable default for the name of the output file. To users, the macro
will look exactly like a rule. Here is the resulting BUILD
file:
load(":rules.bzl", "demo_macro")
demo_macro(
name = "macros",
srcs = [
"english.sh",
],
out = "hello.sh",
)
demo_macro(
name = "bonjour",
srcs = [
"french.sh",
],
)
As we can see, I replaced the demo_binary
name from the
previous example
and added another target using the same rule, this time without the
out
attribute. The target should build a bonjour
script with
a French greeting.
Implementation #
Macros are mostly a convenience mechanism and cannot change the rule
internals, so neither the original rule nor its implementation function
needs to change in this example. However, we don’t export the demo_binary
function any longer, so it would be a good idea to make it private:
_demo_binary = rule(
...
Adding a macro to rules.bzl
is straight-forward:
def demo_macro(name, srcs, out = None, **kwargs):
_demo_binary(
name = name,
srcs = srcs,
out = "{}.sh".format(name) if out == None else out,
**kwargs
)
We can see that it defines several parameters, including:
- mandatory (
name
andsrcs
); - an argument with a default value (
out
); **kwards
so that users could pass an unspecified amount of keyworded parameters to the macro.
The macro then calls the original rule and conditionally passes
the target name as the out
attribute.
We can build and run both targets now:
$ bazel run //macros
...
Target //macros:macros up-to-date:
bazel-bin/macros/hello.sh
...
Hello, World!
$ bazel run //macros:bonjour
...
Target //macros:french up-to-date:
bazel-bin/macros/bonjour.sh
...
Bonjour monde!
It can be really useful to see what the macros expand into. We can use a Bazel query for that:
$ bz query --output=build //macros:all
# /Users/sb/repo/examples/bazel_rules/macros/BUILD.bazel:3:11
_demo_binary(
name = "macros",
generator_name = "macros",
generator_function = "demo_macro",
generator_location = "macros/BUILD.bazel:3:11",
out = "//macros:hello.sh",
srcs = ["//macros:english.sh"],
)
...
_demo_binary(
name = "bonjour",
generator_name = "bonjour",
generator_function = "demo_macro",
generator_location = "macros/BUILD.bazel:12:11",
out = "//macros:bonjour.sh",
srcs = ["//macros:french.sh"],
)
...
Macros: Best Practices #
It is easy to write a macro that is not convenient to use idiomatically. One of the common mistakes is to hardcode the name of the rule within the macro, which will prevent it from being re-used in the same package:
def dont_do_that():
some_rule(
name = "bad_idea",
...
Instead, a good macro defines and uses a name
argument.
If we need a default value, None
is usually the best option, as the
macro can then pass it to its rules. They will treat None
as
an absent parameter.
Re-usable macros should define and pass **kwargs
so that the users
can control the standard rule parameters (visibility
, etc.) It might
be a good idea to include visibility
explicitly.
Finally, please use a docstring to document your public macros.
The complete code for the example is in the repo.