build_setting_default not configurable in user defined build setting

256 views
Skip to first unread message

Fredrik Frantzen

unread,
Sep 30, 2021, 8:46:54 AM9/30/21
to bazel-discuss

Hello,

At Ericsson, we had a build system before that allowed the user to have a configurable build_setting_default value that would enable / disable a feature depending on hardware, but also allow the user to override the default value on command line.

In Bazel, it is prohibited to have selects to configure the default value.

I found that it was a small change to enable it, just to try if it works in src/main/java/com/google/devtools/build/lib/packages/RuleClass.java, by just removing a call that makes the attribute non configurable.

if (buildSetting != null) {
Type<?> type = buildSetting.getType();
Attribute.Builder<?> defaultAttrBuilder =
attr(STARLARK_BUILD_SETTING_DEFAULT_ATTR_NAME, type)
//.nonconfigurable(BUILD_SETTING_DEFAULT_NONCONFIGURABLE)
.mandatory();


After this change it enables us to have select statements for the build_setting_default attribute.

bool_flag(
name = "my_user_defined_build_setting",
build_setting_default = select({":true_case": True, "//conditions:default": False})
)

Bazel seem to handle it flawlessly so my question is why it has been decided to not be configurable.

BR
Fredrik

Aleksei Gorbushin

unread,
Sep 30, 2021, 12:10:55 PM9/30/21
to bazel-discuss
Hello Frederik,

I might be wrong but might be "platform" concept can give you that flexibility. Each platform can be represented by the set of features. Check https://docs.bazel.build/versions/main/platforms.html

--
WBR,
Aleksei

Fredrik Frantzen

unread,
Oct 1, 2021, 3:10:29 AM10/1/21
to bazel-discuss
I dont think you are wrong, we are using platforms in our project as well, we've used it to describe our hardware paltform and its features. But we can probably extend it to enable/disable software features based on some arbitrary software platform.
I guess we are just trying to avoid the overhead of creating this structure, since the number of platforms we would need to describe is so large. We probably want to minimize the amount of variation to reduce the number of platforms we need to describe.

However, I am asking this just to understand if there is some technical or philosofical reasoning for why this configurable build setting feature is disabled.

Alex Humesky

unread,
Oct 4, 2021, 3:55:36 PM10/4/21
to Fredrik Frantzen, bazel-discuss
One thing to note is that the parameter to nonconfigurable() is a "reason string":
"Build setting defaults are referenced during analysis."

I'm not totally sure what exactly that means though, since configuration information (i.e. what's needed for select()'s) is available at analysis time. Perhaps there are other areas of the code that break when build_setting_default contains a select(), I'm not sure.

Either way though, you might be able to get pretty close to what you're looking for by adding an attribute to a build_setting rule:

defs.bzl:

FooProvider = provider(fields = ['foo'])

def _foo_impl(ctx):
  if ctx.build_setting_value == "":
    value = ctx.attr.default
  else:
    value = ctx.build_setting_value
  return FooProvider(foo = value)

foo_setting = rule(
  implementation = _foo_impl,
  build_setting = config.string(flag = True),
  attrs = {
    "default": attr.string(),
  },
)

def _bar_rule_impl(ctx):
  out_file = ctx.actions.declare_file(ctx.label.name + ".txt")
  ctx.actions.write(
    output = out_file,
    content = "foo setting is: " + ctx.attr._foo[FooProvider].foo + "\n",
  )
  return DefaultInfo(files = depset([out_file]))

bar_rule = rule(
  implementation = _bar_rule_impl,
  attrs = {
    "_foo": attr.label(default = "//:foo"),
  }
)


BUILD:

load("//:defs.bzl", "foo_setting", "bar_rule")

config_setting(
  name = "x86",
  values = {
    "cpu": "x86",
  }
)

config_setting(
  name = "arm",
  values = {
    "cpu": "arm",
  }
)

foo_setting(
  name = "foo",
  build_setting_default = "", # not the real default
  default = select({
    ":x86": "value for x86",
    ":arm": "value for arm",
  }),
)

bar_rule(
  name = "bar",
)


usage:

$ bazel build bar --cpu=x86 &>/dev/null && cat bazel-bin/bar.txt
foo setting is: value for x86

$ bazel build bar --cpu=arm &>/dev/null && cat bazel-bin/bar.txt
foo setting is: value for arm

$ bazel build bar --cpu=x86 --//:foo=abc &>/dev/null && cat bazel-bin/bar.txt
foo setting is: abc

$ bazel build bar --cpu=arm --//:foo=abc &>/dev/null && cat bazel-bin/bar.txt
foo setting is: abc


One cosmetic drawback is that there isn't a tri-state config type available, so for booleans you couldn't do --//:foo on the command line to set the flag to true, you'd have to use config.string and set it explicitly to --//:foo=true

(This example might raise a further question, "if it works for custom attributes, why is build_setting_default different, and I'm not sure about that either :) )

--
You received this message because you are subscribed to the Google Groups "bazel-discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to bazel-discus...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/bazel-discuss/8e62142d-23c3-4d52-a5b3-e5672a31a2e3n%40googlegroups.com.

Carlos Galvez

unread,
Jun 12, 2023, 10:51:25 AM6/12/23
to bazel-discuss
Hi!

This last solution works well, except it cannot be used in "select" statements (since they are run at analysis time, at which point the value of the flag is the dummy default one).

Has anyone found a way to overcome that issue?

Thanks!

Zak Cutner

unread,
Oct 10, 2023, 7:32:11 PM10/10/23
to bazel-discuss
I came across this problem today and managed to come up with a possible workaround. I'm not sure if this works for your use case, but I wanted to share it on the off-chance that it's useful for somebody else too.

Essentially, I wanted to have something like:

bool_flag(
    name = "thing",
    build_setting_default = select({
        ":linux": True,
        ":darwin": False,
    }),
)

config_setting(
    name = "use_thing",
    flag_values = {":thing": "true"},
)

select({
    ":use_thing": "foo",
    "//conditions:default": "bar",
})

Instead, for my workaround, I ended up creating:

string_flag(
    name = "thing",
    build_setting_default = "auto",
    values = ["auto", "yes", "no"],
)

I then used a macro to generate a load of config_settings from this:

config_setting(
    name = "yes_thing",
    flag_values = {":thing": "yes"},
)

config_setting(
    name = "yes_thing_linux",
    flag_values = {":thing": "yes"},
    constraint_values = [":linux_toolchain"],
)

config_setting(
    name = "yes_thing_darwin",
    flag_values = {":thing": "yes"},
    constraint_values = [":darwin_toolchain"],
)

...

When you use the config_settings in a select, you can then effectively have a default value by specifying the auto case for each of :linux and :darwin:

selects.with_or({
    (":auto_thing", ":no_thing"): "bar",
    (":auto_thing_linux", ":yes_thing_linux"): "foo",
})

This example effectively allows us to say that :linux has a default value of yes, and everything else has a default value of no. The nice thing about this is that we can also make some options unsupported—for example, :yes_thing_darwin is not covered so :thing cannot be set to yes on :darwin.

Since this part stumped me at first, the reason that :auto_thing and :auto_thing_linux are allowed in the same select is because :auto_thing_linux is an unambiguous specialisation of :auto_thing.
Reply all
Reply to author
Forward
0 new messages