Support proxy tree artifact values in the action cache.

When a non-empty `TreeArtifactValue` is comprised of only `ProxyFileArtifactValue` children, record it as a proxy output and reconstruct it using `ProxyMetadataFactory`.

PiperOrigin-RevId: 811937775
Change-Id: I394af43428017ba8cb716722993a4a7dc7e0cae1
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionCacheChecker.java b/src/main/java/com/google/devtools/build/lib/actions/ActionCacheChecker.java
index 3c2251a..2273101 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/ActionCacheChecker.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionCacheChecker.java
@@ -39,6 +39,7 @@
 import com.google.devtools.build.lib.events.EventKind;
 import com.google.devtools.build.lib.skyframe.TreeArtifactValue;
 import com.google.devtools.build.lib.skyframe.TreeArtifactValue.ArchivedRepresentation;
+import com.google.devtools.build.lib.vfs.Dirent;
 import com.google.devtools.build.lib.vfs.OutputPermissions;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import java.io.FileNotFoundException;
@@ -326,6 +327,17 @@
     for (Artifact artifact : action.getOutputs()) {
       if (artifact.isTreeArtifact()) {
         SpecialArtifact parent = (SpecialArtifact) artifact;
+
+        if (proxyOutputs.contains(parent.getExecPathString())) {
+          try {
+            TreeArtifactValue metadata = constructProxyTreeMetadata(parent);
+            mergedTreeMetadata.put(parent, metadata);
+          } catch (IOException e) {
+            // Ignore - we'll get an action cache miss.
+          }
+          continue;
+        }
+
         SerializableTreeArtifactValue cachedTreeMetadata = entry.getOutputTree(parent);
         if (cachedTreeMetadata == null) {
           continue;
@@ -410,6 +422,24 @@
         mergedFileMetadata.buildOrThrow(), mergedTreeMetadata.buildOrThrow());
   }
 
+  private TreeArtifactValue constructProxyTreeMetadata(SpecialArtifact parent)
+      throws IOException, InterruptedException {
+    TreeArtifactValue.Builder tree = TreeArtifactValue.newBuilder(parent);
+    TreeArtifactValue.visitTree(
+        parent.getPath(),
+        (parentRelativePath, type, traversedSymlink) -> {
+          if (type != Dirent.Type.DIRECTORY) {
+            TreeFileArtifact child = TreeFileArtifact.createTreeOutput(parent, parentRelativePath);
+            FileArtifactValue metadata = proxyMetadataFactory.createProxyMetadata(child);
+            // visitTree() uses multiple threads and putChild() is not thread-safe
+            synchronized (tree) {
+              tree.putChild(child, metadata);
+            }
+          }
+        });
+    return tree.build();
+  }
+
   /**
    * Checks whether {@code action} needs to be executed and returns a non-null {@link Token} if so.
    *
diff --git a/src/main/java/com/google/devtools/build/lib/actions/cache/ActionCache.java b/src/main/java/com/google/devtools/build/lib/actions/cache/ActionCache.java
index 5a3e008..1249c94 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/cache/ActionCache.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/cache/ActionCache.java
@@ -168,10 +168,7 @@
 
     /**
      * Returns a list of exec path strings for {@linkplain ProxyFileArtifactValue proxied} outputs.
-     *
-     * <p>Tree artifacts are not currently supported and are never included here.
      */
-    // TODO: b/440119558 - Support action caching of proxied tree artifacts.
     public ImmutableList<String> getProxyOutputs() {
       checkState(!isCorrupted());
       return proxyOutputs;
@@ -371,7 +368,13 @@
         checkArgument(output.isTreeArtifact(), "artifact must be a tree artifact: %s", output);
         String execPath = output.getExecPathString();
         if (saveTreeMetadata) {
-          outputTreeMetadata.put(execPath, SerializableTreeArtifactValue.create(metadata));
+          if (!metadata.getChildValues().isEmpty()
+              && metadata.getChildValues().values().stream()
+                  .allMatch(ProxyFileArtifactValue.class::isInstance)) {
+            proxyOutputs.add(output.getExecPathString());
+          } else {
+            outputTreeMetadata.put(execPath, SerializableTreeArtifactValue.create(metadata));
+          }
         }
         metadataMap.put(execPath, metadata.getMetadata());
         return this;