3.4.2 Gazelle (BUILD File Generation)

Gazelle is a BUILD-file generator. Once its rule is wired into the repo, the daily loop becomes: edit a source file, add or remove an import, run bazel run //:gazelle, commit the refreshed BUILD.bazel alongside the code change. You stop hand-maintaining srcs and deps. The tool infers them from what the source actually imports.1,2 Writing custom extensions and directive-level configuration are the deep-dive track in T4 Gazelle. This article is the maintainer's awareness view — what Gazelle does, how it updates files, and where it fits in the workflow.

Gazelle transforms source files into an updated BUILD file
Each stage has one job and passes one result to the next.
Load
Read the repository
Source tree + existing BUILD files
Walk directories and collect file metadata
→ directory metadata
Generate
Create rules from source
Language extension per directory
Turn files into rule shapes and import strings
→ rules without resolved deps
Resolve
Connect imports to labels
Repository index across directories
Map each import string to a Bazel target
→ rules with deps
Write
Merge the generated rules
Existing BUILD + generated rules
Preserve human edits, format, and save
→ updated BUILD.bazel
Generate decides which rules should exist. Resolve decides which labels belong in their deps.

Bootstrap the repository target

Choose and review a Gazelle release in the Bazel Central Registry, then pin it in MODULE.bazel. For example, the current BCR entry at review time is:3

bazel_dep(name = "gazelle", version = "0.51.3")

Expose the binary through a checked-in target (the exact macro and extensions depend on the languages in the repo), then make bazel run //:gazelle the documented update command. Commit the resulting BUILD.bazel diff with the source change and run the same command in CI to detect drift. The pin, target, and command are the reproducible contract. A globally installed gazelle binary is useful for evaluation but should not be the team's source of truth.

Why generate BUILD files at all

Most of a BUILD file is a restatement of facts that already exist in source. The srcs list mirrors files on disk. The deps list mirrors import, include, or require statements in those files. Roughly 80% of a typical BUILD file is information a tool can extract from the source with no guesswork — leaving about 20% (deployment metadata, visibility, hand-tuned attributes) that a human genuinely has to write.1

Hand-writing that 80% is Bazel's phase zero: the step you have to do before Bazel even begins its own loading, analysis, and execution phases.1 New developers land on it immediately ("why do I have to repeat my imports in a separate file?"), and in a large repo it becomes the dominant source of stale deps, forgotten srcs entries, and merge conflicts on generated-looking files. Gazelle answers that friction: developers write code, the tool turns it into BUILD structure.

How Gazelle updates a BUILD file

Gazelle processes updates in four ordered stages.2

  1. Load. Gazelle walks the directory tree, parses existing BUILD / BUILD.bazel files, and lists the files and subdirectories in each directory. Entries matched by # gazelle:exclude directives or .bazelignore are skipped during this walk.2
  2. Generate. In each directory Gazelle has been asked to update, a language extension reads the source files and returns the rules the BUILD file should contain (a Gen list) plus rules that should no longer exist (an Empty list).2
  3. Resolve. Gazelle maps every import string it saw in sources to a Bazel label and fills in deps on the generated rules. Cross-directory resolution uses an in-memory index built from library rules elsewhere in the repo.2
  4. Write. The in-memory edits are formatted (the same machinery buildifier uses) and written back to disk.2

The upstream repository keeps the supported orientation path explicit: how-gazelle-works.md explains this pipeline, while extend.md moves from the user workflow to the Language interface and extension tests.4 That separation is useful during diagnosis—verify generator behavior in the workflow document before escalating into a particular language implementation.

Generation and resolution both produce structured rule objects, and both call into the same merge machinery (merger.MergeFile) to reconcile those objects with the rules already present in the file.2 That merge is what makes Gazelle safe to re-run over a hand-edited repo.

Merging without overwriting

Gazelle merges, it does not overwrite.1,2 An existing rule with the same kind and name as a generated rule (e.g., both are a go_library named lib) keeps its position and its hand-written surroundings. Only its machine-managed attributes get refreshed.2

Attributes are classified per rule kind as mergeable or not:2

  • Mergeable attributes — typically srcs, deps. Gazelle may overwrite these, because they are meant to be derived from source.
  • Non-mergeable attributes — typically visibility. If the attribute is missing, Gazelle may add a default value. If a human has set one, Gazelle leaves it alone.

The explicit escape hatch is the # keep comment. It can be placed on a rule, on an attribute, or on an individual list value. Gazelle leaves anything marked # keep alone:2

go_library(
    name = "lib",
    srcs = [
        "lib.go",
        "generated.go",  # keep
    ],
    visibility = ["//visibility:public"],
)

Two useful corollaries follow from the merge contract:

  • When every real source for a rule disappears, the language extension returns the rule in its Empty list and Gazelle deletes it.2
  • The BUILD file itself is never deleted, even when it ends up empty, because removing it could silently let a parent package's glob() match files it previously could not.2

Languages and the extension model

Gazelle's lineage is Go-specific. Inside Google, a tool named Glaze generated BUILD files for Go packages fast enough — around 50ms — that developers ran it on every editor save. Outside Google, Gazelle was created in 2016 and grew a generic extension interface in 2018 so other languages could plug in.5 Go and Protocol Buffers ship built in. Everything else is a plugin.

Each plugin implements Gazelle's Language interface — essentially a GenerateRules function that turns a directory of source files into rules, plus an Imports / Resolve pair that maps language-specific import specs to Bazel labels.6 Common extensions in the open-source ecosystem include plugins for Python, JavaScript/TypeScript, Java, Kotlin, C/C++, Scala, and Haskell, with maintainers spread across bazel-contrib, Aspect, EngFlow, and VirtusLab.1,6

For a concrete non-Go contract, gazelle_rust pairs a runnable Rust workspace with checked-in BUILD.inBUILD.out cases for Cargo aliases, build scripts, features, tests, and examples.7 Those golden pairs define the expected end-to-end output and provide small reproductions. Locating a discrepancy in generation, dependency resolution, or merge behavior still requires a narrower test or inspection of that stage. Rust-specific directives are not part of Gazelle's universal API.

The historical constraint was that every plugin had to be written in Go and linked into a single gazelle_binary built from source. Prebuilt distributions can lower that barrier: current Aspect CLI's gazelle task can point at aspect_gazelle_prebuilt for Starlark-defined extensions, while the former bundled configure command has moved to the standalone aspect-gazelle project.1,3 For a Level 3 maintainer, the practical rule is straightforward — if Gazelle supports your languages, stop hand-maintaining BUILD files in those languages. If it does not, someone has to provide an extension, and the deep dive in T4 Gazelle is where to start.

Where Gazelle fits in the workflow

Gazelle modifies the source tree. A normal Bazel action is not allowed to do that — sandboxing, caching, and hermeticity all assume actions write only into their declared outputs under bazel-out/. So Gazelle runs outside the build graph, as a pre-build step that mutates the checkout before bazel build reads it. This is the same boundary described in 3.7.1 Bazel's Output Boundary and picked up again in 3.7.2 Workflows Outside Bazel: Gazelle is task-runner territory, not a build action.

Two integration patterns follow from that positioning:

  • Locally. bazel run //:gazelle after touching imports, or wire it into an editor save hook for languages where it is fast enough.5
  • In CI. Run Gazelle against the clean checkout and fail the job if any BUILD file changed. That enforces "no hand-edited BUILD files drift from source" without relying on every developer to remember the command. Uber runs this pattern across a ~1M-file monorepo with >10,000 commits per week.8

The build-maintenance Gazelle plan helper is a reviewable sketch of that workflow — it does not invoke Gazelle (the example is intentionally lightweight and standalone on Bazel 9.0.0), but it prints the literal gazelle -build_file_name=BUILD.bazel ./... step plus the git diff and buildozer cleanup that a real ruleset's bazel run //:gazelle invocation would feed into.

For sibling tooling that complements Gazelle, 3.4.3 Buildozer is the CLI for scripted BUILD edits — useful when you want to script a change across many existing targets rather than regenerate them from source.

extra

Lazy Indexing

Classic Gazelle re-indexed every BUILD file in the repo even for a one-directory edit, because nothing in a generic import string told it where the providing target lived. In large repos that walk dominated runtime — around 25 seconds in a large Go monorepo and around 30 seconds in Kubernetes at its peak.5 Lazy indexing (-index=lazy, combined with -r=false) lets each language extension return just the directories it actually needs for resolution, taking Gazelle from O(repo) to O(directories touched). The same feature has cut a 25-second walk to 352ms — a 99% reduction — and an 839ms walk to 107ms.5 Speed matters because it changes how the tool is used: at 50ms Gazelle can run on every file save. At 10s it becomes a manual step. At 100s teams stop using it and go back to hand-editing.5 Lazy indexing is implemented in Go, proto, and C/C++ extensions today. Gazelle 2.0 is likely to make it the default.5

key takeaway

Gazelle turns source-code imports into BUILD structure. A four-stage pipeline — Load, Generate, Resolve, Write — reads the source tree, generates the rules a directory should contain, resolves their deps, and merges the result into existing BUILD files without clobbering human edits. Use # keep on any rule, attribute, or list value you want Gazelle to leave alone. Go and proto work out of the box. Other languages need a plugin. Because Gazelle writes into the source tree, it runs before bazel build, not inside it. When you outgrow awareness and need custom extensions, directive configuration, or scale tuning, continue in T4 Gazelle.

Check your understanding · 3 questions

1.What does Gazelle's '# keep' comment do, and where can it be placed?

Select one answer

2.True or false about Gazelle's behavior:

Choose True or False for each sentence

Gazelle deletes BUILD files that become empty after removing all generated rules.
Gazelle can be run as a normal Bazel build action because it only writes to declared output locations.
Gazelle merges generated rules with existing ones by name and kind, updating only machine-managed attributes like srcs and deps.

3.What is lazy indexing in Gazelle and why does it matter for developer experience?

Select one answer

0 of 3 answered

Footnotes

  1. Mastering Bazel Build File Automation with Aspect CLI and Gazelle — Alex Eagle's "~80% of a BUILD file is inferable from source" framing, the "phase zero" argument, Gazelle's merge-not-overwrite contract, the list of supported languages, the Go-only extension API and Starlark alternative 1 2 3 4 5 6

  2. How Gazelle Works — upstream description of the Load / Generate / Resolve / Write pipeline, # gazelle:exclude and .bazelignore behavior during Load, the Gen / Empty lists, mergeable vs non-mergeable attributes, # keep comments, empty-rule deletion, and why empty BUILD files are preserved 1 2 3 4 5 6 7 8 9 10 11 12 13

  3. Gazelle in the Bazel Central Registry and the current Aspect CLI README — current Bzlmod pin, tested Bazel range, and the graph/AXL wiring required by aspect gazelle 1 2

  4. Gazelle repository maphow-gazelle-works.md documents the generator pipeline and extend.md defines the language-extension route.

  5. Lightning-fast BUILD file generation with Gazelle lazy indexing — Glaze-to-Gazelle history and timeline, the "speed is the killer feature" thresholds (50ms / 10s / 100s), lazy indexing design (-index=lazy, -r=false), per-repo measurements, extension coverage, and Gazelle 2.0 direction 1 2 3 4 5 6

  6. Gazelle Across the Stack: Same Tool, Different Problems — Gazelle's Language interface, GenerateRules / Imports / Resolve contract, file-based vs symbol-based resolution, and the cross-language extension ecosystem (C/C++, JVM, Scala) 1 2

  7. gazelle_rust — Gazelle language plugin for Rust — its runnable example/ shows adoption, while generation_tests/cargo/ records focused generator inputs and expected BUILD output.

  8. Optimizing Gazelle for Scale and Performance in Uber's Monorepo — the "run Gazelle, reject if BUILD files changed" CI pattern, Uber's monorepo scale (~1M files, >10K commits/week), and the run-on-save local workflow