3.3.3 Constraint Values

When 3.3.2 Platform Vocabulary introduced the three platform roles, it left open the question: how does Bazel know what a platform is? The answer is a two-rule type system. A constraint_setting declares a dimension — a property that matters when distinguishing machines. A constraint_value declares a specific value within that dimension. Together, they form the vocabulary that platforms, select(), and toolchain resolution all speak1.

The Type/Value Model

constraint_setting is the category — think of it as an enum type2. It says "there is a property called cpu" without specifying which CPU:

constraint_setting(name = "cpu")

constraint_value is a specific value within that category — an enum member. It points back to its parent setting:

constraint_value(
    name = "x86_64",
    constraint_setting = ":cpu",
)

constraint_value(
    name = "arm64",
    constraint_setting = ":cpu",
)

Both are regular build rules, defined in BUILD files and referenced by label1. If the above live in cpus/BUILD, you reference the x86_64 constraint as //cpus:x86_64.

This separation is the key design insight: the setting defines what kind of question to ask ("what CPU?"), while the values enumerate the possible answers3. Any rule, select(), or toolchain can match against values without needing to know the full set of possibilities.

Standard Constraints: @platforms

Most projects never need to define constraint_setting or constraint_value directly. The @platforms repository ships with Bazel and provides standard constraints for the two most common dimensions1:

OS@platforms//os:os is the setting. Values include @platforms//os:linux, @platforms//os:macos, @platforms//os:windows, @platforms//os:android, @platforms//os:ios, and others1.

CPU@platforms//cpu:cpu is the setting. Values include @platforms//cpu:x86_64, @platforms//cpu:aarch64 (ARM64), @platforms//cpu:s390x, and others4.

These standard constraints are what select() branches on in 3.3.1 Configurable Attributes (select()), what 3.3.4 Target Compatibility checks in target_compatible_with, and what 3.3.5 --platforms Flag activates through --platforms. You can list all available constraints with bazel query @platforms//...4.

The upstream repository makes the interoperability boundary concrete: cpu/BUILD and os/BUILD are the canonical label catalogs, including published compatibility aliases such as cpu:arm64 to cpu:aarch64 and os:macos to os:osx.5 Its README reserves shared upstream additions for truly ubiquitous semantics. Domain-specific and project-specific values stay with their owning repositories.

The @platforms//host platform — Bazel's default when --platforms is not set — auto-detects the local machine's OS and CPU and maps them to the corresponding @platforms constraint values6. This is why single-platform builds work without any explicit platform configuration.

Composing Platforms

A platform() target is a named collection of constraint_values that together describe a machine1:

platform(
    name = "linux_x86",
    constraint_values = [
        "@platforms//os:linux",
        "@platforms//cpu:x86_64",
    ],
)

This says: linux_x86 is a machine that runs Linux on an x86_64 CPU. Building with --platforms=//platforms:linux_x86 activates those constraint values across the entire build — for select() resolution, toolchain matching, and target compatibility checks1. The maintainer-workspace platforms file shows the same shape for linux_x86_64 and macos_arm64, each composed from @platforms constraints plus a repo-local libc value.

One hard rule applies: a platform can have at most one constraint_value per constraint_setting1. A machine can't be both Linux and macOS, or both x86_64 and ARM64. If you need to model a machine with two CPUs, you'd need a separate constraint_setting for the second value — and the same logic applies to independent axes like GPU vendor, where a new setting is the right solution1.

Custom Constraints

The standard @platforms dimensions cover OS and CPU, which is sufficient for most projects1. But real-world build matrices sometimes involve properties that OS and CPU don't capture. When that happens, define your own constraint_setting and constraint_value pair3 — the maintainer-workspace example does exactly this with a repo-local libc setting and glibc/apple_libc values:

A production toolchain may need several such axes rather than one. hermetic-llvm, for example, keeps libc, C++ standard library, kernel, PIE, FPU, and Windows ABI/CRT choices as separate constraint families. Its libc family makes the variant a setting and generates one value per supported libc version.7 Treat that as evidence for the modeling rule, not a vocabulary to copy wholesale: add an axis only when it changes compatibility or toolchain selection in your own build.

# //build/constraints/BUILD
constraint_setting(name = "gpu_vendor")
constraint_value(name = "nvidia", constraint_setting = ":gpu_vendor")
constraint_value(name = "amd", constraint_setting = ":gpu_vendor")
constraint_value(name = "none", constraint_setting = ":gpu_vendor")

Now platforms can include GPU information:

platform(
    name = "linux_x86_nvidia",
    constraint_values = [
        "@platforms//os:linux",
        "@platforms//cpu:x86_64",
        "//build/constraints:nvidia",
    ],
)

And select() can branch on it:

cc_library(
    name = "compute_backend",
    srcs = select({
        "//build/constraints:nvidia": ["cuda_backend.cc"],
        "//build/constraints:amd": ["rocm_backend.cc"],
        "//conditions:default": ["cpu_backend.cc"],
    }),
)

You can also extend an existing constraint_setting by defining new values for it, as long as visibility allows1. For example, if your organization uses a custom OS not covered by @platforms, you can add a new constraint_value that references @platforms//os:os:

constraint_value(
    name = "my_custom_os",
    constraint_setting = "@platforms//os:os",
)

The guideline is straightforward: common, cross-language properties like OS and CPU belong in @platforms. Properties unique to your rules or organization belong in your own repository6. Custom-purpose OS or CPU values should also live in your repo, not in @platforms.

Where Constraint Values Appear

Constraint values are the shared vocabulary across four mechanisms in Bazel:

ContextHow constraint values are used
select() keyA constraint_value label matches if the target platform includes it2
config_settingconstraint_values attribute bundles multiple constraints for compound conditions2
target_compatible_withDeclares which constraints a target requires — platforms missing them make the target incompatible1
toolchain matchingtarget_compatible_with and exec_compatible_with on toolchains determine which toolchain serves which platform4

The first three are covered in this section: 3.3.1 Configurable Attributes (select()) for select() and config_setting patterns, 3.3.4 Target Compatibility for target compatibility. Toolchain resolution — how Bazel automatically selects the right compiler and linker based on platform constraints — starts with the rule-author platform model in 4.6.1 Platform Model for Rule Authors and continues through 4.6 Toolchains & Platform Resolution.

This shared vocabulary is what makes the platform system coherent. Before it existed, each language had its own ad-hoc flags (--cpu, --crosstool_top, --javabase) that didn't interoperate3. The constraint model replaced that with a single API that all languages, tools, and select() branches understand the same way.

key takeaway

constraint_setting defines a dimension (like cpu or os). constraint_value names a specific value within that dimension (like x86_64 or linux). The @platforms repo provides standard OS and CPU constraints sufficient for most projects. A platform() is a named set of constraint values — one per setting — describing a complete machine. Define custom constraints only when your build matrix has dimensions beyond OS and CPU.

Check your understanding · 3 questions

1.What is the relationship between constraint_setting and constraint_value?

Select one answer

2.True or false about platform composition and constraint rules:

Choose True or False for each sentence

A platform() target can include two constraint_values from the same constraint_setting (e.g., both @platforms//os:linux and @platforms//os:macos).
You can extend an existing constraint_setting by defining new constraint_values that reference it, as long as visibility allows.
Custom constraint_settings for organization-specific dimensions should be contributed to @platforms.

3.Match each context to how it uses constraint values:

Drag each answer onto the matching prompt, or click an answer and then click a prompt

Answers
select() key (constraint_value label)
config_setting constraint_values attribute
target_compatible_with attribute
toolchain exec_compatible_with
0 of 3 answered

Footnotes

  1. Platforms — constraint_setting and constraint_value rules, platform composition, one-value-per-setting rule, extending existing settings, target_compatible_with 1 2 3 4 5 6 7 8 9 10 11

  2. Configurable Build Attributes — constraint_value as select() key, config_setting constraint_values attribute, "enum type / enum value" analogy 1 2 3

  3. Configurable Builds - Part 1 — motivation for constraint model replacing ad-hoc flags, constraint_setting/constraint_value design, platform composition example 1 2 3

  4. Writing Bazel rules: platforms and toolchains — constraint_value and constraint_setting concepts, @platforms listing, toolchain constraint matching 1 2 3

  5. Bazel platforms repository mapcpu/BUILD, os/BUILD, and the README.md define the current shared labels and their ownership policy.

  6. Migrating to Platforms — @platforms standard declarations, @platforms//host auto-detection, custom vs common constraint ownership guidelines 1 2

  7. hermetic-llvm repository map — the constraints/ families, including the focused libc definitions, show independent domain-specific axes used by a production cross-toolchain.