Configurable Build Attributes
Configurable attributes, commonly known as select()
, is a Bazel feature that lets users toggle the values
of build rule attributes at the command line.
This can be used, for example, for a multiplatform library that automatically chooses the appropriate implementation for the architecture, or for a feature-configurable binary that can be customized at build time.
Example
# myapp/BUILD
cc_binary(
name = "mybinary",
srcs = ["main.cc"],
deps = select({
":arm_build": [":arm_lib"],
":x86_debug_build": [":x86_dev_lib"],
"//conditions:default": [":generic_lib"],
}),
)
config_setting(
name = "arm_build",
values = {"cpu": "arm"},
)
config_setting(
name = "x86_debug_build",
values = {
"cpu": "x86",
"compilation_mode": "dbg",
},
)
This declares a cc_binary
that “chooses” its deps based on the flags at the
command line. Specficially, deps
becomes:
Command | deps = |
bazel build //myapp:mybinary --cpu=arm |
[":arm_lib"] |
bazel build //myapp:mybinary -c dbg --cpu=x86 |
[":x86_dev_lib"] |
bazel build //myapp:mybinary --cpu=ppc |
[":generic_lib"] |
bazel build //myapp:mybinary -c dbg --cpu=ppc |
[":generic_lib"] |
select()
serves as a placeholder for a value that will be chosen based on
configuration conditions, which are labels referencing config_setting
targets. By using select()
in a configurable attribute, the attribute
effectively adopts different values when different conditions hold.
Matches must be unambiguous: either exactly one condition must match or, if
multiple conditions match, one’s values
must be a strict superset of all
others’. For example, values = {"cpu": "x86", "compilation_mode": "dbg"}
is an
unambiguous specialization of values = {"cpu": "x86"}
. The built-in condition
//conditions:default
automatically matches when
nothing else does.
While this example uses deps
, select()
works just as well on srcs
,
resources
, cmd
, and most other attributes. Only a small number of attributes
are non-configurable, and these are clearly annotated. For example,
config_setting
’s own
values
attribute is non-configurable.
select()
and dependencies
Certain attributes change the build parameters for all transitive dependencies
under a target. For example, genrule
’s tools
changes --cpu
to the CPU of
the machine running Bazel (which, thanks to cross-compilation, may be different
than the CPU the target is built for). This is known as a
configuration transition.
Given
#myapp/BUILD
config_setting(
name = "arm_cpu",
values = {"cpu": "arm"},
)
config_setting(
name = "x86_cpu",
values = {"cpu": "x86"},
)
genrule(
name = "my_genrule",
srcs = select({
":arm_cpu": ["g_arm.src"],
":x86_cpu": ["g_x86.src"],
}),
tools = select({
":arm_cpu": [":tool1"],
":x86_cpu": [":tool2"],
}),
)
cc_binary(
name = "tool1",
srcs = select({
":arm_cpu": ["armtool.cc"],
":x86_cpu": ["x86tool.cc"],
}),
)
running
$ bazel build //myapp:my_genrule --cpu=arm
on an x86
developer machine binds the build to g_arm.src
, tool1
, and
x86tool.cc
. Both of the select
s attached to my_genrule
use my_genrule
’s
build parameters, which include --cpu=arm
. The tools
attribute changes
--cpu
to x86
for tool1
and its transitive dependencies. The select
on
tool1
uses tool1
’s build parameters, which include --cpu=x86
.
Configuration conditions
Each key in a configurable attribute is a label reference to a
config_setting
or
constraint_value
.
config_setting
is just a collection of
expected command line flag settings. By encapsulating these in a target, it’s
easy to maintain “standard” conditions users can reference from multiple places.
constraint_value
provides support for multi-platform behavior.
Built-in flags
Flags like --cpu
are built into Bazel: the build tool natively understands
them for all builds in all projects. These are specified with
config_setting
’s
values
attribute:
config_setting(
name = "meaningful_condition_name",
values = {
"flag1": "value1",
"flag2": "value2",
...
},
)
flagN
is a flag name (without --
, so "cpu"
instead of "--cpu"
). valueN
is the expected value for that flag. :meaningful_condition_name
matches if
every entry in values
matches. Order is irrelevant.
valueN
is parsed as if it was set on the command line. This means:
values = { "compilation_mode": "opt" }
matchesbazel build -c opt
values = { "force_pic": "true" }
matchesbazel build --force_pic=1
values = { "force_pic": "0" }
matchesbazel build --noforce_pic
config_setting
only supports flags that affect target behavior. For example,
--show_progress
isn’t allowed because
it only affects how Bazel reports progress to the user. Targets can’t use that
flag to construct their results. The exact set of supported flags isn’t
documented. In practice, most flags that “make sense” work.
Custom flags
You can model your own project-specific flags with Starlark build settings. Unlike built-in flags, these are defined as build targets, so Bazel references them with target labels.
These are triggered with config_setting
’s
flag_values
attribute:
config_setting(
name = "meaningful_condition_name",
flag_values = {
"//myflags:flag1": "value1",
"//myflags:flag2": "value2",
...
},
)
Behavior is the same as for built-in flags. See here for a working example.
--define
is an alternative legacy syntax for custom flags (for example
--define foo=bar
). This can be expressed either in the
values attribute
(values = {"define": "foo=bar"}
) or the
define_values attribute
(define_values = {"foo": "bar"}
). --define
is only supported for backwards
compatibility. Prefer Starlark build settings whenever possible.
values
, flag_values
, and define_values
evaluate independently. The
config_setting
matches if all values across all of them match.
The default condition
The built-in condition //conditions:default
matches when no other condition
matches.
Because of the “exactly one match” rule, a configurable attribute with no match
and no default condition emits a "no matching conditions"
error. This can
protect against silent failures from unexpected settings:
# myapp/BUILD
config_setting(
name = "x86_cpu",
values = {"cpu": "x86"},
)
cc_library(
name = "x86_only_lib",
srcs = select({
":x86_cpu": ["lib.cc"],
}),
)
$ bazel build //myapp:x86_only_lib --cpu=arm
ERROR: Configurable attribute "srcs" doesn't match this configuration (would
a default condition help?).
Conditions checked:
//myapp:x86_cpu
For even clearer errors, you can set custom messages with select()
’s
no_match_error
attribute.
Platforms
While the ability to specify multiple flags on the command line provides flexibility, it can also be burdensome to individually set each one every time you want to build a target. Platforms let you consolidate these into simple bundles.
# myapp/BUILD
sh_binary(
name = "my_rocks",
srcs = select({
":basalt": ["pyroxene.sh"],
":marble": ["calcite.sh"],
"//conditions:default": ["feldspar.sh"],
}),
)
config_setting(
name = "basalt",
constraint_values = [
":black",
":igneous",
],
)
config_setting(
name = "marble",
constraint_values = [
":white",
":metamorphic",
],
)
# constraint_setting acts as an enum type, and constraint_value as an enum value.
constraint_setting(name = "color")
constraint_value(name = "black", constraint_setting = "color")
constraint_value(name = "white", constraint_setting = "color")
constraint_setting(name = "texture")
constraint_value(name = "smooth", constraint_setting = "texture")
constraint_setting(name = "type")
constraint_value(name = "igneous", constraint_setting = "type")
constraint_value(name = "metamorphic", constraint_setting = "type")
platform(
name = "basalt_platform",
constraint_values = [
":black",
":igneous",
],
)
platform(
name = "marble_platform",
constraint_values = [
":white",
":smooth",
":metamorphic",
],
)
The platform can be specified on the command line. It activates the
config_setting
s that contain a subset of the platform’s constraint_values
,
allowing those config_setting
s to match in select()
expressions.
For example, in order to set the srcs
attribute of my_rocks
to calcite.sh
,
you can simply run
bazel build //my_app:my_rocks --platforms=//myapp:marble_platform
Without platforms, this might look something like
bazel build //my_app:my_rocks --define color=white --define texture=smooth --define type=metamorphic
select()
can also directly read constraint_value
s:
constraint_setting(name = "type")
constraint_value(name = "igneous", constraint_setting = "type")
constraint_value(name = "metamorphic", constraint_setting = "type")
sh_binary(
name = "my_rocks",
srcs = select({
":igneous": ["igneous.sh"],
":metamorphic" ["metamorphic.sh"],
}),
)
This saves the need for boilerplate config_setting
s when you only need to
check against single values.
Platforms are still under development. See the documentation for details.
Combining select()
s
select
can appear multiple times in the same attribute:
sh_binary(
name = "my_target",
srcs = ["always_include.sh"]
select({
":armeabi_mode": ["armeabi_src.sh"],
":x86_mode": ["x86_src.sh"],
})
select({
":opt_mode": ["opt_extras.sh"],
":dbg_mode": ["dbg_extras.sh"],
}),
)
select
cannot appear inside another select
. If you need to nest selects
and your attribute takes other targets as values, use an intermediate target:
sh_binary(
name = "my_target",
srcs = ["always_include.sh"],
deps = select({
":armeabi_mode": [":armeabi_lib"],
...
}),
)
sh_library(
name = "armeabi_lib",
srcs = select({
":opt_mode": ["armeabi_with_opt.sh"],
...
}),
)
If you need a select
to match when multiple conditions match, consider AND
chaining.
OR chaining
Consider the following:
sh_binary(
name = "my_target",
srcs = ["always_include.sh"],
deps = select({
":config1": [":standard_lib"],
":config2": [":standard_lib"],
":config3": [":standard_lib"],
":config4": [":special_lib"],
}),
)
Most conditions evaluate to the same dep. But this syntax is hard to read and
maintain. It would be nice to not have to repeat [":standard_lib"]
multiple
times.
One option is to predefine the value as a BUILD variable:
STANDARD_DEP = [":standard_lib"]
sh_binary(
name = "my_target",
srcs = ["always_include.sh"],
deps = select({
":config1": STANDARD_DEP,
":config2": STANDARD_DEP,
":config3": STANDARD_DEP,
":config4": [":special_lib"],
}),
)
This makes it easier to manage the dependency. But it still causes unnecessary duplication.
For more direct support, use one of the following:
selects.with_or
The
with_or
macro in Skylib’s
selects
module supports OR
ing conditions directly inside a select
:
load("@bazel_skylib//lib:selects.bzl", "selects")
sh_binary(
name = "my_target",
srcs = ["always_include.sh"],
deps = selects.with_or({
(":config1", ":config2", ":config3"): [":standard_lib"],
":config4": [":special_lib"],
}),
)
selects.config_setting_group
The
config_setting_group
macro in Skylib’s
selects
module supports OR
ing multiple config_setting
s:
load("@bazel_skylib//lib:selects.bzl", "selects")
config_setting(
name = "config1",
values = {"cpu": "arm"},
)
config_setting(
name = "config2",
values = {"compilation_mode": "dbg"},
)
selects.config_setting_group(
name = "config1_or_2",
match_any = [":config1", ":config2"],
)
sh_binary(
name = "my_target",
srcs = ["always_include.sh"],
deps = select({
":config1_or_2": [":standard_lib"],
"//conditions:default": [":other_lib"],
}),
)
Unlike selects.with_or
, different targets can share :config1_or_2
across
different attributes.
It’s an error for multiple conditions to match unless one is an unambiguous “specialization” of the others. See here for details.
AND chaining
If you need a select
branch to match when multiple conditions match, use the
Skylib macro
config_setting_group:
config_setting(
name = "config1",
values = {"cpu": "arm"},
)
config_setting(
name = "config2",
values = {"compilation_mode": "dbg"},
)
selects.config_setting_group(
name = "config1_and_2",
match_all = [":config1", ":config2"],
)
sh_binary(
name = "my_target",
srcs = ["always_include.sh"],
deps = select({
":config1_and_2": [":standard_lib"],
"//conditions:default": [":other_lib"],
}),
)
Unlike OR chaining, existing config_setting
s can’t be directly AND
ed
inside a select
. You have to explicitly wrap them in a config_setting_group
.
Custom error messages
By default, when no condition matches, the target the select()
is attached to
fails with the error:
ERROR: Configurable attribute "deps" doesn't match this configuration (would
a default condition help?).
Conditions checked:
//tools/cc_target_os:darwin
//tools/cc_target_os:android
This can be customized with the no_match_error
attribute:
cc_library(
name = "my_lib",
deps = select(
{
"//tools/cc_target_os:android": [":android_deps"],
"//tools/cc_target_os:windows": [":windows_deps"],
},
no_match_error = "Please build with an Android or Windows toolchain",
),
)
$ bazel build //myapp:my_lib
ERROR: Configurable attribute "deps" doesn't match this configuration: Please
build with an Android or Windows toolchain
Rules compatibility
Rule implementations receive the resolved values of configurable attributes. For example, given:
# myapp/BUILD
some_rule(
name = "my_target",
some_attr = select({
":foo_mode": [":foo"],
":bar_mode": [":bar"],
}),
)
$ bazel build //myapp/my_target --define mode=foo
Rule implementation code sees ctx.attr.some_attr
as [":foo"]
.
Macros can accept select()
clauses and pass them through to native
rules. But they cannot directly manipulate them. For example, there’s no way
for a macro to convert
select({"foo": "val"}, ...)
to
select({"foo": "val_with_suffix"}, ...)
This is for two reasons.
First, macros that need to know which path a select
will choose cannot work
because macros are evaluated in Bazel’s loading phase,
which occurs before flag values are known.
This is a core Bazel design restriction that’s unlikely to change any time soon.
Second, macros that just need to iterate over all select
paths, while
technically feasible, lack a coherent UI. Further design is necessary to change
this.
Bazel query and cquery
Bazel query
operates over Bazel’s
loading phase.
This means it doesn’t know what command line flags a target uses since those
flags aren’t evaluated until later in the build (in the
analysis phase).
So it can’t determine which select()
branches are chosen.
Bazel cquery
operates after Bazel’s analysis phase, so it has
all this information and can accurately resolve select()
s.
Consider:
load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
# myapp/BUILD
string_flag(
name = "dog_type",
build_setting_default = "cat"
)
cc_library(
name = "my_lib",
deps = select({
":long": [":foo_dep"],
":short": [":bar_dep"],
}),
)
config_setting(
name = "long",
flag_values = {":dog_type": "dachshund"},
)
config_setting(
name = "short",
flag_values = {":dog_type": "pug"},
)
query
overapproximates :my_lib
’s dependencies:
$ bazel query 'deps(//myapp:my_lib)'
//myapp:my_lib
//myapp:foo_dep
//myapp:bar_dep
while cquery
shows its exact dependencies:
$ bazel cquery 'deps(//myapp:my_lib)' --//myapp:dog_type=pug
//myapp:my_lib
//myapp:bar_dep
FAQ
Why doesn’t select() work in macros?
select() does work in rules! See Rules compatibility for details.
The key issue this question usually means is that select() doesn’t work in macros. These are different than rules. See the documentation on rules and macros to understand the difference. Here’s an end-to-end example:
Define a rule and macro:
# myapp/defs.bzl
# Rule implementation: when an attribute is read, all select()s have already
# been resolved. So it looks like a plain old attribute just like any other.
def _impl(ctx):
name = ctx.attr.name
allcaps = ctx.attr.my_config_string.upper() # This works fine on all values.
print("My name is " name " with custom message: " allcaps)
# Rule declaration:
my_custom_bazel_rule = rule(
implementation = _impl,
attrs = {"my_config_string": attr.string()},
)
# Macro declaration:
def my_custom_bazel_macro(name, my_config_string):
allcaps = my_config_string.upper() # This line won't work with select(s).
print("My name is " name " with custom message: " allcaps)
Instantiate the rule and macro:
# myapp/BUILD
load("//myapp:defs.bzl", "my_custom_bazel_rule")
load("//myapp:defs.bzl", "my_custom_bazel_macro")
my_custom_bazel_rule(
name = "happy_rule",
my_config_string = select({
"//tools/target_cpu:x86": "first string",
"//tools/target_cpu:ppc": "second string",
}),
)
my_custom_bazel_macro(
name = "happy_macro",
my_config_string = "fixed string",
)
my_custom_bazel_macro(
name = "sad_macro",
my_config_string = select({
"//tools/target_cpu:x86": "first string",
"//tools/target_cpu:ppc": "other string",
}),
)
Building fails because sad_macro
can’t process the select()
:
$ bazel build //myapp:all
ERROR: /myworkspace/myapp/BUILD:17:1: Traceback
(most recent call last):
File "/myworkspace/myapp/BUILD", line 17
my_custom_bazel_macro(name = "sad_macro", my_config_stri..."}))
File "/myworkspace/myapp/defs.bzl", line 4, in
my_custom_bazel_macro
my_config_string.upper()
type 'select' has no method upper().
ERROR: error loading package 'myapp': Package 'myapp' contains errors.
Building succeeds when you comment out sad_macro
:
# Comment out sad_macro so it doesn't mess up the build.
$ bazel build //myapp:all
DEBUG: /myworkspace/myapp/defs.bzl:5:3: My name is happy_macro with custom message: FIXED STRING.
DEBUG: /myworkspace/myapp/hi.bzl:15:3: My name is happy_rule with custom message: FIRST STRING.
This is impossible to change because by definition macros are evaluated before Bazel reads the build’s command line flags. That means there isn’t enough information to evaluate select()s.
Macros can, however, pass select()
s as opaque blobs to rules:
# myapp/defs.bzl
def my_custom_bazel_macro(name, my_config_string):
print("Invoking macro " name)
my_custom_bazel_rule(
name = name "_as_target",
my_config_string = my_config_string,
)
$ bazel build //myapp:sad_macro_less_sad
DEBUG: /myworkspace/myapp/defs.bzl:23:3: Invoking macro sad_macro_less_sad.
DEBUG: /myworkspace/myapp/defs.bzl:15:3: My name is sad_macro_less_sad with custom message: FIRST STRING.
Why does select() always return true?
Because macros (but not rules) by definition
can’t evaluate select()
s, any attempt to do so
usually produces an error:
ERROR: /myworkspace/myapp/BUILD:17:1: Traceback
(most recent call last):
File "/myworkspace/myapp/BUILD", line 17
my_custom_bazel_macro(name = "sad_macro", my_config_stri..."}))
File "/myworkspace/myapp/defs.bzl", line 4, in
my_custom_bazel_macro
my_config_string.upper()
type 'select' has no method upper().
Booleans are a special case that fail silently, so you should be particularly vigilant with them:
$ cat myapp/defs.bzl
def my_boolean_macro(boolval):
print("TRUE" if boolval else "FALSE")
$ cat myapp/BUILD
load("//myapp:defs.bzl", "my_boolean_macro")
my_boolean_macro(
boolval = select({
"//tools/target_cpu:x86": True,
"//tools/target_cpu:ppc": False,
}),
)
$ bazel build //myapp:all --cpu=x86
DEBUG: /myworkspace/myapp/defs.bzl:4:3: TRUE.
$ bazel build //mypro:all --cpu=ppc
DEBUG: /myworkspace/myapp/defs.bzl:4:3: TRUE.
This happens because macros don’t understand the contents of select()
.
So what they’re really evaluting is the select()
object itself. According to
Pythonic design
standards, all objects aside from a very small number of exceptions
automatically return true.
Can I read select() like a dict?
Macros can’t evaluate select(s) because macros evaluate before
Bazel knows what the build’s command line parameters are. Can they at least read
the select()
’s dictionary to, for example, add a suffix to each value?
Conceptually this is possible, but it isn’t yet a Bazel feature.
What you can do today is prepare a straight dictionary, then feed it into a
select()
:
$ cat myapp/defs.bzl
def selecty_genrule(name, select_cmd):
for key in select_cmd.keys():
select_cmd[key] = " WITH SUFFIX"
native.genrule(
name = name,
outs = [name ".out"],
srcs = [],
cmd = "echo " select(select_cmd {"//conditions:default": "default"})
" > $@"
)
$ cat myapp/BUILD
selecty_genrule(
name = "selecty",
select_cmd = {
"//tools/target_cpu:x86": "x86 mode",
},
)
$ bazel build //testapp:selecty --cpu=x86 && cat bazel-genfiles/testapp/selecty.out
x86 mode WITH SUFFIX
If you’d like to support both select()
and native types, you can do this:
$ cat myapp/defs.bzl
def selecty_genrule(name, select_cmd):
cmd_suffix = ""
if type(select_cmd) == "string":
cmd_suffix = select_cmd " WITH SUFFIX"
elif type(select_cmd) == "dict":
for key in select_cmd.keys():
select_cmd[key] = " WITH SUFFIX"
cmd_suffix = select(select_cmd {"//conditions:default": "default"})
native.genrule(
name = name,
outs = [name ".out"],
srcs = [],
cmd = "echo " cmd_suffix "> $@",
)
Why doesn’t select() work with bind()?
Because bind()
is a WORKSPACE rule, not a BUILD rule.
Workspace rules do not have a specific configuration, and aren’t evaluated in
the same way as BUILD rules. Therefore, a select()
in a bind()
can’t
actually evaluate to any specific branch.
Instead, you should use alias()
, with a select()
in
the actual
attribute, to perform this type of run-time determination. This
works correctly, since alias()
is a BUILD rule, and is evaluated with a
specific configuration.
You can even have a bind()
target point to an alias()
, if needed.
$ cat WORKSPACE
workspace(name = "myapp")
bind(name = "openssl", actual = "//:ssl")
http_archive(name = "alternative", ...)
http_archive(name = "boringssl", ...)
$ cat BUILD
config_setting(
name = "alt_ssl",
define_values = {
"ssl_library": "alternative",
},
)
alias(
name = "ssl",
actual = select({
"//:alt_ssl": "@alternative//:ssl",
"//conditions:default": "@boringssl//:ssl",
}),
)
With this setup, you can pass --define ssl_library=alternative
, and any target
that depends on either //:ssl
or //external:ssl
will see the alternative
located at @alternative//:ssl
.
Why doesn’t my select() choose what I expect?
If //myapp:foo
has a select()
that doesn’t choose the condition you expect,
use cquery and bazel config
to debug:
If //myapp:foo
is the top-level target you’re building, run:
$ bazel cquery //myapp:foo <desired build flags>
//myapp:foo (12e23b9a2b534a)
If you’re building some other target //bar
that depends on
//myapp:foo somewhere in its subgraph, run:
$ bazel cquery 'somepath(//bar, //myapp:foo)' <desired build flags>
//bar:bar (3ag3193fee94a2)
//bar:intermediate_dep (12e23b9a2b534a)
//myapp:foo (12e23b9a2b534a)
The (12e23b9a2b534a)
next to //myapp:foo
is a hash of the
configuration that resolves //myapp:foo
’s select()
. You can inspect its
values with bazel config
:
$ bazel config 12e23b9a2b534a
BuildConfiguration 12e23b9a2b534a
Fragment com.google.devtools.build.lib.analysis.config.CoreOptions {
cpu: darwin
compilation_mode: fastbuild
...
}
Fragment com.google.devtools.build.lib.rules.cpp.CppOptions {
linkopt: [-Dfoo=bar]
...
}
...
Then compare this output against the settings expected by each config_setting
.
//myapp:foo
may exist in different configurations in the same build. See the
cquery docs for guidance on using somepath
to get the right
one.
Caution: To prevent restarting the Bazel server, invoke bazel config
with the
same command line flags as the bazel cquery
. The config
command relies on
the configuration nodes from the still-running server of the previous command.