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(