It’s fairly common to need to alter your builds based on a command-line flag. Maybe you want to change the distribution target for a mobile app or enable additional debug functionality. An easy way to do that is via a custom command-line flag.

This is a topic where Bazel newcomers often struggle, so let’s explore how to create custom flags in Bazel.

Build Settings

A build setting is a rule—just like any other rule—but with additional capabilities. Specifically, build settings allow us to define custom command-line flags that influence the build configuration.

Pre-defined Settings

Because build settings are rules, we can write our own. However, bazel-skylib provides several commonly used build settings out of the box.

You can see the full list in the skylib repository.

For simplicity, we’ll use a pre-defined setting in this article. Keep in mind, though, that you’re not limited to what skylib offers—you can implement your own build setting if needed.

Creating a Flag

Suppose we want to build differently for our local development environment versus what we release to the App Store. We’ll call this a flavor, with two variants:

  • dev
  • store

To create a flag, we need to instantiate a build setting in BUILD.bazel:

load("@bazel_skylib//rules:common_settings.bzl", "string_flag")

string_flag(
    name = "flavor",
    values = ["dev", "store"],
    build_setting_default = "dev",
)

Because this is a target, it has a label. That’s why we pass it on the command line using label syntax:

bazel build //:my_target --//:flavor=store

However, defining the flag alone is not enough.

Reacting to Build Settings

Bazel does not branch directly on build settings. Instead, it evaluates configuration conditions, represented by config_setting targets.

So we need to associate our flag with configuration conditions:

config_setting(
    name = "dev",
    flag_values = {":flavor": "dev"},
    visibility = ["//visibility:public"],
)

config_setting(
    name = "store",
    flag_values = {":flavor": "store"},
    visibility = ["//visibility:public"],
)

Here’s the mental model:

  • string_flag defines a configurable value.
  • config_setting defines a configuration condition.
  • select() switches on config_setting.

This separation is important: select() operates on config_setting targets, not directly on flags.

Using select()

Now that we have configuration conditions, we can branch using select().

To demonstrate that our flag works, we’ll create a simple genrule that writes which flavor was selected:

genrule(
    name = "which_flavor",
    outs = ["output.txt"],
    cmd = select({
        ":dev": "echo dev > $@",
        ":store": "echo store > $@",
    }),
)

Now build the target:

bazel build :which_flavor --//:flavor=store

You should see something like this:

INFO: Analyzed target //:which_flavor (6 packages loaded, 10 targets configured).
INFO: Found 1 target...
Target //:which_flavor up-to-date:
  bazel-bin/output.txt
INFO: Elapsed time: 0.297s, Critical Path: 0.02s
INFO: 2 processes: 1 internal, 1 darwin-sandbox.
INFO: Build completed successfully, 2 total actions

Opening bazel-bin/output.txt will reveal:

store

Closing Thoughts

To create a custom command-line flag in Bazel, remember:

  1. You need a build setting (string_flag, bool_flag, etc.).
  2. You need one or more config_setting targets that describe configuration conditions.
  3. You use select() to branch on those configuration conditions.

If you’re not writing custom rules, using pre-defined settings from skylib is probably the right approach in most cases. If you need more flexibility, you can write your own build setting rule—but that’s a topic for another day.