Add ability to write param files to disk.

PiperOrigin-RevId: 169418286
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 9b7eaf4..eb4de93 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
@@ -30,6 +30,7 @@
 import com.google.devtools.build.lib.analysis.actions.CommandLine;
 import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
 import com.google.devtools.build.lib.analysis.actions.ParamFileInfo;
+import com.google.devtools.build.lib.analysis.actions.ParameterFileWriteAction;
 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;
@@ -236,8 +237,12 @@
       @Param(name = "output", type = Artifact.class, doc = "the output file.", named = true),
       @Param(
         name = "content",
-        type = String.class,
-        doc = "the contents of the file.",
+        type = Object.class,
+        allowedTypes = {@ParamType(type = String.class), @ParamType(type = Args.class)},
+        doc =
+            "the contents of the file. "
+                + "May be a either a string or actions.args() object. "
+                + "See <a href=\"actions.html#args\">ctx.actions.args()</a>.",
         named = true
       ),
       @Param(
@@ -249,10 +254,23 @@
       )
     }
   )
-  public void write(Artifact output, String content, Boolean isExecutable) throws EvalException {
+  public void write(Artifact output, Object content, Boolean isExecutable) throws EvalException {
     context.checkMutable("actions.write");
-    FileWriteAction action =
-        FileWriteAction.create(ruleContext, output, content, isExecutable);
+    final Action action;
+    if (content instanceof String) {
+      action = FileWriteAction.create(ruleContext, output, (String) content, isExecutable);
+    } else if (content instanceof Args) {
+      Args args = (Args) content;
+      action =
+          new ParameterFileWriteAction(
+              ruleContext.getActionOwner(),
+              output,
+              args.build(),
+              args.parameterFileType,
+              StandardCharsets.UTF_8);
+    } else {
+      throw new AssertionError("Unexpected type: " + content.getClass().getSimpleName());
+    }
     ruleContext.registerAction(action);
   }
 
@@ -635,7 +653,16 @@
           stringArgs = new ArrayList<>();
         }
         Args args = (Args) value;
-        builder.addCommandLine(args.commandLine.build(), args.paramFileInfo);
+        ParamFileInfo paramFileInfo = null;
+        if (args.flagFormatString != null) {
+          paramFileInfo =
+              ParamFileInfo.builder(args.parameterFileType)
+                  .setFlagFormatString(args.flagFormatString)
+                  .setUseAlways(args.useAlways)
+                  .setCharset(StandardCharsets.UTF_8)
+                  .build();
+        }
+        builder.addCommandLine(args.commandLine.build(), paramFileInfo);
       } else {
         throw new EvalException(
             null,
@@ -846,7 +873,9 @@
 
     private final SkylarkRuleContext context;
     private final SkylarkCustomCommandLine.Builder commandLine;
-    private ParamFileInfo paramFileInfo;
+    private ParameterFileType parameterFileType = ParameterFileType.SHELL_QUOTED;
+    private String flagFormatString;
+    private boolean useAlways;
 
     @SkylarkSignature(
       name = "add",
@@ -1018,22 +1047,10 @@
               "whether to always spill the args to a params file. If false, "
                   + "bazel will decide whether the arguments need to be spilled "
                   + "based on your system and arg length."
-        ),
-        @Param(
-          name = "format",
-          type = String.class,
-          named = true,
-          positional = false,
-          defaultValue = "\"shell\"",
-          doc =
-              "the format of the param file. Must be one of:<br>"
-                  + "\"shell\": All arguments are shell quoted and separated by whitespace<br>"
-                  + "\"multiline\": All arguments are unquoted and separated by newline characters"
         )
       }
     )
-    public void useParamsFile(String paramFileArg, Boolean useAlways, String format)
-        throws EvalException {
+    public void useParamsFile(String paramFileArg, Boolean useAlways) throws EvalException {
       if (isImmutable()) {
         throw new EvalException(null, "cannot modify frozen value");
       }
@@ -1042,22 +1059,44 @@
             null,
             "Invalid value for parameter \"param_file_arg\": Expected string with a single \"%s\"");
       }
-      final ParameterFileType parameterFileType;
-      if (format.equals("shell")) {
-        parameterFileType = ParameterFileType.SHELL_QUOTED;
-      } else if (format.equals("multiline")) {
-        parameterFileType = ParameterFileType.UNQUOTED;
-      } else {
-        throw new EvalException(
-            null,
-            "Invalid value for parameter \"format\": Expected one of \"shell\", \"multiline\"");
-      }
+      this.flagFormatString = paramFileArg;
+      this.useAlways = useAlways;
+    }
 
-      paramFileInfo =
-          ParamFileInfo.builder(parameterFileType)
-              .setUseAlways(useAlways)
-              .setFlagFormatString(paramFileArg)
-              .build();
+    @SkylarkCallable(
+      name = "set_param_file_format",
+      doc = "sets the format of the param file when written to disk",
+      parameters = {
+        @Param(
+          name = "format",
+          type = String.class,
+          named = true,
+          doc =
+              "the format of the param file. Must be one of:<br>"
+                  + "\"shell\": All arguments are shell quoted and separated by whitespace<br>"
+                  + "\"multiline\": All arguments are unquoted and separated by newline characters"
+                  + "The format defaults to \"shell\" if not called."
+        )
+      }
+    )
+    public void setParamFileFormat(String format) throws EvalException {
+      if (isImmutable()) {
+        throw new EvalException(null, "cannot modify frozen value");
+      }
+      final ParameterFileType parameterFileType;
+      switch (format) {
+        case "shell":
+          parameterFileType = ParameterFileType.SHELL_QUOTED;
+          break;
+        case "multiline":
+          parameterFileType = ParameterFileType.UNQUOTED;
+          break;
+        default:
+          throw new EvalException(
+              null,
+              "Invalid value for parameter \"format\": Expected one of \"shell\", \"multiline\"");
+      }
+      this.parameterFileType = parameterFileType;
     }
 
     public Args(
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 983539a..c4f5f5a 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
@@ -57,6 +57,7 @@
 import java.nio.charset.StandardCharsets;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.regex.Pattern;
 import org.junit.Before;
 import org.junit.Rule;
@@ -624,8 +625,7 @@
     SkylarkRuleContext ruleContext = createRuleContext("//foo:foo");
     checkErrorContains(
         ruleContext,
-        "Cannot convert parameter 'content' to type string, in method "
-            + "write(File output, int content, bool is_executable) of 'actions'",
+        "Cannot convert parameter 'content' to type string or Args",
         "ruleContext.actions.write(",
         "  output = ruleContext.files.srcs[0],",
         "  content = 1,",
@@ -1841,6 +1841,27 @@
   }
 
   @Test
+  public void testWriteArgsToParamFile() throws Exception {
+    SkylarkRuleContext ruleContext = createRuleContext("//foo:foo");
+    evalRuleContextCode(
+        ruleContext,
+        "args = ruleContext.actions.args()",
+        "args.add('--foo')",
+        "output=ruleContext.actions.declare_file('out')",
+        "ruleContext.actions.write(",
+        "  output=output,",
+        "  content=args,",
+        ")");
+    List<ActionAnalysisMetadata> actions =
+        ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions();
+    Optional<ActionAnalysisMetadata> action =
+        actions.stream().filter(a -> a instanceof ParameterFileWriteAction).findFirst();
+    assertThat(action.isPresent()).isTrue();
+    ParameterFileWriteAction paramAction = (ParameterFileWriteAction) action.get();
+    assertThat(paramAction.getContents()).containsExactly("--foo");
+  }
+
+  @Test
   public void testLazyArgsWithParamFileInvalidFormatString() throws Exception {
     SkylarkRuleContext ruleContext = createRuleContext("//foo:foo");
     checkError(
@@ -1855,8 +1876,7 @@
     checkError(
         ruleContext,
         "Invalid value for parameter \"format\": Expected one of \"shell\", \"multiline\"",
-        "args = ruleContext.actions.args()\n"
-            + "args.use_param_file('--file=%s', format='illegal')");
+        "args = ruleContext.actions.args()\n" + "args.set_param_file_format('illegal')");
   }
 
   @Test