- Study the tool CLI
- Start from the BUILD DX
- Starlark Rules API
.bzl files containing a simple “pre-processor” construct
called a Macro, which gave us the power to make good BUILD file authoring experiences.
Most of the time, this is all you need. But “rulesets” must be written for many tools.
This section dives into the deeper problem of custom rules.
Introducing custom rules
First, recall that an Action is a transformation from some inputs to some outputs, by spawning a tool.DefinitionA “Rule” extends Bazel to understand how to produce an action sub-graph from the user’s dependency graph.
- Output Groups: Multiple named sets of outputs
- Can run multiple actions. Which actions run depends on which outputs are requested.
- Inter-operate with other rules: “Providers”
- Walk their dependency graph: “Aspects”
When to write a custom rule
You should always prefer existing rules, and then macros, where possible. 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.Principles of successful rule authorship
- Be very sparing in the public API you commit to. Bugs should be in the tool you call, not your rule.
- Don’t write a Bazel-specific tool if you can help it. Rules should wrap tools that are already mature and in wide use for the problem being solved.
- Avoid JDK and Node.js runtime. These are meant for long-running processes. Bazel has “Persistent Workers” but these introduce as many problems as they solve.
- Fetch tools as pre-built binaries. It’s possible for Bazel to build them from source, but it’s slow (how many times have you watched Bazel build
protoc) and also introduces a failure mode for users whose toolchain doesn’t function properly to build them.
Step 1: use the template
We maintain an excellent template for new rulesets. If your rules are going to be public, this is definitely the place to start: https://github.com/bazel-contrib/rules-template This takes care of:- creating needed
bzl_librarytargets - automated API documentation generation
- platform-specific toolchain registration for
genruleand rules - CI testing with GitHub Actions, linting with pre-commit
- WORKSPACE and bzlmod usage
- publish releases just by pushing a tag to the repository
Step 2: Fetch toolchain
We need the tool to be on the user’s machine. Research how the tool is currently published. We want a reliable way to fetch it from the maintainers. Look for binaries published on a GitHub release, artifacts published to a repository like Maven, PyPI, NPM, etc. At the end of this step you should be able tobazel run the tool.
Step 3: Study the CLI
The “man page” for the tool you’re running provides the guide for how your rule API should be formed. This allows you to make the thinnest possible layer, and ensures that documentation for the tool outside Bazel is a pretty good start to understanding how to use your rule. Things to look for in the CLI:- Are there flags related to hermeticity, like
-dont-download-stuff? - Can it accept a “flag file” so that very long
argvdoesn’t spill the OS limit? - How do you specify the location of output files?
Step 4: Create an example of usage to drive development
Make something likeexamples/simple/BUILD.bazel.
First, just make a genrule. This is going to be the “break glass” for users who just want to call
the tool without using your rule at all.
For example:
Step 5: Private API
This should generally be in the following form:- The implementation function
- (opt) helper functions to form arguments, or declare actions
- A struct for others to build their own rule from your library
Step 6: Public API
The public API nearly always wraps the rule in a macro. Since macros and rules are not distinguishable at the use-site, you can always change this later. We wrap in a macro to provide a few benefits:- Recommended way to pre-declare outputs, so users can refer to individual outputs with a label
- Lets you yield extra targets like
[name].update - Can have polymorphic attribute types like “pass a list of strings OR the label of a target”
- Can compose multiple rules - but take care! Macros are a leaky abstraction, and it’s difficult to
determine which
*kwargsneed to be forwarded on to each target. Best to do this only with rules you declare in this API.
my_rule() in their BUILD file, we always want bazel query to show my_rule.
This requires a bit of juggling. In your private API, declare the rule, for example:
Exercise
Let’s study a rule relevant to your Bazel use and see these patterns in context there. More ambitiously, let’s pair-program, adding a (simple) custom rule to the bazel-examples repository.- Someone in the class, propose a rule that might be interesting for your org.
- We will spend 10-15 minutes trying to write one. In the interest of time, the instructor will present.

