4.7.5 Platform-based Flags
extraA surprising number of "real" Starlark transitions do almost nothing interesting. They flip --platforms to a target device, set one or two label-typed build settings that travel with that device, and stop. Once the conspect already covers 4.7.1 Build Settings for Rule Authors and 4.7.2 Starlark Transitions, the natural follow-up question is: if the only thing the transition does is bundle some flags with a platform, can the platform itself just carry the flags?1 Platform-based flags are Bazel's answer — a flags = [...] attribute on the platform() rule that applies the listed flag values whenever Bazel uses that platform as a target or execution platform.2
The flags Attribute on platform()
A platform rule normally collects constraint_value labels that describe a machine. With platform-based flags, the same target also lists command-line flag values that come along whenever this platform is selected:3
platform(
name = "custom",
constraint_values = [...],
flags = [
"--custom_malloc=//label",
"--other_flag",
"--//example/starlark:flag=23",
],
)
The strings inside flags are parsed exactly the way they would be on the command line or in a .bazelrc line, so the same syntax covers native flags (--custom_malloc=...), repeatable native flags, and Starlark build settings (--//pkg:flag=value). Relative labels are not supported. Everything addressed by the flag must be a fully qualified label.2
The attribute is intentionally restricted in two ways. First, it is not configurable — you cannot wrap it in a select(). The list of flags is a static property of the platform target.2 Second, you cannot put --platforms itself in the list, because that would set up a circular evaluation where selecting a platform changes which platform is selected.2 The same exclusion applies, in spirit, to other platform-like flags.
When Bazel Selects the Platform
The flags do not fire merely because a target depends on a platform definition. They fire when Bazel uses the platform — either as the target platform via a top-level --platforms=//:custom on the command line or .bazelrc, via a transition deeper in the graph that flips --platforms to the platform's label, or as an execution platform selected for actions.3,2 Bazel applies the platform's flags with the highest precedence: they overwrite earlier values from the command line, .bazelrc, or transitions, so any rule that reads those settings sees the platform-bound values automatically.2
There are two important interaction rules around this:
- Platform flags win over earlier settings. Treat the platform as the owner of the bundled values. If a user, rc file, or parent transition set the same flag differently, the selected platform's value wins.2
- Platform-based flags suppress platform mappings. If the selected platform has a non-empty
flagsattribute, Bazel skips the legacy platform-mappings file (the bridge introduced for migration described in 3.3.5 --platforms Flag). This avoids the confusion of two mechanisms quietly competing over the same flag values.2
The intent is the same as platform mappings — bundle related flags with a platform — but with the bundling expressed on the platform target itself rather than in an external mapping file.1
Inheritance: Platforms That Extend Platforms
Platforms can declare parents = [...], and platform-based flags follow inheritance. A child platform can re-declare a flag to override its parent's value, or add new flags that the parent does not set:4
platform(
name = "parent",
constraint_values = [...],
flags = ["--flag=1"],
)
platform(
name = "custom",
parents = [":parent"],
flags = ["--flag=2"],
)
Building under --platforms=//:custom sets --flag=2. Building under --platforms=//:parent sets --flag=1. Inheritance is what makes platform-based flags scale to large device matrices: a base platform can carry the cross-cutting defaults for a family of devices, and per-device platforms only need to spell out what differs.
You can reproduce the inheritance behavior with the platform-based-flags snippet. Its parent platform sets a Starlark bool build setting to false. Its child platform overrides the same flag to true. A tiny genrule copies host.txt or device.txt according to the resulting configuration.
What This Replaces: Custom Transitions That Only Switch Platforms
The pattern this attribute is designed to retire is the "wrapper rule with a transition that only changes platforms and one or two settings" pattern. An embedded project such as Pigweed is the clearest example.1 The Era 2 form is a custom Starlark rule that wraps a vanilla cc_binary and uses a transition to switch the platform plus a label_flag-typed FreeRTOS config:
def _m4_transition_impl(settings, attr):
return {
"//command_line_option:platforms":
"//actuator/firmware/targets/m4:platform",
"@freertos//:freertos_config":
"//actuator/firmware/targets/m4:freertos_config",
}
That is a real transition (allowlisted, with declared inputs and outputs, attached to a custom wrapper rule), but its body does nothing the platform itself could not say declaratively. The Era 3 form moves the same information onto the platform:1
# //targets/rp2/BUILD.bazel
platform(
name = "rp2040",
flags = [
"--@freertos//:freertos_config=//targets/rp2040:freertos_cfg",
],
)
Now bazel build --platforms=//targets/rp2:rp2040 //app:firmware builds the very same vanilla cc_binary with the right FreeRTOS config, no wrapper rule and no Starlark transition required. The platform definition has absorbed the bundling that the transition function used to perform.
Building For "This Other Platform" From One Top-Level Build
Platform-based flags answer the configuration half of the wrapper-rule pattern, but a separate question often comes up alongside it: "I want a single top-level build that produces artifacts for several platforms — how do I attach an artifact to a non-default platform without writing a custom transition rule?" Platform-based flags pair naturally with the generic platform_data rule from rules_platform, which simply transitions a single dependency edge to a chosen platform:1
# //apps/blinky/BUILD.bazel
platform_data(
name = "rp2040_blinky.elf",
target = ":rp2040_blinky", # a vanilla cc_binary
platform = "//targets/rp2:rp2040",
)
platform_data is a thin, generic transition rule — the kind of wrapper teams used to handcraft for each new device family — packaged once in rules_platform so it does not need to be reinvented per project. Combined with platform-based flags, the pair takes care of the two common needs together: switch the platform, and let the platform pull in its own associated flag values.1
The relationship between the two pieces is worth stating plainly: platform-based flags are a Bazel core feature. platform_data is a Starlark rule from rules_platform that lives outside Bazel itself. You can adopt platform-based flags without platform_data and vice versa — they just compose well for the embedded "build for this device" use case.
Where Custom Transitions Are Still The Right Tool
It is tempting to read this section and conclude that platform-based flags obsolete Starlark transitions. They do not. They obsolete one specific pattern within transitions — the one whose only job is to bundle flags with a --platforms change. Several adjacent patterns still belong in 4.7.2 Starlark Transitions or 4.7.3 Split Transitions:
- Configuration changes that don't move the platform. Switching
--compilation_mode=optfor one subtree, or enabling instrumentation flags for a test subtree, has nothing to do with picking a different machine. A transition function is still the only way to express "this dep tree builds with these settings, regardless of the target platform." - Logic that depends on rule attrs. Platform-based flags are static — the list lives on the
platform()target. A transition function can readattr.*fields and compute different flag values per consumer, which a platform definition cannot. - Multi-arch fan-out. Producing a universal binary or multi-arch container artifact is a 4.7.3 Split Transitions job: you need several configurations under one parent edge, not a single platform swap.
- Values that must be configurable. Anything you would naturally want to wrap in a
select()at the platform layer is out of reach, because theflagsattribute is non-configurable.
Reading this article alongside 4.7.4 Transition Boundaries & Gotchas also clarifies a non-obvious win of the platform-based form. A transition rule applied at a wrapper edge will, by default, propagate to all of its dependencies' attrs unless the rule author explicitly constrains it — which is one of the cited gotchas of transition spread. Moving the bundling onto the platform target shifts the configuration change to the top-level --platforms flag (or to a single platform_data edge) and avoids creating a custom transition whose boundaries must be maintained.
Boundary Conditions and Sharp Edges
Two limitations are worth knowing before you adopt this pattern:
- Repeated flags overwrite, not append. Repeatable flags such as
--copt,--features, and any Starlark build setting declared withconfig.string_list(repeatable = True)do not append fromplatform.flags— setting them on a platform replaces the value rather than extending it.2 The same defect applies to platform mappings. Both are tracked together as a known Starlark options-parser issue. If your platform tries to add an extra--copton top of a user-supplied list, the user's value will silently disappear. - Errors surface late. Bazel does not pre-validate the strings in
flagswhen the platform target is loaded. An unknown or malformed flag is reported only when the platform is actually used as a target or execution platform.2 In practice that means a typo in a rarely-selected platform can hide until a CI job picks that platform up.
These are the kind of details that justify the item's extra focus: the pattern is genuinely simpler than custom transitions for the cases it covers, but it is also younger, has documented edges, and is not the right answer for every configurable build.
Status In The Wider Bazel Story
Platform-based flags are part of the platforms-and-toolchains track of work, alongside aspects propagating to target toolchains, and the direction is driven by the embedded use case.5 That framing matters: this is not a niche escape hatch, it is the direction the platform model is moving, with embedded and cross-build users as the most visible drivers.
The honest assessment from the conspect — "a promising simplification, not yet the default answer for every configurable build" — still holds. The ecosystem guidance for platform-based flags is thinner than for classic transitions, and a lot of existing Starlark rules and tutorials predate the feature. But for the specific shape of "build for this device, with these device-bundled flags," reaching for platform.flags and platform_data before reaching for transition() is now the recommended path.
platform() accepts a flags = [...] attribute that lists flag values to apply whenever Bazel uses the platform as a target or execution platform. Combined with platform_data from rules_platform, this collapses the very common "wrapper rule with a transition that only switches --platforms and a couple of label flags" pattern into a declarative platform definition.
Reach for it when the configuration genuinely belongs to a target device. Stick with custom Starlark transitions when the configuration is independent of the platform, depends on rule attrs, fans out into multiple architectures, or needs select()-based decisions.
Check your understanding · 4 questions
1.A selected platform sets --//pkg:mode=embedded, while .bazelrc already set --//pkg:mode=host. Which value do rules see?
Select one answer
2.A platform() declares flags = ["--typo_flag=1"], but that platform is never selected in CI. When is the typo likely to surface?
Select one answer
3.You want one top-level bazel build //app:firmware to also build //app:firmware configured for //targets/rp2:rp2040 without a custom wrapper transition. Which pattern matches the article?
Select one answer
4.Which statements describe platform-based flags correctly?
Select all that apply
Footnotes
-
Pigweed and Bazel for Embedded Development — Era 3 / "Modern Era" section: motivating question ("if all we're doing is switching between platforms and associated label flag bundles, can we do something simpler than custom transitions?") and the before/after Pigweed Sense example using
platform.flagsandplatform_data. ↩1 ↩2 ↩3 ↩4 ↩5 ↩6 -
Platforms and Toolchains Rules —
platform.flagssyntax, non-configurability,--platformsexclusion, highest-precedence overwrite semantics, execution-platform application, platform-mapping suppression, late validation of malformed flags, and repeated-flag overwrite behavior. ↩1 ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 -
Pigweed and Bazel for Embedded Development — Era 3 syntax:
platform(name = "rp2040", flags = ["--@freertos//:freertos_config=//targets/rp2040:freertos_cfg"])and the accompanying explanation that "flags are now part of the platform definition." ↩1 ↩2 -
Pigweed and Bazel for Embedded Development —
parentsexample showing a child platform overriding a parent's--flag=1with--flag=2, demonstrating per-device specialization on top of a shared base platform. ↩ -
State of the Union (BazelCon 2024) — Platforms and Toolchains roadmap section: "Platform-based flags: flags that vary depending on target platform" announced as part of the recommended platform/toolchain story, with Pigweed credited as the driving use case. ↩