feat: add option to exit early if analysis cache is discarded

Adds `--allow_analysis_cache_discard` option that allows the build to exit early if the analysis cache would have been discarded.

The flag name is of course open to bikeshedding etc.

fixes #16804

Closes #16805.

PiperOrigin-RevId: 552575951
Change-Id: Ia336eb3a5b7d7e41665fd0e0adf3edc03ed50f18
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/AnalysisOptions.java b/src/main/java/com/google/devtools/build/lib/analysis/AnalysisOptions.java
index 5ccce70..dd20140 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/AnalysisOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/AnalysisOptions.java
@@ -40,6 +40,17 @@
   public boolean discardAnalysisCache;
 
   @Option(
+      name = "allow_analysis_cache_discard",
+      defaultValue = "true",
+      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+      effectTags = {OptionEffectTag.EAGERNESS_TO_EXIT},
+      help =
+          "If discarding the analysis cache due to a change in the build system, setting this"
+              + " option to false will cause bazel to exit, rather than continuing with the build."
+              + " This option has no effect when 'discard_analysis_cache' is also set.")
+  public boolean allowAnalysisCacheDiscards;
+
+  @Option(
     name = "max_config_changes_to_show",
     defaultValue = "3",
     documentationCategory = OptionDocumentationCategory.LOGGING,
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java b/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java
index a0e9892..c3d2c9e 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java
@@ -255,7 +255,10 @@
     }
 
     skyframeBuildView.setConfiguration(
-        eventHandler, topLevelConfig, viewOptions.maxConfigChangesToShow);
+        eventHandler,
+        topLevelConfig,
+        viewOptions.maxConfigChangesToShow,
+        viewOptions.allowAnalysisCacheDiscards);
 
     eventBus.post(new MakeEnvironmentEvent(topLevelConfig.getMakeEnvironment()));
     eventBus.post(topLevelConfig.toBuildEvent());
@@ -510,7 +513,7 @@
       ImmutableMap<Label, Target> labelToTargetMap,
       boolean includeExecutionPhase)
       throws InterruptedException {
-    Set<Label> testsToRun = loadingResult.getTestsToRunLabels();
+    ImmutableSet<Label> testsToRun = loadingResult.getTestsToRunLabels();
     Set<ConfiguredTarget> configuredTargets =
         Sets.newLinkedHashSet(skyframeAnalysisResult.getConfiguredTargets());
     ImmutableMap<AspectKey, ConfiguredAspect> aspects = skyframeAnalysisResult.getAspects();
diff --git a/src/main/java/com/google/devtools/build/lib/buildeventstream/BUILD b/src/main/java/com/google/devtools/build/lib/buildeventstream/BUILD
index 8a19572..d538569 100644
--- a/src/main/java/com/google/devtools/build/lib/buildeventstream/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/buildeventstream/BUILD
@@ -28,6 +28,7 @@
         "//src/main/java/com/google/devtools/build/lib/skyframe:build_configuration",
         "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec",
         "//src/main/java/com/google/devtools/build/lib/util",
+        "//src/main/java/com/google/devtools/build/lib/util:detailed_exit_code",
         "//src/main/java/com/google/devtools/build/lib/util:exit_code",
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
diff --git a/src/main/java/com/google/devtools/build/lib/buildeventstream/BuildCompletingEvent.java b/src/main/java/com/google/devtools/build/lib/buildeventstream/BuildCompletingEvent.java
index 8390012..b028b4f 100644
--- a/src/main/java/com/google/devtools/build/lib/buildeventstream/BuildCompletingEvent.java
+++ b/src/main/java/com/google/devtools/build/lib/buildeventstream/BuildCompletingEvent.java
@@ -16,9 +16,11 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId;
+import com.google.devtools.build.lib.util.DetailedExitCode;
 import com.google.devtools.build.lib.util.ExitCode;
 import com.google.protobuf.util.Timestamps;
 import java.util.Collection;
+import javax.annotation.Nullable;
 
 /**
  * Class all events completing a build inherit from.
@@ -27,6 +29,7 @@
  * However, subclasses do not have to implement anything.
  */
 public abstract class BuildCompletingEvent implements BuildEvent {
+  @Nullable private final DetailedExitCode detailedExitCode;
   private final ExitCode exitCode;
   private final long finishTimeMillis;
 
@@ -34,11 +37,20 @@
 
   public BuildCompletingEvent(
       ExitCode exitCode, long finishTimeMillis, Collection<BuildEventId> children) {
+    this.detailedExitCode = null;
     this.exitCode = exitCode;
     this.finishTimeMillis = finishTimeMillis;
     this.children = children;
   }
 
+  public BuildCompletingEvent(
+      DetailedExitCode detailedExitCode, long finishTimeMillis, Collection<BuildEventId> children) {
+    this.detailedExitCode = detailedExitCode;
+    this.exitCode = detailedExitCode.getExitCode();
+    this.finishTimeMillis = finishTimeMillis;
+    this.children = children;
+  }
+
   public BuildCompletingEvent(ExitCode exitCode, long finishTimeMillis) {
     this(exitCode, finishTimeMillis, ImmutableList.of());
   }
@@ -65,13 +77,17 @@
             .setCode(exitCode.getNumericExitCode())
             .build();
 
-    BuildEventStreamProtos.BuildFinished finished =
+    BuildEventStreamProtos.BuildFinished.Builder finished =
         BuildEventStreamProtos.BuildFinished.newBuilder()
             .setOverallSuccess(ExitCode.SUCCESS.equals(exitCode))
             .setExitCode(protoExitCode)
             .setFinishTime(Timestamps.fromMillis(finishTimeMillis))
-            .setFinishTimeMillis(finishTimeMillis)
-            .build();
-    return GenericBuildEvent.protoChaining(this).setFinished(finished).build();
+            .setFinishTimeMillis(finishTimeMillis);
+
+    if (detailedExitCode != null && detailedExitCode.getFailureDetail() != null) {
+      finished.setFailureDetail(detailedExitCode.getFailureDetail());
+    }
+
+    return GenericBuildEvent.protoChaining(this).setFinished(finished.build()).build();
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/buildeventstream/proto/build_event_stream.proto b/src/main/java/com/google/devtools/build/lib/buildeventstream/proto/build_event_stream.proto
index 16f616d..12bfa92 100644
--- a/src/main/java/com/google/devtools/build/lib/buildeventstream/proto/build_event_stream.proto
+++ b/src/main/java/com/google/devtools/build/lib/buildeventstream/proto/build_event_stream.proto
@@ -836,6 +836,9 @@
   google.protobuf.Timestamp finish_time = 5;
 
   AnomalyReport anomaly_report = 4 [deprecated = true];
+
+  // Only populated if success = false, and sometimes not even then.
+  failure_details.FailureDetail failure_detail = 6;
 }
 
 message BuildMetrics {
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/BuildCompleteEvent.java b/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/BuildCompleteEvent.java
index 2b77fa5..ff63128 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/BuildCompleteEvent.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/BuildCompleteEvent.java
@@ -32,7 +32,7 @@
 
   /** Construct the BuildCompleteEvent. */
   public BuildCompleteEvent(BuildResult result, Collection<BuildEventId> children) {
-    super(result.getDetailedExitCode().getExitCode(), result.getStopTime(), children);
+    super(result.getDetailedExitCode(), result.getStopTime(), children);
     this.result = checkNotNull(result);
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeBuildView.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeBuildView.java
index 0557529..9fccf1e 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeBuildView.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeBuildView.java
@@ -66,6 +66,7 @@
 import com.google.devtools.build.lib.analysis.config.BuildOptions;
 import com.google.devtools.build.lib.analysis.config.BuildOptions.OptionsDiff;
 import com.google.devtools.build.lib.analysis.config.ConfigConditions;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
 import com.google.devtools.build.lib.analysis.config.StarlarkTransitionCache;
 import com.google.devtools.build.lib.analysis.test.AnalysisFailurePropagationException;
 import com.google.devtools.build.lib.analysis.test.CoverageActionFinishedEvent;
@@ -89,6 +90,7 @@
 import com.google.devtools.build.lib.packages.TargetUtils;
 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.skyframe.ArtifactConflictFinder.ConflictException;
 import com.google.devtools.build.lib.skyframe.AspectKeyCreator.AspectKey;
 import com.google.devtools.build.lib.skyframe.AspectKeyCreator.TopLevelAspectsKey;
@@ -279,7 +281,11 @@
   /** Sets the configuration. Not thread-safe. DO NOT CALL except from tests! */
   @VisibleForTesting
   public void setConfiguration(
-      EventHandler eventHandler, BuildConfigurationValue configuration, int maxDifferencesToShow) {
+      EventHandler eventHandler,
+      BuildConfigurationValue configuration,
+      int maxDifferencesToShow,
+      boolean allowAnalysisCacheDiscards)
+      throws InvalidConfigurationException {
     if (skyframeAnalysisWasDiscarded) {
       eventHandler.handle(
           Event.warn(
@@ -290,6 +296,12 @@
     } else {
       String diff = describeConfigurationDifference(configuration, maxDifferencesToShow);
       if (diff != null) {
+        if (!allowAnalysisCacheDiscards) {
+          String message = String.format("%s, analysis cache would have been discarded.", diff);
+          throw new InvalidConfigurationException(
+              message,
+              FailureDetails.BuildConfiguration.Code.CONFIGURATION_DISCARDED_ANALYSIS_CACHE);
+        }
         eventHandler.handle(
             Event.warn(
                 diff
diff --git a/src/main/protobuf/failure_details.proto b/src/main/protobuf/failure_details.proto
index 17d313b..ca793a2 100644
--- a/src/main/protobuf/failure_details.proto
+++ b/src/main/protobuf/failure_details.proto
@@ -608,6 +608,7 @@
     // possibilities in Bazel, so we go with the more straightforward
     // command-line error exit code 2.
     INVALID_OUTPUT_DIRECTORY_MNEMONIC = 11 [(metadata) = { exit_code: 2 }];
+    CONFIGURATION_DISCARDED_ANALYSIS_CACHE = 12 [(metadata) = { exit_code: 2 }];
   }
 
   Code code = 1;
@@ -1197,6 +1198,7 @@
     CONFIGURED_VALUE_CREATION_FAILED = 18 [(metadata) = { exit_code: 1 }];
     INCOMPATIBLE_TARGET_REQUESTED = 19 [(metadata) = { exit_code: 1 }];
     ANALYSIS_FAILURE_PROPAGATION_FAILED = 20 [(metadata) = { exit_code: 1 }];
+    ANALYSIS_CACHE_DISCARDED = 21 [(metadata) = { exit_code: 1 }];
   }
 
   Code code = 1;
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/AnalysisCachingTest.java b/src/test/java/com/google/devtools/build/lib/analysis/AnalysisCachingTest.java
index b014003..e19cada 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/AnalysisCachingTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/AnalysisCachingTest.java
@@ -25,6 +25,7 @@
 import com.google.devtools.build.lib.analysis.config.BuildOptionsView;
 import com.google.devtools.build.lib.analysis.config.Fragment;
 import com.google.devtools.build.lib.analysis.config.FragmentOptions;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
 import com.google.devtools.build.lib.analysis.config.RequiresOptions;
 import com.google.devtools.build.lib.analysis.config.transitions.NoTransition;
 import com.google.devtools.build.lib.analysis.config.transitions.PatchTransition;
@@ -1163,4 +1164,16 @@
     assertDoesNotContainEvent("Build option");
     assertContainsEvent("discarding analysis cache");
   }
+
+  @Test
+  public void throwsIfAnalysisCacheIsDiscardedWhenOptionSet() throws Exception {
+    setupDiffResetTesting();
+    scratch.file("test/BUILD", "load(':lib.bzl', 'normal_lib')", "normal_lib(name='top')");
+    useConfiguration("--definitely_relevant=old");
+    update("//test:top");
+    useConfiguration("--noallow_analysis_cache_discard", "--definitely_relevant=new");
+
+    Throwable t = assertThrows(InvalidConfigurationException.class, () -> update("//test:top"));
+    assertThat(t.getMessage().contains("analysis cache would have been discarded")).isTrue();
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/BUILD b/src/test/java/com/google/devtools/build/lib/analysis/BUILD
index 6dc075d..28fc819 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/analysis/BUILD
@@ -85,6 +85,7 @@
         "//src/main/java/com/google/devtools/build/lib/analysis:config/fragment",
         "//src/main/java/com/google/devtools/build/lib/analysis:config/fragment_options",
         "//src/main/java/com/google/devtools/build/lib/analysis:config/fragment_registry",
+        "//src/main/java/com/google/devtools/build/lib/analysis:config/invalid_configuration_exception",
         "//src/main/java/com/google/devtools/build/lib/analysis:config/per_label_options",
         "//src/main/java/com/google/devtools/build/lib/analysis:config/run_under",
         "//src/main/java/com/google/devtools/build/lib/analysis:config/run_under_converter",
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewForTesting.java b/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewForTesting.java
index 95ca620..ae8a327 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewForTesting.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewForTesting.java
@@ -258,7 +258,18 @@
   /** Sets the configuration. Not thread-safe. */
   public void setConfigurationForTesting(
       EventHandler eventHandler, BuildConfigurationValue configuration) {
-    skyframeBuildView.setConfiguration(eventHandler, configuration, /* maxDifferencesToShow= */ -1);
+    try {
+      skyframeBuildView.setConfiguration(
+          eventHandler,
+          configuration,
+          /* maxDifferencesToShow= */ -1, /* allowAnalysisCacheDiscards */
+          true);
+    } catch (InvalidConfigurationException e) {
+      throw new UnsupportedOperationException(
+          "InvalidConfigurationException was thrown and caught during a test, "
+              + "this case is not yet handled",
+          e);
+    }
   }
 
   public ArtifactFactory getArtifactFactory() {