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