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 fromBUILD 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.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 newmacro constructor call in a .bzl file with a separate implementation function:
- 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.
run_binary rule introduced earlier is a great candidate for the some_rule here.
Read more: genrule bestrule
- Allow polymorphic attribute types, and vary their behavior based on the type. For example
rules_ociuses 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.globto infer a default value for missing attributes based on what files exist.
Example 1
This example just wraps a singlerun_binary rule, in this case it’s a third-party tool called “mocha”
which was fetched from npm.
Usage:
Example 2
This example composes a few building blocks from bazel_skylib and aspect_bazel_lib. Usage:Example 3
This example creates a macro wrapping a repository rule rather than a build rule. (Actually, it usesalias 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.When a Macro isn’t enough
Rules create actions, which transform inputs to outputs. Usingts_project as an example, this couldn’t be a macro for several reasons:
- It creates a tree of actions, which might use one tool to transpile
.jsoutputs, and a different tool for producing TypeScript types (.d.tsfiles). - It requires that
srcshave aJsInfoprovider so that it can understand their structure. - It produces a
JsInfoprovider 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 yourBUILD files to call the macro.
