1.. _docs-blog-02-bazel-feature-flags: 2 3================================================== 4Pigweed Eng Blog #2: Feature flags in Bazel builds 5================================================== 6By Ted Pudlik 7 8Published 2024-05-31 9 10Let's say you're migrating your build system to Bazel. Your project heavily 11relies on preprocessor defines to configure its code. 12 13.. code-block:: 14 15 -DBUILD_FEATURE_CPU_PROFILE 16 -DBUILD_FEATURE_HEAP_PROFILE 17 -DBUILD_FEATURE_HW_SHA256 18 19In your source files, you use these preprocessor variables to conditionally 20compile some sections, via ``#ifdef``. When building the same code for 21different final product configurations, you want to set different defines. 22 23How do you model this in Bazel? 24 25This post discusses three possible approaches: 26 27#. :ref:`docs-blog-02-config-with-copts` 28#. :ref:`docs-blog-02-platform-based-skylib-flags` 29#. :ref:`docs-blog-02-chromium-pattern` 30 31Which one to choose? If you have the freedom to refactor your code to use the 32Chromium pattern, give it a try! It is not difficult to maintain once 33implemented, and can prevent real production issues by detecting typos. 34 35.. _docs-blog-02-config-with-copts: 36 37------------------------------------------- 38Easy but limited: bazelrc config with copts 39------------------------------------------- 40Let's start with the simplest approach: you can put the compiler options into 41your `bazelrc configuration file <https://bazel.build/run/bazelrc>`_. 42 43.. code-block:: 44 45 # .bazelrc 46 47 common:mydevice_evt1 --copts=-DBUILD_FEATURE_CPU_PROFILE 48 common:mydevice_evt1 --copts=-DBUILD_FEATURE_HEAP_PROFILE 49 common:mydevice_evt1 --copts=-DBUILD_FEATURE_HW_SHA256 50 # and so on 51 52Then, when you build your application, the defines will all be applied: 53 54 55.. code-block:: sh 56 57 bazel build --config=mydevice_evt1 //src:application 58 59 60Configs are expanded recursively, allowing you to group options together and 61reuse them: 62 63.. code-block:: 64 65 # .bazelrc 66 67 common:full_profile --copts=-DBUILD_FEATURE_CPU_PROFILE 68 common:full_profile --copts=-DBUILD_FEATURE_HEAP_PROFILE 69 70 # When building for mydevice_evt1, use full_profile. 71 common:mydevice_evt1 --config=full_profile 72 73 # When building for mydevice_evt2, additionally enable HW_SHA256. 74 common:mydevice_evt2 --config=full_profile 75 common:mydevice_evt1 --copts=-DBUILD_FEATURE_HW_SHA256 76 77Downsides 78========= 79While it *is* simple, the config-with-copts approach has a few downsides. 80 81.. _docs-blog-02-config-dangeous-typos: 82 83Dangerous typos 84--------------- 85If you misspell ``BUILDDD_FEATURE_CPU_PROFILE`` [sic!] in your ``.bazelrc``, 86the actual ``BUILD_FEATURE_CPU_PROFILE`` variable will `take the default value 87of 0 <https://stackoverflow.com/q/5085392/24291280>`__. So, although you 88intended to enable this feature, it will just remain disabled! 89 90This isn't just a Bazel problem, but a general issue with the simple 91``BUILD_FEATURE`` macro pattern. If you misspell ``BUILD_FEATUER_CPU_PROFILE`` 92[sic!] in your C++ file, you'll get it to evaluate to 0 in any build system! 93 94One way to avoid this issue is to use the :ref:`"Chromium-style" build flag 95pattern <docs-blog-02-chromium-pattern>`, ``BUILDFLAG(CPU_PROFILE)``. If you 96do, a misspelled or missing define becomes a compiler error. However, the 97config-with-copts approach is a little *too* simple to express this pattern, 98which requires code generation of the build flag headers. 99 100No multi-platform build support 101------------------------------- 102Bazel allows you to perform multi-platform builds. For example, in a single 103Bazel invocation you can build a Python flasher program (that will run on your 104laptop) which embeds as a data dependency the microcontroller firmware to flash 105(that will run on the microcontroller). `We do this in Pigweed's own 106examples 107<https://cs.opensource.google/pigweed/examples/+/main:examples/01_blinky/BUILD.bazel>`__. 108 109Unfortunately, the config-with-copts pattern doesn't work nicely with 110multi-platform build primitives. (Technically, the problem is that Bazel 111`doesn't support transitioning on --config 112<https://bazel.build/extending/config#unsupported-native-options>`__.) If you 113want a multi-platform build, you need some more sophisticated idiom. 114 115No build system variables 116------------------------- 117This approach doesn't introduce any variable that can be used within the build 118system to e.g. conditionally select different source files for a library, 119choose a different library as a dependency, or remove some targets from the 120build altogether. We're really just setting preprocessor defines here. 121 122 123Limited multirepo support 124------------------------- 125The ``.bazelrc`` files are not automatically inherited when another repo 126depends on yours. They can be `imported 127<https://bazel.build/run/bazelrc#imports>`__, but it's an all-or-nothing 128affair. 129 130.. _docs-blog-02-platform-based-skylib-flags: 131 132------------------------------------------------------------ 133More power with no code changes: Platform-based Skylib flags 134------------------------------------------------------------ 135Let's address some shortcomings of the approach above by representing the build 136features as `Skylib flags 137<https://github.com/bazelbuild/bazel-skylib/blob/main/docs/common_settings_doc.md>`__ 138and grouping them through `platform-based flags 139<https://github.com/bazelbuild/proposals/blob/main/designs/2023-06-08-platform-based-flags.md>`__. 140(Important note: this feature is `still under development 141<https://github.com/bazelbuild/bazel/issues/19409>`__! See :ref:`the Appendix 142<docs-blog-02-old-bazel>` for workarounds for older Bazel versions.) 143 144The platform sets a bunch of flags: 145 146.. code-block:: python 147 148 # //platform/BUILD.bazel 149 150 # The platform definition 151 platform( 152 name = "mydevice_evt1", 153 flags = [ 154 "--//build/feature:cpu_profile=true", 155 "--//build/feature:heap_profile=true", 156 "--//build/feature:hw_sha256=true", 157 ], 158 ) 159 160The flags have corresponding code-generated C++ libraries: 161 162.. code-block:: python 163 164 # //build/feature/BUILD.bazel 165 load("@bazel_skylib//rules:common_settings.bzl", "bool_flag") 166 # I'll show one possible implementation of feature_cc_library later. 167 load("//:feature_cc_library.bzl", "feature_cc_library") 168 169 # This is a boolean flag, but there's support for int- and string-valued 170 # flags, too. 171 bool_flag( 172 name = "cpu_profile", 173 build_setting_default = False, 174 ) 175 176 # This is a custom rule that generates a cc_library target that exposes 177 # a header "cpu_profile.h", the contents of which are either, 178 # 179 # BUILD_FEATURE_CPU_PROFILE=1 180 # 181 # or, 182 # 183 # BUILD_FEATURE_CPU_PROFILE=0 184 # 185 # depending on the value of the cpu_profile bool_flag. This "code 186 # generation" is so simple that it can actually be done in pure Starlark; 187 # see below. 188 feature_cc_library( 189 name = "cpu_profile_cc", 190 flag = ":cpu_profile", 191 ) 192 193 # Analogous library that exposes the constant in Python. 194 feature_py_library( 195 name = "cpu_profile_py", 196 flag = ":cpu_profile", 197 ) 198 199 # And in Rust, why not? 200 feature_rs_library( 201 name = "cpu_profile_rs", 202 flag = ":cpu_profile", 203 ) 204 205 bool_flag( 206 name = "heap_profile", 207 build_setting_default = False, 208 ) 209 210 feature_cc_library( 211 name = "heap_profile_cc", 212 flag = ":heap_profile", 213 ) 214 215 bool_flag( 216 name = "hw_sha256", 217 build_setting_default = False, 218 ) 219 220 feature_cc_library( 221 name = "hw_sha256_cc", 222 flag = ":hw_sha256", 223 ) 224 225C++ libraries that want to access the variable needs to depend on the 226``cpu_profile_cc`` (or ``heap_profile_cc``, ``hw_sha256_cc``) library. 227 228Here's one possible implementation of ``feature_cc_library``: 229 230.. code-block:: python 231 232 # feature_cc_library.bzl 233 load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") 234 235 def feature_cc_library(name, build_setting): 236 hdrs_name = name + ".hdr" 237 238 flag_header_file( 239 name = hdrs_name, 240 build_setting = build_setting, 241 ) 242 243 native.cc_library( 244 name = name, 245 hdrs = [":" + hdrs_name], 246 ) 247 248 def _impl(ctx): 249 out = ctx.actions.declare_file(ctx.attr.build_setting.label.name + ".h") 250 251 # Convert boolean flags to canonical integer values. 252 value = ctx.attr.build_setting[BuildSettingInfo].value 253 if type(value) == type(True): 254 if value: 255 value = 1 256 else: 257 value = 0 258 259 ctx.actions.write( 260 output = out, 261 content = r""" 262 #pragma once 263 #define {}={} 264 """.format(ctx.attr.build_setting.label.name.upper(), value), 265 ) 266 return [DefaultInfo(files = depset([out]))] 267 268 flag_header_file = rule( 269 implementation = _impl, 270 attrs = { 271 "build_setting": attr.label( 272 doc = "Build setting (flag) to construct the header from.", 273 mandatory = True, 274 ), 275 }, 276 ) 277 278Advantages 279========== 280 281Composability of platforms 282-------------------------- 283A neat feature of the simple config-based approach was that configs could be 284composed through recursive expansion. Fortunately, platforms can be composed, 285too! There are two mechanisms for doing so: 286 287#. Use platforms' `support for inheritance 288 <https://bazel.build/reference/be/platforms-and-toolchains#platform_inheritance>`__. 289 This allows "subplatforms" to override entries from "superplatforms". But, 290 only single inheritance is supported (each platform has at most one parent). 291 292#. The other approach is to compose lists of flags directly, through concatenation: 293 294 .. code-block:: python 295 296 FEATURES_CORTEX_M7 = [ 297 "--//build/feature:some_feature", 298 ] 299 300 FEATURES_MYDEVICE_EVT1 = FEATURES_CORTEX_M7 + [ 301 "--//build/feature:some_other_feature", 302 ] 303 304 platform( 305 name = "mydevice_evt1", 306 flags = FEATURES_MYDEVICE_EVT1, 307 ) 308 309 Concatenation doesn't allow overriding entries, but frees you from the 310 single-parent limitation of inheritance. 311 312 .. tip:: 313 314 This approach can also be used to define custom host platforms: 315 ``HOST_CONSTRAINTS`` in ``@local_config_platform//:constraints.bzl`` 316 contains the autodetected ``@platform//os`` and ``@platforms//cpu`` 317 constraints set by Bazel's default host platform. 318 319Multi-platform build support 320---------------------------- 321How do you actually associate the platform with a binary you want to build? One 322approach is to just specify the platform on the command-line when building a 323``cc_binary``: 324 325.. code-block:: sh 326 327 bazel build --platforms=//platform:mydevice_evt1 //src:main 328 329But another approach is to leverage multi-platform build, through 330`platform_data <https://github.com/bazelbuild/rules_platform/blob/main/platform_data/defs.bzl>`__: 331 332.. code-block:: python 333 334 # //src/BUILD.bazel 335 load("@rules_platform//platform_data:defs.bzl", "platform_data") 336 337 cc_binary(name = "main") 338 339 platform_data( 340 name = "main_mydevice_evt1", 341 target = ":main", 342 platform = "//platform:mydevice_evt1", 343 ) 344 345Then you can keep your command-line simple: 346 347.. code-block:: sh 348 349 bazel build //src:main_mydevice_evt1 350 351 352 353Flags correspond to build variables 354----------------------------------- 355You can make various features of the build conditional on the value of the 356flag. For example, you can select different dependencies: 357 358.. code-block:: python 359 360 # //build/feature/BUILD.bazel 361 config_setting( 362 name = "hw_sha256=true", 363 flag_values = { 364 ":hw_sha256": "true", 365 }, 366 ) 367 368 # //src/BUILD.bazel 369 cc_library( 370 name = "my_library", 371 deps = [ 372 "//some/unconditional:dep", 373 ] + select({ 374 "//build/feature:hw_sha256=true": ["//extra/dep/for/hw_sha256:only"], 375 "//conditions:default": [], 376 }) 377 378Any Bazel rule attribute described as `"configurable" 379<https://bazel.build/docs/configurable-attributes>`__ can take a value that 380depends on the flag in this way. Library header lists and source lists are 381common examples, but the vast majority of attributes in Bazel are configurable. 382 383Downsides 384========= 385 386Typos remain dangerous 387---------------------- 388If you used :ref:`"Chromium-style" build flags <docs-blog-02-chromium-pattern>` 389you *would* be immune to dangerous typos when using this Bazel pattern. But 390until then, you still have this problem, and actually it got worse! 391 392If you forget to ``#include "build/features/hw_sha256.h"`` in the C++ file that 393references the preprocessor variable, the build system or compiler will still 394not yell at you. Instead, the ``BUILD_FEATURE_HA_SHA256`` variable will take 395the default value of 0. 396 397This is similar to the :ref:`typo problem with the config approach 398<docs-blog-02-config-dangeous-typos>`, but worse, because it's easier to miss 399an ``#include`` than to misspell a name, and you'll need to add these 400``#include`` statements in many places. 401 402One way to mitigate this problem is to make the individual 403``feature_cc_library`` targets private, and gather them into one big library 404that all targets will depend on: 405 406.. code-block:: python 407 408 feature_cc_library( 409 name = "cpu_profile_cc", 410 flag = ":cpu_profile", 411 visibility = ["//visibility:private"], 412 ) 413 414 feature_cc_library( 415 name = "heap_profile_cc", 416 flag = ":heap_profile", 417 visibility = ["//visibility:private"], 418 ) 419 420 feature_cc_library( 421 name = "hw_sha256_cc", 422 flag = ":hw_sha256", 423 visibility = ["//visibility:private"], 424 ) 425 426 # Code-generated cc_library that #includes all the individual 427 # feature_cc_library headers. 428 all_features_cc_library( 429 name = "all_features", 430 deps = [ 431 ":cpu_profile_cc", 432 ":heap_profile_cc", 433 ":hw_sha256_cc", 434 # ... and many more. 435 ], 436 visibility = ["//visibility:public"], 437 ) 438 439However, a more satisfactory solution is to adopt :ref:`Chromium-style build 440flags <docs-blog-02-chromium-pattern>`, which we discuss next. 441 442Build settings have mandatory default values 443-------------------------------------------- 444The Skylib ``bool_flag`` that represents the build flag within Bazel has a 445``build_setting_default`` attribute. This attribute is mandatory. 446 447This may be a disappointment if you were hoping to provide no default, and have 448Bazel return errors if no value is explicitly set for a flag (either via a 449platform, through ``.bazelrc``, or on the command line). The Skylib build flags 450don't support this. 451 452The danger here is that the default value may be unsafe, and you forget to 453override it when adding a new platform (or for some existing platform, when 454adding a new flag). 455 456There is an alternative pattern that allows you to define default-less build 457flags: instead of representing build flags as Skylib flags, you can represent 458them as ``constraint_setting`` objects. I won't spell this pattern out in 459this blog post, but it comes with its own drawbacks: 460 461* The custom code-generation rules are more complex, and need to parse the 462 ``constraint_value`` names to infer the build flag values. 463* All supported flag values must be explicitly enumerated in the ``BUILD`` 464 files, and the code-generation rules need explicit dependencies on them. 465 This leads to substantially more verbose ``BUILD`` files. 466 467On the whole, I'd recommend sticking with the Skylib flags! 468 469 470.. _docs-blog-02-chromium-pattern: 471 472------------------------------------------------------------ 473Error-preventing approach: Chromium-style build flag pattern 474------------------------------------------------------------ 475This pattern builds on :ref:`docs-blog-02-platform-based-skylib-flags` by 476adding a macro helper for retrieving flag values that guards against typos. The 477``BUILD.bazel`` files look exactly the same as in the :ref:`previous section 478<docs-blog-02-platform-based-skylib-flags>`, but: 479 480#. Users of flags access them in C++ files via ``BUILDFLAG(SOME_NAME)``. 481#. The code generated by ``feature_cc_library`` is a little more elaborate than 482 a plain ``SOME_NAME=1`` or ``SOME_NAME=0``, and it includes a dependency on 483 the `Chromium build flag header 484 <https://chromium.googlesource.com/chromium/src/build/+/refs/heads/main/buildflag.h>`__. 485 486Here's the ``feature_cc_library`` implementation: 487 488.. code-block:: python 489 490 load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") 491 492 def feature_cc_library(name, build_setting): 493 """Generates a cc_library from a common build setting. 494 495 The generated cc_library exposes a header [build_setting.name].h that 496 defines a corresponding build flag. 497 498 Example: 499 500 feature_cc_library( 501 name = "evt1_cc", 502 build_setting = ":evt1", 503 ) 504 505 * This target is a cc_library that exposes a header you can include via 506 #include "build/flags/evt1.h". 507 * That header defines a build flag you can access in your code through 508 BUILDFLAGS(EVT1). 509 * If you wish to use the build flag from a cc_library, add the target 510 evt1_cc to your cc_library's deps. 511 512 Args: 513 name: Name for the generated cc_library. 514 build_setting: One of the Skylib "common settings": bool_flag, int_flag, 515 string_flag, etc. See 516 https://github.com/bazelbuild/bazel-skylib/blob/main/docs/common_settings_doc.md 517 """ 518 hdrs_name = name + ".hdr" 519 520 flag_header_file( 521 name = hdrs_name, 522 build_setting = build_setting, 523 ) 524 525 native.cc_library( 526 name = name, 527 hdrs = [":" + hdrs_name], 528 # //:buildflag is a cc_library containing the 529 # Chromium build flag header. 530 deps = ["//:buildflag"], 531 ) 532 533 def _impl(ctx): 534 out = ctx.actions.declare_file(ctx.attr.build_setting.label.name + ".h") 535 536 # Convert boolean flags to canonical integer values. 537 value = ctx.attr.build_setting[BuildSettingInfo].value 538 if type(value) == type(True): 539 if value: 540 value = 1 541 else: 542 value = 0 543 544 ctx.actions.write( 545 output = out, 546 content = r""" 547 #pragma once 548 549 #include "buildflag.h" 550 551 #define BUILDFLAG_INTERNAL_{}() ({}) 552 """.format(ctx.attr.build_setting.label.name.upper(), value), 553 ) 554 return [DefaultInfo(files = depset([out]))] 555 556 flag_header_file = rule( 557 implementation = _impl, 558 attrs = { 559 "build_setting": attr.label( 560 doc = "Build setting (flag) to construct the header from.", 561 mandatory = True, 562 ), 563 }, 564 ) 565 566----------- 567Bottom line 568----------- 569If you have the freedom to refactor your code to use the Chromium pattern, 570Bazel provides safe and convenient idioms for expressing configuration through 571build flags. Give it a try! 572 573Otherwise, you can still use platform-based Skylib flags, but beware typos and 574missing ``#include`` statements! 575 576-------- 577Appendix 578-------- 579A couple "deep in the weeds" questions came up while this blog post was being 580reviewed. I thought they were interesting enough to discuss here, for the 581interested reader! 582 583Why isn't the reference code a library? 584======================================= 585If you made it this far you might be wondering, why is the code listing for 586``feature_cc_library`` even here? Why isn't it just part of Pigweed, and used 587in our own codebase? 588 589The short answer is that Pigweed is middleware supporting multiple build 590systems, so we don't want to rely on the build system to generate configuration 591headers. 592 593But the longer answer has to do with how this blog post came about. Some time 594ago, I was migrating team A's build from CMake to Bazel. They used Chromium 595build flags, but in CMake, so to do a build migration they needed Bazel support 596for this pattern. So I put an implementation together. I wrote a design 597document, but it had confidential details and was not widely shared. 598 599Then team B comes along and says, "we tried migrating to Bazel but couldn't 600figure out how to support build flags" (not the Chromium flags, but the "naive" 601kind; i.e. their problem statement was exactly the one the blog opens with). So 602I wrote a less-confidential but still internal doc for them saying "here's how 603you could do it"; basically, :ref:`docs-blog-02-platform-based-skylib-flags`. 604 605Then Pigweed's TL comes along and says "Ted, don't you feel like spending a day 606fighting with RST [the markup we use for pigweed.dev]?" Sorry, actually they 607said something more like, "Why is this doc internal, can't we share this more 608widely"? Well we can. So that's the doc you're reading now! 609 610But arguably the story shouldn't end here: Pigweed should probably provide a 611ready-made implementation of Chromium build flags for downstream projects. See 612:bug:`342454993` to check out how that's going! 613 614Do you need to generate actual files? 615===================================== 616If you are a Bazel expert, you may ask: do we need to have Bazel write out the 617actual header files, and wrap those in a ``cc_library``? If we're already 618writing a custom rule for ``feature_cc_library``, can we just set ``-D`` 619defines by providing `CcInfo 620<https://bazel.build/rules/lib/providers/CcInfo>`__? That is, do something like 621this: 622 623.. code-block:: python 624 625 define = "{}={}"..format( 626 ctx.attr.build_setting.label.name.upper(), 627 value) 628 return [CcInfo( 629 compilation_context=cc_common.create_compilation_context( 630 defines=depset([define])))] 631 632The honest answer is that this didn't occur to me! But one reason to prefer 633writing out the header files is that this approach generalizes in an obvious 634way to other programming languages: if you want to generate Python or Golang 635constants, you can use the same pattern, just change the contents of the file. 636Generalizing the ``CcInfo`` approach is trickier! 637 638.. _docs-blog-02-old-bazel: 639 640What can I do on older Bazel versions? 641====================================== 642This blog focused on describing approaches that rely on `platform-based 643flags 644<https://github.com/bazelbuild/proposals/blob/main/designs/2023-06-08-platform-based-flags.md>`__. 645But this feature is very new: in fact, as of this writing, it is `still under 646development <https://github.com/bazelbuild/bazel/issues/19409>`__, so it's not 647available in *any* Bazel version! So what can you do? 648 649One approach is to define custom wrapper rules for your ``cc_binary`` targets 650that use a `transition 651<https://bazel.build/extending/config#user-defined-transitions>`__ to set the 652flags. You can see examples of such transitions in the `Pigweed examples 653project 654<https://cs.opensource.google/pigweed/examples/+/main:targets/transition.bzl>`__. 655