diff options
Diffstat (limited to 'lib/selects.bzl')
-rw-r--r-- | lib/selects.bzl | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/lib/selects.bzl b/lib/selects.bzl new file mode 100644 index 0000000..c511fe5 --- /dev/null +++ b/lib/selects.bzl @@ -0,0 +1,249 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Skylib module containing convenience interfaces for select().""" + +def _with_or(input_dict, no_match_error = ""): + """Drop-in replacement for `select()` that supports ORed keys. + + Example: + + ```build + deps = selects.with_or({ + "//configs:one": [":dep1"], + ("//configs:two", "//configs:three"): [":dep2or3"], + "//configs:four": [":dep4"], + "//conditions:default": [":default"] + }) + ``` + + Key labels may appear at most once anywhere in the input. + + Args: + input_dict: The same dictionary `select()` takes, except keys may take + either the usual form `"//foo:config1"` or + `("//foo:config1", "//foo:config2", ...)` to signify + `//foo:config1` OR `//foo:config2` OR `...`. + no_match_error: Optional custom error to report if no condition matches. + + Returns: + A native `select()` that expands + + `("//configs:two", "//configs:three"): [":dep2or3"]` + + to + + ```build + "//configs:two": [":dep2or3"], + "//configs:three": [":dep2or3"], + ``` + """ + return select(_with_or_dict(input_dict), no_match_error = no_match_error) + +def _with_or_dict(input_dict): + """Variation of `with_or` that returns the dict of the `select()`. + + Unlike `select()`, the contents of the dict can be inspected by Starlark + macros. + + Args: + input_dict: Same as `with_or`. + + Returns: + A dictionary usable by a native `select()`. + """ + output_dict = {} + for (key, value) in input_dict.items(): + if type(key) == type(()): + for config_setting in key: + if config_setting in output_dict.keys(): + fail("key %s appears multiple times" % config_setting) + output_dict[config_setting] = value + else: + if key in output_dict.keys(): + fail("key %s appears multiple times" % key) + output_dict[key] = value + return output_dict + +def _config_setting_group(name, match_any = [], match_all = [], visibility = None): + """Matches if all or any of its member `config_setting`s match. + + Example: + + ```build + config_setting(name = "one", define_values = {"foo": "true"}) + config_setting(name = "two", define_values = {"bar": "false"}) + config_setting(name = "three", define_values = {"baz": "more_false"}) + + config_setting_group( + name = "one_two_three", + match_all = [":one", ":two", ":three"] + ) + + cc_binary( + name = "myapp", + srcs = ["myapp.cc"], + deps = select({ + ":one_two_three": [":special_deps"], + "//conditions:default": [":default_deps"] + }) + ``` + + Args: + name: The group's name. This is how `select()`s reference it. + 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. + 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. + 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.') + _check_duplicates(match_any) + _check_duplicates(match_all) + + 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) + else: + _config_setting_and_group(name, match_all, visibility) + +def _check_duplicates(settings): + """Fails if any entry in settings appears more than once.""" + seen = {} + for setting in settings: + if setting in seen: + fail(setting + " appears more than once. Duplicates not allowed.") + seen[setting] = True + +def _remove_default_condition(settings): + """Returns settings with "//conditions:default" entries filtered out.""" + new_settings = [] + for setting in settings: + if settings != "//conditions:default": + new_settings.append(setting) + return new_settings + +def _config_setting_or_group(name, settings, visibility): + """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 + (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. + """ + + # "//conditions:default" is present, the whole chain is automatically true. + if len(_remove_default_condition(settings)) < len(settings): + _config_setting_always_true(name, visibility) + return + + elif len(settings) == 1: # One entry? Just alias directly to it. + native.alias( + name = name, + actual = settings[0], + visibility = 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]) + + for i in range(1, len(settings)): + native.alias( + name = name if i == 1 else name + "_" + str(i), + actual = select({ + settings[i - 1]: settings[i - 1], + "//conditions:default": actual[i - 1], + }), + visibility = visibility if i == 1 else ["//visibility:private"], + ) + +def _config_setting_and_group(name, settings, visibility): + """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. + """ + + # "//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 + + # 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]) + + for i in range(1, len(settings)): + native.alias( + name = name if i == 1 else name + "_" + str(i), + actual = select({ + settings[i - 1]: actual[i - 1], + "//conditions:default": settings[i - 1], + }), + 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. + + This is achieved by constructing a two-entry OR chain where each + config_setting takes opposite values of a boolean flag. + """ + 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"}, + ) + return _config_setting_or_group(name, [":" + name_on, ":" + name_off], visibility) + +selects = struct( + with_or = _with_or, + with_or_dict = _with_or_dict, + config_setting_group = _config_setting_group, +) |