4.6.6 Custom Constraints & Custom Platforms
recommendedMost builds never need custom constraints. The @platforms repository already ships standard constraint_settings for OS and CPU, plus @platforms//host to auto-detect the local machine.1 Reach for custom constraints only when those standard dimensions cannot describe a real distinction your build matrix has to make: a GPU capability the toolchain depends on, a libc variant that leaks into binaries, a staging-versus-production hardware generation, or anything else that meaningfully changes which toolchain or sources Bazel should select.
A constraint is a distinguishing property of a build or production machine. CPU architecture, presence or absence of a GPU, or the version of a locally installed compiler are common examples, but a constraint can be anything that meaningfully distinguishes machines when orchestrating build work.2 The vocabulary already exists from 3.3.3 Constraint Values. What changes at this level is that you decide when to add to it.
When @platforms Already Covers You
Before defining anything new, check whether the dimension you care about is already a constraint in @platforms. The Bazel team maintains github.com/bazelbuild/platforms precisely so OS and CPU vocabulary stays consistent across the ecosystem.3 If your build only needs to distinguish "Linux x86_64 vs macOS arm64," everything you need is already available — @platforms//os:linux, @platforms//cpu:x86_64, and so on. A custom constraint_setting for cpu would duplicate that vocabulary and break interoperability with other rule sets that read @platforms//cpu.
The same logic applies to extending an existing setting with a new value. Custom-purpose OSes or CPUs should be declared in your rule's repo rather than in @platforms, because rule-specific extensions belong to the owner of the rule, not to the shared ecosystem.4 You can do this directly: if visibility allows, you may add a new constraint_value for an existing constraint_setting such as @platforms//cpu.5
In short, default to @platforms. Add custom dimensions only when the distinction is real, your toolchain or sources need to branch on it, and no @platforms vocabulary covers it.
Defining a Custom Constraint
A constraint_setting declares a dimension. A constraint_value is a named value within that dimension. Both are plain BUILD-level targets.6
package(default_visibility = ["//visibility:public"])
constraint_setting(name = "gpu")
constraint_value(
name = "cuda",
constraint_setting = ":gpu",
)
constraint_value(
name = "rocm",
constraint_setting = ":gpu",
)
constraint_value(
name = "no_gpu",
constraint_setting = ":gpu",
)
If these live in //constraints/BUILD.bazel, downstream BUILD files refer to them as //constraints:cuda, //constraints:no_gpu, and so on — ordinary labels. They participate in everything that already accepts a constraint_value: platform(), config_setting, target_compatible_with, and toolchain target_compatible_with / exec_compatible_with clauses.6
A constraint_setting may also nominate one of its constraint_values as the setting's default. A target_compatible_with or exec_compatible_with clause matches a platform when, for each constraint_value in its list, the platform also has that constraint_value — either explicitly or as a default.7 In practice this means a "neutral" value can stand in for platforms that don't say anything about that setting — for example, marking :no_gpu as the default for the :gpu setting so platforms that omit a GPU value still match toolchains that require :no_gpu.
Refining an Existing Constraint Value
Some custom dimensions only make sense underneath a broader constraint. A glibc_version setting, for example, is not an arbitrary machine property in the same way OS or CPU is. It is a Linux-specific refinement. The Build Encyclopedia now exposes that relationship directly with constraint_setting(refines_constraint_value = ...).8
constraint_setting(
name = "glibc_version",
default_constraint_value = ":glibc_unspecified",
refines_constraint_value = "@platforms//os:linux",
)
constraint_value(
name = "glibc_unspecified",
constraint_setting = ":glibc_version",
)
constraint_value(
name = "glibc_2_31",
constraint_setting = ":glibc_version",
)
platform(
name = "linux_glibc_2_31",
constraint_values = [
"@platforms//os:linux",
":glibc_2_31",
],
)
The refinement is a validity rule, not inheritance. If a platform lists a non-default value from the refining setting, it must also list the refined value — :glibc_2_31 requires @platforms//os:linux in the example above. Platforms that get the default value, either implicitly by omitting the setting or explicitly by listing it, are exempt.8
Refinement also helps select() specificity. If one branch matches the broad value (@platforms//os:linux) and another matches the refining value (:glibc_2_31), Bazel treats the refining condition as more specific when both match, instead of reporting an ambiguous match.8 That is useful for nested vocabularies: choose a broad Linux default, then override only for particular libc versions, SDK families, or hardware generations that refine an existing platform value.
Practical guidance on picking custom constraints:
- One setting per orthogonal dimension. GPU vendor, libc variant, and hardware generation are independent and belong to separate
constraint_settings. A platform may only carry oneconstraint_valueper setting, so you cannot encode multiple values of the same kind on a single platform.9 - PascalCase or lowercase names tied to vocabulary, not products. Prefer
gpuovernvidia_a100_or_amd_mi300, since the value, not the setting, names the concrete option. - Public visibility for cross-package consumption. In a constraints-only package, use
package(default_visibility = ["//visibility:public"]). Otherwise setvisibility = ["//visibility:public"]on each exportedconstraint_setting/constraint_value.
Composing a Custom Platform
A platform() target is a named set of constraint_values.10 To define a platform that uses both standard @platforms constraints and your custom ones, list them together:
platform(
name = "linux_arm64_cuda",
constraint_values = [
"@platforms//os:linux",
"@platforms//cpu:arm64",
"//constraints:cuda",
],
)
This is the shape that puts custom constraints to use: standard OS and CPU vocabulary for portability and interop with toolchains that already read @platforms, plus your project-specific dimension for the distinction @platforms cannot express. The platform() rule does not require a special syntax for "custom" values — every entry in constraint_values is a label to a constraint_value target, whatever repository it lives in.
A platform may only carry one constraint_value per constraint_setting. So a platform cannot describe a machine with "two CPUs" without a second constraint_setting modeling the second slot.9 In practice this means custom constraints should describe orthogonal capabilities, not alternative values of the same capability.
You can see this end-to-end in the target-compatibility snippet: a custom demo_os constraint setting with demo_linux and demo_macos values, two platform() targets composed from them, and filegroups using target_compatible_with to demonstrate the wildcard-skip behavior introduced in 3.3.4 Target Compatibility.
Where Custom Constraints Show Up
Once defined, a custom constraint is a normal constraint_value label. Every place that already accepts @platforms//cpu:arm64 accepts //constraints:cuda:
select()keys. Aconfig_settingwithconstraint_values = ["//constraints:cuda"]matches platforms that include the:cudavalue, just like theis_armexample in the migration guide does for@platforms//cpu:arm.11target_compatible_withon targets and toolchains. A target restricted to["//constraints:cuda"]will be silently skipped underbazel build //...when the target platform does not include:cuda, following the rules from 3.3.4 Target Compatibility.- Toolchain resolution. When you write a
toolchain()target as in 4.6.2 Defining, Registering & Accessing Toolchains, itstarget_compatible_withandexec_compatible_withclauses can list custom constraints. The resolution algorithm from 4.6.3 Toolchain Resolution treats them like any otherconstraint_value: each clause must be satisfied by the candidate platform.7
The mental model is symmetric. Define a constraint once. Consume it in any of the existing platform APIs without adding new machinery. That is what makes custom constraints a localized extension rather than a parallel system.
Common Pitfalls
- Modelling the wrong axis. If two values are alternatives within a single dimension — like "CUDA" and "no GPU" — they should be
constraint_values of the sameconstraint_setting, not separate settings. If they are independent capabilities — like "has GPU" and "uses glibc 2.31" — they need separate settings.9 - Putting common vocabulary in your repo. OS and CPU constraints belong in
@platformsso multiple rule sets read the same labels. Puttingos:linuxin your project's own constraint package fragments the ecosystem and breaks interop with rule sets that already consume@platforms//os:linux.4 - Forgetting visibility. Constraint targets are referenced from BUILD files all over the workspace. If a downstream package cannot see your
constraint_value, everyplatform()andtarget_compatible_withthat references it fails to load. - Treating platforms as arbitrary sets. A
platform()is a constraint vector with at most one value per setting. Custom constraints widen the vector. They do not let you encode multiple values of the same dimension on one platform.9 - Forgetting the refined parent.
refines_constraint_valuedoes not add the parent value automatically. A platform that lists a non-default refining value must also list the value it refines. Use defaults for the neutral or "unspecified" case.8
Custom constraints exist for distinctions that @platforms cannot already express: a real capability or environment dimension your toolchains or sources must branch on. Define a constraint_setting per orthogonal dimension, add constraint_values for the concrete options, and compose them with standard @platforms constraints inside platform() targets. When a custom dimension is only meaningful under an existing value, model that relationship with refines_constraint_value instead of relying on comments or naming conventions.
Everything else — select(), target_compatible_with, toolchain resolution — keeps working unchanged, because a custom constraint_value is just another label in the same shape Bazel already accepts.
Check your understanding · 4 questions
1.Which scenario most clearly justifies defining a new constraint_setting instead of reusing @platforms?
Select one answer
2.True or false: custom constraint_values and platform APIs.
Choose True or False for each sentence
constraint_value can appear as a key in select() via a config_setting with constraint_values = [...].platform() may carry two constraint_values belonging to the same constraint_setting to express alternative options.toolchain() target's target_compatible_with and exec_compatible_with can list custom constraint_value labels alongside @platforms ones.constraint_values only work with target_compatible_with. select() and toolchain resolution require values from @platforms.3.A constraint_setting nominates one of its values as the setting's default. How does Bazel treat a platform that does NOT list any value for that setting when matching a clause like target_compatible_with = [":default_value"]?
Select one answer
4.True or false: refines_constraint_value on a custom constraint_setting.
Choose True or False for each sentence
platform.constraint_values when a refining value is present.select() condition and a refining condition both match, the refining condition is treated as more specific.Footnotes
-
Platforms —
@platforms//hostauto-detects local OS and CPU, and standard constraints live ingithub.com/bazelbuild/platforms. ↩ -
Platforms — definition of a constraint and a platform. Constraints can be any meaningful distinguishing property. ↩
-
Platforms — Bazel team maintains common constraint definitions for popular OSes and CPUs to keep the ecosystem consistent. ↩
-
Migrating to Platforms — common cross-language properties belong in
@platforms. Rule-specific properties belong in the rule's repo. ↩1 ↩2 -
Platforms — extending an existing
constraint_settingby defining your ownconstraint_valueis supported when visibility allows. ↩ -
Platforms —
constraint_settingdeclares a property type,constraint_valuedeclares a possible value, both referenced as labels. ↩1 ↩2 -
Toolchains —
target_compatible_with/exec_compatible_withmatch if every requiredconstraint_valueis on the platform, either explicitly or as a default. ↩1 ↩2 -
Platforms and Toolchains Rules —
constraint_setting.refines_constraint_valuerequires non-default refining values to appear with the refined value, exempts default values, and makes refiningselect()conditions more specific. ↩1 ↩2 ↩3 ↩4 -
Platforms — a platform may have at most one
constraint_valueperconstraint_setting. ↩1 ↩2 ↩3 ↩4 -
Platforms — the
platformrule defines a platform as a collection ofconstraint_values. ↩ -
Migrating to Platforms —
config_settingwithconstraint_valuesletsselect()branch on platform properties. ↩