• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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