Use failure details in InfoCommand, InvalidConfigurationException

Adapts its usage of ExitCode and AbruptExitException to add FailureDetail
values. Failure modes detected there now have corresponding codes in
failure_details.proto.

InvalidConfigurationException now specifies a fine-grained code to more
precisely represent what configuration-related activity failed. This gets
its first use for platform mapping failures. (Platform mapping failures
may benefit from additional refinement.)

RELNOTES: None.
PiperOrigin-RevId: 309104771
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 aa46a17..29d2426 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BUILD
@@ -1622,6 +1622,10 @@
 java_library(
     name = "config/invalid_configuration_exception",
     srcs = ["config/InvalidConfigurationException.java"],
+    deps = [
+        "//src/main/protobuf:failure_details_java_proto",
+        "//third_party:jsr305",
+    ],
 )
 
 java_library(
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/InvalidConfigurationException.java b/src/main/java/com/google/devtools/build/lib/analysis/config/InvalidConfigurationException.java
index 35bca2c..7f2a7f4 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/InvalidConfigurationException.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/InvalidConfigurationException.java
@@ -13,21 +13,39 @@
 // limitations under the License.
 package com.google.devtools.build.lib.analysis.config;
 
+import com.google.devtools.build.lib.server.FailureDetails.BuildConfiguration.Code;
+import javax.annotation.Nullable;
+
 /**
  * Thrown if the configuration options lead to an invalid configuration, or if any of the
  * configuration labels cannot be loaded.
  */
 public class InvalidConfigurationException extends Exception {
 
+  @Nullable private final Code detailedCode;
+
   public InvalidConfigurationException(String message) {
     super(message);
+    this.detailedCode = null;
   }
 
   public InvalidConfigurationException(String message, Throwable cause) {
     super(message, cause);
+    this.detailedCode = null;
   }
 
   public InvalidConfigurationException(Throwable cause) {
-    this(cause.getMessage(), cause);
+    super(cause.getMessage(), cause);
+    this.detailedCode = null;
+  }
+
+  public InvalidConfigurationException(Code detailedCode, Exception cause) {
+    super(cause.getMessage(), cause);
+    this.detailedCode = detailedCode;
+  }
+
+  @Nullable
+  public Code getDetailedCode() {
+    return detailedCode;
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoCommand.java
index 9dc5cc8..f16e709 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoCommand.java
@@ -13,6 +13,7 @@
 // limitations under the License.
 package com.google.devtools.build.lib.runtime.commands;
 
+import com.google.common.base.Strings;
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
@@ -26,7 +27,12 @@
 import com.google.devtools.build.lib.runtime.BlazeRuntime;
 import com.google.devtools.build.lib.runtime.Command;
 import com.google.devtools.build.lib.runtime.CommandEnvironment;
+import com.google.devtools.build.lib.server.FailureDetails;
+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.server.FailureDetails.Interrupted;
 import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.DetailedExitCode;
 import com.google.devtools.build.lib.util.ExitCode;
 import com.google.devtools.build.lib.util.io.OutErr;
 import com.google.devtools.common.options.Option;
@@ -86,24 +92,19 @@
   }
 
   /**
-   * Unchecked variant of ExitCausingException. Below, we need to throw from the Supplier interface,
-   * which does not allow checked exceptions.
+   * Unchecked variant of {@link AbruptExitException}. Below, we need to throw from the Supplier
+   * interface, which does not allow checked exceptions.
    */
-  public static class ExitCausingRuntimeException extends RuntimeException {
+  private static class AbruptExitRuntimeException extends RuntimeException {
 
-    private final ExitCode exitCode;
+    private final DetailedExitCode detailedExitCode;
 
-    public ExitCausingRuntimeException(String message, ExitCode exitCode) {
-      super(message);
-      this.exitCode = exitCode;
+    private AbruptExitRuntimeException(DetailedExitCode exitCode) {
+      this.detailedExitCode = exitCode;
     }
 
-    public ExitCausingRuntimeException(ExitCode exitCode) {
-      this.exitCode = exitCode;
-    }
-
-    public ExitCode getExitCode() {
-      return exitCode;
+    private DetailedExitCode getDetailedExitCode() {
+      return detailedExitCode;
     }
   }
 
@@ -133,13 +134,26 @@
                         /*keepGoing=*/ true);
               } catch (InvalidConfigurationException e) {
                 env.getReporter().handle(Event.error(e.getMessage()));
-                throw new ExitCausingRuntimeException(ExitCode.COMMAND_LINE_ERROR);
+                throw new AbruptExitRuntimeException(
+                    DetailedExitCode.of(
+                        ExitCode.COMMAND_LINE_ERROR,
+                        FailureDetail.newBuilder()
+                            .setMessage(Strings.nullToEmpty(e.getMessage()))
+                            .setBuildConfiguration(
+                                FailureDetails.BuildConfiguration.newBuilder()
+                                    .setCode(
+                                        e.getDetailedCode() == null
+                                            ? Code.BUILD_CONFIGURATION_UNKNOWN
+                                            : e.getDetailedCode()))
+                            .build()));
               } catch (AbruptExitException e) {
-                throw new ExitCausingRuntimeException(
-                    "unknown error: " + e.getMessage(), e.getExitCode());
+                throw new AbruptExitRuntimeException(e.getDetailedExitCode());
               } catch (InterruptedException e) {
                 env.getReporter().handle(Event.error("interrupted"));
-                throw new ExitCausingRuntimeException(ExitCode.INTERRUPTED);
+                throw new AbruptExitRuntimeException(
+                    createInterruptedExit(
+                        "command interrupted while syncing package loading",
+                        Interrupted.Code.PACKAGE_LOADING_SYNC));
               }
             });
 
@@ -156,8 +170,10 @@
 
       List<String> residue = optionsParsingResult.getResidue();
       if (residue.size() > 1) {
-        env.getReporter().handle(Event.error("at most one key may be specified"));
-        return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR);
+        String message = "at most one key may be specified";
+        env.getReporter().handle(Event.error(message));
+        return createFailureResult(
+            message, ExitCode.COMMAND_LINE_ERROR, FailureDetails.InfoCommand.Code.TOO_MANY_KEYS);
       }
 
       String key = residue.size() == 1 ? residue.get(0) : null;
@@ -167,15 +183,23 @@
         if (items.containsKey(key)) {
           value = items.get(key).get(configurationSupplier, env);
         } else {
-          env.getReporter().handle(Event.error("unknown key: '" + key + "'"));
-          return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR);
+          String message = "unknown key: '" + key + "'";
+          env.getReporter().handle(Event.error(message));
+          return createFailureResult(
+              message,
+              ExitCode.COMMAND_LINE_ERROR,
+              FailureDetails.InfoCommand.Code.KEY_NOT_RECOGNIZED);
         }
         try {
           outErr.getOutputStream().write(value);
           outErr.getOutputStream().flush();
         } catch (IOException e) {
-          env.getReporter().handle(Event.error("Cannot write info block: " + e.getMessage()));
-          return BlazeCommandResult.exitCode(ExitCode.ANALYSIS_FAILURE);
+          String message = "Cannot write info block: " + e.getMessage();
+          env.getReporter().handle(Event.error(message));
+          return createFailureResult(
+              message,
+              ExitCode.ANALYSIS_FAILURE,
+              FailureDetails.InfoCommand.Code.INFO_BLOCK_WRITE_FAILURE);
         }
       } else { // print them all
         configurationSupplier.get();  // We'll need this later anyway
@@ -189,17 +213,42 @@
         }
       }
     } catch (AbruptExitException e) {
-      return BlazeCommandResult.exitCode(e.getExitCode());
-    } catch (ExitCausingRuntimeException e) {
-      return BlazeCommandResult.exitCode(e.getExitCode());
+      return BlazeCommandResult.detailedExitCode(e.getDetailedExitCode());
+    } catch (AbruptExitRuntimeException e) {
+      return BlazeCommandResult.detailedExitCode(e.getDetailedExitCode());
     } catch (IOException e) {
-      return BlazeCommandResult.exitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
+      return createFailureResult(
+          "Cannot write info block: " + e.getMessage(),
+          ExitCode.LOCAL_ENVIRONMENTAL_ERROR,
+          FailureDetails.InfoCommand.Code.ALL_INFO_WRITE_FAILURE);
     } catch (InterruptedException e) {
-      return BlazeCommandResult.exitCode(ExitCode.INTERRUPTED);
+      return BlazeCommandResult.detailedExitCode(
+          createInterruptedExit("info interrupted", Interrupted.Code.INFO_ITEM));
     }
     return BlazeCommandResult.exitCode(ExitCode.SUCCESS);
   }
 
+  private static DetailedExitCode createInterruptedExit(
+      String message, Interrupted.Code detailedCode) {
+    return DetailedExitCode.of(
+        ExitCode.INTERRUPTED,
+        FailureDetail.newBuilder()
+            .setMessage(message)
+            .setInterrupted(Interrupted.newBuilder().setCode(detailedCode))
+            .build());
+  }
+
+  private static BlazeCommandResult createFailureResult(
+      String message, ExitCode exitCode, FailureDetails.InfoCommand.Code detailedCode) {
+    return BlazeCommandResult.detailedExitCode(
+        DetailedExitCode.of(
+            exitCode,
+            FailureDetail.newBuilder()
+                .setMessage(message)
+                .setInfoCommand(FailureDetails.InfoCommand.newBuilder().setCode(detailedCode))
+                .build()));
+  }
+
   private static Map<String, InfoItem> getHardwiredInfoItemMap(
       OptionsParsingResult commandOptions, String productName) {
     List<InfoItem> hardwiredInfoItems =
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
index 915f58a..604874d 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
@@ -141,6 +141,7 @@
 import com.google.devtools.build.lib.rules.repository.ResolvedHashesFunction;
 import com.google.devtools.build.lib.runtime.KeepGoingOption;
 import com.google.devtools.build.lib.server.FailureDetails;
+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.skyframe.AspectValueKey.AspectKey;
 import com.google.devtools.build.lib.skyframe.DirtinessCheckerUtils.FileDirtinessChecker;
@@ -2143,7 +2144,8 @@
     EvaluationResult<SkyValue> evaluationResult =
         evaluateSkyKeys(eventHandler, ImmutableSet.of(platformMappingKey));
     if (evaluationResult.hasError()) {
-      throw new InvalidConfigurationException(evaluationResult.getError().getException());
+      throw new InvalidConfigurationException(
+          Code.PLATFORM_MAPPING_EVALUATION_FAILURE, evaluationResult.getError().getException());
     }
     return (PlatformMappingValue) evaluationResult.get(platformMappingKey);
   }
diff --git a/src/main/protobuf/failure_details.proto b/src/main/protobuf/failure_details.proto
index 5954d55..51ecfe5 100644
--- a/src/main/protobuf/failure_details.proto
+++ b/src/main/protobuf/failure_details.proto
@@ -105,6 +105,8 @@
     Spawn spawn = 123;
     GrpcServer grpc_server = 124;
     CanonicalizeFlags canonicalize_flags = 125;
+    BuildConfiguration build_configuration = 126;
+    InfoCommand info_command = 127;
   }
 
   reserved 102; // For internal use
@@ -124,6 +126,7 @@
     PACKAGE_LOADING_SYNC = 6 [(metadata) = { exit_code: 8 }];
     EXECUTOR_COMPLETION = 7 [(metadata) = { exit_code: 8 }];
     COMMAND_DISPATCH = 8 [(metadata) = { exit_code: 8 }];
+    INFO_ITEM = 9 [(metadata) = { exit_code: 8 }];
   }
 
   Code code = 1;
@@ -385,3 +388,34 @@
 
   Code code = 1;
 }
+
+// Failure modes described by this category pertain to the Bazel invocation
+// configuration consumed by Bazel's analysis phase. This category is not
+// intended as a grab-bag for all Bazel flag value constraint violations, which
+// instead generally belong in the category for the subsystem whose flag values
+// participate in the constraint.
+message BuildConfiguration {
+  // TODO(mschaller): many more codes should be added to represent different
+  // configuration-related failure modes. See InvalidConfigurationException.
+  enum Code {
+    BUILD_CONFIGURATION_UNKNOWN = 0 [(metadata) = { exit_code: 2 }];
+    PLATFORM_MAPPING_EVALUATION_FAILURE = 1 [(metadata) = { exit_code: 2 }];
+  }
+
+  Code code = 1;
+}
+
+message InfoCommand {
+  // The distinction between a failure to write a single info item and a failure
+  // to write them all seems sketchy. Why do they have different exit codes?
+  // This reflects current Bazel behavior, but deserves more thought.
+  enum Code {
+    INFO_COMMAND_UNKNOWN = 0 [(metadata) = { exit_code: 37 }];
+    TOO_MANY_KEYS = 1 [(metadata) = { exit_code: 2 }];
+    KEY_NOT_RECOGNIZED = 2 [(metadata) = { exit_code: 2 }];
+    INFO_BLOCK_WRITE_FAILURE = 3 [(metadata) = { exit_code: 7 }];
+    ALL_INFO_WRITE_FAILURE = 4 [(metadata) = { exit_code: 36 }];
+  }
+
+  Code code = 1;
+}
\ No newline at end of file