Encode missing input file failures with FailureDetails

Also refactors missing file message functions so that fewer
MissingInputFileExceptions are needlessly constructed.

RELNOTES: None.
PiperOrigin-RevId: 318356644
diff --git a/src/main/java/com/google/devtools/build/lib/actions/MissingInputFileException.java b/src/main/java/com/google/devtools/build/lib/actions/MissingInputFileException.java
index f37cd06..78ed120 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/MissingInputFileException.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/MissingInputFileException.java
@@ -14,27 +14,28 @@
 
 package com.google.devtools.build.lib.actions;
 
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.syntax.Location;
+import com.google.devtools.build.lib.util.DetailedExitCode;
 
 /**
- * This exception is thrown during a build when an input file is missing, but the file
- * is not the input to any action being executed.
+ * This exception is thrown during a build when an input file is missing, but the file is not the
+ * input to any action being executed.
  *
- * If a missing input file is an input
- * to an action, an {@link ActionExecutionException} is thrown instead.
+ * <p>If a missing input file is an input to an action, an {@link ActionExecutionException} is
+ * thrown instead.
  */
 public class MissingInputFileException extends BuildFailedException {
   private final Location location;
 
-  public MissingInputFileException(String message, Location location) {
-    super(message);
+  public MissingInputFileException(FailureDetail failureDetail, Location location) {
+    super(failureDetail.getMessage(), DetailedExitCode.of(failureDetail));
     this.location = location;
   }
 
   /**
-   * Return a location where this input file is referenced. If there
-   * are multiple such locations, one is chosen arbitrarily. If there
-   * are none, return null.
+   * Return a location where this input file is referenced. If there are multiple such locations,
+   * one is chosen arbitrarily. If there are none, return null.
    */
   public Location getLocation() {
     return location;
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 7f6d752..bbfb4f5 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
@@ -1051,12 +1051,11 @@
               new IllegalStateException("Non-source artifact had IO Exception" + input, e));
         }
 
-        MissingFileArtifactValue missingValue =
-            ArtifactFunction.makeMissingInputFileValue(input, e);
-        MissingInputFileException missingException = missingValue.getException();
         skyframeActionExecutor.printError(
             String.format(
-                "%s: %s", actionForError.getOwner().getLabel(), missingException.getMessage()),
+                "%s: %s",
+                actionForError.getOwner().getLabel(),
+                ArtifactFunction.makeMissingInputFileMessage(input, e)),
             actionForError,
             null);
         // We don't create a specific cause for the artifact as we do in #handleMissingFile because
@@ -1286,7 +1285,7 @@
           missingArtifactCauses.add(
               handleMissingFile(
                   input,
-                  ArtifactFunction.makeMissingInputFileValue(input, e),
+                  ArtifactFunction.makeMissingInputFileMessage(input, e),
                   action.getOwner().getLabel()));
           continue;
         }
@@ -1447,17 +1446,21 @@
 
   static LabelCause handleMissingFile(
       Artifact input, MissingFileArtifactValue missingValue, Label labelInCaseOfBug) {
-    MissingInputFileException e = missingValue.getException();
+    return handleMissingFile(input, missingValue.getException().getMessage(), labelInCaseOfBug);
+  }
+
+  static LabelCause handleMissingFile(
+      Artifact input, String missingMessage, Label labelInCaseOfBug) {
     Label inputLabel = input.getOwner();
     if (inputLabel == null) {
       BugReport.sendBugReport(
           new IllegalStateException(
               String.format(
                   "Artifact %s with missing value %s should have owner (%s)",
-                  input, e.getMessage(), labelInCaseOfBug)));
+                  input, missingMessage, labelInCaseOfBug)));
       inputLabel = labelInCaseOfBug;
     }
-    return new LabelCause(inputLabel, e.getMessage());
+    return new LabelCause(inputLabel, missingMessage);
   }
 
   @Override
@@ -1749,7 +1752,7 @@
         missingArtifactCauses.add(
             handleMissingFile(
                 input,
-                ArtifactFunction.makeMissingInputFileValue(input, e),
+                ArtifactFunction.makeMissingInputFileMessage(input, e),
                 action.getOwner().getLabel()));
       }
     }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ArtifactFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ArtifactFunction.java
index fd5775b..d04081a 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/ArtifactFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ArtifactFunction.java
@@ -254,13 +254,13 @@
     try {
       fileValue = (FileValue) env.getValueOrThrow(fileSkyKey, IOException.class);
     } catch (IOException e) {
-      return makeMissingInputFileValue(artifact, e);
+      return makeMissingSourceInputFileValue(artifact, e);
     }
     if (fileValue == null) {
       return null;
     }
     if (!fileValue.exists()) {
-      return makeMissingInputFileValue(artifact, null);
+      return makeMissingSourceInputFileValue(artifact, null);
     }
 
     // For directory artifacts that are not Filesets, we initiate a directory traversal here, and
@@ -305,17 +305,27 @@
     try {
       return FileArtifactValue.createForSourceArtifact(artifact, fileValue);
     } catch (IOException e) {
-      return makeMissingInputFileValue(artifact, e);
+      return makeMissingSourceInputFileValue(artifact, e);
     }
   }
 
-  static MissingFileArtifactValue makeMissingInputFileValue(Artifact artifact, Exception failure) {
-    String extraMsg = (failure == null) ? "" : (": " + failure.getMessage());
+  static MissingFileArtifactValue makeMissingSourceInputFileValue(
+      Artifact artifact, Exception failure) {
     MissingInputFileException ex =
-        new MissingInputFileException(constructErrorMessage(artifact) + extraMsg, null);
+        new MissingInputFileException(
+            FailureDetail.newBuilder()
+                .setMessage(makeMissingInputFileMessage(artifact, failure))
+                .setExecution(Execution.newBuilder().setCode(Code.SOURCE_INPUT_MISSING))
+                .build(),
+            null);
     return new MissingFileArtifactValue(ex);
   }
 
+  static String makeMissingInputFileMessage(Artifact artifact, Exception failure) {
+    return constructErrorMessage(artifact)
+        + ((failure == null) ? "" : (": " + failure.getMessage()));
+  }
+
   @Nullable
   private static AggregatingArtifactValue createAggregatingValue(
       Artifact artifact,
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/AspectCompletor.java b/src/main/java/com/google/devtools/build/lib/skyframe/AspectCompletor.java
index 56df64e..ed95bde 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/AspectCompletor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/AspectCompletor.java
@@ -27,6 +27,9 @@
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.ExtendedEventHandler;
+import com.google.devtools.build.lib.server.FailureDetails.Execution;
+import com.google.devtools.build.lib.server.FailureDetails.Execution.Code;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.skyframe.AspectCompletionValue.AspectCompletionKey;
 import com.google.devtools.build.lib.skyframe.AspectValueKey.AspectKey;
 import com.google.devtools.build.lib.skyframe.CompletionFunction.Completor;
@@ -59,12 +62,15 @@
   public MissingInputFileException getMissingFilesException(
       AspectValue value, AspectCompletionKey key, int missingCount, Environment env) {
     AspectKey aspectKey = key.actionLookupKey();
+    String message =
+        String.format(
+            "%s, aspect %s %d input file(s) do not exist",
+            aspectKey.getLabel(), aspectKey.getAspectClass().getName(), missingCount);
     return new MissingInputFileException(
-        aspectKey.getLabel()
-            + ", aspect "
-            + aspectKey.getAspectClass().getName()
-            + missingCount
-            + " input file(s) do not exist",
+        FailureDetail.newBuilder()
+            .setMessage(message)
+            .setExecution(Execution.newBuilder().setCode(Code.SOURCE_INPUT_MISSING))
+            .build(),
         value.getLocation());
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/CompletionFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/CompletionFunction.java
index 555de2f..fef0a70 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/CompletionFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/CompletionFunction.java
@@ -229,7 +229,7 @@
         missingCount++;
         handleMissingFile(
             input,
-            ArtifactFunction.makeMissingInputFileValue(input, e),
+            ArtifactFunction.makeMissingSourceInputFileValue(input, e),
             rootCausesBuilder,
             env,
             value,
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunction.java
index 6242832..2799fcc 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunction.java
@@ -26,6 +26,9 @@
 import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.server.FailureDetails.BuildConfiguration;
+import com.google.devtools.build.lib.server.FailureDetails.BuildConfiguration.Code;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.syntax.Location;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.PathFragment;
@@ -79,10 +82,12 @@
       if (fileValue.isDirectory()) {
         throw new PlatformMappingException(
             new MissingInputFileException(
-                String.format(
-                    "--platform_mappings was set to '%s' relative to the top-level workspace '%s'"
-                        + " but that path refers to a directory, not a file",
-                    workspaceRelativeMappingPath, root),
+                createFailureDetail(
+                    String.format(
+                        "--platform_mappings was set to '%s' relative to the top-level workspace"
+                            + " '%s' but that path refers to a directory, not a file",
+                        workspaceRelativeMappingPath, root),
+                    Code.PLATFORM_MAPPINGS_FILE_IS_DIRECTORY),
                 Location.BUILTIN),
             SkyFunctionException.Transience.PERSISTENT);
       }
@@ -105,14 +110,23 @@
     }
     throw new PlatformMappingException(
         new MissingInputFileException(
-            String.format(
-                "--platform_mappings was set to '%s' but no such file exists relative to the "
-                    + "package path roots, '%s'",
-                workspaceRelativeMappingPath, pathEntries),
+            createFailureDetail(
+                String.format(
+                    "--platform_mappings was set to '%s' but no such file exists relative to the "
+                        + "package path roots, '%s'",
+                    workspaceRelativeMappingPath, pathEntries),
+                Code.PLATFORM_MAPPINGS_FILE_NOT_FOUND),
             Location.BUILTIN),
         SkyFunctionException.Transience.PERSISTENT);
   }
 
+  private static FailureDetail createFailureDetail(String message, Code detailedCode) {
+    return FailureDetail.newBuilder()
+        .setMessage(message)
+        .setBuildConfiguration(BuildConfiguration.newBuilder().setCode(detailedCode))
+        .build();
+  }
+
   @Nullable
   @Override
   public String extractTag(SkyKey skyKey) {
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetCompletor.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetCompletor.java
index 9a7fef9..6eac1e1 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/TargetCompletor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetCompletor.java
@@ -25,6 +25,9 @@
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.ExtendedEventHandler;
+import com.google.devtools.build.lib.server.FailureDetails.Execution;
+import com.google.devtools.build.lib.server.FailureDetails.Execution.Code;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.skyframe.CompletionFunction.Completor;
 import com.google.devtools.build.lib.skyframe.TargetCompletionValue.TargetCompletionKey;
 import com.google.devtools.build.skyframe.SkyFunction;
@@ -62,10 +65,13 @@
       return null;
     }
     return new MissingInputFileException(
-        configuredTargetAndData.getTarget().getLocation()
-            + " "
-            + missingCount
-            + " input file(s) do not exist",
+        FailureDetail.newBuilder()
+            .setMessage(
+                String.format(
+                    "%s %d input file(s) do not exist",
+                    configuredTargetAndData.getTarget().getLocation(), missingCount))
+            .setExecution(Execution.newBuilder().setCode(Code.SOURCE_INPUT_MISSING))
+            .build(),
         configuredTargetAndData.getTarget().getLocation());
   }
 
diff --git a/src/main/protobuf/failure_details.proto b/src/main/protobuf/failure_details.proto
index fb70bbc..23fa1b7 100644
--- a/src/main/protobuf/failure_details.proto
+++ b/src/main/protobuf/failure_details.proto
@@ -407,6 +407,7 @@
         [(metadata) = { exit_code: 1 }];
     NON_ACTION_EXECUTION_FAILURE = 34 [(metadata) = { exit_code: 1 }];
     CYCLE = 35 [(metadata) = { exit_code: 1 }];
+    SOURCE_INPUT_MISSING = 36 [(metadata) = { exit_code: 1 }];
   }
 
   Code code = 1;
@@ -541,6 +542,8 @@
   enum Code {
     BUILD_CONFIGURATION_UNKNOWN = 0 [(metadata) = { exit_code: 2 }];
     PLATFORM_MAPPING_EVALUATION_FAILURE = 1 [(metadata) = { exit_code: 2 }];
+    PLATFORM_MAPPINGS_FILE_IS_DIRECTORY = 2 [(metadata) = { exit_code: 1 }];
+    PLATFORM_MAPPINGS_FILE_NOT_FOUND = 3 [(metadata) = { exit_code: 1 }];
   }
 
   Code code = 1;