Been reading the Bazel docs over and over to the point where nothing feels new, but I cannot seem to grasp how to setup configurations and variants for variants other than the native ones, e.g. --cpu and --compilation_mode.
To explain what I mean with configuration and variants, consider this table of Configuration rows, and Variant Points columns, and Variant cells (making this table up, hope it at least makes abstract sense):
| Has hardware module X | Connected on interface | Fail fast | |
|---|---|---|---|
| Customer configuration 1 | Yes | Ethernet | Yes |
| Customer configuration 2 | Yes | USB | Yes |
| Customer configuration 3 | No | USB | No |
| Debug hw configuration | No | UART | Yes |
From a Bazel user perspective:
-DVARIANT_POINT_CONNECTED_ETH for Ethernet connection and -DVARIANT_POINT_CONNECTED_USB for USB connectionbazel build //my/target --//:configuration=debughwselect() in rulesYou're looking for platforms. Build settings (that you found first) are better for independent pieces of configuration where setting them in any permutation is useful, vs platforms tie all the pieces together. The resulting settings and constraints can then be used to drive compiler flags and limit targets to certain configurations.
For your example, I think something like this makes sense:
constraint_setting(name = "has_x")
constraint_value(
name = "x_v1",
constraint_setting = ":has_x",
)
constraint_value(
name = "x_v2",
constraint_setting = ":has_x",
)
constraint_value(
name = "x_none",
constraint_setting = ":has_x",
)
constraint_setting(name = "interface")
constraint_value(
name = "interface_usb",
constraint_setting = ":interface",
)
constraint_value(
name = "interface_ethernet",
constraint_setting = ":interface",
)
constraint_value(
name = "interface_uart",
constraint_setting = ":interface",
)
constraint_setting(name = "failure_reporting")
constraint_value(
name = "fail_fast",
constraint_setting = ":failure_reporting",
)
constraint_value(
name = "fail_bugreport",
constraint_setting = ":failure_reporting",
)
platform(
name = "customer_1",
constraint_values = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
":x_v1",
":interface_ethernet",
":fail_fast",
],
)
platform(
name = "customer_2",
constraint_values = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
":x_v1",
":interface_usb",
":fail_fast",
],
)
platform(
name = "customer_3",
constraint_values = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
":x_none",
":interface_usb",
":fail_bugreport",
],
)
platform(
name = "debug_hw",
constraint_values = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
":x_none",
":interface_uart",
":fail_fast",
],
)
Notice how each platform specifies a setting for each of the constraints. A given compiler command (created by the configured target) will use exactly one platform. Also, each file will be compiled separately for every platform even if the outputs are identical. Platforms are intended to limit which permutations of the constraints are actually built. If you want to deduplicate the constraints, platform.parents lets you build an inheritance tree of platforms, you can't use two platforms at the same time.
The target_compatible_with attribute limits targets to specific constraint values. By default, every target is considered compatible with all platforms. If you use target_compatible_with to limit this, then incompatible targets will produce errors if you try to build them explicitly or just be skipped if included as part of a wildcard. For your use case, this target would build for any of the platforms:
cc_library(
name = "generic_linked_list",
<etc>
)
but this one would only build for customer_1 and customer_2:
cc_library(
name = "hardware_x_driver",
target_compatible_with = [":x_v1", ":x_v2"],
<etc>
)
cc_library(
name = "ethernet_driver",
# Or you could have a separate constraint for "has ethernet hardware",
# to use for some platforms which have the hardware but don't use it
# as the primary interface.
target_compatible_with = [":interface_ethernet"],
<etc>
)
Using different compiler flags, dependencies, or most other rule attributes for different platforms is done with select. Select can read the constraint_values directly. So you could write this:
cc_library(
name = "selected_connection",
copts = select({
":interface_usb": [ "-DVARIANT_POINT_CONNECTED_USB" ],
":interface_ethernet": ["-DVARIANT_POINT_CONNECTED_ETH" ],
":interface_uart": ["-DVARIANT_POINT_CONNECTED_UART" ],
}),
deps = select({
":interface_ethernet": [ ":ethernet_driver" ],
"//conditions:default": [],
}),
<etc>
)
Note that if you want the defines to propagate to dependent targets, use cc_library.defines instead of copts. Also, using cc_library.local_defines would give the same non-inheriting semantics and also be compatible with compilers that use a different command-line flag for that.
The easiest way to specify a platform is with --platforms. In this case, build with something like --platforms=//:customer_1. You can specify that directly on the command line or put it a .bazelrc config section like this:
build:customer_1 --platforms=//some/long/and/annoying/package:customer_1
which you would use with --config=customer_1.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With