1# Copyright 2024 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"""Helper functions for working with args.""" 15 16load("@bazel_skylib//rules/directory:providers.bzl", "DirectoryInfo") 17load("//cc:cc_toolchain_config_lib.bzl", "flag_group", "variable_with_value") 18load("//cc/toolchains:cc_toolchain_info.bzl", "NestedArgsInfo", "VariableInfo") 19load(":collect.bzl", "collect_files", "collect_provider") 20 21visibility([ 22 "//cc/toolchains", 23 "//tests/rule_based_toolchain/...", 24]) 25 26REQUIRES_MUTUALLY_EXCLUSIVE_ERR = "requires_none, requires_not_none, requires_true, requires_false, and requires_equal are mutually exclusive" 27REQUIRES_NOT_NONE_ERR = "requires_not_none only works on options" 28REQUIRES_NONE_ERR = "requires_none only works on options" 29REQUIRES_TRUE_ERR = "requires_true only works on bools" 30REQUIRES_FALSE_ERR = "requires_false only works on bools" 31REQUIRES_EQUAL_ERR = "requires_equal only works on strings" 32REQUIRES_EQUAL_VALUE_ERR = "When requires_equal is provided, you must also provide requires_equal_value to specify what it should be equal to" 33FORMAT_ARGS_ERR = "format only works on string, file, or directory type variables" 34 35# @unsorted-dict-items. 36NESTED_ARGS_ATTRS = { 37 "args": attr.string_list( 38 doc = """json-encoded arguments to be added to the command-line. 39 40Usage: 41cc_args( 42 ..., 43 args = ["--foo={foo}"], 44 format = { 45 "//cc/toolchains/variables:foo": "foo" 46 }, 47) 48 49This is equivalent to flag_group(flags = ["--foo", "%{foo}"]) 50 51Mutually exclusive with nested. 52""", 53 ), 54 "nested": attr.label_list( 55 providers = [NestedArgsInfo], 56 doc = """nested_args that should be added on the command-line. 57 58Mutually exclusive with args.""", 59 ), 60 "data": attr.label_list( 61 allow_files = True, 62 doc = """Files required to add this argument to the command-line. 63 64For example, a flag that sets the header directory might add the headers in that 65directory as additional files. 66""", 67 ), 68 "format": attr.label_keyed_string_dict( 69 doc = "Variables to be used in substitutions", 70 ), 71 "iterate_over": attr.label(providers = [VariableInfo], doc = "Replacement for flag_group.iterate_over"), 72 "requires_not_none": attr.label(providers = [VariableInfo], doc = "Replacement for flag_group.expand_if_available"), 73 "requires_none": attr.label(providers = [VariableInfo], doc = "Replacement for flag_group.expand_if_not_available"), 74 "requires_true": attr.label(providers = [VariableInfo], doc = "Replacement for flag_group.expand_if_true"), 75 "requires_false": attr.label(providers = [VariableInfo], doc = "Replacement for flag_group.expand_if_false"), 76 "requires_equal": attr.label(providers = [VariableInfo], doc = "Replacement for flag_group.expand_if_equal"), 77 "requires_equal_value": attr.string(), 78} 79 80def _var(target): 81 if target == None: 82 return None 83 return target[VariableInfo].name 84 85# TODO: Consider replacing this with a subrule in the future. However, maybe not 86# for a long time, since it'll break compatibility with all bazel versions < 7. 87def nested_args_provider_from_ctx(ctx): 88 """Gets the nested args provider from a rule that has NESTED_ARGS_ATTRS. 89 90 Args: 91 ctx: The rule context 92 Returns: 93 NestedArgsInfo 94 """ 95 return nested_args_provider( 96 label = ctx.label, 97 args = ctx.attr.args, 98 format = ctx.attr.format, 99 nested = collect_provider(ctx.attr.nested, NestedArgsInfo), 100 files = collect_files(ctx.attr.data + getattr(ctx.attr, "allowlist_include_directories", [])), 101 iterate_over = ctx.attr.iterate_over, 102 requires_not_none = _var(ctx.attr.requires_not_none), 103 requires_none = _var(ctx.attr.requires_none), 104 requires_true = _var(ctx.attr.requires_true), 105 requires_false = _var(ctx.attr.requires_false), 106 requires_equal = _var(ctx.attr.requires_equal), 107 requires_equal_value = ctx.attr.requires_equal_value, 108 ) 109 110def nested_args_provider( 111 *, 112 label, 113 args = [], 114 nested = [], 115 format = {}, 116 files = depset([]), 117 iterate_over = None, 118 requires_not_none = None, 119 requires_none = None, 120 requires_true = None, 121 requires_false = None, 122 requires_equal = None, 123 requires_equal_value = "", 124 fail = fail): 125 """Creates a validated NestedArgsInfo. 126 127 Does not validate types, as you can't know the type of a variable until 128 you have a cc_args wrapping it, because the outer layers can change that 129 type using iterate_over. 130 131 Args: 132 label: (Label) The context we are currently evaluating in. Used for 133 error messages. 134 args: (List[str]) The command-line arguments to add. 135 nested: (List[NestedArgsInfo]) command-line arguments to expand. 136 format: (dict[Target, str]) A mapping from target to format string name 137 files: (depset[File]) Files required for this set of command-line args. 138 iterate_over: (Optional[Target]) Target for the variable to iterate over 139 requires_not_none: (Optional[str]) If provided, this NestedArgsInfo will 140 be ignored if the variable is None 141 requires_none: (Optional[str]) If provided, this NestedArgsInfo will 142 be ignored if the variable is not None 143 requires_true: (Optional[str]) If provided, this NestedArgsInfo will 144 be ignored if the variable is false 145 requires_false: (Optional[str]) If provided, this NestedArgsInfo will 146 be ignored if the variable is true 147 requires_equal: (Optional[str]) If provided, this NestedArgsInfo will 148 be ignored if the variable is not equal to requires_equal_value. 149 requires_equal_value: (str) The value to compare the requires_equal 150 variable with 151 fail: A fail function. Use only for testing. 152 Returns: 153 NestedArgsInfo 154 """ 155 if bool(args) and bool(nested): 156 fail("Args and nested are mutually exclusive") 157 158 replacements = {} 159 if iterate_over: 160 # Since the user didn't assign a name to iterate_over, allow them to 161 # reference it as "--foo={}" 162 replacements[""] = iterate_over 163 164 # Intentionally ensure that {} clashes between an explicit user format 165 # string "" and the implicit one provided by iterate_over. 166 for target, name in format.items(): 167 if name in replacements: 168 fail("Both %s and %s have the format string name %r" % ( 169 target.label, 170 replacements[name].label, 171 name, 172 )) 173 replacements[name] = target 174 175 # Intentionally ensure that we do not have to use the variable provided by 176 # iterate_over in the format string. 177 # For example, a valid use case is: 178 # cc_args( 179 # nested = ":nested", 180 # iterate_over = "//cc/toolchains/variables:libraries_to_link", 181 # ) 182 # cc_nested_args( 183 # args = ["{}"], 184 # iterate_over = "//cc/toolchains/variables:libraries_to_link.object_files", 185 # ) 186 args = format_args(args, replacements, must_use = format.values(), fail = fail) 187 188 transitive_files = [ea.files for ea in nested] 189 transitive_files.append(files) 190 191 has_value = [attr for attr in [ 192 requires_not_none, 193 requires_none, 194 requires_true, 195 requires_false, 196 requires_equal, 197 ] if attr != None] 198 199 # We may want to reconsider this down the line, but it's easier to open up 200 # an API than to lock down an API. 201 if len(has_value) > 1: 202 fail(REQUIRES_MUTUALLY_EXCLUSIVE_ERR) 203 204 kwargs = {} 205 206 if args: 207 kwargs["flags"] = args 208 209 requires_types = {} 210 if nested: 211 kwargs["flag_groups"] = [ea.legacy_flag_group for ea in nested] 212 213 unwrap_options = [] 214 215 if iterate_over: 216 kwargs["iterate_over"] = _var(iterate_over) 217 218 if requires_not_none: 219 kwargs["expand_if_available"] = requires_not_none 220 requires_types.setdefault(requires_not_none, []).append(struct( 221 msg = REQUIRES_NOT_NONE_ERR, 222 valid_types = ["option"], 223 after_option_unwrap = False, 224 )) 225 unwrap_options.append(requires_not_none) 226 elif requires_none: 227 kwargs["expand_if_not_available"] = requires_none 228 requires_types.setdefault(requires_none, []).append(struct( 229 msg = REQUIRES_NONE_ERR, 230 valid_types = ["option"], 231 after_option_unwrap = False, 232 )) 233 elif requires_true: 234 kwargs["expand_if_true"] = requires_true 235 requires_types.setdefault(requires_true, []).append(struct( 236 msg = REQUIRES_TRUE_ERR, 237 valid_types = ["bool"], 238 after_option_unwrap = True, 239 )) 240 unwrap_options.append(requires_true) 241 elif requires_false: 242 kwargs["expand_if_false"] = requires_false 243 requires_types.setdefault(requires_false, []).append(struct( 244 msg = REQUIRES_FALSE_ERR, 245 valid_types = ["bool"], 246 after_option_unwrap = True, 247 )) 248 unwrap_options.append(requires_false) 249 elif requires_equal: 250 if not requires_equal_value: 251 fail(REQUIRES_EQUAL_VALUE_ERR) 252 kwargs["expand_if_equal"] = variable_with_value( 253 name = requires_equal, 254 value = requires_equal_value, 255 ) 256 unwrap_options.append(requires_equal) 257 requires_types.setdefault(requires_equal, []).append(struct( 258 msg = REQUIRES_EQUAL_ERR, 259 valid_types = ["string"], 260 after_option_unwrap = True, 261 )) 262 263 for arg in format: 264 if VariableInfo in arg: 265 requires_types.setdefault(arg[VariableInfo].name, []).append(struct( 266 msg = FORMAT_ARGS_ERR, 267 valid_types = ["string", "file", "directory"], 268 after_option_unwrap = True, 269 )) 270 271 return NestedArgsInfo( 272 label = label, 273 nested = nested, 274 files = depset(transitive = transitive_files), 275 iterate_over = _var(iterate_over), 276 unwrap_options = unwrap_options, 277 requires_types = requires_types, 278 legacy_flag_group = flag_group(**kwargs), 279 ) 280 281def _escape(s): 282 return s.replace("%", "%%") 283 284def _format_target(target, fail = fail): 285 if VariableInfo in target: 286 return "%%{%s}" % target[VariableInfo].name 287 elif DirectoryInfo in target: 288 return _escape(target[DirectoryInfo].path) 289 290 files = target[DefaultInfo].files.to_list() 291 if len(files) == 1: 292 return _escape(files[0].path) 293 294 fail("%s should be either a variable, a directory, or a single file." % target.label) 295 296def format_args(args, format, must_use = [], fail = fail): 297 """Lists all of the variables referenced by an argument. 298 299 Eg: format_args(["--foo", "--bar={bar}"], {"bar": VariableInfo(name="bar")}) 300 => ["--foo", "--bar=%{bar}"] 301 302 Args: 303 args: (List[str]) The command-line arguments. 304 format: (Dict[str, Target]) A mapping of substitutions from key to target. 305 must_use: (List[str]) A list of substitutions that must be used. 306 fail: The fail function. Used for tests 307 308 Returns: 309 A string defined to be compatible with flag groups. 310 """ 311 formatted = [] 312 used_vars = {} 313 314 for arg in args: 315 upto = 0 316 out = [] 317 has_format = False 318 319 # This should be "while true". I used this number because it's an upper 320 # bound of the number of iterations. 321 for _ in range(len(arg)): 322 if upto >= len(arg): 323 break 324 325 # Escaping via "{{" and "}}" 326 if arg[upto] in "{}" and upto + 1 < len(arg) and arg[upto + 1] == arg[upto]: 327 out.append(arg[upto]) 328 upto += 2 329 elif arg[upto] == "{": 330 chunks = arg[upto + 1:].split("}", 1) 331 if len(chunks) != 2: 332 fail("Unmatched { in %r" % arg) 333 variable = chunks[0] 334 335 if variable not in format: 336 fail('Unknown variable %r in format string %r. Try using cc_args(..., format = {"//path/to:variable": %r})' % (variable, arg, variable)) 337 elif has_format: 338 fail("The format string %r contained multiple variables, which is unsupported." % arg) 339 else: 340 used_vars[variable] = None 341 has_format = True 342 out.append(_format_target(format[variable], fail = fail)) 343 upto += len(variable) + 2 344 345 elif arg[upto] == "}": 346 fail("Unexpected } in %r" % arg) 347 else: 348 out.append(_escape(arg[upto])) 349 upto += 1 350 351 formatted.append("".join(out)) 352 353 unused_vars = [var for var in must_use if var not in used_vars] 354 if unused_vars: 355 fail("The variable %r was not used in the format string." % unused_vars[0]) 356 357 return formatted 358