You'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.