4.6.7 IncompatiblePlatformProvider

extra

IncompatiblePlatformProvider is the analysis-time marker Bazel attaches to any configured target it decides is incompatible with the current target platform. The declarative side of that decision — the target_compatible_with attribute, transitivity, and the @platforms//:incompatible sentinel — is covered in 3.3.4 Target Compatibility. This item is about the other half of the same mechanism: the provider itself, which is the programmatic surface that CI scripts, rule consumers, and bazel cquery see when a target has been skipped.

The provider exists for one reason: incompatibility needs to be observable from Starlark without buying into the explicit-request error path. When bazel build //... silently drops a target, something downstream still needs to know which targets were dropped and why. IncompatiblePlatformProvider is the answer.1

The Provider Is a Marker, Not a Constructor

The most important thing about IncompatiblePlatformProvider for a rule author is what it is not. It is not a provider that rules return from their implementation. Its Starlark API is an empty interface — a name, with no constructor exposed to .bzl code.2

A custom rule cannot do this:

Won't build
def _my_rule_impl(ctx):
    if some_condition(ctx):
        return [IncompatiblePlatformProvider()]
    return [DefaultInfo(files = depset([...]))]

my_rule = rule(implementation = _my_rule_impl)

Loading the .bzl file fails because no such constructor is exported to Starlark. The decisive diagnostic is that IncompatiblePlatformProvider is not defined in the .bzl environment. Exact source locations and surrounding text depend on the Bazel version and file layout, so treat the undefined-name line as the useful signal rather than relying on one transcript.

Only Bazel itself produces this provider. The Starlark exposure is read-only by design: cquery filters need to recognise that Bazel decided a target is incompatible, but rules do not get to declare the verdict themselves, because incompatibility is a contract about the target/platform pair rather than a status a single rule should assert unilaterally.2

That distinction matters because it changes what "programmatic incompatibility" actually means at this layer. The phrase does not describe rules returning a provider. It describes the fact that any other Starlark code — most often a cquery Starlark output script — can read the marker that Bazel attaches.

How Bazel Decides a Target Is Incompatible

Two paths put IncompatiblePlatformProvider on a configured target. Neither of them involves a rule constructing it.

Direct incompatibility. The target's target_compatible_with attribute resolves to a list of compatibility conditions that the active target platform/configuration pair does not satisfy. The list can contain constraint_value labels and config_setting labels. Constraints are checked against the target platform, while config settings are checked against the target configuration.3 Because target_compatible_with also accepts select(), this is the natural place to encode "evaluate flags or platform conditions and decide if this target is buildable" — the configurable expression runs in the loading/analysis phase like any other select(), and the decision is reflected in the provider Bazel attaches.1

Transitive incompatibility. Any target that depends on an incompatible target becomes incompatible itself, even with no target_compatible_with of its own. The provider propagates along the configured dependency edges. This is what makes bazel build //... safe in cross-platform repos: marking one low-level library as platform-specific shields every consumer from wildcard expansion on the wrong host.1,4

What does not set the provider is toolchain resolution failure by itself: when a compatible target asks for a toolchain that has no registered candidate, Bazel produces an analysis error, not an incompatibility marker. Keep those cases separate: target_compatible_with answers "is this target buildable for this target platform?", while toolchain resolution answers "can this compatible target find the tools it declared?"

The decision lives in attribute evaluation and resolution, not in rule implementation logic. The implementation function never runs for an incompatible target. Analysis short-circuits before action declaration.1

Detecting Incompatibility From cquery

The provider becomes visible through bazel cquery --output=starlark. Inside that Starlark dialect, providers(target) returns a map keyed by provider name, and the only safe way to check for the marker is the presence test — there are no fields to read.5

The canonical filter prints only compatible targets:

def format(target):
    if "IncompatiblePlatformProvider" not in providers(target):
        return target.label
    return ""

Run against the target-compatibility snippet, the same expression — annotated to label each target — cleanly classifies all four targets. The runnable form lives in incompat.cquery, which is identical apart from formatting both branches for readability:

$ bazel cquery --platforms=//platforms:linux //app:all \
    --output=starlark --starlark:file=incompat.cquery
@@//app:depends_on_mac_only [INCOMPATIBLE]
@@//app:linux_only [compatible]
@@//app:mac_only [INCOMPATIBLE]
@@//app:portable [compatible]

Two observations from this output are worth pinning down. First, //app:depends_on_mac_only carries no target_compatible_with attribute itself, yet it is marked incompatible because its only dependency — //app:mac_only — is. That is direct evidence of transitive propagation through the provider, not through some separate dependency walk. Second, the provider is opaque: there is no reason or failed_constraints field. If a script needs richer compatibility diagnostics, explicitly build the target for that platform to get Bazel's incompatibility message, or inspect the target's resolved target_compatible_with / BUILD metadata. Use --toolchain_resolution_debug only for separate toolchain-resolution failures.2

That is what makes IncompatiblePlatformProvider a useful primitive for CI without making it a leaky abstraction. A pre-merge job that wants "every target that will build under this platform" gets exactly that signal from one cquery invocation. Nothing else needs to be cross-referenced.

When Rule Authors Reach for "Programmatic" Incompatibility

Because the provider is not constructible, the rule-author question becomes: how do I express "this target is unbuildable in this configuration" from inside a custom rule? The honest answer is to encode the condition where Bazel can act on it, not where the rule implementation runs.

The most expressive lever is target_compatible_with. For simple AND-style conditions, list constraint_value and config_setting labels directly. For branching, OR, or NOT logic, wrap the attribute in select() so flag-based, platform-based, or config_setting-based incompatibility can be declared without touching the implementation function:3

my_rule(
    name = "gpu_only_tool",
    target_compatible_with = select({
        "//conditions:cuda_available": [],
        "//conditions:default": ["@platforms//:incompatible"],
    }),
)

When the build configuration does not satisfy //conditions:cuda_available, the resolved list contains @platforms//:incompatible — a constraint value no platform satisfies — and Bazel attaches the provider during analysis. The rule implementation never runs in that configuration. This is the same pattern used for OR/NOT logic in 3.3.4 Target Compatibility, applied to ruleset-defined conditions.1

A second lever is optional toolchains. A rule whose toolchains = [...] entry uses config_common.toolchain_type(..., mandatory = False) gets None from ctx.toolchains["//pkg:type"] when no candidate matches the current platforms, instead of failing resolution. The rule implementation can then degrade gracefully — for example, by producing an empty DefaultInfo and an explanatory message — but the target stays "compatible" from the platform model's point of view. IncompatiblePlatformProvider is not involved. Pick this path when the rule has a sensible fallback. Pick configurable target_compatible_with when the right answer is "skip this target entirely."6

What these two levers share is that they both encode the condition into the attribute or toolchain layer Bazel evaluates before the implementation function. The implementation function is the wrong layer for compatibility decisions: by the time it runs, the target has already passed the compatibility check for the current platform.

Where This Sits in the Section

IncompatiblePlatformProvider is the read-only side of the platform model from 4.6.1 Platform Model for Rule Authors. The declarative compatibility surface — target_compatible_with and the @platforms//:incompatible sentinel — lives in 3.3.4 Target Compatibility. Toolchain matching and ordering live in 4.6.3 Toolchain Resolution. CI consumers read the resulting marker through 5.2.2 bazel cquery — Configured Graph. The provider is extra for the same reason it is small: most rule authors never need to think about it directly. They configure compatibility declaratively, and Bazel and downstream tooling do the rest.

extra

Why the Provider Has No Fields

The Starlark surface is deliberately a marker with no fields and no constructor — the API page documents exactly one sentence of behaviour and points consumers at the cquery workflow.2 That shape is consistent with the rest of Bazel's resolution APIs: the result of resolution is observable, but its internal reasoning is not.

Two consequences follow. A cquery filter is the right tool for "which targets are incompatible?", but a richer script that wants to know why must ask a different question: build the target under that platform to get Bazel's compatibility error, or inspect the target's resolved compatibility metadata. The provider itself cannot answer the question. And because adding fields is non-breaking but exposing a constructor would be, the API is free to grow toward richer diagnostics later without making IncompatiblePlatformProvider() something rules are tempted to mint themselves.

For a ruleset-sized use, inspect the mini-ruleset's platform declarations and the hello_linux_only target that applies the custom constraint through target_compatible_with.

key takeaway

IncompatiblePlatformProvider is a marker Bazel attaches. It is not a provider Starlark rules construct. The Starlark API surface is an empty interface, exposed only so that bazel cquery --output=starlark can detect targets Bazel has decided to skip — directly via an unsatisfied target_compatible_with, or transitively via an incompatible dependency.

When a rule needs "programmatic" incompatibility, the right layer is target_compatible_with = select({...}) or optional toolchains, not the rule's implementation function. The implementation never runs for an incompatible target — the decision happens earlier, and the provider is its public artifact.

Check your understanding · 4 questions

1.A custom rule author wants gpu_only_tool to be skipped by bazel build //... when CUDA is unavailable, while keeping the failure mode (no "no matching toolchain") clean. Which is the correct mechanism?

Select one answer

2.True or false: observing IncompatiblePlatformProvider from tooling.

Choose True or False for each sentence

providers(target) in a cquery --output=starlark formatter can detect the marker by provider name.
The marker has no reason field. Richer diagnostics require asking a different question, such as explicitly building the target or inspecting compatibility metadata.
A cquery script should use --toolchain_resolution_debug to explain ordinary target-platform incompatibility.
Because the provider is read-only, tooling can observe Bazel's incompatibility decision without giving rules a way to mint that decision themselves.

3.In a bazel cquery --output=starlark script, what is the correct way to detect that a configured target was skipped because Bazel decided it is incompatible?

Select one answer

4.Match each rule-author lever to what it actually does about platform incompatibility:

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

Answers
target_compatible_with = select({...}) resolving to @platforms//:incompatible
config_common.toolchain_type("//pkg:type", mandatory = False) with no matching toolchain
fail("unsupported") from inside the rule implementation
0 of 4 answered

Footnotes

  1. Platformstarget_compatible_with, the @platforms//:incompatible sentinel, OR/NOT patterns with select(), transitive propagation of incompatibility, wildcard-skip vs explicit-request error, and the canonical cquery filter using IncompatiblePlatformProvider. 1 2 3 4 5

  2. IncompatiblePlatformProvider — provider definition. The Starlark API surface is a marker with no fields and no public constructor. 1 2 3 4

  3. Common definitionstarget_compatible_with accepts both constraint_values and config_settings. Mismatched constraints or config settings make the target incompatible. 1 2

  4. Incompatible target skipping — design motivation for target_compatible_with and explicit treatment of transitivity through the configured dependency graph.

  5. Configurable Query (cquery)--output=starlark, the providers(target) map, and the format(target) function contract for per-target output.

  6. Toolchains — optional toolchain types via config_common.toolchain_type(..., mandatory = False) and the None result from ctx.toolchains[...] when no candidate matches.