xref: /aosp_15_r20/external/bazel-skylib/lib/selects.bzl (revision bcb5dc7965af6ee42bf2f21341a2ec00233a8c8a)
1# Copyright 2017 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#    http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Skylib module containing convenience interfaces for select()."""
16
17def _with_or(input_dict, no_match_error = ""):
18    """Drop-in replacement for `select()` that supports ORed keys.
19
20    Example:
21
22          ```build
23          deps = selects.with_or({
24              "//configs:one": [":dep1"],
25              ("//configs:two", "//configs:three"): [":dep2or3"],
26              "//configs:four": [":dep4"],
27              "//conditions:default": [":default"]
28          })
29          ```
30
31          Key labels may appear at most once anywhere in the input.
32
33    Args:
34      input_dict: The same dictionary `select()` takes, except keys may take
35          either the usual form `"//foo:config1"` or
36          `("//foo:config1", "//foo:config2", ...)` to signify
37          `//foo:config1` OR `//foo:config2` OR `...`.
38      no_match_error: Optional custom error to report if no condition matches.
39
40    Returns:
41      A native `select()` that expands
42
43      `("//configs:two", "//configs:three"): [":dep2or3"]`
44
45      to
46
47      ```build
48      "//configs:two": [":dep2or3"],
49      "//configs:three": [":dep2or3"],
50      ```
51    """
52    return select(_with_or_dict(input_dict), no_match_error = no_match_error)
53
54def _with_or_dict(input_dict):
55    """Variation of `with_or` that returns the dict of the `select()`.
56
57    Unlike `select()`, the contents of the dict can be inspected by Starlark
58    macros.
59
60    Args:
61      input_dict: Same as `with_or`.
62
63    Returns:
64      A dictionary usable by a native `select()`.
65    """
66    output_dict = {}
67    for (key, value) in input_dict.items():
68        if type(key) == type(()):
69            for config_setting in key:
70                if config_setting in output_dict.keys():
71                    fail("key %s appears multiple times" % config_setting)
72                output_dict[config_setting] = value
73        else:
74            if key in output_dict.keys():
75                fail("key %s appears multiple times" % key)
76            output_dict[key] = value
77    return output_dict
78
79def _config_setting_group(name, match_any = [], match_all = [], visibility = None):
80    """Matches if all or any of its member `config_setting`s match.
81
82    Example:
83
84      ```build
85      config_setting(name = "one", define_values = {"foo": "true"})
86      config_setting(name = "two", define_values = {"bar": "false"})
87      config_setting(name = "three", define_values = {"baz": "more_false"})
88
89      config_setting_group(
90          name = "one_two_three",
91          match_all = [":one", ":two", ":three"]
92      )
93
94      cc_binary(
95          name = "myapp",
96          srcs = ["myapp.cc"],
97          deps = select({
98              ":one_two_three": [":special_deps"],
99              "//conditions:default": [":default_deps"]
100          })
101      ```
102
103    Args:
104      name: The group's name. This is how `select()`s reference it.
105      match_any: A list of `config_settings`. This group matches if *any* member
106          in the list matches. If this is set, `match_all` must not be set.
107      match_all: A list of `config_settings`. This group matches if *every*
108          member in the list matches. If this is set, `match_any` must be not
109          set.
110      visibility: Visibility of the config_setting_group.
111    """
112    empty1 = not bool(len(match_any))
113    empty2 = not bool(len(match_all))
114    if (empty1 and empty2) or (not empty1 and not empty2):
115        fail('Either "match_any" or "match_all" must be set, but not both.')
116    _check_duplicates(match_any)
117    _check_duplicates(match_all)
118
119    if ((len(match_any) == 1 and match_any[0] == "//conditions:default") or
120        (len(match_all) == 1 and match_all[0] == "//conditions:default")):
121        # If the only entry is "//conditions:default", the condition is
122        # automatically true.
123        _config_setting_always_true(name, visibility)
124    elif not empty1:
125        _config_setting_or_group(name, match_any, visibility)
126    else:
127        _config_setting_and_group(name, match_all, visibility)
128
129def _check_duplicates(settings):
130    """Fails if any entry in settings appears more than once."""
131    seen = {}
132    for setting in settings:
133        if setting in seen:
134            fail(setting + " appears more than once. Duplicates not allowed.")
135        seen[setting] = True
136
137def _remove_default_condition(settings):
138    """Returns settings with "//conditions:default" entries filtered out."""
139    new_settings = []
140    for setting in settings:
141        if settings != "//conditions:default":
142            new_settings.append(setting)
143    return new_settings
144
145def _config_setting_or_group(name, settings, visibility):
146    """ORs multiple config_settings together (inclusively).
147
148    The core idea is to create a sequential chain of alias targets where each is
149    select-resolved as follows: If alias n matches config_setting n, the chain
150    is true so it resolves to config_setting n. Else it resolves to alias n+1
151    (which checks config_setting n+1, and so on). If none of the config_settings
152    match, the final alias resolves to one of them arbitrarily, which by
153    definition doesn't match.
154    """
155
156    # "//conditions:default" is present, the whole chain is automatically true.
157    if len(_remove_default_condition(settings)) < len(settings):
158        _config_setting_always_true(name, visibility)
159        return
160
161    elif len(settings) == 1:  # One entry? Just alias directly to it.
162        native.alias(
163            name = name,
164            actual = settings[0],
165            visibility = visibility,
166        )
167        return
168
169    # We need n-1 aliases for n settings. The first alias has no extension. The
170    # second alias is named name + "_2", and so on. For the first n-2 aliases,
171    # if they don't match they reference the next alias over. If the n-1st alias
172    # doesn't match, it references the final setting (which is then evaluated
173    # directly to determine the final value of the AND chain).
174    actual = [name + "_" + str(i) for i in range(2, len(settings))]
175    actual.append(settings[-1])
176
177    for i in range(1, len(settings)):
178        native.alias(
179            name = name if i == 1 else name + "_" + str(i),
180            actual = select({
181                settings[i - 1]: settings[i - 1],
182                "//conditions:default": actual[i - 1],
183            }),
184            visibility = visibility if i == 1 else ["//visibility:private"],
185        )
186
187def _config_setting_and_group(name, settings, visibility):
188    """ANDs multiple config_settings together.
189
190    The core idea is to create a sequential chain of alias targets where each is
191    select-resolved as follows: If alias n matches config_setting n, it resolves to
192    alias n+1 (which evaluates config_setting n+1, and so on). Else it resolves to
193    config_setting n, which doesn't match by definition. The only way to get a
194    matching final result is if all config_settings match.
195    """
196
197    # "//conditions:default" is automatically true so doesn't need checking.
198    settings = _remove_default_condition(settings)
199
200    # One config_setting input? Just alias directly to it.
201    if len(settings) == 1:
202        native.alias(
203            name = name,
204            actual = settings[0],
205            visibility = visibility,
206        )
207        return
208
209    # We need n-1 aliases for n settings. The first alias has no extension. The
210    # second alias is named name + "_2", and so on. For the first n-2 aliases,
211    # if they match they reference the next alias over. If the n-1st alias matches,
212    # it references the final setting (which is then evaluated directly to determine
213    # the final value of the AND chain).
214    actual = [name + "_" + str(i) for i in range(2, len(settings))]
215    actual.append(settings[-1])
216
217    for i in range(1, len(settings)):
218        native.alias(
219            name = name if i == 1 else name + "_" + str(i),
220            actual = select({
221                settings[i - 1]: actual[i - 1],
222                "//conditions:default": settings[i - 1],
223            }),
224            visibility = visibility if i == 1 else ["//visibility:private"],
225        )
226
227def _config_setting_always_true(name, visibility):
228    """Returns a config_setting with the given name that's always true.
229
230    This is achieved by constructing a two-entry OR chain where each
231    config_setting takes opposite values of a boolean flag.
232    """
233    name_on = name + "_stamp_binary_on_check"
234    name_off = name + "_stamp_binary_off_check"
235    native.config_setting(
236        name = name_on,
237        values = {"stamp": "1"},
238    )
239    native.config_setting(
240        name = name_off,
241        values = {"stamp": "0"},
242    )
243    return _config_setting_or_group(name, [":" + name_on, ":" + name_off], visibility)
244
245selects = struct(
246    with_or = _with_or,
247    with_or_dict = _with_or_dict,
248    config_setting_group = _config_setting_group,
249)
250