Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Negation support #412

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/selects_doc.md
Expand Up @@ -79,7 +79,7 @@ A dictionary usable by a native `select()`.
## selects.config_setting_group

<pre>
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-visibility">visibility</a>, <a href="#selects.config_setting_group-negated">negated</a>)
</pre>

Matches if all or any of its member `config_setting`s match.
Expand Down Expand Up @@ -115,5 +115,6 @@ Example:
| <a id="selects.config_setting_group-match_any"></a>match_any | A list of <code>config_settings</code>. This group matches if *any* member in the list matches. If this is set, <code>match_all</code> must not be set. | <code>[]</code> |
| <a id="selects.config_setting_group-match_all"></a>match_all | A list of <code>config_settings</code>. This group matches if *every* member in the list matches. If this is set, <code>match_any</code> must be not set. | <code>[]</code> |
| <a id="selects.config_setting_group-visibility"></a>visibility | Visibility of the config_setting_group. | <code>None</code> |
| <a id="selects.config_setting_group-negated"></a>negated | If set, this argument should list a subset of the targets passed to <code>any_of</code> or <code>all_of</code>. The targets listed here will be negated, such that groups can express things such as "match :condition1 and don't match :condition2". The special group <code>//conditions:default</code> may not be negated. | <code>[]</code> |


32 changes: 32 additions & 0 deletions lib/BUILD
@@ -1,4 +1,5 @@
load("//:bzl_library.bzl", "bzl_library")
load("//rules:common_settings.bzl", "string_setting")

licenses(["notice"])

Expand Down Expand Up @@ -107,3 +108,34 @@ bzl_library(
name = "old_sets",
srcs = ["old_sets.bzl"],
)

# Define a hidden setting which no other modules can change.
# No references to this setting other than the two config_setting targets below
# should exist.
string_setting(
name = "private_setting",
build_setting_default = "constant",
values = [
"constant",
"not_constant",
],
visibility = ["//visibility:private"],
)

# Define a config setting that will always match, since nothing ever changes
# private_setting from its default value of "constant".
config_setting(
name = "always_true",
flag_values = {
":private_setting": "constant",
},
)

# Define a config setting that will never match, since nothing ever changes
# private_setting from its default value of "constant".
config_setting(
name = "always_false",
flag_values = {
":private_setting": "not_constant",
},
)
195 changes: 139 additions & 56 deletions lib/selects.bzl
Expand Up @@ -14,6 +14,14 @@

"""Skylib module containing convenience interfaces for select()."""

# Target which will always resolve to True. Referenced through a top-level alias
# so that it will work with WORKSPACE files in bazel.
_TRUE_TARGET = Label("//lib:always_true")

# Target which will always resolve to False. Referenced through a top-level alias
# so that it will work with WORKSPACE files in bazel.
_FALSE_TARGET = Label("//lib:always_false")

def _with_or(input_dict, no_match_error = ""):
"""Drop-in replacement for `select()` that supports ORed keys.

Expand Down Expand Up @@ -76,7 +84,12 @@ 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 = [],
visibility = None,
negated = []):
"""Matches if all or any of its member `config_setting`s match.

Example:
Expand Down Expand Up @@ -108,23 +121,36 @@ def _config_setting_group(name, match_any = [], match_all = [], visibility = Non
member in the list matches. If this is set, `match_any` must be not
set.
visibility: Visibility of the config_setting_group.
negated: If set, this argument should list a subset of the targets
passed to `any_of` or `all_of`. The targets listed here will be
negated, such that groups can express things such as
"match :condition1 and don't match :condition2". The special group
`//conditions:default` may not be negated.
"""
empty1 = not bool(len(match_any))
empty2 = not bool(len(match_all))
if (empty1 and empty2) or (not empty1 and not empty2):
any_empty = not bool(len(match_any))
all_empty = not bool(len(match_all))
if (any_empty and all_empty) or (not any_empty and not all_empty):
fail('Either "match_any" or "match_all" must be set, but not both.')
_check_duplicates(match_any)
_check_duplicates(match_all)
_check_negated_is_subset(
match = match_any if not any_empty else match_all,
negated = negated,
)

negated_dict = {n: True for n in negated}
if "//conditions:default" in negated_dict:
fail("The special target //conditions:default may not be negated.")

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)
native.alias(name = name, actual = _TRUE_TARGET, visibility = visibility)
elif not any_empty:
_config_setting_or_group(name, match_any, visibility, negated_dict)
else:
_config_setting_and_group(name, match_all, visibility)
_config_setting_and_group(name, match_all, visibility, negated_dict)

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

def _check_negated_is_subset(match, negated):
"""Fails if any item in negated does not appear in match."""
match_dict = {item: True for item in match}
for negated_condition in negated:
if negated_condition not in match_dict:
fail("Negated condition " + negated_condition + " not found in " +
"match argument.")

def _remove_default_condition(settings):
"""Returns settings with "//conditions:default" entries filtered out."""
new_settings = []
Expand All @@ -142,105 +176,154 @@ def _remove_default_condition(settings):
new_settings.append(setting)
return new_settings

def _config_setting_or_group(name, settings, visibility):
def _config_setting_or_group(name, settings, visibility, negated_dict = {}):
"""ORs multiple config_settings together (inclusively).

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, the chain
is true so it resolves to config_setting n. Else it resolves to alias n+1
is true so it resolves to the always_true target. Else it resolves to alias n+1
(which checks config_setting n+1, and so on). If none of the config_settings
match, the final alias resolves to one of them arbitrarily, which by
definition doesn't match.
match, the final alias resolves to the always_false target.

Negation of any target other than the final target is implemented by
swapping the select conditions. Negation on the final target is implemented
by constructing a not node (see _config_setting_not).
"""

# "//conditions:default" is present, the whole chain is automatically true.
if len(_remove_default_condition(settings)) < len(settings):
_config_setting_always_true(name, visibility)
native.alias(name = name, actual = _TRUE_TARGET, visibility = visibility)
return

elif len(settings) == 1: # One entry? Just alias directly to it.
native.alias(
name = name,
actual = settings[0],
visibility = visibility,
)
return
elif len(settings) == 1:
if not settings[0] in negated_dict:
# One nonnegated entry? Just alias directly to it.
native.alias(
name = name,
actual = settings[0],
visibility = visibility,
)
return
else:
# One negated config_setting input? That's just a not target.
_config_setting_not(name, settings[0], visibility)
return

# We need n-1 aliases for n settings. The first alias has no extension. The
# second alias is named name + "_2", and so on. For the first n-2 aliases,
# if they don't match they reference the next alias over. If the n-1st alias
# doesn't match, 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])
alias_names = [name + "_" + str(i) for i in range(2, len(settings))]

# The final alias isn't evaluated by a select, so if it is negated,
# construct a final not node to invert it.
final_setting = settings[-1]
if final_setting in negated_dict:
final_setting = name + "_" + str(len(settings)) + "_not"
_config_setting_not(final_setting, settings[-1], ["//visibility:private"])
alias_names.append(final_setting)

for i in range(1, len(settings)):
this_setting = settings[i - 1]
next_alias_name = alias_names[i - 1]
if this_setting in negated_dict:
select_dict = {
this_setting: next_alias_name,
"//conditions:default": _TRUE_TARGET,
}
else:
select_dict = {
this_setting: _TRUE_TARGET,
"//conditions:default": next_alias_name,
}
native.alias(
name = name if i == 1 else name + "_" + str(i),
actual = select({
settings[i - 1]: settings[i - 1],
"//conditions:default": actual[i - 1],
}),
actual = select(select_dict),
visibility = visibility if i == 1 else ["//visibility:private"],
)

def _config_setting_and_group(name, settings, visibility):
def _config_setting_and_group(name, settings, visibility, negated_dict = {}):
"""ANDs multiple config_settings together.

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.
the always-false target. The only way to get a matching final result is if all
config_settings match.

Negation of any target other than the final target is implemented by
swapping the select conditions. Negation on the final target is implemented
by constructing a not node (see _config_setting_not).
"""

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

# One config_setting input? Just alias directly to it.
if len(settings) == 1:
native.alias(
name = name,
actual = settings[0],
visibility = visibility,
)
return
if not settings[0] in negated_dict:
# One non-negated config_setting input? Just alias directly to it.
native.alias(
name = name,
actual = settings[0],
visibility = visibility,
)
return
else:
# One negated config_setting input? That's just the not target.
_config_setting_not(name, settings[0], visibility)
return

# We need n-1 aliases for n settings. 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])
alias_names = [name + "_" + str(i) for i in range(2, len(settings))]

# The final alias isn't evaluated by a select, so if it is negated,
# construct a final not node to invert it.
final_setting = settings[-1]
if final_setting in negated_dict:
final_setting = name + "_" + str(len(settings)) + "_not"
_config_setting_not(final_setting, settings[-1], ["//visibility:private"])
alias_names.append(final_setting)

for i in range(1, len(settings)):
this_setting = settings[i - 1]
next_alias = alias_names[i - 1]
if this_setting in negated_dict:
select_dict = {
this_setting: _FALSE_TARGET,
"//conditions:default": next_alias,
}
else:
select_dict = {
this_setting: next_alias,
"//conditions:default": _FALSE_TARGET,
}
native.alias(
name = name if i == 1 else name + "_" + str(i),
actual = select({
settings[i - 1]: actual[i - 1],
"//conditions:default": settings[i - 1],
}),
actual = select(select_dict),
visibility = visibility if i == 1 else ["//visibility:private"],
)

def _config_setting_always_true(name, visibility):
"""Returns a config_setting with the given name that's always true.
def _config_setting_not(name, setting, visibility):
"""Returns an alias that will match if the given setting does not match.

This is achieved by constructing a two-entry OR chain where each
config_setting takes opposite values of a boolean flag.
Args:
name: Name for the alias rule to define.
setting: The config_setting to invert.
visibility: The visibility to assign to the generated alias.
"""
name_on = name + "_stamp_binary_on_check"
name_off = name + "_stamp_binary_off_check"
native.config_setting(
name = name_on,
values = {"stamp": "1"},
)
native.config_setting(
name = name_off,
values = {"stamp": "0"},
native.alias(
name = name,
actual = select({
setting: _FALSE_TARGET,
"//conditions:default": _TRUE_TARGET,
}),
visibility = visibility,
)
return _config_setting_or_group(name, [":" + name_on, ":" + name_off], visibility)

selects = struct(
with_or = _with_or,
Expand Down