Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to do configuration and variant handling in Bazel?

Tags:

bazel

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:

  • All targets should be buildable for all configurations by default
    • Not ok adding "Customer configuration X" everywhere on adding a configuration
    • Configuration specific components should be possible to explicitly state which configuration it is specific to
      • I think this is what constraint_value is for
  • Rules should add flags based on configuration
    • For example -DVARIANT_POINT_CONNECTED_ETH for Ethernet connection and -DVARIANT_POINT_CONNECTED_USB for USB connection
  • I want a simple build command for building/testing/running a single configuration
    • Something like bazel build //my/target --//:configuration=debughw
    • Think there is something about it in the skylark config docs, but not seeing how to go from the example to using constraint_values and select() in rules
like image 997
Andreas Avatar asked Dec 13 '25 09:12

Andreas


1 Answers

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.

like image 138
Brian Silverman Avatar answered Dec 16 '25 21:12

Brian Silverman



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!