Skip to main content
Goal: by the end of this section, you’ll know how to make more readable BUILD files by using a simple code-sharing technique provided by Bazel.

Macros

Bazel Macros are like pre-processor definitions, which compose existing rules in a novel way and provide “syntax sugar” to developers who call them from BUILD files.
Macro/Rule Isomorphism:At a BUILD file usage site, you cannot distinguish macro from rule. This is to allow a rule to be wrapped with a macro without a breaking change.
Thanks to this design, we can start by imagining the right way for a user to express their “bare facts” in the BUILD file, then write Starlark code that supports it. We can start with a macro as they are much easier, but we can always introduce a custom rule when the requirements make it necessary.

Symbolic Macros

Symbolic macros were introduced in Bazel 8, and use a dedicated new macro constructor call in a .bzl file with a separate implementation function:
Symbolic macros have some advantages:
  • providing types for their attributes makes better errors when misused
  • targets they create are private by default, so they don’t accidentally expose their internals
  • they enforce targets are created using the naming schema, to avoid collisions when the macro is called more than once in a package

Legacy Macros

Starting in Bazel 8, the construct which had been referred to as a “Macro” is now a “Legacy Macro”. However it’s still useful to learn it because it can do things a Symbolic macro cannot, and it’s simpler. A legacy macro is just a function definition in a .bzl file which composes some existing rules.
The run_binary rule introduced earlier is a great candidate for the some_rule here.
Read more: genrule bestrule
Legacy macros can:
  • Allow polymorphic attribute types, and vary their behavior based on the type. For example rules_oci uses this to permit an attribute that’s either a label of a file, or a string value that should be written into a file.
  • Use native.glob to infer a default value for missing attributes based on what files exist.
However, legacy macros are untyped. To provide a good user experience when developers make a mistake, it’s necessary to check the types manually.
Leaky Abstraction If you bazel print (Aspect CLI only) which is a syntactic operation on the BUILD file, you see the macro as it was called. However, macros are expanded during the loading phase, so if you run a bazel query you’ll see the result of the macro evaluation.If the macro is named differently from the underlying rule, this can be confusing for users and also affect usability, for example --test_lang_filters applies to the underlying rule’s name.

Example 1

This example just wraps a single run_binary rule, in this case it’s a third-party tool called “mocha” which was fetched from npm. Usage:
Definition:

Example 2

This example composes a few building blocks from bazel_skylib and aspect_bazel_lib. Usage:
Definition:

Example 3

This example creates a macro wrapping a repository rule rather than a build rule. (Actually, it uses alias which is even shorter than a macro, it passes all attributes through.) It uses select to get a binary for the host platform, bypassing the need for toolchains which are a tricky part of Custom rules.
If you run an un-configured build (for example with bazel query) then select will eagerly load every label on the right-hand-side. This causes an eager fetch of tools which don’t run on the host platform and wastes the developers time. This is a good reason to get in the habit of always using bazel cquery instead, so that the build is configured.
Usage:

When a Macro isn’t enough

Rules create actions, which transform inputs to outputs. Using ts_project as an example, this couldn’t be a macro for several reasons:
  1. It creates a tree of actions, which might use one tool to transpile .js outputs, and a different tool for producing TypeScript types (.d.ts files).
  2. It requires that srcs have a JsInfo provider so that it can understand their structure.
  3. It produces a JsInfo provider for inter-op with downstream rules that depend on it.
Even when Providers get in your way of “just using a macro”, you can often write a tiny adapter rule and then put most of your logic in a more easily understood macro. For example, this code adapts a ProtoInfo on its sources to a DefaultInfo output.

Try it: write a macro

Add any macro in your repository, even a trivial one. Then change one of your BUILD files to call the macro.