rules_cc/cc/toolchains/impl/nested_args.bzl

358 lines
13 KiB
Python

# Copyright 2024 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.
"""Helper functions for working with args."""
load("@bazel_skylib//rules/directory:providers.bzl", "DirectoryInfo")
load("//cc:cc_toolchain_config_lib.bzl", "flag_group", "variable_with_value")
load("//cc/toolchains:cc_toolchain_info.bzl", "NestedArgsInfo", "VariableInfo")
load(":collect.bzl", "collect_files", "collect_provider")
visibility([
"//cc/toolchains",
"//tests/rule_based_toolchain/...",
])
REQUIRES_MUTUALLY_EXCLUSIVE_ERR = "requires_none, requires_not_none, requires_true, requires_false, and requires_equal are mutually exclusive"
REQUIRES_NOT_NONE_ERR = "requires_not_none only works on options"
REQUIRES_NONE_ERR = "requires_none only works on options"
REQUIRES_TRUE_ERR = "requires_true only works on bools"
REQUIRES_FALSE_ERR = "requires_false only works on bools"
REQUIRES_EQUAL_ERR = "requires_equal only works on strings"
REQUIRES_EQUAL_VALUE_ERR = "When requires_equal is provided, you must also provide requires_equal_value to specify what it should be equal to"
FORMAT_ARGS_ERR = "format only works on string, file, or directory type variables"
# @unsorted-dict-items.
NESTED_ARGS_ATTRS = {
"args": attr.string_list(
doc = """json-encoded arguments to be added to the command-line.
Usage:
cc_args(
...,
args = ["--foo={foo}"],
format = {
"//cc/toolchains/variables:foo": "foo"
},
)
This is equivalent to flag_group(flags = ["--foo", "%{foo}"])
Mutually exclusive with nested.
""",
),
"nested": attr.label_list(
providers = [NestedArgsInfo],
doc = """nested_args that should be added on the command-line.
Mutually exclusive with args.""",
),
"data": attr.label_list(
allow_files = True,
doc = """Files required to add this argument to the command-line.
For example, a flag that sets the header directory might add the headers in that
directory as additional files.
""",
),
"format": attr.label_keyed_string_dict(
doc = "Variables to be used in substitutions",
),
"iterate_over": attr.label(providers = [VariableInfo], doc = "Replacement for flag_group.iterate_over"),
"requires_not_none": attr.label(providers = [VariableInfo], doc = "Replacement for flag_group.expand_if_available"),
"requires_none": attr.label(providers = [VariableInfo], doc = "Replacement for flag_group.expand_if_not_available"),
"requires_true": attr.label(providers = [VariableInfo], doc = "Replacement for flag_group.expand_if_true"),
"requires_false": attr.label(providers = [VariableInfo], doc = "Replacement for flag_group.expand_if_false"),
"requires_equal": attr.label(providers = [VariableInfo], doc = "Replacement for flag_group.expand_if_equal"),
"requires_equal_value": attr.string(),
}
def _var(target):
if target == None:
return None
return target[VariableInfo].name
# TODO: Consider replacing this with a subrule in the future. However, maybe not
# for a long time, since it'll break compatibility with all bazel versions < 7.
def nested_args_provider_from_ctx(ctx):
"""Gets the nested args provider from a rule that has NESTED_ARGS_ATTRS.
Args:
ctx: The rule context
Returns:
NestedArgsInfo
"""
return nested_args_provider(
label = ctx.label,
args = ctx.attr.args,
format = ctx.attr.format,
nested = collect_provider(ctx.attr.nested, NestedArgsInfo),
files = collect_files(ctx.attr.data + getattr(ctx.attr, "allowlist_include_directories", [])),
iterate_over = ctx.attr.iterate_over,
requires_not_none = _var(ctx.attr.requires_not_none),
requires_none = _var(ctx.attr.requires_none),
requires_true = _var(ctx.attr.requires_true),
requires_false = _var(ctx.attr.requires_false),
requires_equal = _var(ctx.attr.requires_equal),
requires_equal_value = ctx.attr.requires_equal_value,
)
def nested_args_provider(
*,
label,
args = [],
nested = [],
format = {},
files = depset([]),
iterate_over = None,
requires_not_none = None,
requires_none = None,
requires_true = None,
requires_false = None,
requires_equal = None,
requires_equal_value = "",
fail = fail):
"""Creates a validated NestedArgsInfo.
Does not validate types, as you can't know the type of a variable until
you have a cc_args wrapping it, because the outer layers can change that
type using iterate_over.
Args:
label: (Label) The context we are currently evaluating in. Used for
error messages.
args: (List[str]) The command-line arguments to add.
nested: (List[NestedArgsInfo]) command-line arguments to expand.
format: (dict[Target, str]) A mapping from target to format string name
files: (depset[File]) Files required for this set of command-line args.
iterate_over: (Optional[Target]) Target for the variable to iterate over
requires_not_none: (Optional[str]) If provided, this NestedArgsInfo will
be ignored if the variable is None
requires_none: (Optional[str]) If provided, this NestedArgsInfo will
be ignored if the variable is not None
requires_true: (Optional[str]) If provided, this NestedArgsInfo will
be ignored if the variable is false
requires_false: (Optional[str]) If provided, this NestedArgsInfo will
be ignored if the variable is true
requires_equal: (Optional[str]) If provided, this NestedArgsInfo will
be ignored if the variable is not equal to requires_equal_value.
requires_equal_value: (str) The value to compare the requires_equal
variable with
fail: A fail function. Use only for testing.
Returns:
NestedArgsInfo
"""
if bool(args) and bool(nested):
fail("Args and nested are mutually exclusive")
replacements = {}
if iterate_over:
# Since the user didn't assign a name to iterate_over, allow them to
# reference it as "--foo={}"
replacements[""] = iterate_over
# Intentionally ensure that {} clashes between an explicit user format
# string "" and the implicit one provided by iterate_over.
for target, name in format.items():
if name in replacements:
fail("Both %s and %s have the format string name %r" % (
target.label,
replacements[name].label,
name,
))
replacements[name] = target
# Intentionally ensure that we do not have to use the variable provided by
# iterate_over in the format string.
# For example, a valid use case is:
# cc_args(
# nested = ":nested",
# iterate_over = "//cc/toolchains/variables:libraries_to_link",
# )
# cc_nested_args(
# args = ["{}"],
# iterate_over = "//cc/toolchains/variables:libraries_to_link.object_files",
# )
args = format_args(args, replacements, must_use = format.values(), fail = fail)
transitive_files = [ea.files for ea in nested]
transitive_files.append(files)
has_value = [attr for attr in [
requires_not_none,
requires_none,
requires_true,
requires_false,
requires_equal,
] if attr != None]
# We may want to reconsider this down the line, but it's easier to open up
# an API than to lock down an API.
if len(has_value) > 1:
fail(REQUIRES_MUTUALLY_EXCLUSIVE_ERR)
kwargs = {}
if args:
kwargs["flags"] = args
requires_types = {}
if nested:
kwargs["flag_groups"] = [ea.legacy_flag_group for ea in nested]
unwrap_options = []
if iterate_over:
kwargs["iterate_over"] = _var(iterate_over)
if requires_not_none:
kwargs["expand_if_available"] = requires_not_none
requires_types.setdefault(requires_not_none, []).append(struct(
msg = REQUIRES_NOT_NONE_ERR,
valid_types = ["option"],
after_option_unwrap = False,
))
unwrap_options.append(requires_not_none)
elif requires_none:
kwargs["expand_if_not_available"] = requires_none
requires_types.setdefault(requires_none, []).append(struct(
msg = REQUIRES_NONE_ERR,
valid_types = ["option"],
after_option_unwrap = False,
))
elif requires_true:
kwargs["expand_if_true"] = requires_true
requires_types.setdefault(requires_true, []).append(struct(
msg = REQUIRES_TRUE_ERR,
valid_types = ["bool"],
after_option_unwrap = True,
))
unwrap_options.append(requires_true)
elif requires_false:
kwargs["expand_if_false"] = requires_false
requires_types.setdefault(requires_false, []).append(struct(
msg = REQUIRES_FALSE_ERR,
valid_types = ["bool"],
after_option_unwrap = True,
))
unwrap_options.append(requires_false)
elif requires_equal:
if not requires_equal_value:
fail(REQUIRES_EQUAL_VALUE_ERR)
kwargs["expand_if_equal"] = variable_with_value(
name = requires_equal,
value = requires_equal_value,
)
unwrap_options.append(requires_equal)
requires_types.setdefault(requires_equal, []).append(struct(
msg = REQUIRES_EQUAL_ERR,
valid_types = ["string"],
after_option_unwrap = True,
))
for arg in format:
if VariableInfo in arg:
requires_types.setdefault(arg[VariableInfo].name, []).append(struct(
msg = FORMAT_ARGS_ERR,
valid_types = ["string", "file", "directory"],
after_option_unwrap = True,
))
return NestedArgsInfo(
label = label,
nested = nested,
files = depset(transitive = transitive_files),
iterate_over = _var(iterate_over),
unwrap_options = unwrap_options,
requires_types = requires_types,
legacy_flag_group = flag_group(**kwargs),
)
def _escape(s):
return s.replace("%", "%%")
def _format_target(target, fail = fail):
if VariableInfo in target:
return "%%{%s}" % target[VariableInfo].name
elif DirectoryInfo in target:
return _escape(target[DirectoryInfo].path)
files = target[DefaultInfo].files.to_list()
if len(files) == 1:
return _escape(files[0].path)
fail("%s should be either a variable, a directory, or a single file." % target.label)
def format_args(args, format, must_use = [], fail = fail):
"""Lists all of the variables referenced by an argument.
Eg: format_args(["--foo", "--bar={bar}"], {"bar": VariableInfo(name="bar")})
=> ["--foo", "--bar=%{bar}"]
Args:
args: (List[str]) The command-line arguments.
format: (Dict[str, Target]) A mapping of substitutions from key to target.
must_use: (List[str]) A list of substitutions that must be used.
fail: The fail function. Used for tests
Returns:
A string defined to be compatible with flag groups.
"""
formatted = []
used_vars = {}
for arg in args:
upto = 0
out = []
has_format = False
# This should be "while true". I used this number because it's an upper
# bound of the number of iterations.
for _ in range(len(arg)):
if upto >= len(arg):
break
# Escaping via "{{" and "}}"
if arg[upto] in "{}" and upto + 1 < len(arg) and arg[upto + 1] == arg[upto]:
out.append(arg[upto])
upto += 2
elif arg[upto] == "{":
chunks = arg[upto + 1:].split("}", 1)
if len(chunks) != 2:
fail("Unmatched { in %r" % arg)
variable = chunks[0]
if variable not in format:
fail('Unknown variable %r in format string %r. Try using cc_args(..., format = {"//path/to:variable": %r})' % (variable, arg, variable))
elif has_format:
fail("The format string %r contained multiple variables, which is unsupported." % arg)
else:
used_vars[variable] = None
has_format = True
out.append(_format_target(format[variable], fail = fail))
upto += len(variable) + 2
elif arg[upto] == "}":
fail("Unexpected } in %r" % arg)
else:
out.append(_escape(arg[upto]))
upto += 1
formatted.append("".join(out))
unused_vars = [var for var in must_use if var not in used_vars]
if unused_vars:
fail("The variable %r was not used in the format string." % unused_vars[0])
return formatted