Implement CustomCommandLine support for Skylark.
RELNOTES: Support ctx.actions.args() for more efficient Skylark command line construction.
PiperOrigin-RevId: 166694148
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkActionFactory.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkActionFactory.java
index 4773988..dbbd772 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkActionFactory.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkActionFactory.java
@@ -23,26 +23,36 @@
import com.google.devtools.build.lib.analysis.FilesToRunProvider;
import com.google.devtools.build.lib.analysis.PseudoAction;
import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.actions.CommandLine;
import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction;
import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction.Substitution;
+import com.google.devtools.build.lib.analysis.skylark.SkylarkCustomCommandLine.ScalarArg;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.skylarkinterface.Param;
import com.google.devtools.build.lib.skylarkinterface.ParamType;
import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModuleCategory;
import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkSignature;
import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
+import com.google.devtools.build.lib.syntax.BaseFunction;
+import com.google.devtools.build.lib.syntax.BuiltinFunction;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.EvalUtils;
import com.google.devtools.build.lib.syntax.Runtime;
+import com.google.devtools.build.lib.syntax.Runtime.NoneType;
import com.google.devtools.build.lib.syntax.SkylarkDict;
import com.google.devtools.build.lib.syntax.SkylarkList;
import com.google.devtools.build.lib.syntax.SkylarkNestedSet;
+import com.google.devtools.build.lib.syntax.SkylarkSemanticsOptions;
+import com.google.devtools.build.lib.syntax.SkylarkSignatureProcessor;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.nio.charset.StandardCharsets;
import java.util.List;
@@ -60,11 +70,15 @@
public class SkylarkActionFactory implements SkylarkValue {
private final SkylarkRuleContext context;
+ private final SkylarkSemanticsOptions skylarkSemanticsOptions;
private RuleContext ruleContext;
-
- public SkylarkActionFactory(SkylarkRuleContext context, RuleContext ruleContext) {
+ public SkylarkActionFactory(
+ SkylarkRuleContext context,
+ SkylarkSemanticsOptions skylarkSemanticsOptions,
+ RuleContext ruleContext) {
this.context = context;
+ this.skylarkSemanticsOptions = skylarkSemanticsOptions;
this.ruleContext = ruleContext;
}
@@ -235,120 +249,125 @@
}
@SkylarkCallable(
- name = "run",
- doc =
- "Creates an action that runs an executable.",
- parameters = {
- @Param(
- name = "outputs",
- type = SkylarkList.class,
- generic1 = Artifact.class,
- named = true,
- positional = false,
- doc = "list of the output files of the action."
- ),
- @Param(
- name = "inputs",
- allowedTypes = {
- @ParamType(type = SkylarkList.class),
- @ParamType(type = SkylarkNestedSet.class),
- },
- generic1 = Artifact.class,
- defaultValue = "[]",
- named = true,
- positional = false,
- doc = "list of the input files of the action."
- ),
- @Param(
- name = "executable",
- type = Object.class,
- allowedTypes = {
- @ParamType(type = Artifact.class),
- @ParamType(type = String.class),
- },
- named = true,
- positional = false,
- doc = "the executable file to be called by the action."
- ),
- @Param(
- name = "arguments",
- type = SkylarkList.class,
- generic1 = String.class,
- defaultValue = "[]",
- named = true,
- positional = false,
- doc = "command line arguments of the action."
- ),
- @Param(
- name = "mnemonic",
- type = String.class,
- noneable = true,
- defaultValue = "None",
- named = true,
- positional = false,
- doc = "a one-word description of the action, e.g. CppCompile or GoLink."
- ),
- @Param(
- name = "progress_message",
- type = String.class,
- noneable = true,
- defaultValue = "None",
- named = true,
- positional = false,
- doc =
- "progress message to show to the user during the build, "
- + "e.g. \"Compiling foo.cc to create foo.o\"."
- ),
- @Param(
- name = "use_default_shell_env",
- type = Boolean.class,
- defaultValue = "False",
- named = true,
- positional = false,
- doc = "whether the action should use the built in shell environment or not."
- ),
- @Param(
- name = "env",
- type = SkylarkDict.class,
- noneable = true,
- defaultValue = "None",
- named = true,
- positional = false,
- doc = "sets the dictionary of environment variables."
- ),
- @Param(
- name = "execution_requirements",
- type = SkylarkDict.class,
- noneable = true,
- defaultValue = "None",
- named = true,
- positional = false,
- doc =
- "information for scheduling the action. See "
- + "<a href=\"/docs/be/common-definitions.html#common.tags\">tags</a> "
- + "for useful keys."
- ),
- @Param(
- // TODO(bazel-team): The name here isn't accurate anymore.
- // This is technically experimental, so folks shouldn't be too attached,
- // but consider renaming to be more accurate/opaque.
- name = "input_manifests",
- type = SkylarkList.class,
- noneable = true,
- defaultValue = "None",
- named = true,
- positional = false,
- doc =
- "(Experimental) sets the input runfiles metadata; "
- + "they are typically generated by resolve_command."
- )
- }
+ name = "run",
+ doc = "Creates an action that runs an executable.",
+ parameters = {
+ @Param(
+ name = "outputs",
+ type = SkylarkList.class,
+ generic1 = Artifact.class,
+ named = true,
+ positional = false,
+ doc = "list of the output files of the action."
+ ),
+ @Param(
+ name = "inputs",
+ allowedTypes = {
+ @ParamType(type = SkylarkList.class),
+ @ParamType(type = SkylarkNestedSet.class),
+ },
+ generic1 = Artifact.class,
+ defaultValue = "[]",
+ named = true,
+ positional = false,
+ doc = "list of the input files of the action."
+ ),
+ @Param(
+ name = "executable",
+ type = Object.class,
+ allowedTypes = {
+ @ParamType(type = Artifact.class),
+ @ParamType(type = String.class),
+ },
+ named = true,
+ positional = false,
+ doc = "the executable file to be called by the action."
+ ),
+ @Param(
+ name = "arguments",
+ type = Object.class,
+ allowedTypes = {
+ @ParamType(type = SkylarkList.class),
+ @ParamType(type = Args.class),
+ },
+ defaultValue = "[]",
+ named = true,
+ positional = false,
+ doc =
+ "command line arguments of the action. "
+ + "May be either a list or an actions.args() object. "
+ + "See <a href=\"actions.html#args\">ctx.actions.args()</a>."
+ ),
+ @Param(
+ name = "mnemonic",
+ type = String.class,
+ noneable = true,
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc = "a one-word description of the action, e.g. CppCompile or GoLink."
+ ),
+ @Param(
+ name = "progress_message",
+ type = String.class,
+ noneable = true,
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc =
+ "progress message to show to the user during the build, "
+ + "e.g. \"Compiling foo.cc to create foo.o\"."
+ ),
+ @Param(
+ name = "use_default_shell_env",
+ type = Boolean.class,
+ defaultValue = "False",
+ named = true,
+ positional = false,
+ doc = "whether the action should use the built in shell environment or not."
+ ),
+ @Param(
+ name = "env",
+ type = SkylarkDict.class,
+ noneable = true,
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc = "sets the dictionary of environment variables."
+ ),
+ @Param(
+ name = "execution_requirements",
+ type = SkylarkDict.class,
+ noneable = true,
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc =
+ "information for scheduling the action. See "
+ + "<a href=\"/docs/be/common-definitions.html#common.tags\">tags</a> "
+ + "for useful keys."
+ ),
+ @Param(
+ // TODO(bazel-team): The name here isn't accurate anymore.
+ // This is technically experimental, so folks shouldn't be too attached,
+ // but consider renaming to be more accurate/opaque.
+ name = "input_manifests",
+ type = SkylarkList.class,
+ noneable = true,
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc =
+ "(Experimental) sets the input runfiles metadata; "
+ + "they are typically generated by resolve_command."
+ )
+ }
)
public void run(
SkylarkList outputs,
Object inputs,
Object executableUnchecked,
- SkylarkList arguments,
+ Object arguments,
Object mnemonicUnchecked,
Object progressMessage,
Boolean useDefaultShellEnv,
@@ -359,10 +378,15 @@
context.checkMutable("actions.run");
SpawnAction.Builder builder = new SpawnAction.Builder();
- @SuppressWarnings("unchecked")
- List<String> argumentsContents = arguments.getContents(String.class, "arguments");
-
- builder.setCommandLine(CustomCommandLine.builder().addAll(argumentsContents).build());
+ if (arguments instanceof SkylarkList) {
+ SkylarkList skylarkList = ((SkylarkList) arguments);
+ @SuppressWarnings("unchecked")
+ List<String> argumentsContents = skylarkList.getContents(String.class, "arguments");
+ builder.setCommandLine(CustomCommandLine.builder().addAll(argumentsContents).build());
+ } else {
+ Args args = (Args) arguments;
+ builder.setCommandLine(args.build());
+ }
if (executableUnchecked instanceof Artifact) {
Artifact executable = (Artifact) executableUnchecked;
builder.addInput(executable);
@@ -386,124 +410,128 @@
envUnchecked, executionRequirementsUnchecked, inputManifestsUnchecked, builder);
}
-
@SkylarkCallable(
- name = "run_shell",
- doc = "Creates an action that runs a shell command.",
- parameters = {
- @Param(
- name = "outputs",
- type = SkylarkList.class,
- generic1 = Artifact.class,
- named = true,
- positional = false,
- doc = "list of the output files of the action."
- ),
- @Param(
- name = "inputs",
- allowedTypes = {
- @ParamType(type = SkylarkList.class),
- @ParamType(type = SkylarkNestedSet.class),
- },
- generic1 = Artifact.class,
- defaultValue = "[]",
- named = true,
- positional = false,
- doc = "list of the input files of the action."
- ),
- @Param(
- name = "arguments",
- type = SkylarkList.class,
- generic1 = String.class,
- defaultValue = "[]",
- named = true,
- positional = false,
- doc = "command line arguments of the action."
- ),
- @Param(
- name = "mnemonic",
- type = String.class,
- noneable = true,
- defaultValue = "None",
- named = true,
- positional = false,
- doc = "a one-word description of the action, e.g. CppCompile or GoLink."
- ),
- @Param(
- name = "command",
- type = Object.class,
- allowedTypes = {
- @ParamType(type = String.class),
- @ParamType(type = SkylarkList.class, generic1 = String.class),
- @ParamType(type = Runtime.NoneType.class),
- },
- defaultValue = "None",
- named = true,
- positional = false,
- doc =
- "shell command to execute. "
- + "Arguments are available with <code>$1</code>, <code>$2</code>, etc."
- ),
- @Param(
- name = "progress_message",
- type = String.class,
- noneable = true,
- defaultValue = "None",
- named = true,
- positional = false,
- doc =
- "progress message to show to the user during the build, "
- + "e.g. \"Compiling foo.cc to create foo.o\"."
- ),
- @Param(
- name = "use_default_shell_env",
- type = Boolean.class,
- defaultValue = "False",
- named = true,
- positional = false,
- doc = "whether the action should use the built in shell environment or not."
- ),
- @Param(
- name = "env",
- type = SkylarkDict.class,
- noneable = true,
- defaultValue = "None",
- named = true,
- positional = false,
- doc = "sets the dictionary of environment variables."
- ),
- @Param(
- name = "execution_requirements",
- type = SkylarkDict.class,
- noneable = true,
- defaultValue = "None",
- named = true,
- positional = false,
- doc =
- "information for scheduling the action. See "
- + "<a href=\"/docs/be/common-definitions.html#common.tags\">tags</a> "
- + "for useful keys."
- ),
- @Param(
- // TODO(bazel-team): The name here isn't accurate anymore.
- // This is technically experimental, so folks shouldn't be too attached,
- // but consider renaming to be more accurate/opaque.
- name = "input_manifests",
- type = SkylarkList.class,
- noneable = true,
- defaultValue = "None",
- named = true,
- positional = false,
- doc =
- "(Experimental) sets the input runfiles metadata; "
- + "they are typically generated by resolve_command."
- )
- }
+ name = "run_shell",
+ doc = "Creates an action that runs a shell command.",
+ parameters = {
+ @Param(
+ name = "outputs",
+ type = SkylarkList.class,
+ generic1 = Artifact.class,
+ named = true,
+ positional = false,
+ doc = "list of the output files of the action."
+ ),
+ @Param(
+ name = "inputs",
+ allowedTypes = {
+ @ParamType(type = SkylarkList.class),
+ @ParamType(type = SkylarkNestedSet.class),
+ },
+ generic1 = Artifact.class,
+ defaultValue = "[]",
+ named = true,
+ positional = false,
+ doc = "list of the input files of the action."
+ ),
+ @Param(
+ name = "arguments",
+ allowedTypes = {
+ @ParamType(type = SkylarkList.class),
+ @ParamType(type = Args.class),
+ },
+ defaultValue = "[]",
+ named = true,
+ positional = false,
+ doc =
+ "command line arguments of the action."
+ + "May be either a list or an actions.args() object."
+ + "See <a href=\"actions.html#args\">ctx.actions.args()</a>."
+ ),
+ @Param(
+ name = "mnemonic",
+ type = String.class,
+ noneable = true,
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc = "a one-word description of the action, e.g. CppCompile or GoLink."
+ ),
+ @Param(
+ name = "command",
+ type = Object.class,
+ allowedTypes = {
+ @ParamType(type = String.class),
+ @ParamType(type = SkylarkList.class, generic1 = String.class),
+ @ParamType(type = Runtime.NoneType.class),
+ },
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc =
+ "shell command to execute. "
+ + "Arguments are available with <code>$1</code>, <code>$2</code>, etc."
+ ),
+ @Param(
+ name = "progress_message",
+ type = String.class,
+ noneable = true,
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc =
+ "progress message to show to the user during the build, "
+ + "e.g. \"Compiling foo.cc to create foo.o\"."
+ ),
+ @Param(
+ name = "use_default_shell_env",
+ type = Boolean.class,
+ defaultValue = "False",
+ named = true,
+ positional = false,
+ doc = "whether the action should use the built in shell environment or not."
+ ),
+ @Param(
+ name = "env",
+ type = SkylarkDict.class,
+ noneable = true,
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc = "sets the dictionary of environment variables."
+ ),
+ @Param(
+ name = "execution_requirements",
+ type = SkylarkDict.class,
+ noneable = true,
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc =
+ "information for scheduling the action. See "
+ + "<a href=\"/docs/be/common-definitions.html#common.tags\">tags</a> "
+ + "for useful keys."
+ ),
+ @Param(
+ // TODO(bazel-team): The name here isn't accurate anymore.
+ // This is technically experimental, so folks shouldn't be too attached,
+ // but consider renaming to be more accurate/opaque.
+ name = "input_manifests",
+ type = SkylarkList.class,
+ noneable = true,
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc =
+ "(Experimental) sets the input runfiles metadata; "
+ + "they are typically generated by resolve_command."
+ )
+ }
)
public void runShell(
SkylarkList outputs,
Object inputs,
- SkylarkList arguments,
+ Object arguments,
Object mnemonicUnchecked,
Object commandUnchecked,
Object progressMessage,
@@ -516,18 +544,25 @@
// TODO(bazel-team): builder still makes unnecessary copies of inputs, outputs and args.
SpawnAction.Builder builder = new SpawnAction.Builder();
- CustomCommandLine.Builder commandLine = CustomCommandLine.builder();
- if (arguments.size() > 0) {
- // When we use a shell command, add an empty argument before other arguments.
- // e.g. bash -c "cmd" '' 'arg1' 'arg2'
- // bash will use the empty argument as the value of $0 (which we don't care about).
- // arg1 and arg2 will be $1 and $2, as a user expects.
- commandLine.add("");
- }
+ if (arguments instanceof SkylarkList) {
+ CustomCommandLine.Builder commandLine = CustomCommandLine.builder();
+ SkylarkList argumentList = (SkylarkList) arguments;
+ if (argumentList.size() > 0) {
+ // When we use a shell command, add an empty argument before other arguments.
+ // e.g. bash -c "cmd" '' 'arg1' 'arg2'
+ // bash will use the empty argument as the value of $0 (which we don't care about).
+ // arg1 and arg2 will be $1 and $2, as a user expects.
+ commandLine.add("");
+ }
- @SuppressWarnings("unchecked")
- List<String> argumentsContents = arguments.getContents(String.class, "arguments");
- commandLine.addAll(argumentsContents);
+ @SuppressWarnings("unchecked")
+ List<String> argumentsContents = argumentList.getContents(String.class, "arguments");
+ commandLine.addAll(argumentsContents);
+ builder.setCommandLine(commandLine.build());
+ } else {
+ Args args = (Args) arguments;
+ builder.setCommandLine(CommandLine.concat(ImmutableList.of(""), args.build()));
+ }
if (commandUnchecked instanceof String) {
builder.setShellCommand((String) commandUnchecked);
@@ -545,7 +580,6 @@
"expected string or list of strings for command instead of "
+ EvalUtils.getDataTypeName(commandUnchecked));
}
- builder.setCommandLine(commandLine.build());
registerSpawnAction(
outputs,
inputs,
@@ -713,6 +747,7 @@
/**
* Returns the proper UTF-8 representation of a String that was erroneously read using Latin1.
+ *
* @param latin1 Input string
* @return The input string, UTF8 encoded
*/
@@ -720,7 +755,199 @@
return new String(latin1.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
}
+ @SkylarkModule(
+ name = "Args",
+ category = SkylarkModuleCategory.BUILTIN,
+ doc =
+ "module providing methods for building memory-efficient command lines.<br><br>"
+ + "See <a href=\"actions.html#run\">ctx.actions.run()</a> or ."
+ + "<a href=\"actions.html#run_shell\">ctx.actions.run_shell()</a>"
+ + "Example:"
+ + "<pre class=language-python>\n"
+ + "# foo_deps and bar_deps are each a large depset of artifacts\n"
+ + "args = ctx.actions.args()\n"
+ + "args.add(\"--foo\")\n"
+ + "args.add(foo_deps)\n"
+ + "args.add(\"--bar\")"
+ + "args.add(bar_deps, join_with=\":\")\n"
+ + "ctx.run(\n"
+ + " arguments = args,\n"
+ + " ...\n"
+ + ")\n"
+ + "# Expands to [\n"
+ + "# \"--foo\",\""
+ + "# ...artfacts from foo_deps,\n"
+ + "# \"--bar\",\n"
+ + "# ...artifacts from bar_deps joined with ':',\n"
+ + "#]"
+ + "</pre>"
+ )
+ static class Args {
+ private final SkylarkCustomCommandLine.Builder commandLine;
+
+ @SkylarkSignature(
+ name = "add",
+ objectType = Args.class,
+ returnType = NoneType.class,
+ doc = "Adds an argument to be dynamically expanded at evaluation time.",
+ parameters = {
+ @Param(name = "self", type = Args.class, doc = "This args object."),
+ @Param(
+ name = "value",
+ type = Object.class,
+ doc =
+ "the object to add to the argument list. "
+ + "If the object is scalar, the object's string representation is added. "
+ + "If it's a <a href=\"list.html\">list</a> or "
+ + "<a href=\"depset.html\">depset</a>, "
+ + "each element's string representation is added."
+ ),
+ @Param(
+ name = "format",
+ type = String.class,
+ named = true,
+ positional = false,
+ defaultValue = "None",
+ noneable = true,
+ doc =
+ "a format string used to format the object(s). "
+ + "The format string is as per pattern % tuple. "
+ + "Limitations: only %d %s %r %% are supported."
+ ),
+ @Param(
+ name = "before_each",
+ type = String.class,
+ named = true,
+ positional = false,
+ defaultValue = "None",
+ noneable = true,
+ doc =
+ "each object in the list is prepended by this string. "
+ + "Only supported for vector arguments."
+ ),
+ @Param(
+ name = "join_with",
+ type = String.class,
+ named = true,
+ positional = false,
+ defaultValue = "None",
+ noneable = true,
+ doc =
+ "each object in the list is joined with this string. "
+ + "Only supported for vector arguments."
+ ),
+ @Param(
+ name = "map_fn",
+ type = BaseFunction.class,
+ named = true,
+ positional = false,
+ defaultValue = "None",
+ noneable = true,
+ doc = "The passed objects are passed through a map function. "
+ )
+ },
+ useLocation = true
+ )
+ public static final BuiltinFunction add =
+ new BuiltinFunction("add") {
+ @SuppressWarnings("unused")
+ public NoneType invoke(
+ Args self,
+ Object value,
+ Object format,
+ Object beforeEach,
+ Object joinWith,
+ Object mapFn,
+ Location loc)
+ throws EvalException {
+ if (value instanceof SkylarkNestedSet || value instanceof SkylarkList) {
+ self.addVectorArg(value, format, beforeEach, joinWith, mapFn, loc);
+ } else {
+ self.addScalarArg(value, format, beforeEach, joinWith, mapFn, loc);
+ }
+ return Runtime.NONE;
+ }
+ };
+
+ private void addVectorArg(
+ Object value, Object format, Object beforeEach, Object joinWith, Object mapFn, Location loc)
+ throws EvalException {
+ if (beforeEach != Runtime.NONE && joinWith != Runtime.NONE) {
+ throw new EvalException(null, "cannot pass both 'before_each' and 'join_with'");
+ }
+ SkylarkCustomCommandLine.VectorArg.Builder vectorArg;
+ if (value instanceof SkylarkNestedSet) {
+ NestedSet<?> nestedSet = ((SkylarkNestedSet) value).getSet(Object.class);
+ vectorArg = new SkylarkCustomCommandLine.VectorArg.Builder(nestedSet);
+ } else {
+ SkylarkList skylarkList = (SkylarkList) value;
+ vectorArg = new SkylarkCustomCommandLine.VectorArg.Builder(skylarkList);
+ }
+ vectorArg.setLocation(loc);
+ if (format != Runtime.NONE) {
+ vectorArg.setFormat((String) format);
+ }
+ if (beforeEach != Runtime.NONE) {
+ vectorArg.setBeforeEach((String) beforeEach);
+ }
+ if (joinWith != Runtime.NONE) {
+ vectorArg.setJoinWith((String) joinWith);
+ }
+ if (mapFn != Runtime.NONE) {
+ vectorArg.setMapFn((BaseFunction) mapFn);
+ }
+ commandLine.add(vectorArg);
+ }
+
+ private void addScalarArg(
+ Object value, Object format, Object beforeEach, Object joinWith, Object mapFn, Location loc)
+ throws EvalException {
+ if (!EvalUtils.isImmutable(value)) {
+ throw new EvalException(null, "arg must be an immutable type");
+ }
+ if (beforeEach != Runtime.NONE) {
+ throw new EvalException(null, "'before_each' is not supported for scalar arguments");
+ }
+ if (joinWith != Runtime.NONE) {
+ throw new EvalException(null, "'join_with' is not supported for scalar arguments");
+ }
+ if (format == Runtime.NONE && mapFn == Runtime.NONE) {
+ commandLine.add(value);
+ } else {
+ ScalarArg.Builder scalarArg = new ScalarArg.Builder(value);
+ scalarArg.setLocation(loc);
+ if (format != Runtime.NONE) {
+ scalarArg.setFormat((String) format);
+ }
+ if (mapFn != Runtime.NONE) {
+ scalarArg.setMapFn((BaseFunction) mapFn);
+ }
+ commandLine.add(scalarArg);
+ }
+ }
+
+ public Args(SkylarkSemanticsOptions skylarkSemantics, EventHandler eventHandler) {
+ this.commandLine = new SkylarkCustomCommandLine.Builder(skylarkSemantics, eventHandler);
+ }
+
+ public SkylarkCustomCommandLine build() {
+ return commandLine.build();
+ }
+
+ static {
+ SkylarkSignatureProcessor.configureSkylarkFunctions(Args.class);
+ }
+ }
+
+ @SkylarkCallable(
+ name = "args",
+ doc = "returns an Args object that can be used to build memory-efficient command lines."
+ )
+ public Args args() {
+ return new Args(
+ skylarkSemanticsOptions, ruleContext.getAnalysisEnvironment().getEventHandler());
+ }
@Override
public boolean isImmutable() {
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkCustomCommandLine.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkCustomCommandLine.java
new file mode 100644
index 0000000..166713e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkCustomCommandLine.java
@@ -0,0 +1,476 @@
+// Copyright 2017 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.
+package com.google.devtools.build.lib.analysis.skylark;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Interner;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.CommandLineExpansionException;
+import com.google.devtools.build.lib.analysis.actions.CommandLine;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.BlazeInterners;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.BaseFunction;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.Mutability;
+import com.google.devtools.build.lib.syntax.Printer;
+import com.google.devtools.build.lib.syntax.SkylarkList;
+import com.google.devtools.build.lib.syntax.SkylarkSemanticsOptions;
+import java.util.ArrayList;
+import java.util.IllegalFormatException;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/** Supports ctx.actions.args() from Skylark. */
+class SkylarkCustomCommandLine extends CommandLine {
+ private final SkylarkSemanticsOptions skylarkSemantics;
+ private final EventHandler eventHandler;
+ private final ImmutableList<Object> arguments;
+
+ private static final Joiner LINE_JOINER = Joiner.on("\n").skipNulls();
+ private static final Joiner FIELD_JOINER = Joiner.on(": ").skipNulls();
+
+ static final class VectorArg {
+ private static Interner<VectorArg> interner = BlazeInterners.newStrongInterner();
+
+ private final boolean isNestedSet;
+ private final boolean hasFormat;
+ private final boolean hasBeforeEach;
+ private final boolean hasJoinWith;
+ private final boolean hasMapFn;
+ private final boolean hasLocation;
+
+ VectorArg(
+ boolean isNestedSet,
+ boolean hasFormat,
+ boolean hasBeforeEach,
+ boolean hasJoinWith,
+ boolean hasMapFn,
+ boolean hasLocation) {
+ this.isNestedSet = isNestedSet;
+ this.hasFormat = hasFormat;
+ this.hasBeforeEach = hasBeforeEach;
+ this.hasJoinWith = hasJoinWith;
+ this.hasMapFn = hasMapFn;
+ this.hasLocation = hasLocation;
+ }
+
+ private static void push(ImmutableList.Builder<Object> arguments, Builder arg) {
+ boolean wantsLocation = arg.format != null || arg.mapFn != null;
+ boolean hasLocation = arg.location != null && wantsLocation;
+ VectorArg vectorArg =
+ new VectorArg(
+ arg.nestedSet != null,
+ arg.format != null,
+ arg.beforeEach != null,
+ arg.joinWith != null,
+ arg.mapFn != null,
+ hasLocation);
+ vectorArg = interner.intern(vectorArg);
+ arguments.add(vectorArg);
+ if (vectorArg.isNestedSet) {
+ arguments.add(arg.nestedSet);
+ } else {
+ ImmutableList<?> list = arg.list.getImmutableList();
+ int count = list.size();
+ arguments.add(count);
+ for (int i = 0; i < count; ++i) {
+ arguments.add(list.get(i));
+ }
+ }
+ if (hasLocation) {
+ arguments.add(arg.location);
+ }
+ if (vectorArg.hasMapFn) {
+ arguments.add(arg.mapFn);
+ }
+ if (vectorArg.hasFormat) {
+ arguments.add(arg.format);
+ }
+ if (vectorArg.hasBeforeEach) {
+ arguments.add(arg.beforeEach);
+ }
+ if (vectorArg.hasJoinWith) {
+ arguments.add(arg.joinWith);
+ }
+ }
+
+ private int eval(
+ List<Object> arguments,
+ int argi,
+ ImmutableList.Builder<String> builder,
+ SkylarkSemanticsOptions skylarkSemantics,
+ EventHandler eventHandler)
+ throws CommandLineExpansionException {
+ final List<Object> mutatedValues;
+ final int count;
+ if (isNestedSet) {
+ NestedSet<?> nestedSet = (NestedSet<?>) arguments.get(argi++);
+ mutatedValues = Lists.newArrayList(nestedSet);
+ count = mutatedValues.size();
+ } else {
+ count = (Integer) arguments.get(argi++);
+ mutatedValues = new ArrayList<>(count);
+ for (int i = 0; i < count; ++i) {
+ mutatedValues.add(arguments.get(argi++));
+ }
+ }
+ final Location location = hasLocation ? (Location) arguments.get(argi++) : null;
+ if (hasMapFn) {
+ BaseFunction mapFn = (BaseFunction) arguments.get(argi++);
+ Object result = applyMapFn(mapFn, mutatedValues, location, skylarkSemantics, eventHandler);
+ if (!(result instanceof List)) {
+ throw new CommandLineExpansionException(
+ errorMessage(
+ "map_fn must return a list, got " + result.getClass().getSimpleName(),
+ location,
+ null));
+ }
+ List resultAsList = (List) result;
+ mutatedValues.clear();
+ mutatedValues.addAll(resultAsList);
+ }
+ for (int i = 0; i < count; ++i) {
+ mutatedValues.set(i, valueToString(mutatedValues.get(i)));
+ }
+ if (hasFormat) {
+ String formatStr = (String) arguments.get(argi++);
+ Formatter formatter = new Formatter(formatStr, location);
+ try {
+ for (int i = 0; i < count; ++i) {
+ mutatedValues.set(i, formatter.format(mutatedValues.get(i)));
+ }
+ } catch (IllegalFormatException e) {
+ throw new CommandLineExpansionException(errorMessage(e.getMessage(), location, null));
+ }
+ }
+ if (hasBeforeEach) {
+ String beforeEach = (String) arguments.get(argi++);
+ for (int i = 0; i < count; ++i) {
+ builder.add(beforeEach);
+ builder.add((String) mutatedValues.get(i));
+ }
+ } else if (hasJoinWith) {
+ String joinWith = (String) arguments.get(argi++);
+ builder.add(Joiner.on(joinWith).join(mutatedValues));
+ } else {
+ for (int i = 0; i < count; ++i) {
+ builder.add((String) mutatedValues.get(i));
+ }
+ }
+ return argi;
+ }
+
+ static class Builder {
+ @Nullable private final SkylarkList<?> list;
+ @Nullable private final NestedSet<?> nestedSet;
+ private String format;
+ private String beforeEach;
+ private String joinWith;
+ private Location location;
+ private BaseFunction mapFn;
+
+ public Builder(SkylarkList<?> list) {
+ this.list = list;
+ this.nestedSet = null;
+ }
+
+ public Builder(NestedSet<?> nestedSet) {
+ this.list = null;
+ this.nestedSet = nestedSet;
+ }
+
+ Builder setLocation(Location location) {
+ this.location = location;
+ return this;
+ }
+
+ Builder setFormat(String format) {
+ this.format = format;
+ return this;
+ }
+
+ Builder setBeforeEach(String beforeEach) {
+ this.beforeEach = beforeEach;
+ return this;
+ }
+
+ public Builder setJoinWith(String joinWith) {
+ this.joinWith = joinWith;
+ return this;
+ }
+
+ public Builder setMapFn(BaseFunction mapFn) {
+ this.mapFn = mapFn;
+ return this;
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ VectorArg vectorArg = (VectorArg) o;
+ return isNestedSet == vectorArg.isNestedSet
+ && hasFormat == vectorArg.hasFormat
+ && hasBeforeEach == vectorArg.hasBeforeEach
+ && hasJoinWith == vectorArg.hasJoinWith
+ && hasMapFn == vectorArg.hasMapFn
+ && hasLocation == vectorArg.hasLocation;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(
+ isNestedSet, hasFormat, hasBeforeEach, hasJoinWith, hasMapFn, hasLocation);
+ }
+ }
+
+ static final class ScalarArg {
+ private static Interner<ScalarArg> interner = BlazeInterners.newStrongInterner();
+
+ private final boolean hasFormat;
+ private final boolean hasMapFn;
+ private final boolean hasLocation;
+
+ public ScalarArg(boolean hasFormat, boolean hasMapFn, boolean hasLocation) {
+ this.hasFormat = hasFormat;
+ this.hasMapFn = hasMapFn;
+ this.hasLocation = hasLocation;
+ }
+
+ private static void push(ImmutableList.Builder<Object> arguments, Builder arg) {
+ boolean wantsLocation = arg.format != null || arg.mapFn != null;
+ boolean hasLocation = arg.location != null && wantsLocation;
+ ScalarArg scalarArg = new ScalarArg(arg.format != null, arg.mapFn != null, hasLocation);
+ scalarArg = interner.intern(scalarArg);
+ arguments.add(scalarArg);
+ arguments.add(arg.object);
+ if (hasLocation) {
+ arguments.add(arg.location);
+ }
+ if (scalarArg.hasMapFn) {
+ arguments.add(arg.mapFn);
+ }
+ if (scalarArg.hasFormat) {
+ arguments.add(arg.format);
+ }
+ }
+
+ private int eval(
+ List<Object> arguments,
+ int argi,
+ ImmutableList.Builder<String> builder,
+ SkylarkSemanticsOptions skylarkSemantics,
+ EventHandler eventHandler)
+ throws CommandLineExpansionException {
+ Object object = arguments.get(argi++);
+ final Location location = hasLocation ? (Location) arguments.get(argi++) : null;
+ if (hasMapFn) {
+ BaseFunction mapFn = (BaseFunction) arguments.get(argi++);
+ object = applyMapFn(mapFn, object, location, skylarkSemantics, eventHandler);
+ }
+ object = valueToString(object);
+ if (hasFormat) {
+ String formatStr = (String) arguments.get(argi++);
+ Formatter formatter = new Formatter(formatStr, location);
+ object = formatter.format(object);
+ }
+ builder.add((String) object);
+ return argi;
+ }
+
+ static class Builder {
+ private Object object;
+ private String format;
+ private BaseFunction mapFn;
+ private Location location;
+
+ Builder(Object object) {
+ this.object = object;
+ }
+
+ Builder setLocation(Location location) {
+ this.location = location;
+ return this;
+ }
+
+ Builder setFormat(String format) {
+ this.format = format;
+ return this;
+ }
+
+ public Builder setMapFn(BaseFunction mapFn) {
+ this.mapFn = mapFn;
+ return this;
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ScalarArg scalarArg = (ScalarArg) o;
+ return hasFormat == scalarArg.hasFormat
+ && hasMapFn == scalarArg.hasMapFn
+ && hasLocation == scalarArg.hasLocation;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(hasFormat, hasMapFn, hasLocation);
+ }
+ }
+
+ static class Builder {
+ private final SkylarkSemanticsOptions skylarkSemantics;
+ private final ImmutableList.Builder<Object> arguments = ImmutableList.builder();
+ private final EventHandler eventHandler;
+
+ public Builder(SkylarkSemanticsOptions skylarkSemantics, EventHandler eventHandler) {
+ this.skylarkSemantics = skylarkSemantics;
+ this.eventHandler = eventHandler;
+ }
+
+ void add(Object object) {
+ arguments.add(object);
+ }
+
+ void add(VectorArg.Builder vectorArg) {
+ VectorArg.push(arguments, vectorArg);
+ }
+
+ void add(ScalarArg.Builder scalarArg) {
+ ScalarArg.push(arguments, scalarArg);
+ }
+
+ SkylarkCustomCommandLine build() {
+ return new SkylarkCustomCommandLine(this);
+ }
+ }
+
+ SkylarkCustomCommandLine(Builder builder) {
+ this.arguments = builder.arguments.build();
+ this.skylarkSemantics = builder.skylarkSemantics;
+ this.eventHandler = builder.eventHandler;
+ }
+
+ @Override
+ public Iterable<String> arguments() throws CommandLineExpansionException {
+ ImmutableList.Builder<String> result = ImmutableList.builder();
+ for (int argi = 0; argi < arguments.size(); ) {
+ Object arg = arguments.get(argi++);
+ if (arg instanceof VectorArg) {
+ argi = ((VectorArg) arg).eval(arguments, argi, result, skylarkSemantics, eventHandler);
+ } else if (arg instanceof ScalarArg) {
+ argi = ((ScalarArg) arg).eval(arguments, argi, result, skylarkSemantics, eventHandler);
+ } else {
+ result.add(valueToString(arg));
+ }
+ }
+ return result.build();
+ }
+
+ private static String valueToString(Object value) {
+ if (value instanceof Artifact) {
+ Artifact artifact = (Artifact) value;
+ return artifact.getExecPath().getPathString();
+ }
+ return value.toString();
+ }
+
+ private static class Formatter {
+ private final String formatStr;
+ @Nullable private final Location location;
+ private final ArrayList<Object> args;
+
+ public Formatter(String formatStr, Location location) {
+ this.formatStr = formatStr;
+ this.location = location;
+ this.args = new ArrayList<>(1); // Reused arg list to reduce GC
+ this.args.add(null);
+ }
+
+ String format(Object object) throws CommandLineExpansionException {
+ try {
+ args.set(0, object);
+ return Printer.getPrinter().formatWithList(formatStr, args).toString();
+ } catch (IllegalFormatException e) {
+ throw new CommandLineExpansionException(errorMessage(e.getMessage(), location, null));
+ }
+ }
+ }
+
+ private static Object applyMapFn(
+ BaseFunction mapFn,
+ Object arg,
+ Location location,
+ SkylarkSemanticsOptions skylarkSemantics,
+ EventHandler eventHandler)
+ throws CommandLineExpansionException {
+ ImmutableList<Object> args = ImmutableList.of(arg);
+ try (Mutability mutability = Mutability.create("map_fn")) {
+ Environment env =
+ Environment.builder(mutability)
+ .setSemantics(skylarkSemantics)
+ .setEventHandler(eventHandler)
+ .build();
+ return mapFn.call(args, ImmutableMap.of(), null, env);
+ } catch (EvalException e) {
+ throw new CommandLineExpansionException(errorMessage(e.getMessage(), location, e.getCause()));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new CommandLineExpansionException(
+ errorMessage("Thread was interrupted", location, null));
+ }
+ }
+
+ private static String errorMessage(
+ String message, @Nullable Location location, @Nullable Throwable cause) {
+ return LINE_JOINER.join(
+ "\n", FIELD_JOINER.join(location, message), getCauseMessage(cause, message));
+ }
+
+ private static String getCauseMessage(@Nullable Throwable cause, String message) {
+ if (cause == null) {
+ return null;
+ }
+ String causeMessage = cause.getMessage();
+ if (causeMessage == null) {
+ return null;
+ }
+ if (message == null) {
+ return causeMessage;
+ }
+ // Skip the cause if it is redundant with the message so far.
+ if (message.contains(causeMessage)) {
+ return null;
+ }
+ return causeMessage;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java
index 7569fe7..f7b76ab 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java
@@ -202,7 +202,7 @@
@Nullable AspectDescriptor aspectDescriptor,
SkylarkSemanticsOptions skylarkSemantics)
throws EvalException, InterruptedException {
- this.actionFactory = new SkylarkActionFactory(this, ruleContext);
+ this.actionFactory = new SkylarkActionFactory(this, skylarkSemantics, ruleContext);
this.ruleContext = Preconditions.checkNotNull(ruleContext);
this.ruleLabelCanonicalName = ruleContext.getLabel().getCanonicalForm();
this.fragments = new FragmentCollection(ruleContext, ConfigurationTransition.NONE);
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleImplementationFunctions.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleImplementationFunctions.java
index 2e78a66..f83eb5b 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleImplementationFunctions.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleImplementationFunctions.java
@@ -25,6 +25,7 @@
import com.google.devtools.build.lib.analysis.Runfiles;
import com.google.devtools.build.lib.analysis.RunfilesProvider;
import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.skylark.SkylarkActionFactory.Args;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.skylarkinterface.Param;
@@ -117,12 +118,16 @@
),
@Param(
name = "arguments",
- type = SkylarkList.class,
- generic1 = String.class,
+ allowedTypes = {
+ @ParamType(type = SkylarkList.class),
+ @ParamType(type = Args.class),
+ },
defaultValue = "[]",
named = true,
positional = false,
- doc = "command line arguments of the action."
+ doc =
+ "command line arguments of the action."
+ + "May be either a list or an actions.args() object."
),
@Param(
name = "mnemonic",
@@ -213,7 +218,7 @@
SkylarkList outputs,
Object inputs,
Object executableUnchecked,
- SkylarkList arguments,
+ Object arguments,
Object mnemonicUnchecked,
Object commandUnchecked,
Object progressMessage,
@@ -224,8 +229,8 @@
Location loc,
Environment env)
throws EvalException {
- checkDeprecated("ctx.actions.run or ctx.actions.run_shell", "ctx.action", loc,
- env.getSemantics());
+ checkDeprecated(
+ "ctx.actions.run or ctx.actions.run_shell", "ctx.action", loc, env.getSemantics());
ctx.checkMutable("action");
if ((commandUnchecked == Runtime.NONE) == (executableUnchecked == Runtime.NONE)) {
throw new EvalException(
@@ -233,31 +238,32 @@
}
boolean hasCommand = commandUnchecked != Runtime.NONE;
if (!hasCommand) {
- ctx.actions().run(
- outputs,
- inputs,
- executableUnchecked,
- arguments,
- mnemonicUnchecked,
- progressMessage,
- useDefaultShellEnv,
- envUnchecked,
- executionRequirementsUnchecked,
- inputManifestsUnchecked);
+ ctx.actions()
+ .run(
+ outputs,
+ inputs,
+ executableUnchecked,
+ arguments,
+ mnemonicUnchecked,
+ progressMessage,
+ useDefaultShellEnv,
+ envUnchecked,
+ executionRequirementsUnchecked,
+ inputManifestsUnchecked);
} else {
- ctx.actions().runShell(
- outputs,
- inputs,
- arguments,
- mnemonicUnchecked,
- commandUnchecked,
- progressMessage,
- useDefaultShellEnv,
- envUnchecked,
- executionRequirementsUnchecked,
- inputManifestsUnchecked
- );
+ ctx.actions()
+ .runShell(
+ outputs,
+ inputs,
+ arguments,
+ mnemonicUnchecked,
+ commandUnchecked,
+ progressMessage,
+ useDefaultShellEnv,
+ envUnchecked,
+ executionRequirementsUnchecked,
+ inputManifestsUnchecked);
}
return Runtime.NONE;
}
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleImplementationFunctionsTest.java b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleImplementationFunctionsTest.java
index 975a98b..1a443a1 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleImplementationFunctionsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleImplementationFunctionsTest.java
@@ -22,6 +22,7 @@
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.CommandLineExpansionException;
import com.google.devtools.build.lib.actions.CompositeRunfilesSupplier;
import com.google.devtools.build.lib.actions.RunfilesSupplier;
import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
@@ -1735,6 +1736,181 @@
getConfiguredTarget("//test:silly");
}
+ @Test
+ public void testLazyArgs() throws Exception {
+ SkylarkRuleContext ruleContext = createRuleContext("//foo:foo");
+ evalRuleContextCode(
+ ruleContext,
+ "def map_scalar(val): return 'mapped' + val",
+ "def map_vector(vals): return [x + 1 for x in vals]",
+ "args = ruleContext.actions.args()",
+ "args.add('--foo')",
+ "args.add('foo', format='format%s')",
+ "args.add('foo', map_fn=map_scalar)",
+ "args.add([1, 2])",
+ "args.add([1, 2], join_with=':')",
+ "args.add([1, 2], before_each='-before')",
+ "args.add([1, 2], format='format/%s')",
+ "args.add([1, 2], map_fn=map_vector)",
+ "args.add([1, 2], format='format/%s', join_with=':')",
+ "args.add(ruleContext.files.srcs)",
+ "args.add(ruleContext.files.srcs, format='format/%s')",
+ "ruleContext.actions.run(",
+ " inputs = depset(ruleContext.files.srcs),",
+ " outputs = ruleContext.files.srcs,",
+ " arguments = args,",
+ " executable = ruleContext.files.tools[0],",
+ ")");
+ SpawnAction action =
+ (SpawnAction)
+ Iterables.getOnlyElement(
+ ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+ assertThat(action.getArguments())
+ .containsExactly(
+ "foo/t.exe",
+ "--foo",
+ "formatfoo",
+ "mappedfoo",
+ "1",
+ "2",
+ "1:2",
+ "-before",
+ "1",
+ "-before",
+ "2",
+ "format/1",
+ "format/2",
+ "2",
+ "3",
+ "format/1:format/2",
+ "foo/a.txt",
+ "foo/b.img",
+ "format/foo/a.txt",
+ "format/foo/b.img")
+ .inOrder();
+ }
+
+ @Test
+ public void testScalarJoinWithErrorMessage() throws Exception {
+ SkylarkRuleContext ruleContext = createRuleContext("//foo:foo");
+ checkError(
+ ruleContext,
+ "'join_with' is not supported for scalar arguments",
+ "args = ruleContext.actions.args()\n" + "args.add(1, join_with=':')");
+ }
+
+ @Test
+ public void testScalarBeforeEachErrorMessage() throws Exception {
+ SkylarkRuleContext ruleContext = createRuleContext("//foo:foo");
+ checkError(
+ ruleContext,
+ "'before_each' is not supported for scalar arguments",
+ "args = ruleContext.actions.args()\n" + "args.add(1, before_each='illegal')");
+ }
+
+ @Test
+ public void testLazyArgIllegalFormatString() throws Exception {
+ SkylarkRuleContext ruleContext = createRuleContext("//foo:foo");
+ evalRuleContextCode(
+ ruleContext,
+ "args = ruleContext.actions.args()",
+ "args.add([1, 2], format='format/%s%s')", // Expects two args, will only be given one
+ "ruleContext.actions.run(",
+ " inputs = depset(ruleContext.files.srcs),",
+ " outputs = ruleContext.files.srcs,",
+ " arguments = args,",
+ " executable = ruleContext.files.tools[0],",
+ ")");
+ SpawnAction action =
+ (SpawnAction)
+ Iterables.getOnlyElement(
+ ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+ try {
+ action.getArguments();
+ fail();
+ } catch (CommandLineExpansionException e) {
+ assertThat(e.getMessage()).contains("not enough arguments");
+ }
+ }
+
+ @Test
+ public void testLazyArgBadMapFn() throws Exception {
+ SkylarkRuleContext ruleContext = createRuleContext("//foo:foo");
+ evalRuleContextCode(
+ ruleContext,
+ "args = ruleContext.actions.args()",
+ "def bad_fn(args): 'hello'.nosuchmethod()",
+ "args.add([1, 2], map_fn=bad_fn)",
+ "ruleContext.actions.run(",
+ " inputs = depset(ruleContext.files.srcs),",
+ " outputs = ruleContext.files.srcs,",
+ " arguments = args,",
+ " executable = ruleContext.files.tools[0],",
+ ")");
+ SpawnAction action =
+ (SpawnAction)
+ Iterables.getOnlyElement(
+ ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+ try {
+ action.getArguments();
+ fail();
+ } catch (CommandLineExpansionException e) {
+ assertThat(e.getMessage()).contains("type 'string' has no method nosuchmethod()");
+ }
+ }
+
+ @Test
+ public void testLazyArgMapFnReturnsWrongType() throws Exception {
+ SkylarkRuleContext ruleContext = createRuleContext("//foo:foo");
+ evalRuleContextCode(
+ ruleContext,
+ "args = ruleContext.actions.args()",
+ "def bad_fn(args): return None",
+ "args.add([1, 2], map_fn=bad_fn)",
+ "ruleContext.actions.run(",
+ " inputs = depset(ruleContext.files.srcs),",
+ " outputs = ruleContext.files.srcs,",
+ " arguments = args,",
+ " executable = ruleContext.files.tools[0],",
+ ")");
+ SpawnAction action =
+ (SpawnAction)
+ Iterables.getOnlyElement(
+ ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+ try {
+ action.getArguments();
+ fail();
+ } catch (CommandLineExpansionException e) {
+ assertThat(e.getMessage()).contains("map_fn must return a list, got NoneType");
+ }
+ }
+
+ @Test
+ public void createShellWithLazyArgs() throws Exception {
+ SkylarkRuleContext ruleContext = createRuleContext("//foo:foo");
+ evalRuleContextCode(
+ ruleContext,
+ "args = ruleContext.actions.args()",
+ "args.add('--foo')",
+ "ruleContext.actions.run_shell(",
+ " inputs = ruleContext.files.srcs,",
+ " outputs = ruleContext.files.srcs,",
+ " arguments = args,",
+ " mnemonic = 'DummyMnemonic',",
+ " command = 'dummy_command',",
+ " progress_message = 'dummy_message',",
+ " use_default_shell_env = True)");
+ SpawnAction action =
+ (SpawnAction)
+ Iterables.getOnlyElement(
+ ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
+ List<String> args = action.getArguments();
+ // We don't need to assert the entire arg list, just check that
+ // the dummy empty string is inserted followed by '--foo'
+ assertThat(args.get(args.size() - 2)).isEmpty();
+ assertThat(Iterables.getLast(args)).isEqualTo("--foo");
+ }
+
private void setupThrowFunction(BuiltinFunction func) throws Exception {
throwFunction = func;
throwFunction.configure(