Customize Bazel with Aspect CLI plugins

Customize Bazel with Aspect CLI plugins

A consistent theme of Bazel deployments at our clients is the steep on-ramp: how difficult it is for developers to understand and for DevInfra teams to integrate. On the other hand, Bazel's popularity is partly because Googlers have experienced how well it can work, and have told their story other companies who want that experience too.

What's the missing piece? Bazel was customized for Google's developer workflow, and now you can customize it for yours!

The aspect CLI is a wrapper around Bazel. If you've ever installed bazelisk following the Bazel recommended install you already run a wrapper around Bazel. In fact, aspect includes bazelisk so it's an even better wrapper.

aspect supports plugins, which give you the missing "control point" to solve for issues in local developer workflows, such as:

  • amend error messages to point to your internal documentation
  • offer to apply auto-fixes to incorrect source code
  • add commands for deploying, linting, rebasing, or other common developer workflows
  • "cookiecutter": stamp out new Bazel projects following your local conventions

Plugins also run on Continuous Integration servers where you might want to:

  • "fail fast" by reporting a red status as soon as the first build/test step fails
  • feed systems that manage flakiness or trigger buildcop actions

A plugin is any program that serves our gRPC protocol. While you can write a plugin from scratch in any language, in practice there's a much faster way to get started: clone our template repository and write your plugin using our Go SDK.

This article shows how to write your first plugin.

Note: the plugin API is still in alpha, so breaking changes are likely.

Tour of the plugin

The template repository includes a full Bazel setup and CI/CD using GitHub actions, so all you need to look at is one file: plugin.go. It serves as an example of how to write your own plugin.

After the imports, you'll see the main one-liner. This just uses the excellent go-plugin library from Hashicorp to start up your plugin and connect it to the CLI.

func main() {
    goplugin.Serve(config.NewConfigFor(&HelloWorldPlugin{}))
}

Next we declare the plugin type, HelloWorldPlugin so we have an instance to store state. The Base field lets us inherit implementations so we only need to implement the functions we care about.

After that, we implement the CustomCommands function, so we can declare that aspect hello-world is available to users. Our command appears in the output of aspect help and when run, it just prints "Hello World!". Your custom command can do anything you like, including spawning sub-processes to interact with your other tools.

The next function in the example is BEPEventCallback. This is called for each BuildEvent received from Bazel. Your editor will give type-completion here, making it pretty easy to navigate the available events in Bazel's build_event_stream protocol buffer. This is all you have to do to consume the Build Event Protocol! In this example we just store some useful information, but you could also immediately call an API like telling your CI server that the build is going to go red.

Finally we implement the PostBuildHook which is called after bazel build exits. In that function we're using the supplied PromptRunner interface to ask the user for input, when we're running in interactive mode.

The fix-visibility plugin shows a very useful real-world use case. We've seen developers new to Bazel struggle with the concept of "visibility" and it's often the first error they run into requiring manual BUILD file edits.

Here's what it looks like in action: asciicast

This plugin uses the same functions we saw in the template. It subscribes to the Build Event Protocol to watch for Bazel analysis failures including the is not visible from target message, storing them for after the build finishes.

When running interactively, it prompts the user if they'd like to have the problem auto-fixed. Note that since our plugin is written in Go, and most of the tooling ecosystem around Bazel is also written in Go, it's easy to reuse buildozer to apply the edits to the BUILD file.

If running non-interactive, like on CI, it prints the equivalent buildozer command the user could run locally to fix their BUILD file.

As a result, users feel that Bazel is "easy to use", and put less support/training burden on your DevInfra team.

What plugins can you imagine?

We can't wait to see how you extend Bazel! As more plugins are available, we'll put together a plugin gallery to help users discover ones that solve their own local workflow woes.