Skip to main content
Version: 2024.41.17

Aspect CLI Plugins

Plugins allow you to customize Bazel's behavior to fit your developer workflows. You can use an existing plugin from the catalog, or write your own.

Overview

A plugin can subscribe to the Build Event Protocol (BEP), to react in real-time during the build. Plugins can contribute custom commands like lint so developers can live in a single tool.

A plugin is a program with a gRPC server that implements our plugin protocol. You can write a plugin in any language. We provide a quickstart for writing plugins in Go, and plan to provide examples for Python and JavaScript plugins soon.

Plugins are hosted and versioned independently from the aspect CLI.

The plugin system is based on the excellent system developed by HashiCorp for the terraform CLI. You can read more about this archecture here: https://github.com/hashicorp/go-plugin/blob/master/README.md

Quickstart

Use https://github.com/aspect-build/aspect-cli-plugin-template to create a starter repository.

This tutorial walks you through how to use that template to write a simple plugin.

Plugin configuration

In the plugins field of your .aspect/cli/config.yaml, list the plugins you'd like to install.

This is a YAML file. As an example, this file will install the plugin defined by the aspect-cli-plugin-template starter repo referenced earlier:

.aspect/cli/config.yaml
plugins:
- name: hello-world
from: github.com/aspect-build/aspect-cli-plugin-template
version: v0.3.1

The from line points to the plugin binary and can take one of these forms:

  1. A string with no slash characters, which is interpreted as a program on your system PATH.

  2. A filesystem path, either relative to the workspace root or absolute.

  3. A string of the form github.com/some-org/some-repo.

    In this case, a version property is required as well. This form follows the convention in https://github.com/aspect-build/aspect-cli-plugin-template where a GitHub release at a tag contains the plugin binaries as assets.

    To get a binary for the right platform, we append one of these platform suffixes before fetching: -darwin_amd64, -darwin_arm64, -linux_amd64, -linux_arm64, -windows_amd64.exe

    In the yaml example above, on an x86_64 architecture Linux machine, we would download from https://github.com/aspect-build/aspect-cli-plugin-template/releases/download/v0.3.1/hello-world-linux_amd64

  4. An http/https URL from which the plugin can be downloaded.

    As in the previous case, a platform suffix is appended to the URL before fetching.

  5. A Bazel target label beginning with // or @ that points to a plugin in your workspace.

    The target must be a go_binary and will be built if needed.

Avoid many plugins

As you add new features to your Aspect CLI plugin set, you'll naturally run into a question: "how many plugins should I write".

Having many plugins has downsides:

  • Each plugin runs a gRPC server, so it consumes some memory on the users machine.
  • Managing each plugin as a separate repository adds governance overhead.

The "composite" design pattern is a good solution. Each independent unit of functionality still implements the plugin API. We like to call these "features". You then write a "composite" plugin that contains a list of these features, and delegates every API call by calling that function on each feature in turn.

Here is a code sample for how the composite pattern might look in Go:

composite_plugin.go
package main

import (
"fmt"

"github.com/aspect-build/silo/cli/core/bazel/buildeventstream"
"github.com/aspect-build/silo/cli/core/pkg/ioutils"
"github.com/aspect-build/silo/cli/core/pkg/plugin/sdk/v1alpha3/config"
"github.com/aspect-build/silo/cli/core/pkg/plugin/sdk/v1alpha3/plugin"
goplugin "github.com/hashicorp/go-plugin"
"sigs.k8s.io/yaml"
)

func main() {
// config.NewConfigFor accepts a plugin implementation and returns the go-plugin
// configuration required to serve the plugin to the CLI core.
goplugin.Serve(config.NewConfigFor(NewDefaultPlugin()))
}

// CompositePlugin implements an aspect CLI plugin. It is just an composite pattern for multiple "child" features.
type CompositePlugin struct {
features map[string]common.Feature
}

// NewPlugin creates a new CompositePlugin with the default dependencies.
func NewPlugin() *CompositePlugin {
features := map[string]common.Feature{
"my_feature_1": xx.NewDefaultFeature(),
"my_feature_2": yy.NewDefaultFeature(),
}
return NewPlugin(features)
}

// BEPEventCallback satisfies the Plugin interface.
func (p *CompositePlugin) BEPEventCallback(event *buildeventstream.BuildEvent) error {
for _, c := range p.features {
if err := c.BEPEventCallback(event); err != nil {
return err
}
}
return nil
}

// CustomCommands satisfies the Plugin interface.
func (p *CompositePlugin) CustomCommands() ([]*plugin.Command, error) {
result := []*plugin.Command{}
for _, c := range p.features {
if childCommands, err := c.CustomCommands(); err != nil {
return nil, err
} else {
result = append(result, childCommands...)
}
}
return result, nil
}

// PostBuildHook satisfies the Plugin interface. You could similarly implement PostTestHook and PostRunHook.
func (p *CompositePlugin) PostBuildHook(
isInteractiveMode bool,
promptRunner ioutils.PromptRunner,
) error {
for _, c := range p.features {
if err := c.PostBuildHook(isInteractiveMode, promptRunner); err != nil {
return err
}

}
return nil
}