Use a MetadataConsumer implementation that assists with tree artifact aggregation.

Remove IOException from MetadataConsumer since it is never thrown.

PiperOrigin-RevId: 320714944
diff --git a/src/main/java/com/google/devtools/build/lib/actions/MetadataConsumer.java b/src/main/java/com/google/devtools/build/lib/actions/MetadataConsumer.java
index d737d54..56313f5 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/MetadataConsumer.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/MetadataConsumer.java
@@ -13,7 +13,7 @@
 // limitations under the License.
 package com.google.devtools.build.lib.actions;
 
-import java.io.IOException;
+import java.util.function.BiConsumer;
 
 /**
  * Consumes metadata for artifacts.
@@ -21,6 +21,4 @@
  * <p>May be called when metadata isn't otherwise propagated at the spawn level.
  */
 @FunctionalInterface
-public interface MetadataConsumer {
-  void accept(Artifact artifact, FileArtifactValue value) throws IOException;
-}
+public interface MetadataConsumer extends BiConsumer<Artifact, FileArtifactValue> {}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java
index ac888d6..b60c2ed 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java
@@ -53,6 +53,7 @@
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.Artifact.ArtifactExpanderImpl;
 import com.google.devtools.build.lib.actions.Artifact.OwnerlessArtifactWrapper;
+import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
 import com.google.devtools.build.lib.actions.ArtifactPathResolver;
 import com.google.devtools.build.lib.actions.CachedActionEvent;
 import com.google.devtools.build.lib.actions.EnvironmentalExecException;
@@ -70,6 +71,7 @@
 import com.google.devtools.build.lib.actions.StoppedScanningActionEvent;
 import com.google.devtools.build.lib.actions.UserExecException;
 import com.google.devtools.build.lib.actions.cache.MetadataHandler;
+import com.google.devtools.build.lib.actions.cache.MetadataInjector;
 import com.google.devtools.build.lib.buildtool.BuildRequestOptions;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
@@ -105,6 +107,7 @@
 import com.google.devtools.build.skyframe.SkyFunction.Environment;
 import com.google.devtools.build.skyframe.SkyKey;
 import com.google.devtools.common.options.OptionsProvider;
+import java.io.Closeable;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.util.Collection;
@@ -407,13 +410,13 @@
       ActionPostprocessing postprocessing,
       boolean hasDiscoveredInputs)
       throws ActionExecutionException, InterruptedException {
+    MetadataAggregator metadataAggregator;
     if (actionFileSystem != null) {
+      metadataAggregator = new MetadataAggregator(metadataHandler);
       updateActionFileSystemContext(
-          action,
-          actionFileSystem,
-          env,
-          metadataHandler.getOutputStore()::injectOutputData,
-          expandedFilesets);
+          action, actionFileSystem, env, metadataAggregator, expandedFilesets);
+    } else {
+      metadataAggregator = null;
     }
 
     ActionExecutionContext actionExecutionContext =
@@ -454,7 +457,8 @@
                         actionStartTime,
                         actionExecutionContext,
                         actionLookupData,
-                        postprocessing)));
+                        postprocessing,
+                        metadataAggregator)));
 
     SharedActionCallback callback =
         getSharedActionCallback(env.getListener(), hasDiscoveredInputs, action, actionLookupData);
@@ -535,17 +539,10 @@
       Action action,
       @Nullable ActionExecutionException finalException)
       throws ActionExecutionException {
-    if (finalException != null) {
-      try {
-        context.close();
-      } catch (IOException | RuntimeException e) {
-        finalException.addSuppressed(e);
+    try (Closeable c = context) {
+      if (finalException != null) {
+        throw finalException;
       }
-      throw finalException;
-    }
-
-    try {
-      context.close();
     } catch (IOException e) {
       String message = "Failed to close action output: " + e.getMessage();
       DetailedExitCode code = createDetailedExitCode(message, Code.ACTION_OUTPUT_CLOSE_FAILURE);
@@ -867,6 +864,7 @@
     private final ActionLookupData actionLookupData;
     private final ActionExecutionStatusReporter statusReporter;
     private final ActionPostprocessing postprocessing;
+    @Nullable private final MetadataAggregator metadataAggregator;
 
     ActionRunner(
         Action action,
@@ -874,7 +872,8 @@
         long actionStartTime,
         ActionExecutionContext actionExecutionContext,
         ActionLookupData actionLookupData,
-        ActionPostprocessing postprocessing) {
+        ActionPostprocessing postprocessing,
+        @Nullable MetadataAggregator metadataAggregator) {
       this.action = action;
       this.metadataHandler = metadataHandler;
       this.actionStartTime = actionStartTime;
@@ -882,6 +881,7 @@
       this.actionLookupData = actionLookupData;
       this.statusReporter = statusReporterRef.get();
       this.postprocessing = postprocessing;
+      this.metadataAggregator = metadataAggregator;
     }
 
     @SuppressWarnings("LogAndThrow") // Thrown exception shown in user output, not info logs.
@@ -1101,6 +1101,10 @@
         Preconditions.checkState(action.inputsDiscovered(),
             "Action %s successfully executed, but inputs still not known", action);
 
+        if (metadataAggregator != null) {
+          metadataAggregator.finish();
+        }
+
         if (!checkOutputs(action, metadataHandler)) {
           throw toActionExecutionException(
               "not all outputs were created or valid",
@@ -1753,4 +1757,31 @@
       return input != null ? input : perBuildFileCache.getInput(execPath);
     }
   }
+
+  /**
+   * Assists with aggregation of tree artifacts for an action file system which is only aware of
+   * individual outputs.
+   */
+  private static final class MetadataAggregator implements MetadataConsumer {
+    private final TreeArtifactValue.MultiBuilder treeArtifacts =
+        TreeArtifactValue.newConcurrentMultiBuilder();
+    private final MetadataInjector metadataInjector;
+
+    MetadataAggregator(MetadataInjector metadataInjector) {
+      this.metadataInjector = metadataInjector;
+    }
+
+    @Override
+    public void accept(Artifact output, FileArtifactValue metadata) {
+      if (output.isChildOfDeclaredDirectory()) {
+        treeArtifacts.putChild((TreeFileArtifact) output, metadata);
+      } else {
+        metadataInjector.injectFile(output, metadata);
+      }
+    }
+
+    void finish() {
+      treeArtifacts.injectTo(metadataInjector);
+    }
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileInfo.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileInfo.java
index 6f148aa..c4fe9a8 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileInfo.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileInfo.java
@@ -25,6 +25,7 @@
 import java.io.OutputStream;
 import java.nio.ByteBuffer;
 import java.nio.channels.ReadableByteChannel;
+import java.util.function.Consumer;
 
 /**
  * InMemoryFileInfo manages file contents by storing them entirely in memory.
@@ -111,10 +112,10 @@
    * A {@link ByteArrayOutputStream} which notifiers a callback when it has flushed its data.
    */
   public static class InMemoryOutputStream extends ByteArrayOutputStream {
-    private final IOByteReceiver receiver;
+    private final Consumer<byte[]> receiver;
     private boolean closed = false;
 
-    public InMemoryOutputStream(IOByteReceiver receiver) {
+    public InMemoryOutputStream(Consumer<byte[]> receiver) {
       this.receiver = receiver;
     }
 
@@ -137,22 +138,14 @@
     }
 
     @Override
-    public synchronized void close() throws IOException {
+    public synchronized void close() {
       flush();
       closed = true;
     }
 
     @Override
-    public synchronized void flush() throws IOException {
+    public synchronized void flush() {
       receiver.accept(toByteArray().clone());
     }
   }
-
-  /**
-   * Similar to {@link com.google.common.base.Receiver}, but allows implementations to throw
-   * {@link IOException}.
-   */
-  public interface IOByteReceiver {
-    void accept(byte[] bytes) throws IOException;
-  }
 }