Bazel build compatibility patterns#
This document describes the Bazel patterns Pigweed uses to express that a build target is compatible with a platform. The main motivation is to enable maintainable wildcard builds of upstream Pigweed for non-host platforms:
bazelisk build --config=rp2040 //...
The bulk of this document describes recommended patterns for expressing compatibility in various scenarios. For context, we also discuss alternative patterns and why they should be avoided. For the implementation plan, see Are we there yet?.
See Appendix: Background and the Platforms documentation for more context.
Intended audience#
This document is targeted at upstream Pigweed developers. The patterns described here are suitable for downstream projects, too, but downstream projects can employ a broader variety of approaches. Because Pigweed is middleware that must preserve users’ flexibility in configuring it, we need to be more careful.
This document assumes you’re familiar with regular Bazel usage, but perhaps not Bazel’s build configurability primitives.
Recommended compatibility patterns#
Summary#
Here’s a short but complete summary of the recommendations.
Alternative patterns (not recommended)#
This section describes alternative build compatibility patterns that we’ve used or considered in the past. They are not recommended. We’ll work to remove their instances from Pigweed, replacing them with the recommended patterns.
Per-facade constraint settings (not recommended)#
This approach was once recommended, although it was never fully rolled out:
For every facade, introduce a
constraint_setting
(e.g.,@pigweed//pw_foo:backend_constraint_setting
). This would be done by whoever defines the facade; if it’s an upstream facade, upstream Pigweed should define this setting.For every backend, introduce a corresponding constraint_value (e.g.,
//backends/pw_foo:board1_backend_constraint_value
). This should be done by whoever defines the backend; for backends defined in downstream projects, it’s done in that project.Mark the backend
target_compatible_with
its associatedconstraint_value
.
Why is this not recommended#
The major difference between this and what we’re recommending is that every backend was associated
with a unique constraint_value
, regardless of whether the backend imposed
any constraints on its platform or not. This implied downstream platforms that
set N backends would also have to list the corresponding N
constraint_values
.
The original motivation for per-facade constraint settings is now obsolete. They were intended to allow backend selection via multiplexers before platform-based flags became available. More details for the curious.
Where they still exist in upstream Pigweed, these constraint settings will be removed (see Are we there yet?).
Config setting from label flag (not recommended except for tests)#
This pattern was an attempt to keep
the central feature of per-facade constraint settings (the selection of a
particular backend can be detected) without forcing downstream users to list
constraint_values
explicitly in their platforms. A config_setting
is
defined that detects if a backend was selected through the label flag:
# pw_sys_io_stm32cube/BUILD.bazel
config_setting(
name = "backend_setting",
flag_values = {
"@pigweed//pw_sys_io:backend": "@pigweed//pw_sys_io_stm32cube",
},
)
cc_library(
name = "pw_sys_io_stm32cube",
target_compatible_with = select({
":backend_setting": [],
"//conditions:default": ["@platforms//:incompatible"],
}),
)
Why is this not recommended#
We’re really insisting on setting the label flag directly to the backend. In particular, we disallow patterns like “point the
label_flag
to analias
that may resolve to different backends based on aselect
” (because the config_setting in the above example will be false in that case).It’s a special pattern just for facade backends. Libraries which need to restrict compatibility but are not facade backends cannot use it.
Using the
config_setting
intarget_compatible_with
requires the weirdselect
trick shown above. It’s not very ergonomic, and definitely surprising.
When to use it anyway#
We may resort to defining private config_settings
following this pattern to
solve special problems like b/336843458 |
“Bazel tests using pw_unit_test_light can still rely on GoogleTest” or
pw_malloc tests.
In addition, some tests are backend-specific (directly include backend
headers). The most common example are tests that depend on
pw_thread but directly #include "pw_thread_stl/options.h"
.
For such tests, we will define private config_settings
following this
pattern.
Board and chipset constraint settings (not recommended)#
Pigweed has historically defined a “board” constraint_setting, and this setting was used to indicate that some modules are compatible with particular boards.
Why is this not recommended#
This is a particularly bad pattern: hardly any Pigweed build targets are only
compatible with a single board. Modules which have been marked as
target_compatible_with = ["//pw_build/constraints/board:mimxrt595_evk"]
are
generally compatible with many other RT595 boards, and even with other NXP
chips. We’ve already run into cases in practice where users want to use a
particular backend for a different board.
The “chipset” constraint_setting has the same problem: the build targets it was applied to don’t contain assembly code, and so are not generally compatible with only a particular chipset. It’s also unclear how to define chipset values in a vendor-agnostic manner.
These constraints will be removed (see Are we there yet?).
RTOS constraint setting (not recommended)#
Some modules include headers provided by an RTOS such as embOS, FreeRTOS or
Zephyr. If they do not make additional assumptions about the platform beyond
the availability of those headers, they could just declare themselves
compatible with the appropriate value of the //pw_build/constraints/rtos:rtos
constraint_setting
. Example:
# pw_chrono_embos/BUILD.bazel
cc_library(
name = "system_clock",
target_compatible_with = ["//pw_build/constraints/rtos:embos"],
)
Why is this not recommended#
At first glance, this seems like a pretty good pattern: RTOSes kind of like OSes, and OSes have their “well-known” constraint. So why not RTOSes?
RTOSes are not like OSes in an important respect: the dependency on them is
already expressed in the build system! A library that uses FreeRTOS headers
will have an explicit dependency on the @freertos
target. (This is in
contrast to OSes: a library that includes Linux system headers will not get
them from an explicit dependency.)
So, we can push the question of compatibility down to that target: if FreeRTOS
is compatible with your platform, then a library that depends on it is (in
general) compatible, too. Most (all?) RTOSes require configuration through
label_flags
(in particular, to specify the port), so platform compatibility
can be elegantly handled by setting the default value of that flag to a target
that’s @platforms//:incompatible
.
Multiplexer targets (not recommended)#
Historically, Pigweed selected default backends for certain facades based on
platform constraint values. For example, this was done by
//pw_chrono:system_clock
:
label_flag(
name = "system_clock_backend",
build_setting_default = ":system_clock_backend_multiplexer",
)
cc_library(
name = "system_clock_backend_multiplexer",
visibility = ["@pigweed//targets:__pkg__"],
deps = select({
"//pw_build/constraints/rtos:embos": ["//pw_chrono_embos:system_clock"],
"//pw_build/constraints/rtos:freertos": ["//pw_chrono_freertos:system_clock"],
"//pw_build/constraints/rtos:threadx": ["//pw_chrono_threadx:system_clock"],
"//conditions:default": ["//pw_chrono_stl:system_clock"],
}),
)
Why is this not recommended#
This pattern made it difficult for the user defining a platform to understand
which backends were being automatically set for them (because this information
was hidden in the BUILD.bazel
files for individual modules).
What to do instead#
Platforms should explicitly set the backends of all facades they use via platform-based flags. For users’ convenience, backend authors may provide default backend collections as dicts for explicit inclusion in the platform definition.
Are we there yet?#
As of this writing, upstream Pigweed does not yet follow the best practices recommended below. b/344654805 tracks fixing this.
Here’s a high-level roadmap for the recommendations’ implementation:
Implement the “syntactic sugar” referenced in the rest of this doc:
boolean_constraint_value
,incompatible_with_mcu
, etc.b/342691352 | “Platforms should set backends for Pigweed facades through label flags”. For each facade,
Remove the multiplexer targets.
Remove the per-facade constraint settings.
Remove any default backends.
b/343487589 | Retire the Board and chipset constraint settings.
Appendix: Background#
Why wildcard builds?#
Pigweed is generic microcontroller middleware: you can use Pigweed to accelerate development on any microcontroller platform. In addition, Pigweed provides explicit support for a number of specific hardware platforms, such as the Raspberry Pi RP2040 or STM32f429i Discovery Board. For these specific platforms, every Pigweed module falls into one of three buckets:
works with the platform, or,
is not intended to work with the platform, because the platform lacks the relevant capabilities (e.g., the pw_spi_mcuxpresso module specifically supports NXP chips, and is not intended to work with the Raspberry Pi Pico).
should work but doesn’t yet; that’s a bug or missing feature in Pigweed.
Bazel’s wildcard builds provide a nice way to ensure each Pigweed build target is known to fall into one of those three buckets. If you run:
bazelisk build --config=rp2040 //...
Bazel will attempt to build all Pigweed build targets for the specified platform, with the exception of targets that are explicitly annotated as not compatible with it. Such incompatible targets will be automatically skipped.
Challenge: designing constraint_values
#
As noted above, for wildcard builds to work we need to annotate some targets as not compatible with certain platforms. This is done through the target_compatible_with attribute, which is set to a list of constraint_values (essentially, enum values). For example, here’s a target only compatible with Linux:
cc_library(
name = "pw_digital_io_linux",
target_compatible_with = ["@platforms//os:linux"],
)
If the platform lists all the constraint_values
that appear in the target’s
target_compatible_with
attribute, then the target is compatible; otherwise,
it’s incompatible, and will be skipped.
If this sounds a little abstract, that’s because it is! Bazel is not very
opinionated about what the constraint_values actually represent. There are only
two sets of canonical constraint_values
, @platforms//os
and
@platforms//cpu
. Here are some possible choices—not necessarily good
ones, but all seen in the wild:
A set of constraint_values representing RTOSes:
@pigweed//pw_build/constraints/rtos:embos
@pigweed//pw_build/constraints/rtos:freertos
A set of representing individual boards:
@pigweed//pw_build/constraints/board:mimxrt595_evk
@pigweed//pw_build/constraints/board:stm32f429i-disc1
A pair of constraint values associated with a single module:
@pigweed//pw_spi_mcuxpresso:compatible
(the module is by definition compatible with any platform containing this constraint value)@pigweed//pw_spi_mcuxpresso:incompatible
There are many more possible structures.
What about constraint_settings
?#
Final piece of background: we mentioned above that constraint_values
are a bit
like enum values. The enums themselves (groups of constraint_values
) are called
constraint_settings
.
Each constraint_value
belongs to a constraint_setting
, and a platform
may specify at most one value from each setting.
Guiding principles#
These are the principles that guided the selection of the recommended patterns:
Be consistent. Make the patterns for different use cases as similar to each other as possible.
Make compatibility granular. Avoid making assumptions about what sets of backends or HAL modules will be simultaneously compatible with the same platforms.
Minimize the amount of boilerplate that downstream users need to put up with.
Support the autodetected host platform. That is, ensure
bazel build --platforms=@platforms//host //...
works. This is necessary internally (for google3) and arguably more convenient for downstream users generally.