Introduce generic post analysis build tool.

This is the first step on the way to a proper action graph query command.

RELNOTES: None
PiperOrigin-RevId: 200026440
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java b/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
index 407f2f6..755ea82 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
@@ -39,7 +39,7 @@
 import com.google.devtools.build.lib.buildeventstream.AbortedEvent;
 import com.google.devtools.build.lib.buildeventstream.BuildEventId;
 import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.Aborted.AbortReason;
-import com.google.devtools.build.lib.buildtool.CqueryBuildTool.ConfiguredTargetQueryCommandLineException;
+import com.google.devtools.build.lib.buildtool.PostAnalysisQueryBuildTool.PostAnalysisQueryCommandLineException;
 import com.google.devtools.build.lib.buildtool.buildevent.BuildCompleteEvent;
 import com.google.devtools.build.lib.buildtool.buildevent.BuildInterruptedEvent;
 import com.google.devtools.build.lib.buildtool.buildevent.BuildStartingEvent;
@@ -131,14 +131,10 @@
    * @param result the build result that is the mutable result of this build
    * @param validator target validator
    */
-  public void buildTargets(
-      BuildRequest request,
-      BuildResult result,
-      TargetValidator validator)
+  public void buildTargets(BuildRequest request, BuildResult result, TargetValidator validator)
       throws BuildFailedException, InterruptedException, ViewCreationFailedException,
           TargetParsingException, LoadingFailedException, AbruptExitException,
-          InvalidConfigurationException, TestExecException,
-          ConfiguredTargetQueryCommandLineException {
+          InvalidConfigurationException, TestExecException, PostAnalysisQueryCommandLineException {
     validateOptions(request);
     BuildOptions buildOptions = runtime.createBuildOptions(request);
     // Sync the package manager before sending the BuildStartingEvent in runLoadingPhase()
@@ -311,8 +307,7 @@
       AnalysisResult analysisResult,
       BuildConfigurationCollection configurations)
       throws InterruptedException, ViewCreationFailedException,
-          ConfiguredTargetQueryCommandLineException {
-  }
+          PostAnalysisQueryCommandLineException {}
 
   private void reportExceptionError(Exception e) {
     if (e.getMessage() != null) {
@@ -376,7 +371,7 @@
     } catch (TargetParsingException | LoadingFailedException | ViewCreationFailedException e) {
       exitCode = ExitCode.PARSING_FAILURE;
       reportExceptionError(e);
-    } catch (ConfiguredTargetQueryCommandLineException e) {
+    } catch (PostAnalysisQueryCommandLineException e) {
       exitCode = ExitCode.COMMAND_LINE_ERROR;
       reportExceptionError(e);
     } catch (TestExecException e) {
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/CqueryBuildTool.java b/src/main/java/com/google/devtools/build/lib/buildtool/CqueryBuildTool.java
index 46ed57c..2bdd2b7 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/CqueryBuildTool.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/CqueryBuildTool.java
@@ -14,151 +14,42 @@
 package com.google.devtools.build.lib.buildtool;
 
 import com.google.common.collect.ImmutableList;
-import com.google.devtools.build.lib.analysis.BuildView.AnalysisResult;
-import com.google.devtools.build.lib.analysis.TargetAndConfiguration;
-import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
 import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
-import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection;
-import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.query2.ConfiguredTargetQueryEnvironment;
-import com.google.devtools.build.lib.query2.CqueryThreadsafeCallback;
 import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryFunction;
-import com.google.devtools.build.lib.query2.engine.QueryEvalResult;
-import com.google.devtools.build.lib.query2.engine.QueryException;
 import com.google.devtools.build.lib.query2.engine.QueryExpression;
-import com.google.devtools.build.lib.query2.engine.TargetLiteral;
 import com.google.devtools.build.lib.query2.output.CqueryOptions;
 import com.google.devtools.build.lib.runtime.CommandEnvironment;
-import com.google.devtools.build.lib.skyframe.SkyframeExecutorWrappingWalkableGraph;
 import com.google.devtools.build.skyframe.WalkableGraph;
-import java.io.IOException;
-import java.util.List;
-import java.util.stream.Collectors;
 
 /** A version of {@link BuildTool} that handles all cquery work. */
-public class CqueryBuildTool extends BuildTool {
-
-  private final QueryExpression queryExpression;
+public class CqueryBuildTool extends PostAnalysisQueryBuildTool {
 
   public CqueryBuildTool(CommandEnvironment env, QueryExpression queryExpression) {
-    super(env);
-    this.queryExpression = queryExpression;
+    super(env, queryExpression);
   }
 
   @Override
-  protected void postProcessAnalysisResult(
-      BuildRequest request,
-      AnalysisResult analysisResult,
-      BuildConfigurationCollection configurations)
-      throws InterruptedException, ViewCreationFailedException,
-          ConfiguredTargetQueryCommandLineException {
-    // TODO: b/71905538 - this query will operate over the graph as constructed by analysis, but
-    // will also pick up any nodes that are in the graph from prior builds. This makes the results
-    // not reproducible at the level of a single command. Either tolerate, or wipe the analysis
-    // graph beforehand if this option is specified, or add another option to wipe if desired
-    // (SkyframeExecutor#handleConfiguredTargetChange should be sufficient).
-    if (queryExpression != null) {
-      if (!env.getSkyframeExecutor().tracksStateForIncrementality()) {
-        throw new ConfiguredTargetQueryCommandLineException(
-            "Configured query is not allowed if incrementality state is not being kept");
-      }
-      try {
-        doConfiguredTargetQuery(
-            request,
-            configurations.getHostConfiguration(),
-            analysisResult.getTopLevelTargetsWithConfigs(),
-            queryExpression);
-      } catch (QueryException | IOException e) {
-        if (!request.getKeepGoing()) {
-          throw new ViewCreationFailedException("Error doing configured target query", e);
-        }
-        env.getReporter().error(null, "Error doing configured target query", e);
-      }
-    }
-  }
-
-  private void doConfiguredTargetQuery(
+  protected ConfiguredTargetQueryEnvironment getQueryEnvironment(
       BuildRequest request,
       BuildConfiguration hostConfiguration,
-      List<TargetAndConfiguration> topLevelTargetsWithConfigs,
-      QueryExpression queryExpression)
-      throws InterruptedException, QueryException, IOException {
-
-    // Currently, CTQE assumes that all top level targets take on the same default config and we
-    // don't have the ability to map multiple configs to multiple top level targets.
-    // So for now, we only allow multiple targets when they all carry the same config.
-    // TODO: b/71508373 - fully support multiple top level targets
-    List<TargetAndConfiguration> nonNullTargets =
-        topLevelTargetsWithConfigs
-            .stream()
-            .filter(targetAndConfig -> targetAndConfig.getConfiguration() != null)
-            .collect(Collectors.toList());
-    BuildConfiguration targetConfig = null;
-    if (!nonNullTargets.isEmpty()) {
-      targetConfig = nonNullTargets.get(0).getConfiguration();
-      for (TargetAndConfiguration targAndConfig : topLevelTargetsWithConfigs) {
-        if (targAndConfig.getConfiguration() != null
-            && !targAndConfig.getConfiguration().equals(targetConfig)) {
-          throw new QueryException(
-              new TargetLiteral(queryExpression.toString()),
-              String.format(
-                  "Top-level targets %s and %s have different configurations (top-level "
-                      + "targets with different configurations is not supported)",
-                  nonNullTargets.get(0).getLabel(), targAndConfig.getLabel()));
-        }
-      }
-    }
-    WalkableGraph walkableGraph =
-        SkyframeExecutorWrappingWalkableGraph.of(env.getSkyframeExecutor());
+      BuildConfiguration targetConfig,
+      WalkableGraph walkableGraph) {
     ImmutableList<QueryFunction> extraFunctions =
         new ImmutableList.Builder<QueryFunction>()
             .addAll(ConfiguredTargetQueryEnvironment.CQUERY_FUNCTIONS)
             .addAll(env.getRuntime().getQueryFunctions())
             .build();
     CqueryOptions cqueryOptions = request.getOptions(CqueryOptions.class);
-    ConfiguredTargetQueryEnvironment configuredTargetQueryEnvironment =
-        new ConfiguredTargetQueryEnvironment(
-            request.getKeepGoing(),
-            env.getReporter(),
-            extraFunctions,
-            targetConfig,
-            hostConfiguration,
-            env.newTargetPatternEvaluator().getOffset(),
-            env.getPackageManager().getPackagePath(),
-            () -> walkableGraph,
-            cqueryOptions.toSettings());
-    Iterable<CqueryThreadsafeCallback> callbacks =
-        configuredTargetQueryEnvironment.getDefaultOutputFormatters(
-            configuredTargetQueryEnvironment.getAccessor(),
-            cqueryOptions,
-            env.getReporter(),
-            env.getSkyframeExecutor(),
-            hostConfiguration,
-            runtime.getRuleClassProvider().getTrimmingTransitionFactory(),
-            cqueryOptions.aspectDeps.createResolver(env.getPackageManager(), env.getReporter()));
-    CqueryThreadsafeCallback callback =
-        CqueryThreadsafeCallback.getCallback(cqueryOptions.outputFormat, callbacks);
-    if (callback == null) {
-      env.getReporter()
-          .handle(
-              Event.error(
-                  String.format(
-                      "Invalid output format '%s'. Valid values are: %s",
-                      cqueryOptions.outputFormat,
-                      CqueryThreadsafeCallback.callbackNames(callbacks))));
-      return;
-    }
-    QueryEvalResult result =
-        configuredTargetQueryEnvironment.evaluateQuery(queryExpression, callback);
-    if (result.isEmpty()) {
-      env.getReporter().handle(Event.info("Empty query results"));
-    }
-  }
-
-  /** Cquery specific command line exception. */
-  protected static class ConfiguredTargetQueryCommandLineException extends Exception {
-    ConfiguredTargetQueryCommandLineException(String message) {
-      super(message);
-    }
+    return new ConfiguredTargetQueryEnvironment(
+        request.getKeepGoing(),
+        env.getReporter(),
+        extraFunctions,
+        targetConfig,
+        hostConfiguration,
+        env.newTargetPatternEvaluator().getOffset(),
+        env.getPackageManager().getPackagePath(),
+        () -> walkableGraph,
+        cqueryOptions);
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/PostAnalysisQueryBuildTool.java b/src/main/java/com/google/devtools/build/lib/buildtool/PostAnalysisQueryBuildTool.java
new file mode 100644
index 0000000..df5205e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/PostAnalysisQueryBuildTool.java
@@ -0,0 +1,165 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.buildtool;
+
+import com.google.devtools.build.lib.analysis.BuildView.AnalysisResult;
+import com.google.devtools.build.lib.analysis.TargetAndConfiguration;
+import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.query2.ConfiguredTargetQueryEnvironment;
+import com.google.devtools.build.lib.query2.CqueryThreadsafeCallback;
+import com.google.devtools.build.lib.query2.engine.QueryEvalResult;
+import com.google.devtools.build.lib.query2.engine.QueryException;
+import com.google.devtools.build.lib.query2.engine.QueryExpression;
+import com.google.devtools.build.lib.query2.engine.TargetLiteral;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutorWrappingWalkableGraph;
+import com.google.devtools.build.skyframe.WalkableGraph;
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Version of {@link BuildTool} that handles all work for queries based on results from the analysis
+ * phase.
+ */
+public abstract class PostAnalysisQueryBuildTool extends BuildTool {
+
+  private final QueryExpression queryExpression;
+
+  public PostAnalysisQueryBuildTool(CommandEnvironment env, QueryExpression queryExpression) {
+    super(env);
+    this.queryExpression = queryExpression;
+  }
+
+  @Override
+  protected void postProcessAnalysisResult(
+      BuildRequest request,
+      AnalysisResult analysisResult,
+      BuildConfigurationCollection configurations)
+      throws InterruptedException, ViewCreationFailedException,
+          PostAnalysisQueryCommandLineException {
+    // TODO: b/71905538 - this query will operate over the graph as constructed by analysis, but
+    // will also pick up any nodes that are in the graph from prior builds. This makes the results
+    // not reproducible at the level of a single command. Either tolerate, or wipe the analysis
+    // graph beforehand if this option is specified, or add another option to wipe if desired
+    // (SkyframeExecutor#handleConfiguredTargetChange should be sufficient).
+    if (queryExpression != null) {
+      if (!env.getSkyframeExecutor().tracksStateForIncrementality()) {
+        throw new PostAnalysisQueryCommandLineException(
+            "Queries based on analysis results are not allowed "
+                + "if incrementality state is not being kept");
+      }
+      try {
+        doPostAnalysisQuery(
+            request,
+            configurations.getHostConfiguration(),
+            analysisResult.getTopLevelTargetsWithConfigs(),
+            queryExpression);
+      } catch (QueryException | IOException e) {
+        if (!request.getKeepGoing()) {
+          throw new ViewCreationFailedException("Error doing post analysis query", e);
+        }
+        env.getReporter().error(null, "Error doing post analysis query", e);
+      }
+    }
+  }
+
+  // TODO(twerth): Make this more generic when introducting a PostAnalysisQueryEnvironment.
+  protected abstract ConfiguredTargetQueryEnvironment getQueryEnvironment(
+      BuildRequest request,
+      BuildConfiguration hostConfiguration,
+      BuildConfiguration targetConfig,
+      WalkableGraph walkableGraph);
+
+  private BuildConfiguration getBuildConfiguration(
+      List<TargetAndConfiguration> topLevelTargetsWithConfigs, QueryExpression queryExpression)
+      throws QueryException {
+    // Currently, CTQE assumes that all top level targets take on the same default config and we
+    // don't have the ability to map multiple configs to multiple top level targets.
+    // So for now, we only allow multiple targets when they all carry the same config.
+    // TODO: b/71508373 - fully support multiple top level targets
+    List<TargetAndConfiguration> nonNullTargets =
+        topLevelTargetsWithConfigs
+            .stream()
+            .filter(targetAndConfig -> targetAndConfig.getConfiguration() != null)
+            .collect(Collectors.toList());
+    BuildConfiguration targetConfig = null;
+    if (!nonNullTargets.isEmpty()) {
+      targetConfig = nonNullTargets.get(0).getConfiguration();
+      for (TargetAndConfiguration targAndConfig : topLevelTargetsWithConfigs) {
+        if (targAndConfig.getConfiguration() != null
+            && !targAndConfig.getConfiguration().equals(targetConfig)) {
+          throw new QueryException(
+              new TargetLiteral(queryExpression.toString()),
+              String.format(
+                  "Top-level targets %s and %s have different configurations (top-level "
+                      + "targets with different configurations is not supported)",
+                  nonNullTargets.get(0).getLabel(), targAndConfig.getLabel()));
+        }
+      }
+    }
+    return targetConfig;
+  }
+
+  private void doPostAnalysisQuery(
+      BuildRequest request,
+      BuildConfiguration hostConfiguration,
+      List<TargetAndConfiguration> topLevelTargetsWithConfigs,
+      QueryExpression queryExpression)
+      throws InterruptedException, QueryException, IOException {
+    BuildConfiguration targetConfig =
+        getBuildConfiguration(topLevelTargetsWithConfigs, queryExpression);
+
+    WalkableGraph walkableGraph =
+        SkyframeExecutorWrappingWalkableGraph.of(env.getSkyframeExecutor());
+
+    ConfiguredTargetQueryEnvironment configuredTargetQueryEnvironment =
+        getQueryEnvironment(request, hostConfiguration, targetConfig, walkableGraph);
+    Iterable<CqueryThreadsafeCallback> callbacks =
+        configuredTargetQueryEnvironment.getDefaultOutputFormatters(
+            configuredTargetQueryEnvironment.getAccessor(),
+            env.getReporter(),
+            env.getSkyframeExecutor(),
+            hostConfiguration,
+            runtime.getRuleClassProvider().getTrimmingTransitionFactory(),
+            env.getPackageManager());
+    String outputFormat = configuredTargetQueryEnvironment.getOutputFormat();
+    CqueryThreadsafeCallback callback =
+        CqueryThreadsafeCallback.getCallback(outputFormat, callbacks);
+    if (callback == null) {
+      env.getReporter()
+          .handle(
+              Event.error(
+                  String.format(
+                      "Invalid output format '%s'. Valid values are: %s",
+                      outputFormat, CqueryThreadsafeCallback.callbackNames(callbacks))));
+      return;
+    }
+    QueryEvalResult result =
+        configuredTargetQueryEnvironment.evaluateQuery(queryExpression, callback);
+    if (result.isEmpty()) {
+      env.getReporter().handle(Event.info("Empty query results"));
+    }
+  }
+
+  /** Post analysis query specific command line exception. */
+  protected static class PostAnalysisQueryCommandLineException extends Exception {
+    PostAnalysisQueryCommandLineException(String message) {
+      super(message);
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/ConfiguredTargetQueryEnvironment.java b/src/main/java/com/google/devtools/build/lib/query2/ConfiguredTargetQueryEnvironment.java
index 5cbbe5a..d709e55 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/ConfiguredTargetQueryEnvironment.java
+++ b/src/main/java/com/google/devtools/build/lib/query2/ConfiguredTargetQueryEnvironment.java
@@ -37,6 +37,7 @@
 import com.google.devtools.build.lib.packages.RuleTransitionFactory;
 import com.google.devtools.build.lib.packages.Target;
 import com.google.devtools.build.lib.pkgcache.FilteringPolicies;
+import com.google.devtools.build.lib.pkgcache.PackageManager;
 import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
 import com.google.devtools.build.lib.pkgcache.TargetPatternEvaluator;
 import com.google.devtools.build.lib.query2.engine.Callback;
@@ -137,6 +138,8 @@
 
   private RecursivePackageProviderBackedTargetPatternResolver resolver;
 
+  private CqueryOptions cqueryOptions;
+
   public ConfiguredTargetQueryEnvironment(
       boolean keepGoing,
       ExtendedEventHandler eventHandler,
@@ -169,6 +172,21 @@
         };
   }
 
+  public ConfiguredTargetQueryEnvironment(
+      boolean keepGoing,
+      ExtendedEventHandler eventHandler,
+      Iterable<QueryFunction> extraFunctions,
+      BuildConfiguration defaultTargetConfiguration,
+      BuildConfiguration hostConfiguration,
+      String parserPrefix,
+      PathPackageLocator pkgPath,
+      Supplier<WalkableGraph> walkableGraphSupplier,
+      CqueryOptions cqueryOptions) {
+    this(keepGoing, eventHandler, extraFunctions, defaultTargetConfiguration, hostConfiguration,
+        parserPrefix, pkgPath, walkableGraphSupplier, cqueryOptions.toSettings());
+    this.cqueryOptions = cqueryOptions;
+  }
+
   private static ImmutableList<QueryFunction> populateFunctions() {
     return new ImmutableList.Builder<QueryFunction>()
         .addAll(QueryEnvironment.DEFAULT_QUERY_FUNCTIONS)
@@ -182,21 +200,22 @@
 
   public ImmutableList<CqueryThreadsafeCallback> getDefaultOutputFormatters(
       TargetAccessor<ConfiguredTarget> accessor,
-      CqueryOptions options,
       Reporter reporter,
       SkyframeExecutor skyframeExecutor,
       BuildConfiguration hostConfiguration,
       @Nullable RuleTransitionFactory trimmingTransitionFactory,
-      AspectResolver resolver) {
+      PackageManager packageManager) {
+    AspectResolver aspectResolver =
+        cqueryOptions.aspectDeps.createResolver(packageManager, reporter);
     OutputStream out = reporter.getOutErr().getOutputStream();
     return new ImmutableList.Builder<CqueryThreadsafeCallback>()
         .add(
             new LabelAndConfigurationOutputFormatterCallback(
-                reporter, options, out, skyframeExecutor, accessor))
+                reporter, cqueryOptions, out, skyframeExecutor, accessor))
         .add(
             new TransitionsOutputFormatterCallback(
                 reporter,
-                options,
+                cqueryOptions,
                 out,
                 skyframeExecutor,
                 accessor,
@@ -204,10 +223,14 @@
                 trimmingTransitionFactory))
         .add(
             new ProtoOutputFormatterCallback(
-                reporter, options, out, skyframeExecutor, accessor, resolver))
+                reporter, cqueryOptions, out, skyframeExecutor, accessor, aspectResolver))
         .build();
   }
 
+  public String getOutputFormat() {
+    return cqueryOptions.outputFormat;
+  }
+
   @Override
   public QueryEvalResult evaluateQuery(
       QueryExpression expr, ThreadSafeOutputFormatterCallback<ConfiguredTarget> callback)