blob: f893bff1c2c39cb587a4b6ac565dd1d74e31b887 [file] [log] [blame] [view] [edit]
Project: /_project.yaml
Book: /_book.yaml
# Using Macros to Create Custom Verbs
{% include "_buttons.html" %}
Day-to-day interaction with Bazel happens primarily through a few commands:
`build`, `test`, and `run`. At times, though, these can feel limited: you may
want to push packages to a repository, publish documentation for end-users, or
deploy an application with Kubernetes. But Bazel doesn't have a `publish` or
`deploy` command – where do these actions fit in?
## The bazel run command
Bazel's focus on hermeticity, reproducibility, and incrementality means the
`build` and `test` commands aren't helpful for the above tasks. These actions
may run in a sandbox, with limited network access, and aren't guaranteed to be
re-run with every `bazel build`.
Instead, rely on `bazel run`: the workhorse for tasks that you *want* to have
side effects. Bazel users are accustomed to rules that create executables, and
rule authors can follow a common set of patterns to extend this to
"custom verbs".
### In the wild: rules_k8s
For example, consider [`rules_k8s`](https://github.com/bazelbuild/rules_k8s),
the Kubernetes rules for Bazel. Suppose you have the following target:
```python
# BUILD file in //application/k8s
k8s_object(
name = "staging",
kind = "deployment",
cluster = "testing",
template = "deployment.yaml",
)
```
The [`k8s_object` rule](https://github.com/bazelbuild/rules_k8s#usage) builds a
standard Kubernetes YAML file when `bazel build` is used on the `staging`
target. However, the additional targets are also created by the `k8s_object`
macro with names like `staging.apply` and `:staging.delete`. These build
scripts to perform those actions, and when executed with `bazel run
staging.apply`, these behave like our own `bazel k8s-apply` or `bazel
k8s-delete` commands.
### Another example: ts_api_guardian_test
This pattern can also be seen in the Angular project. The
[`ts_api_guardian_test` macro](https://github.com/angular/angular/blob/16ac611a8410e6bcef8ffc779f488ca4fa102155/tools/ts-api-guardian/index.bzl#L22)
produces two targets. The first is a standard `nodejs_test` target which compares
some generated output against a "golden" file (that is, a file containing the
expected output). This can be built and run with a normal `bazel
test` invocation. In `angular-cli`, you can run [one such
target](https://github.com/angular/angular-cli/blob/e1269cb520871ee29b1a4eec6e6c0e4a94f0b5fc/etc/api/BUILD)
with `bazel test //etc/api:angular_devkit_core_api`.
Over time, this golden file may need to be updated for legitimate reasons.
Updating this manually is tedious and error-prone, so this macro also provides
a `nodejs_binary` target that updates the golden file, instead of comparing
against it. Effectively, the same test script can be written to run in "verify"
or "accept" mode, based on how it's invoked. This follows the same pattern
you've learned already: there is no native `bazel test-accept` command, but the
same effect can be achieved with
`bazel run //etc/api:angular_devkit_core_api.accept`.
This pattern can be quite powerful, and turns out to be quite common once you
learn to recognize it.
## Adapting your own rules
[Macros](/extending/macros) are the heart of this pattern. Macros are used like
rules, but they can create several targets. Typically, they will create a
target with the specified name which performs the primary build action: perhaps
it builds a normal binary, a Docker image, or an archive of source code. In
this pattern, additional targets are created to produce scripts performing side
effects based on the output of the primary target, like publishing the
resulting binary or updating the expected test output.
To illustrate this, wrap an imaginary rule that generates a website with
[Sphinx](https://www.sphinx-doc.org) with a macro to create an additional
target that allows the user to publish it when ready. Consider the following
existing rule for generating a website with Sphinx:
```python
_sphinx_site = rule(
implementation = _sphinx_impl,
attrs = {"srcs": attr.label_list(allow_files = [".rst"])},
)
```
Next, consider a rule like the following, which builds a script that, when run,
publishes the generated pages:
```python
_sphinx_publisher = rule(
implementation = _publish_impl,
attrs = {
"site": attr.label(),
"_publisher": attr.label(
default = "//internal/sphinx:publisher",
executable = True,
),
},
executable = True,
)
```
Finally, define the following symbolic macro (available in Bazel 8 or newer) to
create targets for both of the above rules together:
```starlark
def _sphinx_site_impl(name, visibility, srcs, **kwargs):
# This creates the primary target, producing the Sphinx-generated HTML. We
# set `visibility = visibility` to make it visible to callers of the
# macro.
_sphinx_site(name = name, visibility = visibility, srcs = srcs, **kwargs)
# This creates the secondary target, which produces a script for publishing
# the site generated above. We don't want it to be visible to callers of
# our macro, so we omit visibility for it.
_sphinx_publisher(name = "%s.publish" % name, site = name, **kwargs)
sphinx_site = macro(
implementation = _sphinx_site_impl,
attrs = {"srcs": attr.label_list(allow_files = [".rst"])},
# Inherit common attributes like tags and testonly
inherit_attrs = "common",
)
```
Or, if you need to support Bazel releases older than Bazel 8, you would instead
define a legacy macro:
```starlark
def sphinx_site(name, srcs = [], **kwargs):
# This creates the primary target, producing the Sphinx-generated HTML.
_sphinx_site(name = name, srcs = srcs, **kwargs)
# This creates the secondary target, which produces a script for publishing
# the site generated above.
_sphinx_publisher(name = "%s.publish" % name, site = name, **kwargs)
```
In the `BUILD` files, use the macro as though it just creates the primary
target:
```python
sphinx_site(
name = "docs",
srcs = ["index.md", "providers.md"],
)
```
In this example, a "docs" target is created, just as though the macro were a
standard, single Bazel rule. When built, the rule generates some configuration
and runs Sphinx to produce an HTML site, ready for manual inspection. However,
an additional "docs.publish" target is also created, which builds a script for
publishing the site. Once you check the output of the primary target, you can
use `bazel run :docs.publish` to publish it for public consumption, just like
an imaginary `bazel publish` command.
It's not immediately obvious what the implementation of the `_sphinx_publisher`
rule might look like. Often, actions like this write a _launcher_ shell script.
This method typically involves using
[`ctx.actions.expand_template`](lib/actions#expand_template)
to write a very simple shell script, in this case invoking the publisher binary
with a path to the output of the primary target. This way, the publisher
implementation can remain generic, the `_sphinx_site` rule can just produce
HTML, and this small script is all that's necessary to combine the two
together.
In `rules_k8s`, this is indeed what `.apply` does:
[`expand_template`](https://github.com/bazelbuild/rules_k8s/blob/f10e7025df7651f47a76abf1db5ade1ffeb0c6ac/k8s/object.bzl#L213-L241)
writes a very simple Bash script, based on
[`apply.sh.tpl`](https://github.com/bazelbuild/rules_k8s/blob/f10e7025df7651f47a76abf1db5ade1ffeb0c6ac/k8s/apply.sh.tpl),
which runs `kubectl` with the output of the primary target. This script can
then be build and run with `bazel run :staging.apply`, effectively providing a
`k8s-apply` command for `k8s_object` targets.