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.
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
- Load. Gazelle walks the directory tree, parses existing
BUILD/BUILD.bazelfiles, and lists the files and subdirectories in each directory. Entries matched by# gazelle:excludedirectives or.bazelignoreare skipped during this walk.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
Genlist) plus rules that should no longer exist (anEmptylist).2 - Resolve. Gazelle maps every import string it saw in sources to a Bazel label and fills in
depson the generated rules. Cross-directory resolution uses an in-memory index built from library rules elsewhere in the repo.2 - 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
Emptylist and Gazelle deletes it.2 - The
BUILDfile itself is never deleted, even when it ends up empty, because removing it could silently let a parent package'sglob()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.in → BUILD.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 //:gazelleafter 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.
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
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
3.What is lazy indexing in Gazelle and why does it matter for developer experience?
Select one answer
Footnotes
-
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
-
How Gazelle Works — upstream description of the Load / Generate / Resolve / Write pipeline,
# gazelle:excludeand.bazelignorebehavior during Load, theGen/Emptylists, mergeable vs non-mergeable attributes,# keepcomments, empty-rule deletion, and why empty BUILD files are preserved ↩1 ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 -
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 -
Gazelle repository map —
how-gazelle-works.mddocuments the generator pipeline andextend.mddefines the language-extension route. ↩ -
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 -
Gazelle Across the Stack: Same Tool, Different Problems — Gazelle's
Languageinterface,GenerateRules/Imports/Resolvecontract, file-based vs symbol-based resolution, and the cross-language extension ecosystem (C/C++, JVM, Scala) ↩1 ↩2 -
gazelle_rust — Gazelle language plugin for Rust — its runnable
example/shows adoption, whilegeneration_tests/cargo/records focused generator inputs and expected BUILD output. ↩ -
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 ↩