3.2.4 Command Line Flags
Once the .bazelrc layering from 3.2.1 .bazelrc Hierarchy decides where flags live, the next question is which flags matter most. Bazel ships hundreds of options, and even experienced users routinely discover ones they have not heard of1. This article focuses on the flags a maintainer reaches for first — the ones that control compilation behavior, pass options to compilers, set user-defined variables, and affect cache keys — plus a workflow for exposing rc contributions and comparing explicit option lists without shorthand.
Expose rc contributions, then normalize explicit flags
Different developers, different machines, different .bazelrc layers. When a build behaves differently on CI than on a laptop, first run the original invocation with --announce_rc to see which rc files and named configs contributed options. Then use bazel canonicalize-flags on the explicit option lists you want to compare. It returns a normalized list with the same effect2:
$ bazel canonicalize-flags -- -c opt -k --define=ENV=prod
--compilation_mode=opt
--keep_going=1
--define=ENV=prod
Short flags expand to their long form (-c becomes --compilation_mode), boolean flags become explicit (-k becomes --keep_going=1), and equivalent input lists produce the same canonical output. That makes two explicitly captured command lines easy to compare without shorthand obscuring a difference.
There are two things canonicalize-flags does not do. It does not expand --config — a --config=ci flag passes through unchanged, because config expansion depends on which .bazelrc files are loaded2. And it currently supports only build and test via --for_command. Options the chosen command does not understand cause an error2. The canonicalize-flags snippet reproduces all three behaviors — short/boolean expansion, the unexpanded --config=ci, and the rejected unknown option — against a tiny workspace whose .bazelrc defines build:ci so the named config has somewhere real to point.
The practical workflow is two-part. Use --announce_rc from 3.2.1 .bazelrc Hierarchy to see which rc files and named configs contributed options, then pass the resulting explicit option lists to canonicalize-flags and diff their canonical forms. canonicalize-flags alone does not inspect the invocation's loaded rc files, defaults, or expanded --config contents. The larger maintainer-workspace example includes a canonicalize-flags --for_command=build command next to its real .bazelrc policy so article authors can verify the same behavior outside the tiny snippet.
--compilation_mode (-c)
The most visible flag. It controls optimization level, debug information, and assertion behavior across all C/C++ compilation actions2:
| Mode | Short | Compiler behavior | Default? |
|---|---|---|---|
fastbuild | -c fastbuild | Minimal debug info (-gmlt -Wl,-S), no optimization. NDEBUG is not set. | Yes |
dbg | -c dbg | Full debug symbols (-g). | No |
opt | -c opt | Optimized (-O2 -DNDEBUG). No debug info unless combined with --copt -g. | No |
Bazel uses a different output directory for each compilation mode, so switching between -c fastbuild and -c opt does not force a full rebuild — only the targets that actually differ need recompilation2. That output-directory separation is the same mechanism described in 2.4 Caching & Incrementality: each mode produces a distinct set of action keys, and those keys coexist in the cache.
--copt
Passes a flag directly to the C/C++ compiler. Repeatable — multiple --copt values accumulate on the compiler command line2:
bazel build --copt="-g0" --copt="-fpic" //foo
Changing --copt forces recompilation of all affected object files2. Rule-level copts attributes are appended after --copt values, so a target can override or supplement the project-wide setting2.
Related flags split the concern by language and link phase2:
| Flag | Scope |
|---|---|
--copt | All C/C++/assembly compilation |
--cxxopt | C++ only (e.g. -fno-implicit-templates) |
--conlyopt | C only (e.g. -Wstrict-prototypes) |
--linkopt | Linker only (e.g. -lssp) |
--host_copt | Exec-configuration compilation |
--per_file_copt | Compiler flags for specific files, regardless of compilation mode |
The separation matters for correctness: a C++-specific flag in --copt will cause warnings or errors when compiling C sources. Placing it in --cxxopt avoids the problem.
--define and its replacement
--define sets user-defined key-value variables that config_setting rules can match in select() from 3.3.1 Configurable Attributes (select())3:
bazel build --define=ENV=prod //app:server
The mechanism works, but --define values are unstructured strings with no type checking and no tooling support3. The Starlark build settings system replaces --define with typed, label-addressed flags4:
bazel build --//config:environment=prod //app:server
A Starlark build setting is a regular rule with build_setting = config.string(flag = True) in its definition4. It appears in select() via config_setting's flag_values attribute4. The benefit is type safety: a build setting can validate its values, and its label makes the ownership and location explicit.
For shorter command lines, --flag_alias binds a long Starlark setting label to a short name2,4:
# .bazelrc
build --flag_alias=env=//config:environment
bazel build --env=prod //app:server
New projects should use Starlark build settings from the start. Existing --define usage still works but should be migrated when practical4. Continue to 4.7 Build Settings & Transitions for the authoring side: defining rules, using ctx.build_setting_value, and writing transitions.
Cache impact of flags
Every flag that affects compilation becomes part of the action key. Change the flag, and Bazel treats the action as new — cache miss2,5. This is correct behavior: a binary compiled with -c opt should not share cache entries with one compiled with -c dbg. But it means that ad hoc flag differences between developers or between CI and local builds silently destroy cache hit rates.
Option effect tags give a useful first-pass signal. Tags such as affects_outputs, changes_inputs, loading_and_analysis, loses_incremental_state, and terminal_output tell you whether a flag can change produced artifacts, alter inputs, invalidate warm server state, or merely change what the terminal prints2. Treat those tags as triage: they help decide which flags deserve careful .bazelrc policy, but the actual blast radius still depends on the command, rules, platforms, and targets in the invocation.
The practical countermeasure is grouping related flags into named configs in .bazelrc:
build:release --compilation_mode=opt
build:release --copt=-flto
build:release --define=ENV=prod
Everyone running --config=release gets the same action keys. A developer who adds --copt=-fsanitize=address on the command line will get cache misses only for the actions that flag actually affects — and those misses are intentional.
When flag divergence between CI and local builds becomes a suspected cause of poor cache sharing, first expose rc contributions with --announce_rc. Then canonicalize and diff the explicit option lists. Project specifications and flag sets — a Bazel 9+ feature covered in 3.2.7 Project Specifications & Flag Sets — aim to formalize this further by letting experts define named flag sets so that regular users never manipulate individual flags6.
Start with --compilation_mode for the optimization/debug split, --copt for compiler flags, and Starlark build settings (not --define) for project-specific configuration. When builds differ across environments, use --announce_rc to expose rc contributions, then use canonicalize-flags to normalize explicit option lists before diffing them. Group related flags in .bazelrc named configs to keep action keys consistent and cache hit rates high.
--per_file_copt: selective overrides
--per_file_copt applies compiler flags to specific source files regardless of the global compilation mode2. The syntax uses a regex filter:
bazel build --per_file_copt='third_party/.*\.cc@-O0' //...
This is useful when a single file triggers a compiler bug at -O2, or when profiling requires debug info for a narrow set of files while keeping the rest optimized. The flag applies independently of --compilation_mode, so you can build with -c opt and selectively drop optimization for specific files2.
Check your understanding · 3 questions
1.A developer uses --define=ENV=prod to configure a build. What is the recommended modern replacement and why?
Select one answer
2.True or false about compilation flags and cache impact:
Choose True or False for each sentence
3.What does bazel canonicalize-flags do, and what is one significant limitation?
Select one answer
Footnotes
-
Bazel Training 101 (Part 8): Flags — Bazel flag surface overview, syntax, rc files, command scoping, and configs ↩
-
Command-Line Reference —
--compilation_mode,--copt,--per_file_copt,--strip,--flag_alias,canonicalize-flags, and option effect-tag semantics ↩1 ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 -
Configurable Builds - Part 1 —
--defineas unstructured. Configuration vs rule attributes ↩1 ↩2 -
Configurations — Starlark build settings definition, CLI syntax,
--flag_alias,select()integration, and transitions ↩1 ↩2 ↩3 ↩4 ↩5 -
Better Bazel Flag Defaults — analysis-cache discards from flag divergence,
commonscoping, and--no_allow_analysis_cache_discard↩ -
Flagsets - Susan Steinman & Greg Estren, Google — project.scl,
--scl_config, named flag sets for reproducibility and cache efficiency ↩