3.4 Code Generation & BUILD Maintenance
Bazel projects drift in two different places. Generated artifacts drift when a file like config.h should be produced by the build instead of checked in. BUILD files drift when srcs, deps, visibility, formatting, and lint wiring stop matching the source tree they describe. Both problems sound like "code generation", but they belong to different sides of Bazel's boundary.
That boundary is the organizing idea of this section. A genrule() creates declared outputs under bazel-out. bazel run //:gazelle, buildozer 'add deps //lib:foo' //app:server, and unused_deps //... > prune.sh mutate or inspect files in the checkout. One is graph execution. The others are maintainer workflows around the graph. Once that split is clear, the tools stop competing with each other and start forming a sequence.
BUILD Maintenance Is A Feedback Loop
The section starts inside Bazel, then moves outward into the workflows that keep a repository legible.
3.4.1 Genrule (The Escape Hatch) is the escape hatch for a small declared transformation: inputs in srcs, outputs in outs, tools in tools, paths through Make variables, and a shell command that Bazel can sandbox and cache. It is first because it teaches the difference between "generate an artifact as a build output" and "rewrite the source tree".
3.4.2 Gazelle (BUILD File Generation) handles the most common BUILD-file maintenance problem: source imports already say which files and dependencies a package needs, so a tool can regenerate the boring parts of BUILD files and merge them with the human-owned parts. It belongs outside normal build actions because it writes back into the checkout before bazel build reads the package graph.
3.4.3 Buildozer is the structured editor for changes Gazelle cannot infer: add an attribute, rename a target, set package defaults, reconcile MODULE.bazel, or apply a fix command printed by another tool. If Gazelle says "derive BUILD files from source", Buildozer says "make this syntactic edit safely across many BUILD files".
3.4.4 Code Quality Integration then broadens the same maintenance problem beyond BUILD files. Formatters, linters, pre-commit hooks, and review bots are not a single bazel lint command in core Bazel. They are a set of workflows layered around bazel run, tests, aspects, and CI.
Finally, 3.4.5 Managing unused_deps closes the loop on dependency drift. Strict deps catches missing direct edges. Unused-deps tooling prunes stale declared edges. The important pattern is the same one Buildozer introduced: analysis tools emit reviewable edits, usually as buildozer commands, and maintainers apply them deliberately.
Put The Output In The Right Tree
If the output is an artifact another target should consume, it belongs in the build graph. A genrule or, later, a custom rule from 4.2 Custom Rules, Providers & Actions declares inputs and outputs so Bazel can reason about caching, sandboxing, and dependency edges. That path is strict because the output is part of the build result.
If the output is a changed BUILD.bazel, a reformatted source file, or a cleanup diff, it belongs outside the graph. Gazelle, Buildozer, formatters, and unused-deps tools are still Bazel ecosystem tools, but they are not ordinary build actions. They prepare the source tree so the next build request is clean. The broader boundary shows up again in 3.7.1 Bazel's Output Boundary and 3.7.2 Workflows Outside Bazel. The workflow-orchestration project gives this split concrete shape: the release package keeps declared genrule outputs alongside a .publish sh_binary launcher, while tools/run_release_targets.sh drives the same set with a query-then-build pass that lives outside the graph.
This distinction prevents two common mistakes. The first is forcing repository maintenance into genrule because "it generates a file". A build action that edits the checkout breaks the model Bazel is trying to protect. The second is treating generated-looking BUILD files as disposable output. In Bazel, BUILD files are source: they define the graph, carry policy, and deserve review even when most of their content is mechanically maintained.
Why The Tools Feel Overlapping
The overlap is real because all five articles are fighting entropy in declared structure.
Gazelle adds or refreshes srcs and deps from source imports. Buildozer edits the syntax tree when a maintainer, migration, or diagnostic already knows the desired change. Code-quality integration keeps formatting and lint policy executable. unused_deps removes edges that survived after the source stopped using them. genrule is the outlier only on the surface: it is still about making a transformation explicit enough that Bazel can track it.
The practical difference is how much judgment the tool has. Gazelle can infer a lot for supported languages, but it must respect human-owned attributes such as visibility. Buildozer knows how to edit BUILD syntax, but it does not know whether the edit is semantically correct. Linters and unused-deps tools can point at drift, but false positives and runtime-only edges mean a human still reviews the patch. A healthy maintainer workflow uses automation for detection and mechanical rewriting, then uses review for policy.
Choose The Workflow By Its Output
Read 3.4.1 Genrule (The Escape Hatch) first if you are trying to produce a file for another target. It gives you the graph-side discipline: declare every input, every output, and every tool, and avoid guessed paths.
Read 3.4.2 Gazelle (BUILD File Generation) and 3.4.3 Buildozer together if your immediate pain is BUILD-file churn. Gazelle is the generator for source-derived structure. Buildozer is the editor for scripted changes and emitted fix commands. If your repo is already large or multi-language, treat T4 Gazelle as the later deep dive rather than trying to solve custom extension design here.
Read 3.4.4 Code Quality Integration when the question changes from "are BUILD files accurate?" to "how do we make format and lint policy repeatable?" Read 3.4.5 Managing unused_deps when dependency lists are technically valid but suspiciously broad, especially in Java-heavy code where strict deps and pruning form a pair.
Put generated artifacts consumed by targets inside Bazel's graph. Keep BUILD-file regeneration, formatting, linting, and dependency pruning as explicit source-tree workflows around it. Automation can detect and apply mechanical changes, but those changes still need review as source.
Sections in this chapter · 5
Built-in rule for running arbitrary shell commands as build actions.
Tool for automatic BUILD file generation and maintenance, originally for Go.
CLI tool for programmatic BUILD file refactoring.
Integrating formatters, linters, and pre-commit hooks into the Bazel build.
Detecting and removing unnecessary dependencies to keep BUILD files lean.