3.3 Configurable Builds & Platform Basics
The first platform surprise usually arrives as a small BUILD-file branch:
deps = select({
"@platforms//os:linux": [":linux_net"],
"@platforms//os:macos": [":darwin_net"],
"//conditions:default": [":portable_net"],
})
It looks like an if statement, but it is not one. Nothing here asks the shell, the compiler, or the source file what machine the developer happens to be using. Bazel resolves this attribute during analysis from the build configuration, and the most important part of that configuration is the target platform. Run bazel build --platforms=//platforms:linux_x86 //app:server, and the graph Bazel analyzes is different from the graph it would analyze for macOS or Android.
That is the section's core shift: configurable builds are not scattered conditionals. They are graph declarations indexed by a shared platform vocabulary. Once that vocabulary is visible, select(), target_compatible_with, platform(), and --platforms stop looking like separate features and start looking like different points in the same conversation.
One Vocabulary, Several Decisions
This section is ordered around the maintainer's workflow: first you see a configurable attribute, then you learn what configuration it reads, then you define the vocabulary that makes platform checks reusable, then you decide which targets should disappear from unsupported builds, and finally you make platform choice ergonomic from the command line or .bazelrc.
3.3.1 Configurable Attributes (select()) is the entry point because it is where configuration becomes visible in BUILD files. If 0.3.8 select() Awareness only taught you to recognize the syntax, this is where the mechanism becomes usable: attribute values can depend on declared configuration, but not on whatever the local machine happens to be.
3.3.2 Platform Vocabulary then slows down and names the roles behind the branch. Its job is not to add more syntax, but to prevent the common mistake of treating "my laptop," "the machine running an action," and "the platform I am building for" as one thing.
3.3.3 Constraint Values explains what platforms are made of. Read it before designing a platform matrix, because the vocabulary chosen there becomes shared by select(), compatibility declarations, and later toolchain resolution in 4.6 Toolchains & Platform Resolution.
3.3.4 Target Compatibility turns the same vocabulary into pruning. This is where platform support becomes repository hygiene: unsupported targets should disappear from broad wildcard builds for declared reasons, not fail later because someone remembered the wrong platform rule by habit.
3.3.5 --platforms Flag closes the loop by making platform choice an invocation-level decision. It also connects back to 3.2.1 .bazelrc Hierarchy and 3.2.4 Command Line Flags, because real projects rarely ask developers to type long platform labels by hand. They put common choices behind named configs.
The Trap Is Treating Platforms As Local State
Developers coming from other build tools often expect platform logic to be local: a script checks uname, a makefile reads an environment variable, or a compiler flag changes based on the current host. Bazel's model is deliberately different. Platform choice is part of the build configuration, and the build configuration is an input to graph analysis.
That difference is why this section belongs after hermeticity and phases from Level 2. A platform-specific dependency should be selected from declared configuration, not from an undeclared environment probe. A target that cannot work on Windows should say so in target_compatible_with, not wait for a compiler error inside //.... A cross-build should be visible as --platforms=..., not hidden in a wrapper script that mutates flags differently per machine.
The hard part is that the same words point in different directions depending on the question. select() usually asks about the target platform: where the resulting binary or library will run. Tools used during the build ask about the execution platform: where the compiler or generator itself can run. The rule-author machinery for that second question is intentionally deferred to 4.6.1 Platform Model for Rule Authors. At this level, the maintainer goal is simpler: be able to read, define, choose, and prune target platforms in ordinary BUILD and .bazelrc files.
How To Read The Section
If you are debugging a branch that picked the wrong dependency, start with 3.3.1 Configurable Attributes (select()) and follow its cquery / bazel config workflow. If your confusion is "but I am on macOS, why did the Linux branch fire?", read 3.3.2 Platform Vocabulary next. The answer is probably target platform versus execution platform. If you are designing a platform matrix for a repository, read 3.3.3 Constraint Values before writing many config_setting targets. The vocabulary you choose there will be reused across the whole repo.
Read 3.3.4 Target Compatibility once your repository has real unsupported targets, especially if broad patterns like //... are noisy. It is a maintenance tool, not just a platform feature. Save 3.3.5 --platforms Flag for the moment you need an actual invocation pattern: local defaults, CI configs, cross-compilation configs, or migration away from older per-language flags.
Compare: A select() chooses Linux sources while the compiler action runs
on macOS. Is that contradictory? Identify the platform role read by the
attribute and the role used to place the action.
Reveal
It is not contradictory. select() and target_compatible_with usually read
the target platform—the environment the requested output is built for—and
--platforms chooses that role for the invocation. Bazel may still run the
compiler on a different execution platform, which describes where the tool
itself runs. Cross-compilation deliberately separates those two roles.
Configurable builds work when platform information is declared once and reused consistently. select() reads the build configuration, platform vocabulary names the machine properties, target_compatible_with prunes unsupported graph edges, and --platforms chooses the target platform for an invocation. Treat those as one model, not four tricks.
Sections in this chapter · 5
Switch dependencies, sources, or flags based on target platform or build settings.
Target platform vs execution platform — what you build for vs what you build on.
Platforms are defined by constraint settings and their values from @platforms.
Mark targets as buildable only for specific platforms. Incompatible ones are skipped in wildcard builds.
Override the target platform from the command line or .bazelrc.