3.4.3 Buildozer

Buildozer is a command-line tool that rewrites BUILD files — the anatomy from 0.3.1 Anatomy of a BUILD File — using structured commands instead of regex or manual edits. It parses the file into its syntax tree, applies the change, and writes the file back — preserving formatting and comments — all without starting a Bazel server or loading the graph from 2.2.1 Loading, Analysis & Execution.1 That purely syntactic pass is why it scales: the same command that changes one target works unchanged across thousands of BUILD files, and the edit completes in milliseconds per file.

The other thing to know up front is that buildozer is not only something you run by hand. Many tools in the Bazel ecosystem — bazel mod graph under Bzlmod,2 strict-deps diagnostics,3 Aspect CLI plugins,4 FawltyDeps5 — print ready-to-paste buildozer commands as their "fix" output. Learning buildozer's syntax is therefore also learning how to read the remediation advice Bazel and its surrounding tooling already emit.

Anatomy of a buildozer invocation

The general form is one or more commands followed by one or more targets:1

buildozer [OPTIONS] 'command arg...' [label]...

Each command is a single positional argument, so commands are quoted to keep spaces intact. Multiple commands and multiple labels can appear in one invocation. Buildozer applies every command to every target and processes files in parallel.1 A minimal example — adding a dependency to a target:

buildozer 'add deps //lib:foo' //my:target

Buildozer accepts the same label grammar as Bazel, plus a few extras that only make sense at the syntax level:1

Target formWhat it means
//pkg:nameOne specific target
//pkg:__pkg__The package() call (for package-level defaults)
//pkg:*Every rule in the BUILD file
//pkg/...:*Every rule in every BUILD file under pkg/
//pkg:%java_libraryEvery java_library in the BUILD file
//pkg:%123The rule that starts at line 123

__pkg__ and %rule_kind have no meaning to Bazel itself — buildozer looks at unevaluated BUILD files, so it talks about package() declarations and rule kinds directly, not about expanded targets. That is also why bazel query and buildozer occasionally disagree about what exists in a package: buildozer sees what's written, query sees what macros produce.1

The commands a maintainer reaches for

Buildozer ships a small vocabulary of edit commands.1 A few carry almost all the day-to-day weight:

# Add //base to deps of //pkg:rule and //pkg:rule2
buildozer 'add deps //base' //pkg:rule //pkg:rule2

# Remove a dep from every cc_library in a package
buildozer 'remove deps //legacy:util' //pkg:%cc_library

# Replace one dep with another (useful for migrations)
buildozer 'replace deps //pkg_v1 //pkg_v2' //pkg:rule

# Rename an attribute across all rules in a file
buildozer 'rename old_attr new_attr' '//pkg:*'

# Set package-wide defaults
buildozer 'set default_visibility //visibility:public' //pkg:__pkg__

There are also read-only commands. print is the one that matters most day-to-day — it lets you inspect BUILD files without running a Bazel query:1

# List all cc_library names in //base
buildozer 'print name' '//base:%cc_library'

# Print the full definition of one target, including comments
buildozer 'print rule' //base:heapcheck

Because print works on the syntax tree, it returns instantly and does not trigger fetching, loading, or analysis — exactly the property to exploit whenever you need to inspect BUILD contents at developer speed.6

Batching: command files and query | xargs buildozer

For a handful of targets you pass commands on the command line. For mass refactoring you switch to a command file fed through -f, with one |-separated line per edit:1

# /tmp/cmds
add deps //base //strings|//buildtools/buildozer:foo
add deps :foo|//buildtools/buildozer
buildozer -f /tmp/cmds

The build-maintenance plan helper is the reviewable shape of this idea: it prints the add deps, replace deps, remove deps, and print rule lines a maintainer would inspect — and optionally drop into a -f file — without invoking buildozer itself.

For the focused target-form exercise, the buildozer-command-file snippet generates a command file and asserts that it contains the single-target, kind-filtered, package-wide, and package pseudo-target forms discussed above.

The companion pattern chains bazel query into buildozer. Query finds the targets that need changes — by label, rule kind, rdeps, or any other predicate from 5.2.1 bazel query — Static Graph Analysis — and xargs feeds them as explicit buildozer arguments. This is the idiom 3.7.2 Workflows Outside Bazel already recommends for avoiding collector targets, and it works just as well for edits:

bazel query 'kind(java_library, //foo/...)' \
  | xargs buildozer 'remove tags manual'

The same pairing is useful during validation: buildozer is the right tool when you need to query BUILD files statically — for example, to confirm that no target under a directory still has a given attribute.7 Compared to running bazel query against every commit, a buildozer print pass has no analysis overhead and no external-fetch side effects.

MODULE.bazel: the special commands

Buildozer also understands the Bzlmod root file from 3.1.1 Bzlmod (MODULE.bazel), with a dedicated set of commands that target top-level use_extension variables:1

  • use_repo_add <var> <repo>... — ensure the listed repos are imported via use_repo() for the extension bound to <var>.
  • use_repo_remove <var> <repo>... — ensure the listed repos are not imported.

This matters because the extensions covered in 3.1.2 Using Extensions can return an extension_metadata object that reports which repos the root module should actually import. When they do, bazel mod graph prints a buildozer command that reconciles your use_repo() with what the extension produced. Running it fixes up MODULE.bazel without hand-editing.2 This is the expected workflow for ruleset authors: ship an extension_metadata return from your extension, and let buildozer be the users' fixup tool.2

The emit-and-apply pattern

The more surprising thing is how often other tools are the ones printing buildozer commands at you. A few concrete examples:

  • Strict deps under Bzlmod. When rules_scala's strict-dependency check reports a missing direct dep, its error message ends with a runnable buildozer line. Under Bzlmod, that line uses the canonical repo name from 3.1.3 Repo Mappingbuildozer 'add deps @@+scala_deps+io_bazel_rules_scala_guava...' //some:target — so the user can paste and run it without worrying about apparent-name resolution.3
  • Aspect CLI fix-visibility (3.5.4 Aspect CLI Extensibility). Subscribes to the Build Event Protocol, detects is not visible from target errors, and either applies the buildozer edit interactively or prints the equivalent command when running on CI.4
  • FawltyDeps (Python). Published as rules_fawltydeps, it runs as an aspect over py_library targets via bazel build and emits one buildozer command per finding. The recommended mass-fix idiom is to grep those lines out of the build output and pipe them to the shell — bazel build ... | grep buildozer | bash.5
  • Preventing production-code experiments. A concrete pattern uses buildozer to mark whole experimental subtrees as testonly from 1.2.4 testonly Attribute, then prints those same idempotent commands back to the developer as the "how to fix a red build" instructions:8
buildozer 'set default_testonly True' //experimental/...:__pkg__
buildozer 'comment code\ in\ experimental\ may\ only\ be\ used\ for\ testing' \
  //experimental/...:__pkg__

The common thread is that buildozer commands are portable remediation artifacts. They survive logs, CI output, and chat copy-paste. A developer does not need to understand the tool that produced them to apply the fix. This is also why breaking string values across lines is discouraged: doing so defeats buildozer's parser and code-search tooling, which would otherwise be able to find and update those values mechanically.9

The build-maintenance buildozer plan helper keeps that emit-then-review shape visible in a Level 3 example: it prints the add deps / replace deps / remove deps / print rule lines a maintainer would inspect before running buildozer, without applying any edit on its own.

The smaller buildozer-command-file assertion pins the same idea as a verification snippet: generate the command file first, then inspect the commands before any edit is applied.

Exit codes for CI

Buildozer's exit codes are designed to distinguish "nothing needed changing" from "change failed" — useful when wiring buildozer into presubmit checks or automation scripts:1

CodeMeaning
0Success. Changes were made, or only read-only commands ran
1Usage error (bad arguments)
2At least one command failed
3Success. No changes were needed

A script that treats "3 means nothing to do" differently from "0 means something was fixed" can, for example, decide whether to open a pull request with the resulting diff or just exit quietly.

Distribution inside a repo

Most teams do not want each developer to run go install to get buildozer. The standard answer in modern setups is to manage it like any other hermetic tool: rules_multitool ships a tools/buildozer:buildozer target via a JSON lockfile, and bazel_env.bzl with direnv can add that binary to PATH automatically so buildozer works in a plain terminal.10 The detail is covered in 3.5.3 Developer Tool Management. The only thing to note here is that the article's examples assume buildozer is available under some consistent name, whether that is a bazel run @buildozer, a tools/buildozer wrapper, or a direnv-injected binary.

key takeaway

Buildozer is the mutation engine for BUILD and MODULE.bazel files. It operates on syntax, not on the Bazel graph, so it is fast, scriptable, and comment-preserving. Reach for it when you need to add, remove, or rename attributes across many targets — often paired with bazel query from 5.2.1 bazel query — Static Graph Analysis to select the targets. Just as importantly, learn to recognize buildozer lines in the output of other tools: strict-deps diagnostics, bazel mod graph, linters, Aspect CLI plugins, and internal automation all print buildozer commands as their "ready-to-run fix." Applying those commands is usually the shortest path from an error message to a clean build. Automated BUILD-file generation is a different concern covered in 3.4.2 Gazelle (BUILD File Generation). Automated removal of dead dependencies ties directly into 3.4.5 Managing unused_deps.

extra

Production automation patterns

Once buildozer is in the workflow, the natural next step is letting systems run it on your behalf:

  • Autosheriff-style services. A rule-driven service can watch merge-queue results and use buildozer to set flaky = true on targets that fail repeatedly, then open PRs for human approval. The same pattern scales down: any CI job that detects a classifiable issue can emit a buildozer command and either apply it or attach it to a generated PR.11
  • Starlark-level wrapping. AXL exposes buildozer via WASM so a Starlark task can call buildozer edits against MODULE.bazel without shelling out. This keeps migration tasks — reconciling a module extension's use_repo() stanza, or scripting a bazel_dep addition — declarable in-repo instead of living in ad-hoc shell scripts.12
  • Idempotent remediation. Buildozer commands are safe to re-run. add deps does not duplicate existing values, and set overwrites in place. Emitting those same commands back to developers as "how to fix this" instructions — as in the default_testonly guardrail earlier in the article — means the CI fix instructions and the automation script are literally the same text.8
Check your understanding · 3 questions

1.bazel mod graph prints a buildozer command recommending use_repo_remove for an extension variable. What does this mean and what should you do?

Select one answer

2.Which of the following are valid buildozer target specifiers (not standard Bazel labels)? Select all that apply.

Select all that apply

3.True or false about buildozer operations:

Choose True or False for each sentence

buildozer add deps //base //pkg:rule is idempotent—running it twice will not create duplicate entries.
buildozer operates on the Bazel build graph and requires a running Bazel server.
buildozer exit code 3 means at least one command failed.
0 of 3 answered

Footnotes

  1. Buildozer — bazelbuild/buildtools — Syntactic BUILD-file editor, invocation grammar, target forms (__pkg__, %rule_kind, *, /...:*, %line), edit vs. print commands, -f command files, MODULE.bazel-only use_repo_add / use_repo_remove, and exit-code semantics. 1 2 3 4 5 6 7 8 9 10

  2. A new way to manage dependencies: How we extended bzlmod — Returning extension_metadata from a module extension so that bazel mod graph prints a buildozer command that fixes up use_repo() in MODULE.bazel. 1 2 3

  3. Migrating to Bazel Modules (a.k.a. Bzlmod) — Maintaining Compatibility, Part 3 — Strict-deps errors emit buildozer 'add deps ...' lines. The label uses the canonical @@+scala_deps+... repo name under Bzlmod. 1 2

  4. Customize Bazel with Aspect CLI plugins — The fix-visibility plugin subscribes to BEP, auto-applies buildozer edits when interactive, and prints the equivalent buildozer command on CI. 1 2

  5. Using Fawltydeps: or When Gazelle Does Not Tame Your Python — FawltyDeps emits a buildozer command per finding. grep buildozer | bash loop as the mass-fix pattern. 1 2

  6. The "outside of Bazel" pattern — Buildozer as a syntactic inspection tool that bypasses Bazel's fetching, loading, and analysis phases.

  7. Managing the Dependency Graph — Using buildozer to query BUILD metadata statically while validating dependency policies.

  8. Preventing production code depending on experimentsset default_testonly True and comment commands across //experimental/...:__pkg__, and emitting the same commands as CI remediation instructions. 1 2

  9. BUILD Style Guide — Broken-up string values interfere with buildozer's ability to find and update values mechanically.

  10. bazel_env.bzl — Distributing buildozer via rules_multitool and exposing it on the developer's PATH through bazel_env + direnv.

  11. Building Self Driving Cars with Bazel — Part 2: Scaling — Autosheriff uses buildozer to set flaky = true on flaky targets and auto-open PRs for approval.

  12. Beyond Make Serve: Starlarkification for Tasks — Wrapping the buildozer Go binary as a WASM export so Starlark tasks can call buildozer edits without shelling out.