Encode some environmental execution failures with FailureDetails

Details failures represented by EnvironmentalExecException in some
general execution contexts (e.g. AbstractAction, SpawnAction,
StarlarkAction) and some language-specific implementations (e.g.
JavaCompileAction, CppCompileAction, IncludeScanning).

The nullability of EnvironmentalExecException's FailureDetails field
is temporary, and intended to bridge between this CL and its immediate
follow-ups.

RELNOTES: None.
PiperOrigin-RevId: 315815272
diff --git a/src/main/java/com/google/devtools/build/lib/actions/AbstractAction.java b/src/main/java/com/google/devtools/build/lib/actions/AbstractAction.java
index 217182c..10c5165 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/AbstractAction.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/AbstractAction.java
@@ -30,6 +30,7 @@
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.EventHandler;
 import com.google.devtools.build.lib.packages.AspectDescriptor;
+import com.google.devtools.build.lib.server.FailureDetails.Execution.Code;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec.VisibleForSerialization;
 import com.google.devtools.build.lib.skylarkbuildapi.ActionApi;
 import com.google.devtools.build.lib.skylarkbuildapi.CommandLineArgsApi;
@@ -398,20 +399,25 @@
    * checking, this method must be called.
    */
   protected void checkInputsForDirectories(
-      EventHandler eventHandler, MetadataProvider metadataProvider) throws IOException {
+      EventHandler eventHandler, MetadataProvider metadataProvider) throws ExecException {
     // Report "directory dependency checking" warning only for non-generated directories (generated
     // ones will be reported earlier).
     for (Artifact input : getMandatoryInputs().toList()) {
       // Assume that if the file did not exist, we would not have gotten here.
-      if (input.isSourceArtifact() && metadataProvider.getMetadata(input).getType().isDirectory()) {
-        // TODO(ulfjack): What about dependency checking of special files?
-        eventHandler.handle(
-            Event.warn(
-                getOwner().getLocation(),
-                String.format(
-                    "input '%s' to %s is a directory; "
-                        + "dependency checking of directories is unsound",
-                    input.prettyPrint(), getOwner().getLabel())));
+      try {
+        if (input.isSourceArtifact()
+            && metadataProvider.getMetadata(input).getType().isDirectory()) {
+          // TODO(ulfjack): What about dependency checking of special files?
+          eventHandler.handle(
+              Event.warn(
+                  getOwner().getLocation(),
+                  String.format(
+                      "input '%s' to %s is a directory; "
+                          + "dependency checking of directories is unsound",
+                      input.prettyPrint(), getOwner().getLabel())));
+        }
+      } catch (IOException e) {
+        throw new EnvironmentalExecException(e, Code.INPUT_DIRECTORY_CHECK_IO_EXCEPTION);
       }
     }
   }
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionException.java b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionException.java
index b529b89..6aa9d7f 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionException.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionException.java
@@ -82,12 +82,15 @@
   }
 
   public ActionExecutionException(
-      String message, ActionAnalysisMetadata action, boolean catastrophe, ExitCode exitCode) {
+      String message,
+      ActionAnalysisMetadata action,
+      boolean catastrophe,
+      DetailedExitCode detailedExitCode) {
     super(message);
     this.action = action;
     this.catastrophe = catastrophe;
-    this.detailedExitCode = DetailedExitCode.justExitCode(exitCode);
-    this.rootCauses = rootCausesFromAction(action, detailedExitCode);
+    this.detailedExitCode = detailedExitCode;
+    this.rootCauses = rootCausesFromAction(action, this.detailedExitCode);
   }
 
   public ActionExecutionException(
diff --git a/src/main/java/com/google/devtools/build/lib/actions/EnvironmentalExecException.java b/src/main/java/com/google/devtools/build/lib/actions/EnvironmentalExecException.java
index 37df0c9..55a60d1 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/EnvironmentalExecException.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/EnvironmentalExecException.java
@@ -15,8 +15,13 @@
 package com.google.devtools.build.lib.actions;
 
 import com.google.common.base.Throwables;
+import com.google.devtools.build.lib.server.FailureDetails;
+import com.google.devtools.build.lib.server.FailureDetails.Execution;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.util.DetailedExitCode;
 import com.google.devtools.build.lib.util.ExitCode;
 import java.io.IOException;
+import javax.annotation.Nullable;
 
 /**
  * An ExecException which is reports an issue executing an action due to an external problem on the
@@ -32,34 +37,43 @@
  * directory or denied file system access.
  */
 public class EnvironmentalExecException extends ExecException {
-  public EnvironmentalExecException(IOException cause) {
+  // TODO(b/138456686): Make this not nullable.
+  @Nullable private final FailureDetail failureDetail;
+
+  public EnvironmentalExecException(IOException cause, FailureDetails.Execution.Code code) {
     super("unexpected I/O exception", cause);
+    this.failureDetail =
+        FailureDetail.newBuilder().setExecution(Execution.newBuilder().setCode(code)).build();
+  }
+
+  public EnvironmentalExecException(IOException cause, FailureDetail failureDetail) {
+    super(failureDetail.getMessage(), cause);
+    this.failureDetail = failureDetail;
   }
 
   public EnvironmentalExecException(String message, Throwable cause) {
     super(message, cause);
+    failureDetail = null;
   }
 
   public EnvironmentalExecException(String message) {
     super(message);
+    failureDetail = null;
   }
 
   @Override
   public ActionExecutionException toActionExecutionException(
       String messagePrefix, boolean verboseFailures, Action action) {
-    if (getCause() != null) {
-      String message =
-          messagePrefix
-              + " failed due to "
-              + getMessage()
-              + "\n"
-              + Throwables.getStackTraceAsString(getCause());
-      return new ActionExecutionException(
-          message, action, isCatastrophic(), ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
-    } else {
-      String message = messagePrefix + " failed due to " + getMessage();
-      return new ActionExecutionException(
-          message, action, isCatastrophic(), ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
-    }
+    String message =
+        String.format(
+            "%s failed due to %s%s",
+            messagePrefix,
+            getMessage(),
+            getCause() == null ? "" : ("\n" + Throwables.getStackTraceAsString(getCause())));
+    DetailedExitCode detailedExitCode =
+        failureDetail == null
+            ? DetailedExitCode.justExitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR)
+            : DetailedExitCode.of(failureDetail.toBuilder().setMessage(message).build());
+    return new ActionExecutionException(message, action, isCatastrophic(), detailedExitCode);
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BUILD b/src/main/java/com/google/devtools/build/lib/analysis/BUILD
index dd89c11..95e66af 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BUILD
@@ -1414,6 +1414,7 @@
         "//src/main/java/com/google/devtools/build/lib/syntax:evaluator",
         "//src/main/java/com/google/devtools/build/lib/util",
         "//src/main/java/com/google/devtools/build/lib/util:string",
+        "//src/main/protobuf:failure_details_java_proto",
         "//third_party:guava",
         "//third_party:jsr305",
     ],
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/LocalTemplateExpansionStrategy.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/LocalTemplateExpansionStrategy.java
index cba7c74..01fe4df 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/actions/LocalTemplateExpansionStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/LocalTemplateExpansionStrategy.java
@@ -18,12 +18,13 @@
 import com.google.devtools.build.lib.actions.ArtifactPathResolver;
 import com.google.devtools.build.lib.actions.EnvironmentalExecException;
 import com.google.devtools.build.lib.actions.SpawnContinuation;
+import com.google.devtools.build.lib.server.FailureDetails;
 import com.google.devtools.build.lib.util.StringUtilities;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 
-/** Strategy to perform tempate expansion locally */
+/** Strategy to perform template expansion locally. */
 public class LocalTemplateExpansionStrategy implements TemplateExpansionContext {
   public static final Class<LocalTemplateExpansionStrategy> TYPE =
       LocalTemplateExpansionStrategy.class;
@@ -46,7 +47,9 @@
           .beginWriteOutputToFile(
               action, ctx, deterministicWriter, action.makeExecutable(), /*isRemotable=*/ true);
     } catch (IOException e) {
-      return SpawnContinuation.failedWithExecException(new EnvironmentalExecException(e));
+      return SpawnContinuation.failedWithExecException(
+          new EnvironmentalExecException(
+              e, FailureDetails.Execution.Code.LOCAL_TEMPLATE_EXPANSION_FAILURE));
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/SpawnAction.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/SpawnAction.java
index 8ae3bdf..d677b57 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/actions/SpawnAction.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/SpawnAction.java
@@ -49,7 +49,6 @@
 import com.google.devtools.build.lib.actions.CommandLines.ExpandedCommandLines;
 import com.google.devtools.build.lib.actions.CompositeRunfilesSupplier;
 import com.google.devtools.build.lib.actions.EmptyRunfilesSupplier;
-import com.google.devtools.build.lib.actions.EnvironmentalExecException;
 import com.google.devtools.build.lib.actions.ExecException;
 import com.google.devtools.build.lib.actions.FilesetOutputSymlink;
 import com.google.devtools.build.lib.actions.ParamFileInfo;
@@ -88,7 +87,6 @@
 import com.google.errorprone.annotations.DoNotCall;
 import com.google.errorprone.annotations.FormatMethod;
 import com.google.errorprone.annotations.FormatString;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -304,7 +302,8 @@
   }
 
   /** Hook for subclasses to perform work before the spawn is executed. */
-  protected void beforeExecute(ActionExecutionContext actionExecutionContext) throws IOException {}
+  protected void beforeExecute(ActionExecutionContext actionExecutionContext)
+      throws ExecException {}
 
   /**
    * Hook for subclasses to perform work after the spawn is executed. This method is only executed
@@ -313,7 +312,7 @@
    */
   protected void afterExecute(
       ActionExecutionContext actionExecutionContext, List<SpawnResult> spawnResults)
-      throws IOException, ExecException {}
+      throws ExecException {}
 
   @Override
   public final ActionContinuationOrResult beginExecution(
@@ -324,9 +323,8 @@
     try {
       beforeExecute(actionExecutionContext);
       spawn = getSpawn(actionExecutionContext);
-    } catch (IOException e) {
-      throw toActionExecutionException(
-          new EnvironmentalExecException(e), actionExecutionContext.showVerboseFailures(label));
+    } catch (ExecException e) {
+      throw toActionExecutionException(e, actionExecutionContext.showVerboseFailures(label));
     } catch (CommandLineExpansionException e) {
       throw createDetailedException(e, Code.COMMAND_LINE_EXPANSION_FAILURE);
     }
@@ -1404,9 +1402,6 @@
           return ActionContinuationOrResult.of(ActionResult.create(nextContinuation.get()));
         }
         return new SpawnActionContinuation(actionExecutionContext, nextContinuation, label);
-      } catch (IOException e) {
-        throw toActionExecutionException(
-            new EnvironmentalExecException(e), actionExecutionContext.showVerboseFailures(label));
       } catch (ExecException e) {
         throw toActionExecutionException(e, actionExecutionContext.showVerboseFailures(label));
       }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/StarlarkAction.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/StarlarkAction.java
index 6950b57..2fef68c 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/actions/StarlarkAction.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/StarlarkAction.java
@@ -26,6 +26,7 @@
 import com.google.devtools.build.lib.actions.CommandLineExpansionException;
 import com.google.devtools.build.lib.actions.CommandLines;
 import com.google.devtools.build.lib.actions.CommandLines.CommandLineLimits;
+import com.google.devtools.build.lib.actions.EnvironmentalExecException;
 import com.google.devtools.build.lib.actions.ExecException;
 import com.google.devtools.build.lib.actions.ExecutionRequirements;
 import com.google.devtools.build.lib.actions.ResourceSet;
@@ -37,6 +38,9 @@
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.server.FailureDetails;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.server.FailureDetails.StarlarkAction.Code;
 import java.io.BufferedReader;
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -175,7 +179,7 @@
   @Override
   protected void afterExecute(
       ActionExecutionContext actionExecutionContext, List<SpawnResult> spawnResults)
-      throws IOException, ExecException {
+      throws ExecException {
     if (!unusedInputsList.isPresent()) {
       return;
     }
@@ -195,6 +199,10 @@
         }
         usedInputs.remove(line);
       }
+    } catch (IOException e) {
+      throw new EnvironmentalExecException(
+          e,
+          createFailureDetail("Unused inputs read failure", Code.UNUSED_INPUT_LIST_READ_FAILURE));
     }
     updateInputs(NestedSetBuilder.wrap(Order.STABLE_ORDER, usedInputs.values()));
   }
@@ -210,6 +218,13 @@
     return allInputs;
   }
 
+  private static FailureDetail createFailureDetail(String message, Code detailedCode) {
+    return FailureDetail.newBuilder()
+        .setMessage(message)
+        .setStarlarkAction(FailureDetails.StarlarkAction.newBuilder().setCode(detailedCode))
+        .build();
+  }
+
   /** Builder class to construct {@link StarlarkAction} instances. */
   public static class Builder extends SpawnAction.Builder {
 
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/extra/ExtraAction.java b/src/main/java/com/google/devtools/build/lib/analysis/extra/ExtraAction.java
index 34a5673..541b274 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/extra/ExtraAction.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/extra/ExtraAction.java
@@ -29,12 +29,15 @@
 import com.google.devtools.build.lib.actions.CommandLines;
 import com.google.devtools.build.lib.actions.CommandLines.CommandLineLimits;
 import com.google.devtools.build.lib.actions.CompositeRunfilesSupplier;
+import com.google.devtools.build.lib.actions.EnvironmentalExecException;
+import com.google.devtools.build.lib.actions.ExecException;
 import com.google.devtools.build.lib.actions.RunfilesSupplier;
 import com.google.devtools.build.lib.actions.SpawnResult;
 import com.google.devtools.build.lib.analysis.actions.SpawnAction;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.server.FailureDetails.Execution.Code;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import java.io.IOException;
 import java.util.Collection;
@@ -156,13 +159,17 @@
   @Override
   protected void afterExecute(
       ActionExecutionContext actionExecutionContext, List<SpawnResult> spawnResults)
-      throws IOException {
+      throws ExecException {
     // PHASE 3: create dummy output.
     // If the user didn't specify output, we need to create dummy output
     // to make blaze schedule this action.
     if (createDummyOutput) {
       for (Artifact output : getOutputs()) {
-        FileSystemUtils.touchFile(actionExecutionContext.getInputPath(output));
+        try {
+          FileSystemUtils.touchFile(actionExecutionContext.getInputPath(output));
+        } catch (IOException e) {
+          throw new EnvironmentalExecException(e, Code.EXTRA_ACTION_OUTPUT_CREATION_FAILURE);
+        }
       }
     }
   }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java
index 788e2af..fd3ada0 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java
@@ -55,6 +55,7 @@
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.server.FailureDetails.Execution.Code;
 import com.google.devtools.build.lib.util.Fingerprint;
 import com.google.devtools.build.lib.util.LoggingUtil;
 import com.google.devtools.build.lib.util.Pair;
@@ -815,7 +816,8 @@
     } catch (ExecException e) {
       throw e.toActionExecutionException(this);
     } catch (IOException e) {
-      throw new EnvironmentalExecException(e).toActionExecutionException(this);
+      throw new EnvironmentalExecException(e, Code.TEST_RUNNER_IO_EXCEPTION)
+          .toActionExecutionException(this);
     }
   }
 
@@ -1105,7 +1107,8 @@
       } catch (ExecException e) {
         throw e.toActionExecutionException(TestRunnerAction.this);
       } catch (IOException e) {
-        throw new EnvironmentalExecException(e).toActionExecutionException(TestRunnerAction.this);
+        throw new EnvironmentalExecException(e, Code.TEST_RUNNER_IO_EXCEPTION)
+            .toActionExecutionException(TestRunnerAction.this);
       }
     }
 
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaAction.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaAction.java
index 19cb6a6..453bfbc 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaAction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaAction.java
@@ -27,6 +27,7 @@
 import com.google.devtools.build.lib.actions.CommandLines;
 import com.google.devtools.build.lib.actions.CommandLines.CommandLineLimits;
 import com.google.devtools.build.lib.actions.EnvironmentalExecException;
+import com.google.devtools.build.lib.actions.ExecException;
 import com.google.devtools.build.lib.actions.RunfilesSupplier;
 import com.google.devtools.build.lib.actions.SpawnResult;
 import com.google.devtools.build.lib.analysis.actions.SpawnAction;
@@ -108,7 +109,7 @@
   }
 
   @Override
-  protected void beforeExecute(ActionExecutionContext actionExecutionContext) throws IOException {
+  protected void beforeExecute(ActionExecutionContext actionExecutionContext) throws ExecException {
     if (!TrackSourceDirectoriesFlag.trackSourceDirectories()) {
       checkInputsForDirectories(
           actionExecutionContext.getEventHandler(), actionExecutionContext.getMetadataProvider());
diff --git a/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java
index feaa37c..723f0fe 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java
@@ -44,6 +44,9 @@
 import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionContext;
 import com.google.devtools.build.lib.profiler.Profiler;
 import com.google.devtools.build.lib.profiler.SilentCloseable;
+import com.google.devtools.build.lib.server.FailureDetails;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.server.FailureDetails.Spawn.Code;
 import com.google.devtools.build.lib.util.CommandFailureUtils;
 import com.google.devtools.build.lib.util.io.FileOutErr;
 import com.google.devtools.build.lib.vfs.Path;
@@ -137,7 +140,12 @@
     } catch (InterruptedIOException e) {
       throw new InterruptedException(e.getMessage());
     } catch (IOException e) {
-      throw new EnvironmentalExecException(e);
+      throw new EnvironmentalExecException(
+          e,
+          FailureDetail.newBuilder()
+              .setMessage("Exec failed due to IOException")
+              .setSpawn(FailureDetails.Spawn.newBuilder().setCode(Code.EXEC_IO_EXCEPTION))
+              .build());
     } catch (SpawnExecException e) {
       ex = e;
       spawnResult = e.getSpawnResult();
diff --git a/src/main/java/com/google/devtools/build/lib/exec/BUILD b/src/main/java/com/google/devtools/build/lib/exec/BUILD
index 491df37..1d4cb16 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/exec/BUILD
@@ -26,6 +26,7 @@
         "//src/main/java/com/google/devtools/build/lib/util/io",
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
+        "//src/main/protobuf:failure_details_java_proto",
         "//third_party:guava",
         "//third_party:jsr305",
     ],
@@ -133,6 +134,7 @@
         "//src/main/java/com/google/devtools/build/lib/profiler",
         "//src/main/java/com/google/devtools/build/lib/profiler:google-auto-profiler-utils",
         "//src/main/java/com/google/devtools/build/lib/vfs",
+        "//src/main/protobuf:failure_details_java_proto",
         "//third_party:guava",
     ],
 )
@@ -324,6 +326,7 @@
         "//src/main/java/com/google/devtools/build/lib/util/io",
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
+        "//src/main/protobuf:failure_details_java_proto",
         "//src/main/protobuf:test_status_java_proto",
         "//third_party:guava",
         "//third_party:jsr305",
@@ -354,6 +357,7 @@
         "//src/main/java/com/google/devtools/build/lib/util/io:out-err",
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
+        "//src/main/protobuf:failure_details_java_proto",
         "//third_party:guava",
         "//third_party:jsr305",
     ],
@@ -373,6 +377,7 @@
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/build/lib/vfs:output_service",
         "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
+        "//src/main/protobuf:failure_details_java_proto",
         "//third_party:guava",
     ],
 )
diff --git a/src/main/java/com/google/devtools/build/lib/exec/FileWriteStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/FileWriteStrategy.java
index dab66bf..826ea39 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/FileWriteStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/FileWriteStrategy.java
@@ -24,6 +24,7 @@
 import com.google.devtools.build.lib.analysis.actions.FileWriteActionContext;
 import com.google.devtools.build.lib.profiler.AutoProfiler;
 import com.google.devtools.build.lib.profiler.GoogleAutoProfilerUtils;
+import com.google.devtools.build.lib.server.FailureDetails.Execution.Code;
 import com.google.devtools.build.lib.vfs.Path;
 import java.io.BufferedOutputStream;
 import java.io.IOException;
@@ -60,7 +61,8 @@
           outputPath.setExecutable(true);
         }
       } catch (IOException e) {
-        return SpawnContinuation.failedWithExecException(new EnvironmentalExecException(e));
+        return SpawnContinuation.failedWithExecException(
+            new EnvironmentalExecException(e, Code.FILE_WRITE_IO_EXCEPTION));
       }
     }
     return SpawnContinuation.immediate();
diff --git a/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java
index 75d7e41..c11ca37 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java
@@ -46,6 +46,7 @@
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.collect.nestedset.Order;
 import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.server.FailureDetails.Execution.Code;
 import com.google.devtools.build.lib.util.Pair;
 import com.google.devtools.build.lib.util.io.FileOutErr;
 import com.google.devtools.build.lib.vfs.FileStatus;
@@ -585,7 +586,7 @@
           streamed.close();
         }
       } catch (IOException e) {
-        throw new EnvironmentalExecException(e);
+        throw new EnvironmentalExecException(e, Code.TEST_OUT_ERR_IO_EXCEPTION);
       }
 
       // SpawnActionContext guarantees the first entry to correspond to the spawn passed in (there
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java
index 067adb5..c8aeb0b 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java
@@ -23,6 +23,7 @@
 import com.google.devtools.build.lib.actions.EnvironmentalExecException;
 import com.google.devtools.build.lib.actions.ExecException;
 import com.google.devtools.build.lib.actions.FilesetOutputSymlink;
+import com.google.devtools.build.lib.server.FailureDetails.Execution.Code;
 import com.google.devtools.build.lib.shell.Command;
 import com.google.devtools.build.lib.shell.CommandException;
 import com.google.devtools.build.lib.util.CommandBuilder;
@@ -140,7 +141,7 @@
       symlinkTreeRoot.createDirectoryAndParents();
       FileSystemUtils.copyFile(inputManifest, getOutputManifest());
     } catch (IOException e) {
-      throw new EnvironmentalExecException(e);
+      throw new EnvironmentalExecException(e, Code.SYMLINK_TREE_MANIFEST_COPY_IO_EXCEPTION);
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategy.java
index 4a1f766..5ff0a40 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategy.java
@@ -27,6 +27,7 @@
 import com.google.devtools.build.lib.analysis.actions.SymlinkTreeActionContext;
 import com.google.devtools.build.lib.profiler.AutoProfiler;
 import com.google.devtools.build.lib.profiler.GoogleAutoProfilerUtils;
+import com.google.devtools.build.lib.server.FailureDetails.Execution.Code;
 import com.google.devtools.build.lib.util.Fingerprint;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.OutputService;
@@ -71,12 +72,7 @@
                   : actionExecutionContext.getInputPath(action.getInputManifest());
           Map<PathFragment, PathFragment> symlinks;
           if (action.getRunfiles() != null) {
-            try {
-              symlinks =
-                  Maps.transformValues(runfilesToMap(action, actionExecutionContext), TO_PATH);
-            } catch (IOException e) {
-              throw new EnvironmentalExecException(e);
-            }
+            symlinks = Maps.transformValues(runfilesToMap(action, actionExecutionContext), TO_PATH);
           } else {
             Preconditions.checkState(action.isFilesetTree());
             Preconditions.checkNotNull(inputManifest);
@@ -105,7 +101,8 @@
                 .createSymlinksDirectly(
                     action.getOutputManifest().getPath().getParentDirectory(), runfiles);
           } catch (IOException e) {
-            throw new EnvironmentalExecException(e).toActionExecutionException(action);
+            throw new EnvironmentalExecException(e, Code.SYMLINK_TREE_CREATION_IO_EXCEPTION)
+                .toActionExecutionException(action);
           }
 
           Path inputManifest =
@@ -133,7 +130,7 @@
   }
 
   private static Map<PathFragment, Artifact> runfilesToMap(
-      SymlinkTreeAction action, ActionExecutionContext actionExecutionContext) throws IOException {
+      SymlinkTreeAction action, ActionExecutionContext actionExecutionContext) {
     // This call outputs warnings about overlapping symlinks. However, this is already called by the
     // SourceManifestAction, so it can happen that we generate the warning twice. If the input
     // manifest is null, then we print the warning. Otherwise we assume that the
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/BUILD b/src/main/java/com/google/devtools/build/lib/rules/cpp/BUILD
index 656374b..69aee2d 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/BUILD
@@ -118,6 +118,7 @@
         "//src/main/java/net/starlark/java/annot",
         "//src/main/protobuf:crosstool_config_java_proto",
         "//src/main/protobuf:extra_actions_base_java_proto",
+        "//src/main/protobuf:failure_details_java_proto",
         "//third_party:auto_value",
         "//third_party:flogger",
         "//third_party:guava",
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileAction.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileAction.java
index b55046d..a5fef2f 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileAction.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileAction.java
@@ -72,6 +72,9 @@
 import com.google.devtools.build.lib.rules.cpp.CcCommon.CoptsFilter;
 import com.google.devtools.build.lib.rules.cpp.CcToolchainFeatures.FeatureConfiguration;
 import com.google.devtools.build.lib.rules.cpp.IncludeScanner.IncludeScanningHeaderData;
+import com.google.devtools.build.lib.server.FailureDetails.CppCompile;
+import com.google.devtools.build.lib.server.FailureDetails.CppCompile.Code;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.skyframe.ActionExecutionValue;
 import com.google.devtools.build.lib.skylarkbuildapi.CommandLineArgsApi;
 import com.google.devtools.build.lib.syntax.EvalException;
@@ -424,12 +427,12 @@
       } catch (ExecutionException e) {
         Throwables.throwIfInstanceOf(e.getCause(), ExecException.class);
         Throwables.throwIfInstanceOf(e.getCause(), InterruptedException.class);
-        if (e.getCause() instanceof IORuntimeException) {
+        IOException ioException = getIoExceptionIfAny(e);
+        if (ioException != null) {
           throw new EnvironmentalExecException(
-              ((IORuntimeException) e.getCause()).getCauseIOException());
-        }
-        if (e.getCause() instanceof IOException) {
-          throw new EnvironmentalExecException((IOException) e.getCause());
+              ioException,
+              createFailureDetail(
+                  "Find used headers failure", Code.FIND_USED_HEADERS_IO_EXCEPTION));
         }
         Throwables.throwIfUnchecked(e.getCause());
         throw new IllegalStateException(e.getCause());
@@ -443,6 +446,18 @@
     }
   }
 
+  @Nullable
+  private static IOException getIoExceptionIfAny(ExecutionException e) {
+    IOException ioException = null;
+    if (e.getCause() instanceof IORuntimeException) {
+      ioException = ((IORuntimeException) e.getCause()).getCauseIOException();
+    }
+    if (e.getCause() instanceof IOException) {
+      ioException = (IOException) e.getCause();
+    }
+    return ioException;
+  }
+
   /**
    * Filters discovered headers according to declared rule inputs. This fundamentally mirrors the
    * behavior of {@link #validateInclusions} and just removes inputs that would be considered
@@ -1904,7 +1919,8 @@
             }
           }
         } catch (IOException e) {
-          throw new EnvironmentalExecException(e)
+          throw new EnvironmentalExecException(
+                  e, createFailureDetail("OutErr copy failure", Code.COPY_OUT_ERR_FAILURE))
               .toActionExecutionException(
                   getRawProgressMessage(),
                   actionExecutionContext.showVerboseFailures(getOwner().getLabel()),
@@ -1913,4 +1929,11 @@
       }
     }
   }
+
+  private static FailureDetail createFailureDetail(String message, Code detailedCode) {
+    return FailureDetail.newBuilder()
+        .setMessage(message)
+        .setCppCompile(CppCompile.newBuilder().setCode(detailedCode))
+        .build();
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanning.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanning.java
index a01d398..37078d0 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanning.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanning.java
@@ -31,6 +31,9 @@
 import com.google.devtools.build.lib.profiler.SilentCloseable;
 import com.google.devtools.build.lib.rules.cpp.IncludeScanner.IncludeScannerSupplier;
 import com.google.devtools.build.lib.rules.cpp.IncludeScanner.IncludeScanningHeaderData;
+import com.google.devtools.build.lib.server.FailureDetails;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.server.FailureDetails.IncludeScanning.Code;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.PathFragment;
@@ -117,7 +120,8 @@
           },
           MoreExecutors.directExecutor());
     } catch (IOException e) {
-      throw new EnvironmentalExecException(e);
+      throw new EnvironmentalExecException(
+          e, createFailureDetail("Include scanning IOException", Code.SCANNING_IO_EXCEPTION));
     }
   }
 
@@ -152,4 +156,11 @@
     }
     return inputs;
   }
+
+  private static FailureDetail createFailureDetail(String message, Code detailedCode) {
+    return FailureDetail.newBuilder()
+        .setMessage(message)
+        .setIncludeScanning(FailureDetails.IncludeScanning.newBuilder().setCode(detailedCode))
+        .build();
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/genrule/GenRuleAction.java b/src/main/java/com/google/devtools/build/lib/rules/genrule/GenRuleAction.java
index 5dcd61f..32d7518 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/genrule/GenRuleAction.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/genrule/GenRuleAction.java
@@ -24,13 +24,13 @@
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.CommandLines;
 import com.google.devtools.build.lib.actions.CommandLines.CommandLineLimits;
+import com.google.devtools.build.lib.actions.ExecException;
 import com.google.devtools.build.lib.actions.RunfilesSupplier;
 import com.google.devtools.build.lib.actions.SpawnResult;
 import com.google.devtools.build.lib.analysis.actions.SpawnAction;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.skyframe.TrackSourceDirectoriesFlag;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
-import java.io.IOException;
 import java.util.List;
 
 /**
@@ -73,7 +73,7 @@
   }
 
   @Override
-  protected void beforeExecute(ActionExecutionContext actionExecutionContext) throws IOException {
+  protected void beforeExecute(ActionExecutionContext actionExecutionContext) throws ExecException {
     if (!TrackSourceDirectoriesFlag.trackSourceDirectories()) {
       checkInputsForDirectories(
           actionExecutionContext.getEventHandler(), actionExecutionContext.getMetadataProvider());
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileAction.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileAction.java
index d59a9af..096307d 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileAction.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileAction.java
@@ -549,21 +549,25 @@
       return Deps.Dependencies.parseFrom(input);
     } catch (IOException e) {
       throw toActionExecutionException(
-          new EnvironmentalExecException(e),
+          new EnvironmentalExecException(
+              e, createFailureDetail(".jdeps read IOException", Code.JDEPS_READ_IO_EXCEPTION)),
           actionExecutionContext.showVerboseFailures(getOwner().getLabel()));
     }
   }
 
   private ActionExecutionException createActionExecutionException(Exception e, Code detailedCode) {
     DetailedExitCode detailedExitCode =
-        DetailedExitCode.of(
-            FailureDetail.newBuilder()
-                .setMessage(Strings.nullToEmpty(e.getMessage()))
-                .setJavaCompile(JavaCompile.newBuilder().setCode(detailedCode))
-                .build());
+        DetailedExitCode.of(createFailureDetail(Strings.nullToEmpty(e.getMessage()), detailedCode));
     return new ActionExecutionException(e, this, /*catastrophe=*/ false, detailedExitCode);
   }
 
+  private static FailureDetail createFailureDetail(String message, Code detailedCode) {
+    return FailureDetail.newBuilder()
+        .setMessage(message)
+        .setJavaCompile(JavaCompile.newBuilder().setCode(detailedCode))
+        .build();
+  }
+
   private final class JavaActionContinuation extends ActionContinuationOrResult {
     private final ActionExecutionContext actionExecutionContext;
     @Nullable private final ReducedClasspath reducedClasspath;
@@ -610,7 +614,17 @@
 
         // Fall back to running with the full classpath. This requires first deleting potential
         // artifacts generated by the reduced action and clearing the metadata caches.
-        deleteOutputs(actionExecutionContext.getExecRoot());
+        try {
+          deleteOutputs(actionExecutionContext.getExecRoot());
+        } catch (IOException e) {
+          throw toActionExecutionException(
+              new EnvironmentalExecException(
+                  e,
+                  createFailureDetail(
+                      "Failed to delete reduced action outputs",
+                      Code.REDUCED_CLASSPATH_FALLBACK_CLEANUP_FAILURE)),
+              actionExecutionContext.showVerboseFailures(getOwner().getLabel()));
+        }
         actionExecutionContext.getMetadataHandler().resetOutputs(getOutputs());
         Spawn spawn;
         try {
@@ -627,10 +641,6 @@
                 .beginExecution(spawn, actionExecutionContext);
         return new JavaFallbackActionContinuation(
             actionExecutionContext, results, fallbackContinuation);
-      } catch (IOException e) {
-        throw toActionExecutionException(
-            new EnvironmentalExecException(e),
-            actionExecutionContext.showVerboseFailures(getOwner().getLabel()));
       } catch (ExecException e) {
         throw toActionExecutionException(
             e, actionExecutionContext.showVerboseFailures(getOwner().getLabel()));
diff --git a/src/main/protobuf/failure_details.proto b/src/main/protobuf/failure_details.proto
index 9c31fd2..afac067 100644
--- a/src/main/protobuf/failure_details.proto
+++ b/src/main/protobuf/failure_details.proto
@@ -131,6 +131,8 @@
     WorkspaceStatus workspace_status = 158;
     JavaCompile java_compile = 159;
     ActionRewinding action_rewinding = 160;
+    CppCompile cpp_compile = 161;
+    StarlarkAction starlark_action = 162;
   }
 
   reserved 102; // For internal use
@@ -188,6 +190,7 @@
     EXECUTION_DENIED = 5 [(metadata) = { exit_code: 1 }];
     REMOTE_CACHE_FAILED = 6 [(metadata) = { exit_code: 34 }];
     COMMAND_LINE_EXPANSION_FAILURE = 7 [(metadata) = { exit_code: 1 }];
+    EXEC_IO_EXCEPTION = 8 [(metadata) = { exit_code: 36 }];
   }
   Code code = 1;
 
@@ -350,6 +353,15 @@
         [(metadata) = { exit_code: 36 }];
     LOCAL_OUTPUT_DIRECTORY_SYMLINK_FAILURE = 7 [(metadata) = { exit_code: 36 }];
     ACTION_INPUT_FILES_MISSING = 8 [(metadata) = { exit_code: 1 }];
+    LOCAL_TEMPLATE_EXPANSION_FAILURE = 9 [(metadata) = { exit_code: 36 }];
+    INPUT_DIRECTORY_CHECK_IO_EXCEPTION = 10 [(metadata) = { exit_code: 36 }];
+    EXTRA_ACTION_OUTPUT_CREATION_FAILURE = 11 [(metadata) = { exit_code: 36 }];
+    TEST_RUNNER_IO_EXCEPTION = 12 [(metadata) = { exit_code: 36 }];
+    FILE_WRITE_IO_EXCEPTION = 13 [(metadata) = { exit_code: 36 }];
+    TEST_OUT_ERR_IO_EXCEPTION = 14 [(metadata) = { exit_code: 36 }];
+    SYMLINK_TREE_MANIFEST_COPY_IO_EXCEPTION = 15
+        [(metadata) = { exit_code: 36 }];
+    SYMLINK_TREE_CREATION_IO_EXCEPTION = 16 [(metadata) = { exit_code: 36 }];
   }
 
   Code code = 1;
@@ -590,6 +602,7 @@
   enum Code {
     INCLUDE_SCANNING_UNKNOWN = 0 [(metadata) = { exit_code: 37 }];
     INITIALIZE_INCLUDE_HINTS_ERROR = 1 [(metadata) = { exit_code: 36 }];
+    SCANNING_IO_EXCEPTION = 2 [(metadata) = { exit_code: 36 }];
   }
 
   Code code = 1;
@@ -789,6 +802,9 @@
     JAVA_COMPILE_UNKNOWN = 0 [(metadata) = { exit_code: 37 }];
     REDUCED_CLASSPATH_FAILURE = 1 [(metadata) = { exit_code: 1 }];
     COMMAND_LINE_EXPANSION_FAILURE = 2 [(metadata) = { exit_code: 1 }];
+    JDEPS_READ_IO_EXCEPTION = 3 [(metadata) = { exit_code: 36 }];
+    REDUCED_CLASSPATH_FALLBACK_CLEANUP_FAILURE = 4
+        [(metadata) = { exit_code: 36 }];
   }
 
   Code code = 1;
@@ -803,3 +819,22 @@
 
   Code code = 1;
 }
+
+message CppCompile {
+  enum Code {
+    CPP_COMPILE_UNKNOWN = 0 [(metadata) = { exit_code: 37 }];
+    FIND_USED_HEADERS_IO_EXCEPTION = 1 [(metadata) = { exit_code: 36 }];
+    COPY_OUT_ERR_FAILURE = 2 [(metadata) = { exit_code: 36 }];
+  }
+
+  Code code = 1;
+}
+
+message StarlarkAction {
+  enum Code {
+    STARLARK_ACTION_UNKNOWN = 0 [(metadata) = { exit_code: 37 }];
+    UNUSED_INPUT_LIST_READ_FAILURE = 1 [(metadata) = { exit_code: 36 }];
+  }
+
+  Code code = 1;
+}