https://pigweed.dev/docs/blog/02-bazel-feature-flags.html Skip to main content [ ] [ ] Hide navigation sidebar Hide table of contents sidebar Toggle site navigation sidebar Pigweed Toggle Light / Dark / Auto color theme Toggle table of contents sidebar Logo Pigweed [ ] * Home * Overview[ ] Toggle navigation of Overview + Mission & Philosophies * Get Started[ ] Toggle navigation of Get Started + First-time setup + Bazel quickstart + Bazel integration + Zephyr quickstart + Upstream Pigweed * Concepts[ ] Toggle navigation of Concepts + Facades and backends + Glossary + FAQs * Hardware targets[ ] Toggle navigation of Hardware targets + Android + Ambiq Apollo4 + Ambiq Apollo4 with pw_system + Arduino + docs + Emcraft SmartFusion2 + host + Host Device Simulator + lm3s6965evb-qemu + mimxrt595-evk + Raspberry Pi Pico + stm32f429i-disc1 + stm32f429i-disc1: STM32Cube * Modules[ ] Toggle navigation of Modules + Module Structure + pw_alignment[ ] Toggle navigation of pw_alignment o Source code o Issues + pw_allocator[ ] Toggle navigation of pw_allocator o Guides o API reference o Design & roadmap o Code size analysis o Source code o Issues + pw_analog[ ] Toggle navigation of pw_analog o Source code o Issues + pw_android_toolchain[ ] Toggle navigation of pw_android_toolchain o Source code o Issues + pw_arduino_build[ ] Toggle navigation of pw_arduino_build o Source code o Issues + pw_assert[ ] Toggle navigation of pw_assert o Backends[ ] Toggle navigation of Backends # Basic[ ] Toggle navigation of Basic @ Source code @ Issues # Pigweed logging[ ] Toggle navigation of Pigweed logging @ Source code @ Issues # Tokenized[ ] Toggle navigation of Tokenized @ Source code @ Issues # Zephyr[ ] Toggle navigation of Zephyr @ Source code @ Issues o Source code o Issues + pw_async[ ] Toggle navigation of pw_async o Backends[ ] Toggle navigation of Backends # Basic[ ] Toggle navigation of Basic @ Source code @ Issues o Source code o Issues + pw_async2[ ] Toggle navigation of pw_async2 o Backends[ ] Toggle navigation of Backends # Basic[ ] Toggle navigation of Basic @ Source code @ Issues # Linux epoll[ ] Toggle navigation of Linux epoll @ Source code @ Issues o Source code o Issues + pw_base64[ ] Toggle navigation of pw_base64 o Source code o Issues + pw_bloat[ ] Toggle navigation of pw_bloat o Source code o Issues + pw_blob_store[ ] Toggle navigation of pw_blob_store o Source code o Issues + pw_bluetooth[ ] Toggle navigation of pw_bluetooth o Source code o Issues + pw_bluetooth_hci[ ] Toggle navigation of pw_bluetooth_hci o Source code o Issues + pw_bluetooth_proxy[ ] Toggle navigation of pw_bluetooth_proxy o Source code o Issues + pw_bluetooth_profiles[ ] Toggle navigation of pw_bluetooth_profiles o Source code o Issues + pw_bluetooth_sapphire[ ] Toggle navigation of pw_bluetooth_sapphire o Source code o Issues + pw_boot[ ] Toggle navigation of pw_boot o Backends[ ] Toggle navigation of Backends # Cortex-M[ ] Toggle navigation of Cortex-M @ Source code @ Issues o Source code o Issues + pw_build[ ] Toggle navigation of pw_build o GN / Ninja[ ] Toggle navigation of GN / Ninja # Python GN Templates o CMake o Bazel o Project Builder o Linker Scripts o Source code o Issues + pw_build_android[ ] Toggle navigation of pw_build_android o Source code o Issues + pw_build_info[ ] Toggle navigation of pw_build_info o Source code o Issues + pw_build_mcuxpresso[ ] Toggle navigation of pw_build_mcuxpresso o Source code o Issues + pw_bytes[ ] Toggle navigation of pw_bytes o Source code o Issues + pw_channel[ ] Toggle navigation of pw_channel o Source code o Issues + pw_checksum[ ] Toggle navigation of pw_checksum o Source code o Issues + pw_chre[ ] Toggle navigation of pw_chre o Source code o Issues + pw_chrono[ ] Toggle navigation of pw_chrono o Backends[ ] Toggle navigation of Backends # embOS[ ] Toggle navigation of embOS @ Source code @ Issues # FreeRTOS[ ] Toggle navigation of FreeRTOS @ Source code @ Issues # RP2040[ ] Toggle navigation of RP2040 @ Source code @ Issues # STL[ ] Toggle navigation of STL @ Source code @ Issues # ThreadX[ ] Toggle navigation of ThreadX @ Source code @ Issues # Zephyr[ ] Toggle navigation of Zephyr @ Source code @ Issues o Source code o Issues + pw_cli[ ] Toggle navigation of pw_cli o API Reference o Source code o Issues + pw_clock_tree[ ] Toggle navigation of pw_clock_tree o Examples o APIs o Implementations[ ] Toggle navigation of Implementations # MCUXpresso[ ] Toggle navigation of MCUXpresso @ Source code @ Issues o Source code o Issues + pw_compilation_testing[ ] Toggle navigation of pw_compilation_testing o Source code o Issues + pw_config_loader[ ] Toggle navigation of pw_config_loader o Source code o Issues + pw_console[ ] Toggle navigation of pw_console o User Guide o Embedding Guide o Plugin Guide o Manual Test Procedure o Internal Design o Source code o Issues + pw_containers[ ] Toggle navigation of pw_containers o Source code o Issues + pw_cpu_exception[ ] Toggle navigation of pw_cpu_exception o Backends[ ] Toggle navigation of Backends # Cortex-M[ ] Toggle navigation of Cortex-M @ Source code @ Issues # RISCV[ ] Toggle navigation of RISCV @ Source code @ Issues o Source code o Issues + pw_crypto[ ] Toggle navigation of pw_crypto o Source code o Issues + pw_digital_io[ ] Toggle navigation of pw_digital_io o Backends[ ] Toggle navigation of Backends # Linux[ ] Toggle navigation of Linux @ Source code @ Issues # MCUXpresso[ ] Toggle navigation of MCUXpresso @ Source code @ Issues # RP2040[ ] Toggle navigation of RP2040 @ Source code @ Issues o Source code o Issues + pw_dma_mcuxpresso[ ] Toggle navigation of pw_dma_mcuxpresso o Source code o Issues + pw_docgen[ ] Toggle navigation of pw_docgen o Source code o Issues + pw_doctor[ ] Toggle navigation of pw_doctor o Source code o Issues + pw_emu[ ] Toggle navigation of pw_emu o Get started & guides o CLI reference o API reference o Configuration o Design o SEED-0108 o Source code o Issues + pw_env_setup[ ] Toggle navigation of pw_env_setup o Source code o Issues + pw_env_setup_zephyr[ ] Toggle navigation of pw_env_setup_zephyr o Source code o Issues + pw_file[ ] Toggle navigation of pw_file o Source code o Issues + pw_format[ ] Toggle navigation of pw_format o Source code o Issues + pw_function[ ] Toggle navigation of pw_function o Source code o Issues + pw_fuzzer[ ] Toggle navigation of pw_fuzzer o pw_fuzzer: Concepts o pw_fuzzer: Adding Fuzzers Using FuzzTest o pw_fuzzer: Adding Fuzzers Using LibFuzzer o pw_fuzzer: Using OSS-Fuzz o Source code o Issues + pw_grpc[ ] Toggle navigation of pw_grpc o Source code o Issues + pw_hdlc[ ] Toggle navigation of pw_hdlc o Get started & guides o API reference o Design & roadmap o Code size analysis o RPC over HDLC example o Router o Source code o Source code o Issues + pw_hex_dump[ ] Toggle navigation of pw_hex_dump o Source code o Issues + pw_i2c[ ] Toggle navigation of pw_i2c o Quickstart and guides o Reference o Implementations[ ] Toggle navigation of Implementations # Linux[ ] Toggle navigation of Linux @ Source code @ Issues # MCUXpresso[ ] Toggle navigation of MCUXpresso @ Source code @ Issues # Pico SDK[ ] Toggle navigation of Pico SDK @ Source code @ Issues o Source code o Issues + pw_ide[ ] Toggle navigation of pw_ide o Source code o Issues + pw_interrupt[ ] Toggle navigation of pw_interrupt o Backends[ ] Toggle navigation of Backends # Cortex-M[ ] Toggle navigation of Cortex-M @ Source code @ Issues # Xtensa[ ] Toggle navigation of Xtensa @ Source code @ Issues # Zephyr[ ] Toggle navigation of Zephyr @ Source code @ Issues o Source code o Issues + pw_intrusive_ptr[ ] Toggle navigation of pw_intrusive_ptr o Source code o Issues + pw_json[ ] Toggle navigation of pw_json o Source code o Issues + pw_kvs[ ] Toggle navigation of pw_kvs o Source code o Issues + pw_libc[ ] Toggle navigation of pw_libc o Source code o Issues + pw_libcxx[ ] Toggle navigation of pw_libcxx o Source code o Issues + pw_log[ ] Toggle navigation of pw_log o The pw_log protobuf o Tokenized log arguments o Backends[ ] Toggle navigation of Backends # Android[ ] Toggle navigation of Android @ Source code @ Issues # Basic[ ] Toggle navigation of Basic @ Source code @ Issues # Null[ ] Toggle navigation of Null @ Source code @ Issues # RPC # String[ ] Toggle navigation of String @ Source code @ Issues # Tokenized[ ] Toggle navigation of Tokenized @ Source code @ Issues # Zephyr[ ] Toggle navigation of Zephyr @ Source code @ Issues o Source code o Issues + pw_malloc[ ] Toggle navigation of pw_malloc o Backends[ ] Toggle navigation of Backends # Freelist[ ] Toggle navigation of Freelist @ Source code @ Issues # FreeRTOS[ ] Toggle navigation of FreeRTOS @ Source code @ Issues o Source code o Issues + pw_metric[ ] Toggle navigation of pw_metric o Source code o Issues + pw_minimal_cpp_stdlib[ ] Toggle navigation of pw_minimal_cpp_stdlib o Source code o Issues + pw_module[ ] Toggle navigation of pw_module o Source code o Issues + pw_multibuf[ ] Toggle navigation of pw_multibuf o Source code o Issues + pw_multisink[ ] Toggle navigation of pw_multisink o Source code o Issues + pw_package[ ] Toggle navigation of pw_package o Source code o Issues + pw_perf_test[ ] Toggle navigation of pw_perf_test o Source code o Issues + pw_persistent_ram[ ] Toggle navigation of pw_persistent_ram o Source code o Issues + pw_polyfill[ ] Toggle navigation of pw_polyfill o Source code o Issues + pw_preprocessor[ ] Toggle navigation of pw_preprocessor o Source code o Issues + pw_presubmit[ ] Toggle navigation of pw_presubmit o pw_presubmit: Code formatting o API reference o Source code o Issues + pw_protobuf[ ] Toggle navigation of pw_protobuf o pw_protobuf extended size report o Source code o Issues + pw_protobuf_compiler[ ] Toggle navigation of pw_protobuf_compiler o Source code o Issues + pw_random[ ] Toggle navigation of pw_random o Source code o Issues + pw_result[ ] Toggle navigation of pw_result o Source code o Issues + pw_ring_buffer[ ] Toggle navigation of pw_ring_buffer o Source code o Issues + pw_router[ ] Toggle navigation of pw_router o Source code o Issues + pw_rpc[ ] Toggle navigation of pw_rpc o pw_rpc Python package o pw_rpc Web Module o Source code o Issues o pw_protobuf o nanopb o pw_rpc Benchmarking + pw_rpc_transport[ ] Toggle navigation of pw_rpc_transport o Source code o Issues + pw_rust[ ] Toggle navigation of pw_rust o Source code o Issues + pw_sensor[ ] Toggle navigation of pw_sensor o pw_sensor Python package o Source code o Issues + pw_snapshot[ ] Toggle navigation of pw_snapshot o Setting up a Snapshot Pipeline o Module Usage o Snapshot Proto Format o Design Discussion o Source code o Issues + pw_software_update[ ] Toggle navigation of pw_software_update o pw_software_update: Get started o pw_software_update: Design o pw_software_update: Guide o pw_software_update: CLI reference o Source code o Issues + pw_span[ ] Toggle navigation of pw_span o Source code o Issues + pw_spi[ ] Toggle navigation of pw_spi o Backends[ ] Toggle navigation of Backends # Linux[ ] Toggle navigation of Linux @ Source code @ Issues # MCUXpresso[ ] Toggle navigation of MCUXpresso @ Source code @ Issues # RP2040[ ] Toggle navigation of RP2040 @ Source code @ Issues o Source code o Issues + pw_status[ ] Toggle navigation of pw_status o Get started & guides o Reference o Source code o Issues + pw_stm32cube_build[ ] Toggle navigation of pw_stm32cube_build o Source code o Issues + pw_stream[ ] Toggle navigation of pw_stream o Backends[ ] Toggle navigation of Backends # MCUXpresso[ ] Toggle navigation of MCUXpresso @ Source code @ Issues # UART (Linux)[ ] Toggle navigation of UART (Linux) @ Source code @ Issues # UART (MCUXpresso)[ ] Toggle navigation of UART (MCUXpresso) @ Source code @ Issues o Source code o Issues + pw_string[ ] Toggle navigation of pw_string o Get Started & Guides o API Reference o Design & Roadmap o Code Size Analysis o Source code o Issues + pw_symbolizer[ ] Toggle navigation of pw_symbolizer o Source code o Issues + pw_sync[ ] Toggle navigation of pw_sync o Backends[ ] Toggle navigation of Backends # Bare Metal[ ] Toggle navigation of Bare Metal @ Source code @ Issues # embOS[ ] Toggle navigation of embOS @ Source code @ Issues # FreeRTOS[ ] Toggle navigation of FreeRTOS @ Source code @ Issues # STL[ ] Toggle navigation of STL @ Source code @ Issues # ThreadX[ ] Toggle navigation of ThreadX @ Source code @ Issues # Zephyr[ ] Toggle navigation of Zephyr @ Source code @ Issues o Source code o Issues + pw_sys_io[ ] Toggle navigation of pw_sys_io o Backends[ ] Toggle navigation of Backends # Ambiq Suite SDK[ ] Toggle navigation of Ambiq Suite SDK @ Source code @ Issues # Arduino[ ] Toggle navigation of Arduino @ Source code @ Issues # LM3S6965EVB[ ] Toggle navigation of LM3S6965EVB @ Source code @ Issues # STM32F429[ ] Toggle navigation of STM32F429 @ Source code @ Issues # Emcraft SF2[ ] Toggle navigation of Emcraft SF2 @ Source code @ Issues # MCUXpresso[ ] Toggle navigation of MCUXpresso @ Source code @ Issues # RP2040[ ] Toggle navigation of RP2040 @ Source code @ Issues # Standard I/O[ ] Toggle navigation of Standard I/O @ Source code @ Issues # STM32Cube[ ] Toggle navigation of STM32Cube @ Source code @ Issues # Zephyr[ ] Toggle navigation of Zephyr @ Source code @ Issues o Source code o Issues + pw_system[ ] Toggle navigation of pw_system o pw_system CLI reference o Source code o Issues + pw_target_runner[ ] Toggle navigation of pw_target_runner o Go o Source code o Issues + pw_thread[ ] Toggle navigation of pw_thread o Backends[ ] Toggle navigation of Backends # embOS[ ] Toggle navigation of embOS @ Source code @ Issues # FreeRTOS[ ] Toggle navigation of FreeRTOS @ Source code @ Issues # STL[ ] Toggle navigation of STL @ Source code @ Issues # ThreadX[ ] Toggle navigation of ThreadX @ Source code @ Issues # Zephyr[ ] Toggle navigation of Zephyr @ Source code @ Issues o Source code o Issues + pw_tls_client[ ] Toggle navigation of pw_tls_client o Backends[ ] Toggle navigation of Backends # BoringSSL[ ] Toggle navigation of BoringSSL @ Source code @ Issues # MbedTLS[ ] Toggle navigation of MbedTLS @ Source code @ Issues o Source code o Issues + pw_tokenizer[ ] Toggle navigation of pw_tokenizer o Get started o Tokenization o Token databases o Detokenization o API reference o Source code o Issues + pw_toolchain[ ] Toggle navigation of pw_toolchain o Source code o Issues + pw_toolchain_bazel[ ] Toggle navigation of pw_toolchain_bazel o API reference o Get Started o Source code o Issues + pw_trace[ ] Toggle navigation of pw_trace o Backends[ ] Toggle navigation of Backends # Tokenized[ ] Toggle navigation of Tokenized @ Source code @ Issues o Source code o Issues + pw_transfer[ ] Toggle navigation of pw_transfer o API reference o Source code o Issues + pw_uart[ ] Toggle navigation of pw_uart o Source code o Issues + pw_unit_test[ ] Toggle navigation of pw_unit_test o Source code o Issues + pw_unit_test_zephyr[ ] Toggle navigation of pw_unit_test_zephyr o Source code o Issues + pw_varint[ ] Toggle navigation of pw_varint o Source code o Issues + pw_watch[ ] Toggle navigation of pw_watch o pw_watch how-to guide o pw_watch CLI reference o Source code o Issues + pw_web[ ] Toggle navigation of pw_web o Manual Test Procedure o Log viewer o Source code o Issues + pw_work_queue[ ] Toggle navigation of pw_work_queue o Source code o Issues * What's New In Pigweed * Mailing List * Chat Room * OS Support[ ] Toggle navigation of OS Support + Zephyr[ ] Toggle navigation of Zephyr o Zephyr Kconfig reference * Size Optimizations * Code Editor Support * Third Party Support[ ] Toggle navigation of Third Party Support + Abseil C++ + BoringSSL + Emboss + FreeRTOS + Fuchsia libraries + FuzzTest + GoogleTest + Nanopb + Perfetto + RE2 + TinyUSB + Docker * Source Code * Code Reviews * Issue Tracker * Contributing[ ] Toggle navigation of Contributing + Embedded C++ Guide + Style guides[ ] Toggle navigation of Style guides o C++ o Commit message o CLI o Protobuf o reStructuredText o Doxygen o Writing + Code Reviews + Code of Conduct + Documentation[ ] Toggle navigation of Documentation o Module docs contributor guidelines o Writing o reStructuredText o Doxygen o Changelog updates o pigweed.dev * Infra[ ] Toggle navigation of Infra + GitHub + CI/CQ Intro + Rollers * Automated Analysis * Build System[ ] Toggle navigation of Build System + Pigweed's GN Python Build * SEEDs[ ] Toggle navigation of SEEDs + 0001: The SEED Process + 0002: SEED Template + 0101: pigweed.json + 0102: Consistent Module Documentation + 0103: pw_protobuf: Past, present, and future + 0104: Display Support + 0105: Nested Tokens and Tokenized Log Arguments + 0106: Project Template + 0107: Pigweed Communications + 0108: Emulators Frontend + 0109: Communication Buffers + 0110: Memory Allocation Interfaces + 0111: Make Bazel Pigweed's Primary Build System + 0112: Async Poll Model + 0113: Add modular Bazel C/C++ toolchain API + 0114: Channels + 0115: pw_sensor: Sensors + 0116: pw_net Sockets + 0117: I3C + 0118: Sensor Read API + 0119: Sensors + 0120: Sensor Configuration + 0121: Pigweed Project Server + 0122: Organize Pigweed code samples + 0123: Sprout Bazel C++ toolchain API into a separate repository + 0124: Interfaces for Retrieving Size Information from Multisink + 0125: Statistics Database + 0126: Key-Value Store Interfaces + 0127: pw_sensor Reading + 0128: Abstracting Thread Creation + 0129: pw_assert: Support PW_ASSERT with non-argument message * Kudzu * Eng Blog[*] Toggle navigation of Eng Blog + #1: Kudzu + #2: Feature flags in Bazel builds Back to top Toggle Light / Dark / Auto color theme Toggle table of contents sidebar * Home / * Pigweed Eng Blog / * Pigweed Eng Blog #2: Feature flags in Bazel builds Pigweed Eng Blog #2: Feature flags in Bazel builds# By Ted Pudlik Published 2024-05-31 Let's say you're migrating your build system to Bazel. Your project heavily relies on preprocessor defines to configure its code. -DBUILD_FEATURE_CPU_PROFILE -DBUILD_FEATURE_HEAP_PROFILE -DBUILD_FEATURE_HW_SHA256 In your source files, you use these preprocessor variables to conditionally compile some sections, via #ifdef. When building the same code for different final product configurations, you want to set different defines. How do you model this in Bazel? This post discusses three possible approaches: 1. Easy but limited: bazelrc config with copts 2. More power with no code changes: Platform-based Skylib flags 3. Error-preventing approach: Chromium-style build flag pattern Which one to choose? If you have the freedom to refactor your code to use the Chromium pattern, give it a try! It is not difficult to maintain once implemented, and can prevent real production issues by detecting typos. Easy but limited: bazelrc config with copts# Let's start with the simplest approach: you can put the compiler options into your bazelrc configuration file. # .bazelrc common:mydevice_evt1 --copts=-DBUILD_FEATURE_CPU_PROFILE common:mydevice_evt1 --copts=-DBUILD_FEATURE_HEAP_PROFILE common:mydevice_evt1 --copts=-DBUILD_FEATURE_HW_SHA256 # and so on Then, when you build your application, the defines will all be applied: bazel build --config=mydevice_evt1 //src:application Configs are expanded recursively, allowing you to group options together and reuse them: # .bazelrc common:full_profile --copts=-DBUILD_FEATURE_CPU_PROFILE common:full_profile --copts=-DBUILD_FEATURE_HEAP_PROFILE # When building for mydevice_evt1, use full_profile. common:mydevice_evt1 --config=full_profile # When building for mydevice_evt2, additionally enable HW_SHA256. common:mydevice_evt2 --config=full_profile common:mydevice_evt1 --copts=-DBUILD_FEATURE_HW_SHA256 Downsides# While it is simple, the config-with-copts approach has a few downsides. Dangerous typos# If you misspell BUILDDD_FEATURE_CPU_PROFILE [sic!] in your .bazelrc, the actual BUILD_FEATURE_CPU_PROFILE variable will take the default value of 0. So, although you intended to enable this feature, it will just remain disabled! This isn't just a Bazel problem, but a general issue with the simple BUILD_FEATURE macro pattern. If you misspell BUILD_FEATUER_CPU_PROFILE [sic!] in your C++ file, you'll get it to evaluate to 0 in any build system! One way to avoid this issue is to use the "Chromium-style" build flag pattern, BUILDFLAG(CPU_PROFILE). If you do, a misspelled or missing define becomes a compiler error. However, the config-with-copts approach is a little too simple to express this pattern, which requires code generation of the build flag headers. No multi-platform build support# Bazel allows you to perform multi-platform builds. For example, in a single Bazel invocation you can build a Python flasher program (that will run on your laptop) which embeds as a data dependency the microcontroller firmware to flash (that will run on the microcontroller). We do this in Pigweed's own examples. Unfortunately, the config-with-copts pattern doesn't work nicely with multi-platform build primitives. (Technically, the problem is that Bazel doesn't support transitioning on -config.) If you want a multi-platform build, you need some more sophisticated idiom. No build system variables# This approach doesn't introduce any variable that can be used within the build system to e.g. conditionally select different source files for a library, choose a different library as a dependency, or remove some targets from the build altogether. We're really just setting preprocessor defines here. Limited multirepo support# The .bazelrc files are not automatically inherited when another repo depends on yours. They can be imported, but it's an all-or-nothing affair. More power with no code changes: Platform-based Skylib flags# Let's address some shortcomings of the approach above by representing the build features as Skylib flags and grouping them through platform-based flags. (Important note: this feature is still under development! See the Appendix for workarounds for older Bazel versions.) The platform sets a bunch of flags: # //platform/BUILD.bazel # The platform definition platform( name = "mydevice_evt1", flags = [ "--//build/feature:cpu_profile=true", "--//build/feature:heap_profile=true", "--//build/feature:hw_sha256=true", ], ) The flags have corresponding code-generated C++ libraries: # //build/feature/BUILD.bazel load("@bazel_skylib//rules:common_settings.bzl", "bool_flag") # I'll show one possible implementation of feature_cc_library later. load("//:feature_cc_library.bzl", "feature_cc_library") # This is a boolean flag, but there's support for int- and string-valued # flags, too. bool_flag( name = "cpu_profile", build_setting_default = False, ) # This is a custom rule that generates a cc_library target that exposes # a header "cpu_profile.h", the contents of which are either, # # BUILD_FEATURE_CPU_PROFILE=1 # # or, # # BUILD_FEATURE_CPU_PROFILE=0 # # depending on the value of the cpu_profile bool_flag. This "code # generation" is so simple that it can actually be done in pure Starlark; # see below. feature_cc_library( name = "cpu_profile_cc", flag = ":cpu_profile", ) # Analogous library that exposes the constant in Python. feature_py_library( name = "cpu_profile_py", flag = ":cpu_profile", ) # And in Rust, why not? feature_rs_library( name = "cpu_profile_rs", flag = ":cpu_profile", ) bool_flag( name = "heap_profile", build_setting_default = False, ) feature_cc_library( name = "heap_profile_cc", flag = ":heap_profile", ) bool_flag( name = "hw_sha256", build_setting_default = False, ) feature_cc_library( name = "hw_sha256_cc", flag = ":hw_sha256", ) C++ libraries that want to access the variable needs to depend on the cpu_profile_cc (or heap_profile_cc, hw_sha256_cc) library. Here's one possible implementation of feature_cc_library: # feature_cc_library.bzl load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") def feature_cc_library(name, build_setting): hdrs_name = name + ".hdr" flag_header_file( name = hdrs_name, build_setting = build_setting, ) native.cc_library( name = name, hdrs = [":" + hdrs_name], ) def _impl(ctx): out = ctx.actions.declare_file(ctx.attr.build_setting.label.name + ".h") # Convert boolean flags to canonical integer values. value = ctx.attr.build_setting[BuildSettingInfo].value if type(value) == type(True): if value: value = 1 else: value = 0 ctx.actions.write( output = out, content = r""" #pragma once #define {}={} """.format(ctx.attr.build_setting.label.name.upper(), value), ) return [DefaultInfo(files = depset([out]))] flag_header_file = rule( implementation = _impl, attrs = { "build_setting": attr.label( doc = "Build setting (flag) to construct the header from.", mandatory = True, ), }, ) Advantages# Composability of platforms# A neat feature of the simple config-based approach was that configs could be composed through recursive expansion. Fortunately, platforms can be composed, too! There are two mechanisms for doing so: 1. Use platforms' support for inheritance. This allows "subplatforms" to override entries from "superplatforms". But, only single inheritance is supported (each platform has at most one parent). 2. The other approach is to compose lists of flags directly, through concatenation: FEATURES_CORTEX_M7 = [ "--//build/feature:some_feature", ] FEATURES_MYDEVICE_EVT1 = FEATURES_CORTEX_M7 + [ "--//build/feature:some_other_feature", ] platform( name = "mydevice_evt1", flags = FEATURES_MYDEVICE_EVT1, ) Concatenation doesn't allow overriding entries, but frees you from the single-parent limitation of inheritance. Tip This approach can also be used to define custom host platforms: HOST_CONSTRAINTS in @local_config_platform//:constraints.bzl contains the autodetected @platform//os and @platforms//cpu constraints set by Bazel's default host platform. Multi-platform build support# How do you actually associate the platform with a binary you want to build? One approach is to just specify the platform on the command-line when building a cc_binary: bazel build --platforms=//platform:mydevice_evt1 //src:main But another approach is to leverage multi-platform build, through platform_data: # //src/BUILD.bazel load("@rules_platform//platform_data:defs.bzl", "platform_data") cc_binary(name = "main") platform_data( name = "main_mydevice_evt1", target = ":main", platform = "//platform:mydevice_evt1", ) Then you can keep your command-line simple: bazel build //src:main_mydevice_evt1 Flags correspond to build variables# You can make various features of the build conditional on the value of the flag. For example, you can select different dependencies: # //build/feature/BUILD.bazel config_setting( name = "hw_sha256=true", flag_values = { ":hw_sha256": "true", }, ) # //src/BUILD.bazel cc_library( name = "my_library", deps = [ "//some/unconditional:dep", ] + select({ "//build/feature:hw_sha256=true": ["//extra/dep/for/hw_sha256:only"], "//conditions:default": [], }) Any Bazel rule attribute described as "configurable" can take a value that depends on the flag in this way. Library header lists and source lists are common examples, but the vast majority of attributes in Bazel are configurable. Downsides# Typos remain dangerous# If you used "Chromium-style" build flags you would be immune to dangerous typos when using this Bazel pattern. But until then, you still have this problem, and actually it got worse! If you forget to #include "build/features/hw_sha256.h" in the C++ file that references the preprocessor variable, the build system or compiler will still not yell at you. Instead, the BUILD_FEATURE_HA_SHA256 variable will take the default value of 0. This is similar to the typo problem with the config approach, but worse, because it's easier to miss an #include than to misspell a name, and you'll need to add these #include statements in many places. One way to mitigate this problem is to make the individual feature_cc_library targets private, and gather them into one big library that all targets will depend on: feature_cc_library( name = "cpu_profile_cc", flag = ":cpu_profile", visibility = ["//visibility:private"], ) feature_cc_library( name = "heap_profile_cc", flag = ":heap_profile", visibility = ["//visibility:private"], ) feature_cc_library( name = "hw_sha256_cc", flag = ":hw_sha256", visibility = ["//visibility:private"], ) # Code-generated cc_library that #includes all the individual # feature_cc_library headers. all_features_cc_library( name = "all_features", deps = [ ":cpu_profile_cc", ":heap_profile_cc", ":hw_sha256_cc", # ... and many more. ], visibility = ["//visibility:public"], ) However, a more satisfactory solution is to adopt Chromium-style build flags, which we discuss next. Build settings have mandatory default values# The Skylib bool_flag that represents the build flag within Bazel has a build_setting_default attribute. This attribute is mandatory. This may be a disappointment if you were hoping to provide no default, and have Bazel return errors if no value is explicitly set for a flag (either via a platform, through .bazelrc, or on the command line). The Skylib build flags don't support this. The danger here is that the default value may be unsafe, and you forget to override it when adding a new platform (or for some existing platform, when adding a new flag). There is an alternative pattern that allows you to define default-less build flags: instead of representing build flags as Skylib flags, you can represent them as constraint_setting objects. I won't spell this pattern out in this blog post, but it comes with its own drawbacks: * The custom code-generation rules are more complex, and need to parse the constraint_value names to infer the build flag values. * All supported flag values must be explicitly enumerated in the BUILD files, and the code-generation rules need explicit dependencies on them. This leads to substantially more verbose BUILD files. On the whole, I'd recommend sticking with the Skylib flags! Error-preventing approach: Chromium-style build flag pattern# This pattern builds on More power with no code changes: Platform-based Skylib flags by adding a macro helper for retrieving flag values that guards against typos. The BUILD.bazel files look exactly the same as in the previous section, but: 1. Users of flags access them in C++ files via BUILDFLAG(SOME_NAME). 2. The code generated by feature_cc_library is a little more elaborate than a plain SOME_NAME=1 or SOME_NAME=0, and it includes a dependency on the Chromium build flag header. Here's the feature_cc_library implementation: load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") def feature_cc_library(name, build_setting): """Generates a cc_library from a common build setting. The generated cc_library exposes a header [build_setting.name].h that defines a corresponding build flag. Example: feature_cc_library( name = "evt1_cc", build_setting = ":evt1", ) * This target is a cc_library that exposes a header you can include via #include "build/flags/evt1.h". * That header defines a build flag you can access in your code through BUILDFLAGS(EVT1). * If you wish to use the build flag from a cc_library, add the target evt1_cc to your cc_library's deps. Args: name: Name for the generated cc_library. build_setting: One of the Skylib "common settings": bool_flag, int_flag, string_flag, etc. See https://github.com/bazelbuild/bazel-skylib/blob/main/docs/common_settings_doc.md """ hdrs_name = name + ".hdr" flag_header_file( name = hdrs_name, build_setting = build_setting, ) native.cc_library( name = name, hdrs = [":" + hdrs_name], # //:buildflag is a cc_library containing the # Chromium build flag header. deps = ["//:buildflag"], ) def _impl(ctx): out = ctx.actions.declare_file(ctx.attr.build_setting.label.name + ".h") # Convert boolean flags to canonical integer values. value = ctx.attr.build_setting[BuildSettingInfo].value if type(value) == type(True): if value: value = 1 else: value = 0 ctx.actions.write( output = out, content = r""" #pragma once #include "buildflag.h" #define BUILDFLAG_INTERNAL_{}() ({}) """.format(ctx.attr.build_setting.label.name.upper(), value), ) return [DefaultInfo(files = depset([out]))] flag_header_file = rule( implementation = _impl, attrs = { "build_setting": attr.label( doc = "Build setting (flag) to construct the header from.", mandatory = True, ), }, ) Bottom line# If you have the freedom to refactor your code to use the Chromium pattern, Bazel provides safe and convenient idioms for expressing configuration through build flags. Give it a try! Otherwise, you can still use platform-based Skylib flags, but beware typos and missing #include statements! Appendix# A couple "deep in the weeds" questions came up while this blog post was being reviewed. I thought they were interesting enough to discuss here, for the interested reader! Why isn't the reference code a library?# If you made it this far you might be wondering, why is the code listing for feature_cc_library even here? Why isn't it just part of Pigweed, and used in our own codebase? The short answer is that Pigweed is middleware supporting multiple build systems, so we don't want to rely on the build system to generate configuration headers. But the longer answer has to do with how this blog post came about. Some time ago, I was migrating team A's build from CMake to Bazel. They used Chromium build flags, but in CMake, so to do a build migration they needed Bazel support for this pattern. So I put an implementation together. I wrote a design document, but it had confidential details and was not widely shared. Then team B comes along and says, "we tried migrating to Bazel but couldn't figure out how to support build flags" (not the Chromium flags, but the "naive" kind; i.e. their problem statement was exactly the one the blog opens with). So I wrote a less-confidential but still internal doc for them saying "here's how you could do it"; basically, More power with no code changes: Platform-based Skylib flags. Then Pigweed's TL comes along and says "Ted, don't you feel like spending a day fighting with RST [the markup we use for pigweed.dev]? " Sorry, actually they said something more like, "Why is this doc internal, can't we share this more widely"? Well we can. So that's the doc you're reading now! But arguably the story shouldn't end here: Pigweed should probably provide a ready-made implementation of Chromium build flags for downstream projects. See issue #342454993 to check out how that's going! Do you need to generate actual files?# If you are a Bazel expert, you may ask: do we need to have Bazel write out the actual header files, and wrap those in a cc_library? If we're already writing a custom rule for feature_cc_library, can we just set -D defines by providing CcInfo? That is, do something like this: define = "{}={}"..format( ctx.attr.build_setting.label.name.upper(), value) return [CcInfo( compilation_context=cc_common.create_compilation_context( defines=depset([define])))] The honest answer is that this didn't occur to me! But one reason to prefer writing out the header files is that this approach generalizes in an obvious way to other programming languages: if you want to generate Python or Golang constants, you can use the same pattern, just change the contents of the file. Generalizing the CcInfo approach is trickier! What can I do on older Bazel versions?# This blog focused on describing approaches that rely on platform-based flags. But this feature is very new: in fact, as of this writing, it is still under development, so it's not available in any Bazel version! So what can you do? One approach is to define custom wrapper rules for your cc_binary targets that use a transition to set the flags. You can see examples of such transitions in the Pigweed examples project. Previous Pigweed Eng Blog #1: Kudzu Copyright (c) 2024 The Pigweed Authors Made with Furo On this page * Pigweed Eng Blog #2: Feature flags in Bazel builds + Easy but limited: bazelrc config with copts o Downsides # Dangerous typos # No multi-platform build support # No build system variables # Limited multirepo support + More power with no code changes: Platform-based Skylib flags o Advantages # Composability of platforms # Multi-platform build support # Flags correspond to build variables o Downsides # Typos remain dangerous # Build settings have mandatory default values + Error-preventing approach: Chromium-style build flag pattern + Bottom line + Appendix o Why isn't the reference code a library? o Do you need to generate actual files? o What can I do on older Bazel versions?