Skip to content

Commit c93d01d

Browse files
reutermjcopybara-github
authored andcommitted
Enable formatting of environment variables in cc_args
Copybara Import from #413 BEGIN_PUBLIC Enable formatting of environment variables in cc_args (#413) Windows does not follow the Unix sysroot convention. Instead, it uses the `INCLUDE` and `LIB` environment variables to specify the include and library search paths. This change enables formatting of the values of envirnoment variables set in the `env` attribute of `cc_args` using the standard `format` attribute. This allows Windows users to download a hermetic "sysroot" as part of the build, and set the relevant "INCLUDE" and "LIB" environment variables. Example usage: ``` cc_args( name = "arg-include", actions = [ "@rules_cc//cc/toolchains/actions:c_compile", "@rules_cc//cc/toolchains/actions:cpp_compile_actions", ], env = { "INCLUDE": "{include}", }, format = { "include": ":sysroot-include", }, data = [ ":sysroot-include", ], ) ``` example repo: https://github.com/reutermj/windows-rules-based-toolchain-example Future work: Current implementation for the format substitution only allows for a single variable to be substituted. Users of the API often will need to substitute in multiple paths into the `INCLUDE`/`LIB` environment variables. Closes #413 END_PUBLIC COPYBARA_INTEGRATE_REVIEW=#413 from reutermj:main 4c59d66 PiperOrigin-RevId: 780817009 Change-Id: Ifab4889f94f788f24a2e585d124444b49102839b
1 parent bdd93e8 commit c93d01d

File tree

6 files changed

+265
-62
lines changed

6 files changed

+265
-62
lines changed

cc/toolchains/args.bzl

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ load(
2424
load(
2525
"//cc/toolchains/impl:nested_args.bzl",
2626
"NESTED_ARGS_ATTRS",
27+
"format_dict_values",
2728
"nested_args_provider_from_ctx",
2829
)
2930
load(
@@ -40,9 +41,17 @@ visibility("public")
4041
def _cc_args_impl(ctx):
4142
actions = collect_action_types(ctx.attr.actions)
4243

44+
formatted_env, used_format_vars = format_dict_values(
45+
env = ctx.attr.env,
46+
must_use = [], # checking for unused variables in done when formatting `args`.
47+
format = {k: v for v, k in ctx.attr.format.items()},
48+
)
49+
4350
nested = None
4451
if ctx.attr.args or ctx.attr.nested:
45-
nested = nested_args_provider_from_ctx(ctx)
52+
# Forward the format variables used by the env formatting so they don't trigger
53+
# errors if they go unused during the argument formatting.
54+
nested = nested_args_provider_from_ctx(ctx, used_format_vars)
4655
validate_nested_args(
4756
variables = ctx.attr._variables[BuiltinVariablesInfo].variables,
4857
nested_args = nested,
@@ -60,7 +69,7 @@ def _cc_args_impl(ctx):
6069
actions = actions,
6170
requires_any_of = tuple(requires),
6271
nested = nested,
63-
env = ctx.attr.env,
72+
env = formatted_env,
6473
files = files,
6574
allowlist_include_directories = depset(
6675
direct = [d[DirectoryInfo] for d in ctx.attr.allowlist_include_directories],
@@ -234,10 +243,11 @@ def cc_args(
234243
data: (List[Label]) A list of runtime data dependencies that are required for these
235244
arguments to work as intended.
236245
env: (Dict[str, str]) Environment variables that should be set when the tool is invoked.
237-
format: (Dict[str, Label]) A mapping of format strings to the label of the corresponding
238-
`cc_variable` that the value should be pulled from. All instances of
239-
`{variable_name}` will be replaced with the expanded value of `variable_name` in this
240-
dictionary. The complete list of possible variables can be found in
246+
format: (Dict[str, Label]) A mapping of format strings to the label of a corresponding
247+
target. This target can be a `directory`, `subdirectory`, `cc_variable`, or a single
248+
file that the value should be pulled from. All instances of `{variable_name}` in the
249+
`args` list will be replaced with the expanded value in this dictionary.
250+
The complete list of possible variables can be found in
241251
https://github.com/bazelbuild/rules_cc/tree/main/cc/toolchains/variables/BUILD.
242252
It is not possible to declare custom variables--these are inherent to Bazel itself.
243253
iterate_over: (Label) The label of a `cc_variable` that should be iterated over. This is

cc/toolchains/impl/nested_args.bzl

Lines changed: 86 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,13 @@ def _var(target):
8484

8585
# TODO: Consider replacing this with a subrule in the future. However, maybe not
8686
# for a long time, since it'll break compatibility with all bazel versions < 7.
87-
def nested_args_provider_from_ctx(ctx):
87+
def nested_args_provider_from_ctx(ctx, maybe_used_vars = []):
8888
"""Gets the nested args provider from a rule that has NESTED_ARGS_ATTRS.
8989
9090
Args:
9191
ctx: The rule context
92+
maybe_used_vars: (List[str]) A list of format variables that are not needed during args formatting.
93+
9294
Returns:
9395
NestedArgsInfo
9496
"""
@@ -105,6 +107,7 @@ def nested_args_provider_from_ctx(ctx):
105107
requires_false = _var(ctx.attr.requires_false),
106108
requires_equal = _var(ctx.attr.requires_equal),
107109
requires_equal_value = ctx.attr.requires_equal_value,
110+
maybe_used_vars = maybe_used_vars,
108111
)
109112

110113
def nested_args_provider(
@@ -121,6 +124,7 @@ def nested_args_provider(
121124
requires_false = None,
122125
requires_equal = None,
123126
requires_equal_value = "",
127+
maybe_used_vars = [],
124128
fail = fail):
125129
"""Creates a validated NestedArgsInfo.
126130
@@ -148,6 +152,7 @@ def nested_args_provider(
148152
be ignored if the variable is not equal to requires_equal_value.
149153
requires_equal_value: (str) The value to compare the requires_equal
150154
variable with
155+
maybe_used_vars: (List[str]) A list of format variables that are not needed during args formatting.
151156
fail: A fail function. Use only for testing.
152157
Returns:
153158
NestedArgsInfo
@@ -183,7 +188,12 @@ def nested_args_provider(
183188
# args = ["{}"],
184189
# iterate_over = "//cc/toolchains/variables:libraries_to_link.object_files",
185190
# )
186-
args = format_args(args, replacements, must_use = format.values(), fail = fail)
191+
formatted_args, _ = format_list(
192+
args,
193+
replacements,
194+
must_use = [var for var in format.values() if var not in maybe_used_vars],
195+
fail = fail,
196+
)
187197

188198
transitive_files = [ea.files for ea in nested]
189199
transitive_files.append(files)
@@ -203,8 +213,8 @@ def nested_args_provider(
203213

204214
kwargs = {}
205215

206-
if args:
207-
kwargs["flags"] = args
216+
if formatted_args:
217+
kwargs["flags"] = formatted_args
208218

209219
requires_types = {}
210220
if nested:
@@ -281,8 +291,10 @@ def nested_args_provider(
281291
def _escape(s):
282292
return s.replace("%", "%%")
283293

284-
def _format_target(target, fail = fail):
294+
def _format_target(target, arg, allow_variables, fail = fail):
285295
if VariableInfo in target:
296+
if not allow_variables:
297+
fail("Unsupported cc_variable substitution %s in %r." % (target.label, arg))
286298
return "%%{%s}" % target[VariableInfo].name
287299
elif DirectoryInfo in target:
288300
return _escape(target[DirectoryInfo].path)
@@ -293,10 +305,49 @@ def _format_target(target, fail = fail):
293305

294306
fail("%s should be either a variable, a directory, or a single file." % target.label)
295307

296-
def format_args(args, format, must_use = [], fail = fail):
308+
def _format_string(arg, format, used_vars, allow_variables, fail = fail):
309+
upto = 0
310+
out = []
311+
has_format = False
312+
313+
# This should be "while true".
314+
# This number is used because it's an upper bound of the number of iterations.
315+
for _ in range(len(arg)):
316+
if upto >= len(arg):
317+
break
318+
319+
# Escaping via "{{" and "}}"
320+
if arg[upto] in "{}" and upto + 1 < len(arg) and arg[upto + 1] == arg[upto]:
321+
out.append(arg[upto])
322+
upto += 2
323+
elif arg[upto] == "{":
324+
chunks = arg[upto + 1:].split("}", 1)
325+
if len(chunks) != 2:
326+
fail("Unmatched { in %r" % arg)
327+
variable = chunks[0]
328+
329+
if variable not in format:
330+
fail('Unknown variable %r in format string %r. Try using cc_args(..., format = {"//path/to:variable": %r})' % (variable, arg, variable))
331+
elif has_format:
332+
fail("The format string %r contained multiple variables, which is unsupported." % arg)
333+
else:
334+
used_vars[variable] = None
335+
has_format = True
336+
out.append(_format_target(format[variable], arg, allow_variables, fail = fail))
337+
upto += len(variable) + 2
338+
339+
elif arg[upto] == "}":
340+
fail("Unexpected } in %r" % arg)
341+
else:
342+
out.append(_escape(arg[upto]))
343+
upto += 1
344+
345+
return "".join(out)
346+
347+
def format_list(args, format, must_use = [], fail = fail):
297348
"""Lists all of the variables referenced by an argument.
298349
299-
Eg: format_args(["--foo", "--bar={bar}"], {"bar": VariableInfo(name="bar")})
350+
Eg: format_list(["--foo", "--bar={bar}"], {"bar": VariableInfo(name="bar")})
300351
=> ["--foo", "--bar=%{bar}"]
301352
302353
Args:
@@ -312,46 +363,37 @@ def format_args(args, format, must_use = [], fail = fail):
312363
used_vars = {}
313364

314365
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
366+
formatted.append(_format_string(arg, format, used_vars, True, fail))
367+
368+
unused_vars = [var for var in must_use if var not in used_vars]
369+
if unused_vars:
370+
fail("The variable %r was not used in the format string." % unused_vars[0])
371+
372+
return formatted, used_vars.keys()
373+
374+
def format_dict_values(env, format, must_use = [], fail = fail):
375+
"""Formats the environment variables.
376+
377+
Eg: format_dict_values({"FOO": "some/path", "BAR": "{bar}"}, {"bar": DirectoryInfo(path="path/to/bar")})
378+
=> {"FOO": "some/path", "BAR": "path/to/bar"}
379+
380+
Args:
381+
env: (Dict[str, str]) The environment variables.
382+
format: (Dict[str, Target]) A mapping of substitutions from key to target.
383+
must_use: (List[str]) A list of substitutions that must be used.
384+
fail: The fail function. Used for tests
385+
386+
Returns:
387+
The environment variables with values defined to be compatible with flag groups.
388+
"""
389+
formatted = {}
390+
used_vars = {}
350391

351-
formatted.append("".join(out))
392+
for key, value in env.items():
393+
formatted[key] = _format_string(value, format, used_vars, False, fail)
352394

353395
unused_vars = [var for var in must_use if var not in used_vars]
354396
if unused_vars:
355397
fail("The variable %r was not used in the format string." % unused_vars[0])
356398

357-
return formatted
399+
return formatted, used_vars.keys()

docs/toolchain_api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,7 @@ For more extensive examples, see the usages here:
662662
| <a id="cc_args-args"></a>args | (List[str]) The command-line arguments that are applied by using this rule. This is mutually exclusive with [nested](#cc_args-nested). | `None` |
663663
| <a id="cc_args-data"></a>data | (List[Label]) A list of runtime data dependencies that are required for these arguments to work as intended. | `None` |
664664
| <a id="cc_args-env"></a>env | (Dict[str, str]) Environment variables that should be set when the tool is invoked. | `None` |
665-
| <a id="cc_args-format"></a>format | (Dict[str, Label]) A mapping of format strings to the label of the corresponding [`cc_variable`](#cc_variable) that the value should be pulled from. All instances of `{variable_name}` will be replaced with the expanded value of `variable_name` in this dictionary. The complete list of possible variables can be found in https://github.com/bazelbuild/rules_cc/tree/main/cc/toolchains/variables/BUILD. It is not possible to declare custom variables--these are inherent to Bazel itself. | `{}` |
665+
| <a id="cc_args-format"></a>format | (Dict[str, Label]) A mapping of format strings to the label of a corresponding target. This target can be a `directory`, `subdirectory`, [`cc_variable`](#cc_variable), or a single file that the value should be pulled from. All instances of `{variable_name}` in the `args` list will be replaced with the expanded value in this dictionary. The complete list of possible variables can be found in https://github.com/bazelbuild/rules_cc/tree/main/cc/toolchains/variables/BUILD. It is not possible to declare custom variables--these are inherent to Bazel itself. | `{}` |
666666
| <a id="cc_args-iterate_over"></a>iterate_over | (Label) The label of a [`cc_variable`](#cc_variable) that should be iterated over. This is intended for use with built-in variables that are lists. | `None` |
667667
| <a id="cc_args-nested"></a>nested | (List[Label]) A list of [`cc_nested_args`](#cc_nested_args) rules that should be expanded to command-line arguments when this rule is used. This is mutually exclusive with [args](#cc_args-args). | `None` |
668668
| <a id="cc_args-requires_not_none"></a>requires_not_none | (Label) The label of a [`cc_variable`](#cc_variable) that should be checked for existence before expanding this rule. If the variable is None, this rule will be ignored. | `None` |

tests/rule_based_toolchain/args/BUILD

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
load("@rules_testing//lib:util.bzl", "util")
22
load("//cc/toolchains:args.bzl", "cc_args")
3+
load("//cc/toolchains/impl:variables.bzl", "cc_variable", "types")
34
load("//tests/rule_based_toolchain:analysis_test_suite.bzl", "analysis_test_suite")
45
load(":args_test.bzl", "TARGETS", "TESTS")
56

7+
cc_variable(
8+
name = "some_variable",
9+
type = types.string,
10+
)
11+
612
util.helper_target(
713
cc_args,
814
name = "simple",

0 commit comments

Comments
 (0)