Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion docs/selects_doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ Skylib module containing convenience interfaces for select().
<pre>
load("@bazel_skylib//lib:selects.bzl", "selects")

selects.config_setting_group(<a href="#selects.config_setting_group-name">name</a>, <a href="#selects.config_setting_group-match_any">match_any</a>, <a href="#selects.config_setting_group-match_all">match_all</a>, <a href="#selects.config_setting_group-visibility">visibility</a>)
selects.config_setting_group(<a href="#selects.config_setting_group-name">name</a>, <a href="#selects.config_setting_group-match_any">match_any</a>, <a href="#selects.config_setting_group-match_all">match_all</a>, <a href="#selects.config_setting_group-match_none">match_none</a>, <a href="#selects.config_setting_group-visibility">visibility</a>)
</pre>

Matches if all or any of its member `config_setting`s match.

Members listed in `match_none` must additionally *not* match. `match_none`
can be combined with `match_any` or `match_all` (both requirements then
apply) or set on its own to negate conditions.

Example:

```build
Expand All @@ -26,11 +30,23 @@ Example:
match_all = [":one", ":two", ":three"]
)

config_setting_group(
name = "one_but_not_two",
match_all = [":one"],
match_none = [":two"],
)

config_setting_group(
name = "anything_but_three",
match_none = [":three"],
)

cc_binary(
name = "myapp",
srcs = ["myapp.cc"],
deps = select({
":one_two_three": [":special_deps"],
":one_but_not_two": [":other_deps"],
"//conditions:default": [":default_deps"]
})
```
Expand All @@ -44,6 +60,7 @@ Example:
| <a id="selects.config_setting_group-name"></a>name | The group's name. This is how `select()`s reference it. | none |
| <a id="selects.config_setting_group-match_any"></a>match_any | A list of `config_settings`. This group matches if *any* member in the list matches. If this is set, `match_all` must not be set. | `[]` |
| <a id="selects.config_setting_group-match_all"></a>match_all | A list of `config_settings`. This group matches if *every* member in the list matches. If this is set, `match_any` must be not set. | `[]` |
| <a id="selects.config_setting_group-match_none"></a>match_none | A list of `config_settings`. This group matches only if *no* member in the list matches. This can be set by itself to negate conditions or combined with `match_any` or `match_all`, in which case both requirements must hold. `"//conditions:default"` must not appear in the list. | `[]` |
| <a id="selects.config_setting_group-visibility"></a>visibility | Visibility of the config_setting_group. | `None` |


Expand Down
12 changes: 11 additions & 1 deletion lib/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ bzl_library(
srcs = ["old_sets.bzl"],
)

# The following are used construct an "always true" config_setting, which is used in selects.bzl.
# The following are used to construct "always true" and "always false"
# config_settings, which are used in selects.bzl.

bool_setting(
name = "bool",
Expand All @@ -143,3 +144,12 @@ selects.config_setting_group(
":bool_off",
],
)

# The flag can't be both on and off at the same time.
selects.config_setting_group(
name = "always_false",
match_all = [
":bool_on",
":bool_off",
],
)
190 changes: 156 additions & 34 deletions lib/selects.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,13 @@ def _with_or_dict(input_dict):
output_dict[key] = value
return output_dict

def _config_setting_group(name, match_any = [], match_all = [], visibility = None):
def _config_setting_group(name, match_any = [], match_all = [], match_none = [], visibility = None):
"""Matches if all or any of its member `config_setting`s match.

Members listed in `match_none` must additionally *not* match. `match_none`
can be combined with `match_any` or `match_all` (both requirements then
apply) or set on its own to negate conditions.

Example:

```build
Expand All @@ -91,11 +95,23 @@ def _config_setting_group(name, match_any = [], match_all = [], visibility = Non
match_all = [":one", ":two", ":three"]
)

config_setting_group(
name = "one_but_not_two",
match_all = [":one"],
match_none = [":two"],
)

config_setting_group(
name = "anything_but_three",
match_none = [":three"],
)

cc_binary(
name = "myapp",
srcs = ["myapp.cc"],
deps = select({
":one_two_three": [":special_deps"],
":one_but_not_two": [":other_deps"],
"//conditions:default": [":default_deps"]
})
```
Expand All @@ -107,24 +123,59 @@ def _config_setting_group(name, match_any = [], match_all = [], visibility = Non
match_all: A list of `config_settings`. This group matches if *every*
member in the list matches. If this is set, `match_any` must be not
set.
match_none: A list of `config_settings`. This group matches only if *no*
member in the list matches. This can be set by itself to negate
conditions or combined with `match_any` or `match_all`, in which case
both requirements must hold. `"//conditions:default"` must not appear
in the list.
visibility: Visibility of the config_setting_group.
"""
empty1 = not bool(len(match_any))
empty2 = not bool(len(match_all))
if (empty1 and empty2) or (not empty1 and not empty2):
fail('Either "match_any" or "match_all" must be set, but not both.')
empty_any = not bool(len(match_any))
empty_all = not bool(len(match_all))
empty_none = not bool(len(match_none))
if not empty_any and not empty_all:
fail('"match_any" and "match_all" cannot both be set.')
if empty_any and empty_all and empty_none:
fail('At least one of "match_any", "match_all", or "match_none" must be set.')
_check_duplicates(match_any)
_check_duplicates(match_all)
_check_duplicates(match_none)
_check_match_none(match_none, match_any + match_all)

if empty_none:
if ((len(match_any) == 1 and match_any[0] == "//conditions:default") or
(len(match_all) == 1 and match_all[0] == "//conditions:default")):
# If the only entry is "//conditions:default", the condition is
# automatically true.
_config_setting_always_true(name, visibility)
elif not empty_any:
_config_setting_or_group(name, match_any, visibility)
else:
_config_setting_and_group(name, match_all, [], visibility)
return

if ((len(match_any) == 1 and match_any[0] == "//conditions:default") or
(len(match_all) == 1 and match_all[0] == "//conditions:default")):
# If the only entry is "//conditions:default", the condition is
# automatically true.
_config_setting_always_true(name, visibility)
elif not empty1:
_config_setting_or_group(name, match_any, visibility)
# The group matches if the positive condition (match_any / match_all, or
# vacuously true if neither is set) matches and no member of match_none
# matches. Since NOT (n1 OR n2 OR ...) is the same as
# (NOT n1) AND (NOT n2) AND ..., everything reduces to a single AND chain
# over the positive part and the negated settings.
if not empty_any:
if len(_remove_default_condition(match_any)) < len(match_any):
# "//conditions:default" is a member, so the match_any part is
# automatically true.
positive_settings = []
elif len(match_any) == 1:
positive_settings = match_any
else:
# Compile the match_any part into its own (private) OR group and
# AND it with the negated settings.
_config_setting_or_group(name + "_any", match_any, ["//visibility:private"])
positive_settings = [":" + name + "_any"]
else:
_config_setting_and_group(name, match_all, visibility)
# _config_setting_and_group tolerates "//conditions:default" members
# and an empty settings list.
positive_settings = match_all
_config_setting_and_group(name, positive_settings, match_none, visibility)

def _check_duplicates(settings):
"""Fails if any entry in settings appears more than once."""
Expand All @@ -134,11 +185,30 @@ def _check_duplicates(settings):
fail(setting + " appears more than once. Duplicates not allowed.")
seen[setting] = True

def _check_match_none(match_none, match_settings):
"""Validates match_none entries.

Fails if match_none contains "//conditions:default" or shares a setting
with match_settings (the match_any or match_all list).
"""
if not match_none:
return
negated = {}
for setting in match_none:
if setting == "//conditions:default":
fail('"//conditions:default" is not allowed in "match_none".')
negated[str(native.package_relative_label(setting))] = True
for setting in match_settings:
if (setting != "//conditions:default" and
str(native.package_relative_label(setting)) in negated):
fail(setting + ' appears in both "match_none" and the match list. ' +
"Settings cannot be both matched and negated.")

def _remove_default_condition(settings):
"""Returns settings with "//conditions:default" entries filtered out."""
new_settings = []
for setting in settings:
if settings != "//conditions:default":
if setting != "//conditions:default":
new_settings.append(setting)
return new_settings

Expand Down Expand Up @@ -184,46 +254,98 @@ def _config_setting_or_group(name, settings, visibility):
visibility = visibility if i == 1 else ["//visibility:private"],
)

def _config_setting_and_group(name, settings, visibility):
"""ANDs multiple config_settings together.
def _config_setting_and_group(name, positive_settings, negative_settings, visibility):
"""ANDs multiple config_settings together, optionally negating some.

The core idea is to create a sequential chain of alias targets where each is
select-resolved as follows: If alias n matches config_setting n, it resolves to
alias n+1 (which evaluates config_setting n+1, and so on). Else it resolves to
config_setting n, which doesn't match by definition. The only way to get a
matching final result is if all config_settings match.

Settings in `negative_settings` are chained the same way with inverted
branches: if such a setting matches, its alias resolves to :always_false,
so the chain can't match. Else it resolves to the next alias over. Negated
settings are evaluated first so the chain can end by evaluating a positive
setting directly whenever there is one.
"""

# "//conditions:default" is automatically true so doesn't need checking.
settings = _remove_default_condition(settings)
positive_settings = _remove_default_condition(positive_settings)

# One config_setting input? Just alias directly to it.
if len(settings) == 1:
native.alias(
name = name,
actual = settings[0],
visibility = visibility,
)
# Each literal is a (setting, is_negated) pair the chain checks in order.
literals = [(setting, True) for setting in negative_settings]
literals += [(setting, False) for setting in positive_settings]

# Every entry was "//conditions:default"? The condition is automatically
# true.
if len(literals) == 0:
_config_setting_always_true(name, visibility)
return

# We need n-1 aliases for n settings. The first alias has no extension. The
# One input? Just alias directly to it, or to its negation.
if len(literals) == 1:
setting, is_negated = literals[0]
if is_negated:
_config_setting_not(name, setting, visibility)
else:
native.alias(
name = name,
actual = setting,
visibility = visibility,
)
return

# We need n-1 aliases for n literals. The first alias has no extension. The
# second alias is named name + "_2", and so on. For the first n-2 aliases,
# if they match they reference the next alias over. If the n-1st alias matches,
# it references the final setting (which is then evaluated directly to determine
# the final value of the AND chain).
actual = [name + "_" + str(i) for i in range(2, len(settings))]
actual.append(settings[-1])

for i in range(1, len(settings)):
# the final value of the AND chain). If the final setting is negated (which only
# happens when there are no positive settings at all), it can't be evaluated
# directly, so a negation alias named name + "_n" is chained instead.
actual = [name + "_" + str(i) for i in range(2, len(literals))]
last_setting, last_negated = literals[-1]
if last_negated:
not_name = name + "_" + str(len(literals))
_config_setting_not(not_name, last_setting, ["//visibility:private"])
actual.append(not_name)
else:
actual.append(last_setting)

for i in range(1, len(literals)):
setting, is_negated = literals[i - 1]
if is_negated:
# A negated setting that matches falsifies the whole chain.
resolved = select({
native.package_relative_label(setting): Label(":always_false"),
"//conditions:default": actual[i - 1],
})
else:
resolved = select({
native.package_relative_label(setting): actual[i - 1],
"//conditions:default": setting,
})
native.alias(
name = name if i == 1 else name + "_" + str(i),
actual = select({
native.package_relative_label(settings[i - 1]): actual[i - 1],
"//conditions:default": settings[i - 1],
}),
actual = resolved,
visibility = visibility if i == 1 else ["//visibility:private"],
)

def _config_setting_not(name, setting, visibility):
"""Creates a config_setting-like target that matches iff `setting` doesn't match.

If `setting` matches, the alias resolves to :always_false, else to
:always_true.
"""
native.alias(
name = name,
actual = select({
native.package_relative_label(setting): Label(":always_false"),
"//conditions:default": Label(":always_true"),
}),
visibility = visibility,
)

def _config_setting_always_true(name, visibility):
"""Creates a config_setting with the given name that's always true."""
native.alias(
Expand Down
Loading