Skip to content

Generate BUILD files for new rule sets with Gazelle v2

This post is based on my lightning talk at BazelCon 2026. You can watch the talk on YouTube below.

This talk shows how to support new rule sets by writing an extension with Gazelle's new v2 API, which supports better error handling and dependency resolution. We'll also talk about how Gazelle works with extensions, how to support offline indexing, and how to write extensions in languages other than Go.

A brief history of Gazelle

First, some quick history. Gazelle was originally introduced as part of rules_go in 2016 by Yuki Yugui Sonoda. It's 10 years old! Its original purpose was to generate BUILD files in go_repository, so you'd be able to depend on third-party Go projects without writing BUILD files yourself. Of course, it's useful for migrating projects to Bazel, too.

Later on, I moved Gazelle to its own repo and rewrote it around an extension interface so that I could add support for protobuf. That interface proved to be generally useful, and since then folks have developed extensions for Java, Scala, TypeScript, Python, C++, and more.

Why v2?

While we've been actively improving Gazelle for a long time, any project this old starts to show rough edges. We've generally been very careful to preserve compatibility for extension authors and users, but some of the old functionality is really getting in the way, like support for updating GOPATH-style dependencies in WORKSPACE.

The extension interface itself has some design flaws that make it harder than necessary to implement new extensions and difficult to add new features without breaking compatibility.

Those are the biggest reasons to do v2. It's not so much that v2 is packed with new features; it's really not. It's a compatibility break that's necessary in order to develop those features.

A minimal extension

The extension interface is the main thing that's changing in v2, so let's write a tiny extension.

package intercal

import "github.com/bazel-contrib/bazel-gazelle/v2/language"

func NewV2() language.Language {
    return new(lang)
}

type lang struct{}

func (lang) Name() string { return "intercal" }

So we need basically three things here. We need to define a type in a new package. It's empty, since we don't need to manage any state. We'll define only one method, Name, which returns the name of the extension. And then we'll define a NewV2 constructor that returns our extension.

That's it. This is much simpler than Gazelle v1, where you needed to implement 12 different methods. In v2, only the Name method is required. Everything else is optional.

Generating rules

Let's say we actually want to generate some rules. Most extensions need to do this. We need to implement two methods: Kinds returns some metadata telling Gazelle how to merge rules, and Generate actually creates rules.

func (lang) Kinds() []rule.KindInfo {
    return []rule.KindInfo{{
        Name: "intercal_library",
        LoadedFrom: label.New("rules_intercal", "", "def.bzl"),
        MergeableAttrs: map[string]bool{"srcs": true},
        ResolveAttrs: map[string]bool{"deps": true},
    }}
}

func (lang) Generate(
        ctx context.Context,
        args language.GenerateArgs)
        (language.GenerateResult, error) {
    r := rule.NewRule("intercal_library", "lib")
    return language.GenerateResult{Gen: []*rule.Rule{r}}, nil
}

These are very similar to v1, but I want to highlight three things, since they apply to most other v2 methods.

  1. Generate takes arguments and returns results through struct types. This lets us add new arguments and new optional results without breaking compatibility with existing extensions. This was true for v1 Generate, and we've copied that pattern to most other methods.
  2. Generate now takes a context argument. For those of you who don't write much Go, context is how we support cancellation. It's used when running commands or making network requests. So now, when the user presses ^C, the context gets canceled, and extensions have a chance to shut down external processes and close connections.
  3. Generate returns an error. This is big, actually. Previously, most extensions just printed errors, and the core of Gazelle didn't know when an error happened. In v2, we now have a -strict mode flag that actually works. Gazelle can now exit non-zero when an extension returns an error.

More methods

Here's a list of the other methods you can implement. This is a summary, so I can't go into a lot of detail here, but I just want to give a sense of the general flow. Most extensions implement four or five of these methods.

// Process #gazelle directives
KnownDirectives() []string
Configure(*Config, rel string, *File)

// Generate and edit rules
Kinds() []KindInfo
Fix(contex.Context, FixArgs) error
Generate(context.Context, GenerateArgs) (GenerateResult, error)

// Index libraries, resolve dependencies
Imports(context.Context, ImportsArgs)  (ImportsResult, error)
Find(context.Context, FindArgs) ([]FindResult, error)
Resolve(context.Context, ResolveArgs) error

// Manage external tools and connections
OnStart(context.Context) error
OnResolve(context.Context) error
OnFinish(context.Context) error

We talked about Kinds and Generate earlier.

The Configure method lets you process directives, those special Gazelle comments in BUILD files. Gazelle calls that in each directory it visits.

The Imports method lets you build an index of library rules in memory, including both generated and existing rules.

And finally, the Resolve method lets you use that index to map import strings to Bazel labels so you can set the deps attribute on generated rules. These methods now have a more regular structure than they did before, so they should be a little simpler, though a lot of language-specific logic is still required.

How to migrate your extension

If you're an extension author, you might be thinking "How can I upgrade as quickly as possible"? Or maybe "How can I avoid this upgrade or postpone it as long as possible?" I have good news for both of you.

First, about compatibility. Gazelle v2 is backward compatible. It supports a mix of extensions written against the v1 and v2 interfaces, so you don't actually have to upgrade at all. Your users can still benefit from your extension on Gazelle v2. The latest Gazelle v1 is also forward compatible, so if you do choose to upgrade, your users on v1 won't be broken.

Now, how do you actually migrate your extension? For this, we have a skill that you can use with your agent harness of choice. I used this to migrate all of Gazelle's built-in language extensions, as well as gazelle_cc. It only takes a few minutes and works quite well.

If you're a fan of the historic figures Ned Ludd or perhaps John Henry and you prefer to do this yourself, the skill is actually just markdown, explaining what to do.

How to use v2

If you're a user of Gazelle, you're probably thinking, "Wow, this sounds amazing, I definitely care about implementation details of the thing I have to kick occasionally when my BUILD files are broken, how do I upgrade?"

# Final v2 release expected near the end of the year
bazel_dep(name = "gazelle", version = "2.0.0-beta.2")

# Stable branch, not actively developed.
bazel_dep(name = "gazelle", version = "1.0.0-beta.1")

Well, good news, we published v2.0.0-beta.2 last week. It's in the BCR, so please try it out soon and let us know if you have feedback, either in GitHub issues or in Slack. This is a big change, so feedback really helps. We expect to tag the final v2.0.0 release near the end of this year, after removing WORKSPACE and GOPATH support.

We also tagged v1.0.0-beta.1 after being on v0 for more than 10 years. That is the start of a branch that won't receive breaking changes, but won't be actively developed, so you can still rely on that.

I don't expect there are too many hazards for upgrading. The most noticeable change is that we're enabling lazy indexing by default, so when Gazelle is invoked on a specific directory, it should be much faster. I gave a talk on that last year; please check that out.

Build File Generation Protocol

As I said earlier, v2 is about unblocking new features, so here are two of the features we're looking forward to after v2 ships. Both have been popular requests over the last few years, so I don't claim credit for these ideas.

First, a proposal for a Build File Generation Protocol. This will let you develop extensions in languages other than Go. I remain a big fan of Go, but it's not appropriate for all cases. If you have a complicated language like Scala, you need the front half of a compiler to write a proper Gazelle extension. And that's a lot easier if you can just import that code. It's probably not written in Go.

So the idea here is something like LSP for text editors or MCP for agents. You write a program that implements the Gazelle extension interface in any language and communicates over JSON-RPC on stdin and stdout pipes. Then we'll have a framework to run extensions written that way, so you won't need to write a line of Go.

Offline indexing

Second, a proposal for a standard offline indexing format. If you use C++, Gazelle needs to be able to look at a random include string and figure out what BCR module or Conan library that comes from. If you use Java, maybe you vendored a bunch of .jar files: Gazelle needs to know which .jar file a class comes from without parsing all of them. Either way, you'll need to build an index of everything you might import, perhaps several indexes, one for each language.

Offline indexing is a proposal to standardize a format for index files. Gazelle will be able to read files in that format directly without requiring you to write a specialized extension.

Agentic development

I want to close with some thoughts on agentic development. Two years ago, I didn't believe LLMs would be able to do what Gazelle does. I was of course dead wrong. A year ago at BazelCon, we talked about how agents are beginning to change software engineering. Two weeks later, Opus 4.5 came out, and now agents write most of our code. Gazelle perhaps feels unnecessary. You can absolutely have agents generate and maintain your build files, especially if you're willing to spend a lot of tokens.

I argue Gazelle still has a place in the future though. We want our software tools to be fast, deterministic, and predictable. The difference now is that tools are driven by agents more often than humans. Tools still make agents more productive, just like they help humans. So Gazelle is still useful even if you don't run it yourself: it makes sure your build graph matches your codebase, as long as it has some regular structure. Enforcing that structure is easier than ever with agents.

With v2, it's also easier for agents to write new extensions to support custom conventions and new languages and rule sets. And that's a big reason for us to work on v2: it lets us broaden Gazelle's capabilities, so that migrating and maintaining a Bazel project is faster and easier.