Support filesets in ctx.actions.args().

For the bazel people: Fileset is an internal packaging rule that isn't open sourced.

This change makes filesets a directory and adds directory expansion support in ctx.actions.args(). This should make these artifacts similar to tree artifacts.

RELNOTES: Support fileset expansion in ctx.actions.args(). Controlled by --incompatible_expand_directories.
PiperOrigin-RevId: 211861523
diff --git a/src/main/java/com/google/devtools/build/lib/actions/Artifact.java b/src/main/java/com/google/devtools/build/lib/actions/Artifact.java
index db97a74..290ea13 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/Artifact.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/Artifact.java
@@ -474,8 +474,6 @@
   /**
    * Returns true iff this is a TreeArtifact representing a directory tree containing Artifacts.
    */
-  // TODO(rduan): Document this Skylark method once TreeArtifact is no longer experimental.
-  @Override
   public boolean isTreeArtifact() {
     return false;
   }
@@ -487,6 +485,11 @@
     return false;
   }
 
+  @Override
+  public boolean isDirectory() {
+    return isTreeArtifact() || isFileset();
+  }
+
   /**
    * Returns true iff metadata cache must return constant metadata for the
    * given artifact.
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 be4d487..b4b0eda 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
@@ -172,7 +172,7 @@
           new ParameterFileWriteAction(
               ruleContext.getActionOwner(),
               skylarkSemantics.incompatibleExpandDirectories()
-                  ? args.getTreeArtifacts()
+                  ? args.getDirectoryArtifacts()
                   : ImmutableList.of(),
               (Artifact) output,
               args.build(),
@@ -352,7 +352,7 @@
                   .setInputs(
                       skylarkSemantics.incompatibleExpandDirectories()
                               && !context.getConfiguration().deferParamFiles()
-                          ? args.getTreeArtifacts()
+                          ? args.getDirectoryArtifacts()
                           : ImmutableList.of())
                   .build();
         }
@@ -550,8 +550,8 @@
     private final Mutability mutability;
     private final SkylarkSemantics skylarkSemantics;
     private final SkylarkCustomCommandLine.Builder commandLine;
-    private List<NestedSet<Object>> potentialTreeArtifacts = new ArrayList<>();
-    private final Set<Artifact> treeArtifacts = new HashSet<>();
+    private List<NestedSet<Object>> potentialDirectoryArtifacts = new ArrayList<>();
+    private final Set<Artifact> directoryArtifacts = new HashSet<>();
     private ParameterFileType parameterFileType = ParameterFileType.SHELL_QUOTED;
     private String flagFormatString;
     private boolean useAlways;
@@ -734,14 +734,14 @@
         SkylarkNestedSet skylarkNestedSet = ((SkylarkNestedSet) value);
         NestedSet<Object> nestedSet = skylarkNestedSet.getSet(Object.class);
         if (expandDirectories) {
-          potentialTreeArtifacts.add(nestedSet);
+          potentialDirectoryArtifacts.add(nestedSet);
         }
         vectorArg = new SkylarkCustomCommandLine.VectorArg.Builder(nestedSet);
       } else {
         @SuppressWarnings("unchecked")
         SkylarkList<Object> skylarkList = (SkylarkList<Object>) value;
         if (expandDirectories) {
-          scanForTreeArtifacts(skylarkList);
+          scanForDirectories(skylarkList);
         }
         vectorArg = new SkylarkCustomCommandLine.VectorArg.Builder(skylarkList);
       }
@@ -816,7 +816,7 @@
 
     private void addScalarArg(Object value, String format, BaseFunction mapFn, Location loc)
         throws EvalException {
-      validateNoTreeArtifact(value, loc);
+      validateNoDirectory(value, loc);
       validateFormatString("format", format, loc);
       if (format == null && mapFn == null) {
         commandLine.add(value);
@@ -827,10 +827,8 @@
       }
     }
 
-    private void validateNoTreeArtifact(Object value, Location loc) throws EvalException {
-      if (skylarkSemantics.incompatibleExpandDirectories()
-          && (value instanceof Artifact)
-          && ((Artifact) value).isTreeArtifact()) {
+    private void validateNoDirectory(Object value, Location loc) throws EvalException {
+      if (skylarkSemantics.incompatibleExpandDirectories() && isDirectory(value)) {
         throw new EvalException(
             loc,
             "Cannot add directories to Args#add since they may expand to multiple values. "
@@ -839,6 +837,10 @@
       }
     }
 
+    private static boolean isDirectory(Object object) {
+      return ((object instanceof Artifact) && ((Artifact) object).isDirectory());
+    }
+
     @Override
     public void useParamsFile(String paramFileArg, Boolean useAlways) throws EvalException {
       if (isImmutable()) {
@@ -903,18 +905,18 @@
       }
     }
 
-    ImmutableSet<Artifact> getTreeArtifacts() {
-      for (Iterable<Object> collection : potentialTreeArtifacts) {
-        scanForTreeArtifacts(collection);
+    ImmutableSet<Artifact> getDirectoryArtifacts() {
+      for (Iterable<Object> collection : potentialDirectoryArtifacts) {
+        scanForDirectories(collection);
       }
-      potentialTreeArtifacts.clear();
-      return ImmutableSet.copyOf(treeArtifacts);
+      potentialDirectoryArtifacts.clear();
+      return ImmutableSet.copyOf(directoryArtifacts);
     }
 
-    private void scanForTreeArtifacts(Iterable<?> objects) {
+    private void scanForDirectories(Iterable<?> objects) {
       for (Object object : objects) {
-        if ((object instanceof Artifact) && ((Artifact) object).isTreeArtifact()) {
-          treeArtifacts.add((Artifact) object);
+        if (isDirectory(object)) {
+          directoryArtifacts.add((Artifact) object);
         }
       }
     }
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
index eb404bb..68ffce3 100644
--- 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
@@ -26,11 +26,16 @@
 import com.google.devtools.build.lib.actions.CommandLineExpansionException;
 import com.google.devtools.build.lib.actions.CommandLineItem;
 import com.google.devtools.build.lib.actions.SingleStringArgFormatter;
+import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.concurrent.BlazeInterners;
 import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.events.NullEventHandler;
+import com.google.devtools.build.lib.exec.FilesetManifest;
+import com.google.devtools.build.lib.exec.FilesetManifest.RelativeSymlinkBehavior;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
+import com.google.devtools.build.lib.skylarkbuildapi.FileApi;
+import com.google.devtools.build.lib.skylarkbuildapi.FileRootApi;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
 import com.google.devtools.build.lib.syntax.BaseFunction;
 import com.google.devtools.build.lib.syntax.Environment;
@@ -41,6 +46,8 @@
 import com.google.devtools.build.lib.syntax.SkylarkList;
 import com.google.devtools.build.lib.syntax.SkylarkSemantics;
 import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.IllegalFormatException;
@@ -194,8 +201,8 @@
       }
       List<Object> expandedValues = originalValues;
       if (artifactExpander != null && (features & EXPAND_DIRECTORIES) != 0) {
-        if (hasTreeArtifact(originalValues)) {
-          expandedValues = expandTreeArtifacts(artifactExpander, originalValues);
+        if (hasDirectory(originalValues)) {
+          expandedValues = expandDirectories(artifactExpander, originalValues);
         }
       }
       List<String> stringValues;
@@ -304,26 +311,38 @@
       return argi;
     }
 
-    private static boolean hasTreeArtifact(List<Object> originalValues) {
+    private static boolean hasDirectory(List<Object> originalValues) {
       int n = originalValues.size();
       for (int i = 0; i < n; ++i) {
         Object object = originalValues.get(i);
-        if ((object instanceof Artifact) && ((Artifact) object).isTreeArtifact()) {
+        if (isDirectory(object)) {
           return true;
         }
       }
       return false;
     }
 
-    private static List<Object> expandTreeArtifacts(
-        Artifact.ArtifactExpander artifactExpander, List<Object> originalValues) {
+    private static boolean isDirectory(Object object) {
+      return ((object instanceof Artifact) && ((Artifact) object).isDirectory());
+    }
+
+    private static List<Object> expandDirectories(
+        Artifact.ArtifactExpander artifactExpander, List<Object> originalValues)
+        throws CommandLineExpansionException {
       List<Object> expandedValues;
       int n = originalValues.size();
       expandedValues = new ArrayList<>(n);
       for (int i = 0; i < n; ++i) {
         Object object = originalValues.get(i);
-        if (object instanceof Artifact && ((Artifact) object).isTreeArtifact()) {
-          artifactExpander.expand((Artifact) object, expandedValues);
+        if (isDirectory(object)) {
+          Artifact artifact = (Artifact) object;
+          if (artifact.isTreeArtifact()) {
+            artifactExpander.expand((Artifact) object, expandedValues);
+          } else if (artifact.isFileset()) {
+            expandFileset(artifactExpander, artifact, expandedValues);
+          } else {
+            throw new AssertionError("Unknown artifact type.");
+          }
         } else {
           expandedValues.add(object);
         }
@@ -331,6 +350,23 @@
       return expandedValues;
     }
 
+    private static void expandFileset(
+        Artifact.ArtifactExpander artifactExpander, Artifact fileset, List<Object> expandedValues)
+        throws CommandLineExpansionException {
+      try {
+        FilesetManifest filesetManifest =
+            FilesetManifest.constructFilesetManifest(
+                artifactExpander.getFileset(fileset),
+                fileset.getExecPath(),
+                RelativeSymlinkBehavior.IGNORE);
+        for (PathFragment relativePath : filesetManifest.getEntries().keySet()) {
+          expandedValues.add(new FilesetSymlinkFile(fileset, relativePath));
+        }
+      } catch (IOException e) {
+        throw new CommandLineExpansionException("Could not expand fileset: " + e.getMessage());
+      }
+    }
+
     private int addToFingerprint(
         List<Object> arguments,
         int argi,
@@ -931,4 +967,85 @@
       this.cause = cause;
     }
   }
+
+  /**
+   * When we expand filesets the user might still expect a File object (since the results may be fed
+   * into map_each. Therefore we synthesize a File object from the fileset symlink.
+   */
+  static class FilesetSymlinkFile implements FileApi, CommandLineItem {
+    private final Artifact fileset;
+    private final PathFragment execPath;
+
+    public FilesetSymlinkFile(Artifact fileset, PathFragment execPath) {
+      this.fileset = fileset;
+      this.execPath = execPath;
+    }
+
+    private PathFragment getExecPath() {
+      return execPath;
+    }
+
+    @Override
+    public String getDirname() {
+      PathFragment parent = getExecPath().getParentDirectory();
+      return (parent == null) ? "/" : parent.getSafePathString();
+    }
+
+    @Override
+    public String getFilename() {
+      return getExecPath().getBaseName();
+    }
+
+    @Override
+    public String getExtension() {
+      return getExecPath().getFileExtension();
+    }
+
+    @Override
+    public Label getOwnerLabel() {
+      return fileset.getOwnerLabel();
+    }
+
+    @Override
+    public FileRootApi getRoot() {
+      return fileset.getRoot();
+    }
+
+    @Override
+    public boolean isSourceArtifact() {
+      // This information is lost to us.
+      // Since the symlinks are always in the output tree, settle for saying "no"
+      return false;
+    }
+
+    @Override
+    public boolean isDirectory() {
+      return false;
+    }
+
+    @Override
+    public String getRunfilesPathString() {
+      PathFragment relativePath = execPath.relativeTo(fileset.getExecPath());
+      return fileset.getRunfilesPath().getRelative(relativePath).getPathString();
+    }
+
+    @Override
+    public String getExecPathString() {
+      return getExecPath().getPathString();
+    }
+
+    @Override
+    public String expandToCommandLine() {
+      return getExecPathString();
+    }
+
+    @Override
+    public void repr(SkylarkPrinter printer) {
+      if (isSourceArtifact()) {
+        printer.append("<source file " + getRunfilesPathString() + ">");
+      } else {
+        printer.append("<generated file " + getRunfilesPathString() + ">");
+      }
+    }
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java
index eb30532..4920e8a 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java
@@ -483,9 +483,10 @@
     // Aggregate top-level Filesets with Filesets nested in Runfiles. Both should be used to update
     // the FileSystem context.
     state.expandedFilesets.forEach(filesetMappings::put);
+    ImmutableMap<Artifact, ImmutableList<FilesetOutputSymlink>> expandedFilesets =
+        ImmutableMap.copyOf(filesetMappings);
     try {
-      state.updateFileSystemContext(skyframeActionExecutor, env, metadataHandler,
-          ImmutableMap.copyOf(filesetMappings));
+      state.updateFileSystemContext(skyframeActionExecutor, env, metadataHandler, expandedFilesets);
     } catch (IOException e) {
       throw new ActionExecutionException(
           "Failed to update filesystem context: ", e, action, /*catastrophe=*/ false);
@@ -495,7 +496,7 @@
             perActionFileCache,
             metadataHandler,
             Collections.unmodifiableMap(state.expandedArtifacts),
-            Collections.unmodifiableMap(state.expandedFilesets),
+            expandedFilesets,
             topLevelFilesets,
             state.actionFileSystem,
             skyframeDepsResult)) {
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/FileApi.java b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/FileApi.java
index 1c69792..06bd6c6 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/FileApi.java
+++ b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/FileApi.java
@@ -40,40 +40,45 @@
 )
 public interface FileApi extends SkylarkValue {
 
-  @SkylarkCallable(name = "dirname", structField = true,
-      doc = "The name of the directory containing this file. It's taken from "
-          + "<a href=\"#path\">path</a> and is always relative to the execution directory.")
-  public String getDirname();
+  @SkylarkCallable(
+      name = "dirname",
+      structField = true,
+      doc =
+          "The name of the directory containing this file. It's taken from "
+              + "<a href=\"#path\">path</a> and is always relative to the execution directory.")
+  String getDirname();
 
-  @SkylarkCallable(name = "basename", structField = true,
+  @SkylarkCallable(
+      name = "basename",
+      structField = true,
       doc = "The base name of this file. This is the name of the file inside the directory.")
-  public String getFilename();
+  String getFilename();
 
   @SkylarkCallable(name = "extension", structField = true, doc = "The file extension of this file.")
-  public String getExtension();
-
-  @SkylarkCallable(name = "owner", structField = true, allowReturnNones = true,
-    doc = "A label of a target that produces this File."
-  )
-  public Label getOwnerLabel();
+  String getExtension();
 
   @SkylarkCallable(
-    name = "root",
-    structField = true,
-    doc = "The root beneath which this file resides."
-  )
-  public FileRootApi getRoot();
+      name = "owner",
+      structField = true,
+      allowReturnNones = true,
+      doc = "A label of a target that produces this File.")
+  Label getOwnerLabel();
 
   @SkylarkCallable(
-    name = "is_source",
-    structField = true,
-    doc = "Returns true if this is a source file, i.e. it is not generated."
-  )
-  public boolean isSourceArtifact();
+      name = "root",
+      structField = true,
+      doc = "The root beneath which this file resides.")
+  FileRootApi getRoot();
+
+  @SkylarkCallable(
+      name = "is_source",
+      structField = true,
+      doc = "Returns true if this is a source file, i.e. it is not generated.")
+  boolean isSourceArtifact();
 
   // TODO(rduan): Document this Skylark method once TreeArtifact is no longer experimental.
   @SkylarkCallable(name = "is_directory", structField = true, documented = false)
-  public boolean isTreeArtifact();
+  boolean isDirectory();
 
   @SkylarkCallable(
       name = "short_path",
@@ -81,23 +86,21 @@
       doc =
           "The path of this file relative to its root. This excludes the aforementioned "
               + "<i>root</i>, i.e. configuration-specific fragments of the path. This is also the "
-              + "path under which the file is mapped if it's in the runfiles of a binary."
-  )
-  public String getRunfilesPathString();
+              + "path under which the file is mapped if it's in the runfiles of a binary.")
+  String getRunfilesPathString();
 
   @SkylarkCallable(
-    name = "path",
-    structField = true,
-    doc =
-        "The execution path of this file, relative to the workspace's execution directory. It "
-            + "consists of two parts, an optional first part called the <i>root</i> (see also the "
-            + "<a href=\"root.html\">root</a> module), and the second part which is the "
-            + "<code>short_path</code>. The root may be empty, which it usually is for "
-            + "non-generated files. For generated files it usually contains a "
-            + "configuration-specific path fragment that encodes things like the target CPU "
-            + "architecture that was used while building said file. Use the "
-            + "<code>short_path</code> for the path under which the file is mapped if it's in the "
-            + "runfiles of a binary."
-  )
-  public String getExecPathString();
+      name = "path",
+      structField = true,
+      doc =
+          "The execution path of this file, relative to the workspace's execution directory. It "
+              + "consists of two parts, an optional first part called the <i>root</i> (see also "
+              + "the <a href=\"root.html\">root</a> module), and the second part which is the "
+              + "<code>short_path</code>. The root may be empty, which it usually is for "
+              + "non-generated files. For generated files it usually contains a "
+              + "configuration-specific path fragment that encodes things like the target CPU "
+              + "architecture that was used while building said file. Use the "
+              + "<code>short_path</code> for the path under which the file is mapped if it's in "
+              + "the runfiles of a binary.")
+  String getExecPathString();
 }