xref: /aosp_15_r20/external/pigweed/docs/blog/02-bazel-feature-flags.rst (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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