// Copyright 2014 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 static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.devtools.build.lib.buildtool.AnalysisPhaseRunner.evaluateProjectFile;
import static com.google.devtools.build.lib.skyframe.serialization.analysis.RemoteAnalysisCachingOptions.RemoteAnalysisCacheMode.DOWNLOAD;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.joining;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Stopwatch;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableClassToInstanceMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;
import com.google.common.flogger.GoogleLogger;
import com.google.common.hash.HashCode;
import com.google.devtools.build.lib.actions.Artifact.ArtifactSerializationContext;
import com.google.devtools.build.lib.actions.BuildFailedException;
import com.google.devtools.build.lib.actions.CommandLineExpansionException;
import com.google.devtools.build.lib.actions.TestExecException;
import com.google.devtools.build.lib.analysis.AnalysisAndExecutionResult;
import com.google.devtools.build.lib.analysis.AnalysisResult;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.Project;
import com.google.devtools.build.lib.analysis.Project.ProjectParseException;
import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
import com.google.devtools.build.lib.analysis.actions.TemplateExpansionException;
import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
import com.google.devtools.build.lib.buildeventstream.BuildEvent.LocalFile.LocalFileType;
import com.google.devtools.build.lib.buildeventstream.BuildEventArtifactUploader.UploadContext;
import com.google.devtools.build.lib.buildeventstream.BuildEventIdUtil;
import com.google.devtools.build.lib.buildeventstream.BuildEventProtocolOptions;
import com.google.devtools.build.lib.buildtool.AnalysisPhaseRunner.ProjectEvaluationResult;
import com.google.devtools.build.lib.buildtool.SkyframeMemoryDumper.DisplayMode;
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;
import com.google.devtools.build.lib.buildtool.buildevent.NoExecutionEvent;
import com.google.devtools.build.lib.buildtool.buildevent.StartingAqueryDumpAfterBuildEvent;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.cmdline.TargetParsingException;
import com.google.devtools.build.lib.collect.PathFragmentPrefixTrie;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.events.OutputFilter;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.exec.ExecutionOptions;
import com.google.devtools.build.lib.packages.RuleClassProvider;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.pkgcache.LoadingFailedException;
import com.google.devtools.build.lib.profiler.ProfilePhase;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.profiler.SilentCloseable;
import com.google.devtools.build.lib.query2.aquery.ActionGraphProtoOutputFormatterCallback;
import com.google.devtools.build.lib.runtime.BlazeRuntime;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.server.FailureDetails.ActionQuery;
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.BuildResultListener;
import com.google.devtools.build.lib.skyframe.PrerequisitePackageFunction;
import com.google.devtools.build.lib.skyframe.ProjectValue;
import com.google.devtools.build.lib.skyframe.RepositoryMappingValue.RepositoryMappingResolutionException;
import com.google.devtools.build.lib.skyframe.SequencedSkyframeExecutor;
import com.google.devtools.build.lib.skyframe.SkyframeBuildView.BuildDriverKeyTestContext;
import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
import com.google.devtools.build.lib.skyframe.TargetPatternPhaseValue;
import com.google.devtools.build.lib.skyframe.actiongraph.v2.ActionGraphDump;
import com.google.devtools.build.lib.skyframe.actiongraph.v2.AqueryOutputHandler;
import com.google.devtools.build.lib.skyframe.actiongraph.v2.AqueryOutputHandler.OutputType;
import com.google.devtools.build.lib.skyframe.actiongraph.v2.InvalidAqueryOutputFormatException;
import com.google.devtools.build.lib.skyframe.config.FlagSetValue;
import com.google.devtools.build.lib.skyframe.serialization.FingerprintValueService;
import com.google.devtools.build.lib.skyframe.serialization.ObjectCodecRegistry;
import com.google.devtools.build.lib.skyframe.serialization.ObjectCodecs;
import com.google.devtools.build.lib.skyframe.serialization.SerializationException;
import com.google.devtools.build.lib.skyframe.serialization.SkyValueRetriever.FrontierNodeVersion;
import com.google.devtools.build.lib.skyframe.serialization.SkyValueRetriever.RetrievalResult;
import com.google.devtools.build.lib.skyframe.serialization.analysis.FrontierSerializer;
import com.google.devtools.build.lib.skyframe.serialization.analysis.RemoteAnalysisCachingDependenciesProvider;
import com.google.devtools.build.lib.skyframe.serialization.analysis.RemoteAnalysisCachingEventListener;
import com.google.devtools.build.lib.skyframe.serialization.analysis.RemoteAnalysisCachingOptions;
import com.google.devtools.build.lib.util.AbruptExitException;
import com.google.devtools.build.lib.util.CrashFailureDetails;
import com.google.devtools.build.lib.util.DetailedExitCode;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.util.InterruptedFailureDetails;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.Root;
import com.google.devtools.build.skyframe.EvaluationResult;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import com.google.devtools.common.options.RegexPatternOption;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.Collection;
import java.util.Optional;
import javax.annotation.Nullable;

/**
 * Provides the bulk of the implementation of the 'blaze build' command.
 *
 * <p>The various concrete build command classes handle the command options and request setup, then
 * delegate the handling of the request (the building of targets) to this class.
 *
 * <p>The main entry point is {@link #buildTargets}.
 *
 * <p>Most of analysis is handled in {@link com.google.devtools.build.lib.analysis.BuildView}, and
 * execution in {@link ExecutionTool}.
 */
public class BuildTool {

  private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();

  private static final String SKYFRAME_MEMORY_DUMP_FILE = "skyframe_memory.json";

  private static final AnalysisPostProcessor NOOP_POST_PROCESSOR =
      (req, env, runtime, analysisResult) -> {};

  /** Hook for inserting extra post-analysis-phase processing. Used for implementing {a,c}query. */
  public interface AnalysisPostProcessor {
    void process(
        BuildRequest request,
        CommandEnvironment env,
        BlazeRuntime runtime,
        AnalysisResult analysisResult)
        throws InterruptedException, ViewCreationFailedException, ExitException;
  }

  private final CommandEnvironment env;
  private final BlazeRuntime runtime;
  private final AnalysisPostProcessor analysisPostProcessor;

  /**
   * Constructs a BuildTool.
   *
   * @param env a reference to the command environment of the currently executing command
   */
  public BuildTool(CommandEnvironment env) {
    this(env, NOOP_POST_PROCESSOR);
  }

  public BuildTool(CommandEnvironment env, AnalysisPostProcessor postProcessor) {
    this.env = env;
    this.runtime = env.getRuntime();
    this.analysisPostProcessor = postProcessor;
  }

  /**
   * The crux of the build system: builds the targets specified in the request.
   *
   * <p>Performs loading, analysis and execution for the specified set of targets, honoring the
   * configuration options in the BuildRequest. Returns normally iff successful, throws an exception
   * otherwise.
   *
   * <p>Callers must ensure that {@link #stopRequest} is called after this method, even if it
   * throws.
   *
   * <p>The caller is responsible for setting up and syncing the package cache.
   *
   * <p>During this function's execution, the actualTargets and successfulTargets fields of the
   * request object are set.
   *
   * @param request the build request that this build tool is servicing, which specifies various
   *     options; during this method's execution, the actualTargets and successfulTargets fields of
   *     the request object are populated
   * @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)
      throws BuildFailedException,
          InterruptedException,
          ViewCreationFailedException,
          TargetParsingException,
          LoadingFailedException,
          AbruptExitException,
          InvalidConfigurationException,
          TestExecException,
          ExitException,
          PostExecutionDumpException,
          RepositoryMappingResolutionException {
    try (SilentCloseable c = Profiler.instance().profile("validateOptions")) {
      validateOptions(request);
    }
    BuildOptions buildOptions;
    try (SilentCloseable c = Profiler.instance().profile("createBuildOptions")) {
      buildOptions = runtime.createBuildOptions(request);
    }

    boolean catastrophe = false;
    try {
      try (SilentCloseable c = Profiler.instance().profile("BuildStartingEvent")) {
        env.getEventBus().post(BuildStartingEvent.create(env, request));
      }
      logger.atInfo().log("Build identifier: %s", request.getId());

      // Exit if there are any pending exceptions from modules.
      env.throwPendingException();

      initializeOutputFilter(request);

      TargetPatternPhaseValue targetPatternPhaseValue;
      Profiler.instance().markPhase(ProfilePhase.TARGET_PATTERN_EVAL);
      try (SilentCloseable c = Profiler.instance().profile("evaluateTargetPatterns")) {
        targetPatternPhaseValue = evaluateTargetPatterns(env, request, validator);
      }
      env.setWorkspaceName(targetPatternPhaseValue.getWorkspaceName());

      ProjectEvaluationResult projectEvaluationResult =
          evaluateProjectFile(request, buildOptions, targetPatternPhaseValue, env);

      var analysisCachingDeps =
          RemoteAnalysisCachingDependenciesProviderImpl.forAnalysis(
              env, projectEvaluationResult.activeDirectoriesMatcher());

      if (env.withMergedAnalysisAndExecutionSourceOfTruth()) {
        // a.k.a. Skymeld.
        buildTargetsWithMergedAnalysisExecution(
            request,
            result,
            targetPatternPhaseValue,
            projectEvaluationResult.buildOptions(),
            analysisCachingDeps);
      } else {
        buildTargetsWithoutMergedAnalysisExecution(
            request,
            result,
            targetPatternPhaseValue,
            projectEvaluationResult.buildOptions(),
            analysisCachingDeps);
      }

      logAnalysisCachingStatsAndMaybeUploadFrontier(
          request, projectEvaluationResult.activeDirectoriesMatcher());

      if (env.getSkyframeExecutor().getSkyfocusState().enabled()) {
        // Skyfocus only works at the end of a successful build.
        ImmutableSet<Label> topLevelTargets =
            result.getActualTargets().stream()
                .map(ConfiguredTarget::getLabel)
                .collect(toImmutableSet());
        env.getSkyframeExecutor()
            .runSkyfocus(
                topLevelTargets,
                projectEvaluationResult.activeDirectoriesMatcher(),
                env.getReporter(),
                env.getBlazeWorkspace().getPersistentActionCache(),
                env.getOptions());
      }
    } catch (Error | RuntimeException e) {
      // Don't handle the error here. We will do so in stopRequest.
      catastrophe = true;
      throw e;
    } catch (Exception e) {
      throw e;
    } finally {
      if (!catastrophe) {
        // Delete dirty nodes to ensure that they do not accumulate indefinitely.
        long versionWindow = request.getViewOptions().versionWindowForDirtyNodeGc;
        if (versionWindow != -1) {
          env.getSkyframeExecutor().deleteOldNodes(versionWindow);
        }
        // The workspace status actions will not run with certain flags, or if an error occurs early
        // in the build. Ensure that build info is posted on every build.
        env.ensureBuildInfoPosted();
      }
    }
  }

  private static TargetPatternPhaseValue evaluateTargetPatterns(
      CommandEnvironment env, final BuildRequest request, final TargetValidator validator)
      throws LoadingFailedException, TargetParsingException, InterruptedException {
    boolean keepGoing = request.getKeepGoing();
    TargetPatternPhaseValue result =
        env.getSkyframeExecutor()
            .loadTargetPatternsWithFilters(
                env.getReporter(),
                request.getTargets(),
                env.getRelativeWorkingDirectory(),
                request.getLoadingOptions(),
                request.getLoadingPhaseThreadCount(),
                keepGoing,
                request.shouldRunTests());
    if (validator != null) {
      ImmutableSet<Target> targets =
          result.getTargets(env.getReporter(), env.getSkyframeExecutor().getPackageManager());
      validator.validateTargets(targets, keepGoing);
    }
    return result;
  }

  private void buildTargetsWithoutMergedAnalysisExecution(
      BuildRequest request,
      BuildResult result,
      TargetPatternPhaseValue targetPatternPhaseValue,
      BuildOptions buildOptions,
      RemoteAnalysisCachingDependenciesProvider analysisCachingDeps)
      throws BuildFailedException,
          ViewCreationFailedException,
          TargetParsingException,
          LoadingFailedException,
          AbruptExitException,
          RepositoryMappingResolutionException,
          InterruptedException,
          InvalidConfigurationException,
          TestExecException,
          ExitException,
          PostExecutionDumpException {
    AnalysisResult analysisResult =
        AnalysisPhaseRunner.execute(
            env, request, targetPatternPhaseValue, buildOptions, analysisCachingDeps);
    ExecutionTool executionTool = null;
    try {
      // We cannot move the executionTool down to the execution phase part since it does set up the
      // symlinks for tools.
      // TODO(twerth): Extract embedded tool setup from execution tool and move object creation to
      // execution phase.
      executionTool = new ExecutionTool(env, request);
      if (request.getBuildOptions().performAnalysisPhase) {
        if (!analysisResult.getExclusiveTests().isEmpty()
            && executionTool.getTestActionContext().forceExclusiveTestsInParallel()) {
          String testStrategy = request.getOptions(ExecutionOptions.class).testStrategy;
          for (ConfiguredTarget test : analysisResult.getExclusiveTests()) {
            getReporter()
                .handle(
                    Event.warn(
                        test.getLabel()
                            + " is tagged exclusive, but --test_strategy="
                            + testStrategy
                            + " forces parallel test execution."));
          }
          analysisResult = analysisResult.withExclusiveTestsAsParallelTests();
        }
        if (!analysisResult.getExclusiveIfLocalTests().isEmpty()
            && executionTool.getTestActionContext().forceExclusiveIfLocalTestsInParallel()) {
          analysisResult = analysisResult.withExclusiveIfLocalTestsAsParallelTests();
        }

        result.setBuildConfiguration(analysisResult.getConfiguration());
        result.setActualTargets(analysisResult.getTargetsToBuild());
        result.setTestTargets(analysisResult.getTargetsToTest());

        try (SilentCloseable c = Profiler.instance().profile("analysisPostProcessor.process")) {
          analysisPostProcessor.process(request, env, runtime, analysisResult);
        }

        if (needsExecutionPhase(request.getBuildOptions())) {
          try (SilentCloseable closeable = Profiler.instance().profile("ExecutionTool.init")) {
            executionTool.init();
          }
          executionTool.executeBuild(
              request.getId(),
              analysisResult,
              result,
              analysisResult.getPackageRoots(),
              request.getTopLevelArtifactContext());
        } else {
          env.getReporter().post(new NoExecutionEvent());
        }
        FailureDetail delayedFailureDetail = analysisResult.getFailureDetail();
        if (delayedFailureDetail != null) {
          throw new BuildFailedException(
              delayedFailureDetail.getMessage(), DetailedExitCode.of(delayedFailureDetail));
        }

        // Only run this post-build step for builds with SequencedSkyframeExecutor.
        if (env.getSkyframeExecutor() instanceof SequencedSkyframeExecutor) {
          // Enabling this feature will disable Skymeld, so it only runs in the non-Skymeld path.
          if (request.getBuildOptions().aqueryDumpAfterBuildFormat != null) {
            try (SilentCloseable c = Profiler.instance().profile("postExecutionDumpSkyframe")) {
              dumpSkyframeStateAfterBuild(
                  request.getOptions(BuildEventProtocolOptions.class),
                  request.getBuildOptions().aqueryDumpAfterBuildFormat,
                  request.getBuildOptions().aqueryDumpAfterBuildOutputFile);
            } catch (CommandLineExpansionException | IOException | TemplateExpansionException e) {
              throw new PostExecutionDumpException(e);
            } catch (InvalidAqueryOutputFormatException e) {
              throw new PostExecutionDumpException(
                  "--skyframe_state must be used with "
                      + "--output=proto|streamed_proto|textproto|jsonproto.",
                  e);
            }
          }
        }
      }
    } finally {
      if (executionTool != null) {
        executionTool.shutdown();
      }
    }
  }

  /** Performs the merged analysis and execution phase. */
  private void buildTargetsWithMergedAnalysisExecution(
      BuildRequest request,
      BuildResult result,
      TargetPatternPhaseValue targetPatternPhaseValue,
      BuildOptions buildOptions,
      RemoteAnalysisCachingDependenciesProvider remoteAnalysisCachingDependenciesProvider)
      throws InterruptedException,
          AbruptExitException,
          ViewCreationFailedException,
          BuildFailedException,
          TestExecException,
          InvalidConfigurationException,
          RepositoryMappingResolutionException {
    // See https://github.com/bazelbuild/rules_nodejs/issues/3693.
    env.getSkyframeExecutor().clearSyscallCache();

    boolean hasCatastrophe = false;

    ExecutionTool executionTool = new ExecutionTool(env, request);
    // This timer measures time from the first execution activity to the last.
    Stopwatch executionTimer = Stopwatch.createUnstarted();

    // TODO(b/199053098): implement support for --nobuild.
    AnalysisAndExecutionResult analysisAndExecutionResult = null;
    boolean buildCompleted = false;
    try {
      analysisAndExecutionResult =
          AnalysisAndExecutionPhaseRunner.execute(
              env,
              request,
              buildOptions,
              targetPatternPhaseValue,
              () -> executionTool.prepareForExecution(executionTimer),
              result::setBuildConfiguration,
              new BuildDriverKeyTestContext() {
                @Override
                public String getTestStrategy() {
                  return request.getOptions(ExecutionOptions.class).testStrategy;
                }

                @Override
                public boolean forceExclusiveTestsInParallel() {
                  return executionTool.getTestActionContext().forceExclusiveTestsInParallel();
                }

                @Override
                public boolean forceExclusiveIfLocalTestsInParallel() {
                  return executionTool
                      .getTestActionContext()
                      .forceExclusiveIfLocalTestsInParallel();
                }
              },
              remoteAnalysisCachingDependenciesProvider);
      buildCompleted = true;

      // This value is null when there's no analysis.
      if (analysisAndExecutionResult == null) {
        return;
      }
    } catch (InvalidConfigurationException
        | RepositoryMappingResolutionException
        | ViewCreationFailedException
        | BuildFailedException
        | TestExecException e) {
      // These are non-catastrophic.
      buildCompleted = true;
      throw e;
    } catch (Error | RuntimeException e) {
      // These are catastrophic.
      hasCatastrophe = true;
      throw e;
    } finally {
      if (result.getBuildConfiguration() != null) {
        // We still need to do this even in case of an exception.
        result.setConvenienceSymlinks(
            executionTool.handleConvenienceSymlinks(
                env.getBuildResultListener().getAnalyzedTargets(), result.getBuildConfiguration()));
      }
      executionTool.unconditionalExecutionPhaseFinalizations(
          executionTimer, env.getSkyframeExecutor());

      // For the --noskymeld code path, this is done after the analysis phase.
      BuildResultListener buildResultListener = env.getBuildResultListener();
      result.setActualTargets(buildResultListener.getAnalyzedTargets());
      result.setTestTargets(buildResultListener.getAnalyzedTests());

      if (!hasCatastrophe) {
        executionTool.nonCatastrophicFinalizations(
            result,
            env.getBlazeWorkspace().getInUseActionCacheWithoutFurtherLoading(),
            /* explanationHandler= */ null,
            buildCompleted);
      }
    }

    // This is the --keep_going code path: Time to throw the delayed exceptions.
    // Keeping legacy behavior: for execution errors, keep the message of the BuildFailedException
    // empty.
    if (analysisAndExecutionResult.getExecutionDetailedExitCode() != null) {
      throw new BuildFailedException(
          null, analysisAndExecutionResult.getExecutionDetailedExitCode());
    }

    FailureDetail delayedFailureDetail = analysisAndExecutionResult.getFailureDetail();
    if (delayedFailureDetail != null) {
      throw new BuildFailedException(
          delayedFailureDetail.getMessage(), DetailedExitCode.of(delayedFailureDetail));
    }
  }

  private void dumpSkyframeMemory(
      BuildResult buildResult, BuildEventProtocolOptions bepOptions, String format)
      throws PostExecutionDumpException, InterruptedException {
    if (!env.getSkyframeExecutor().tracksStateForIncrementality()) {
      throw new PostExecutionDumpException(
          "Skyframe memory dump requested, but incremental state is not tracked", null);
    }

    boolean reportTransient = true;
    boolean reportConfiguration = true;
    boolean reportPrecomputed = true;
    boolean reportWorkspaceStatus = true;

    for (String flag : Splitter.on(",").split(format)) {
      switch (flag) {
        case "json" -> {} // JSON is the only format we support, no need to note it explicitly
        case "notransient" -> reportTransient = false;
        case "noconfig" -> reportConfiguration = false;
        case "noprecomputed" -> reportPrecomputed = false;
        case "noworkspacestatus" -> reportWorkspaceStatus = false;
        default -> throw new PostExecutionDumpException("Unknown flag: '" + flag + "'", null);
      }
    }

    try {
      OutputStream outputStream;
      UploadContext streamingContext = null;

      if (bepOptions.streamingLogFileUploads) {
        streamingContext =
            runtime
                .getBuildEventArtifactUploaderFactoryMap()
                .select(bepOptions.buildEventUploadStrategy)
                .create(env)
                .startUpload(LocalFileType.PERFORMANCE_LOG, null);
        outputStream = streamingContext.getOutputStream();
        buildResult
            .getBuildToolLogCollection()
            .addUriFuture(SKYFRAME_MEMORY_DUMP_FILE, streamingContext.uriFuture());
      } else {
        Path localPath = env.getOutputBase().getRelative(SKYFRAME_MEMORY_DUMP_FILE);
        outputStream = localPath.getOutputStream();
        buildResult.getBuildToolLogCollection().addLocalFile(SKYFRAME_MEMORY_DUMP_FILE, localPath);
      }

      try (PrintStream printStream = new PrintStream(outputStream)) {

        SkyframeMemoryDumper dumper =
            new SkyframeMemoryDumper(
                DisplayMode.SUMMARY,
                null,
                runtime.getRuleClassProvider(),
                env.getSkyframeExecutor().getEvaluator().getInMemoryGraph(),
                reportTransient,
                reportConfiguration,
                reportPrecomputed,
                reportWorkspaceStatus);
        dumper.dumpFull(printStream);
      }
    } catch (IOException | SkyframeMemoryDumper.DumpFailedException e) {
      throw new PostExecutionDumpException("cannot write Skyframe dump: " + e.getMessage(), e);
    }
  }

  /**
   * Produces an aquery dump of the state of Skyframe.
   *
   * <p>There are 2 possible output channels: a local file or a remote FS.
   */
  private void dumpSkyframeStateAfterBuild(
      @Nullable BuildEventProtocolOptions besOptions,
      String format,
      @Nullable PathFragment outputFilePathFragment)
      throws CommandLineExpansionException, IOException, InvalidAqueryOutputFormatException,
          TemplateExpansionException {
    Preconditions.checkState(env.getSkyframeExecutor() instanceof SequencedSkyframeExecutor);

    UploadContext streamingContext = null;
    Path localOutputFilePath = null;
    String outputFileName;

    if (outputFilePathFragment == null) {
      outputFileName = getDefaultOutputFileName(format);
      if (besOptions != null && besOptions.streamingLogFileUploads) {
        streamingContext =
            runtime
                .getBuildEventArtifactUploaderFactoryMap()
                .select(besOptions.buildEventUploadStrategy)
                .create(env)
                .startUpload(LocalFileType.PERFORMANCE_LOG, /* inputSupplier= */ null);
      } else {
        localOutputFilePath = env.getOutputBase().getRelative(outputFileName);
      }
    } else {
      localOutputFilePath = env.getOutputBase().getRelative(outputFilePathFragment);
      outputFileName = localOutputFilePath.getBaseName();
    }

    if (localOutputFilePath != null) {
      getReporter().handle(Event.info("Writing aquery dump to " + localOutputFilePath));
      getReporter()
          .post(new StartingAqueryDumpAfterBuildEvent(localOutputFilePath, outputFileName));
    } else {
      getReporter().handle(Event.info("Streaming aquery dump."));
      getReporter().post(new StartingAqueryDumpAfterBuildEvent(streamingContext, outputFileName));
    }

    try (OutputStream outputStream = initOutputStream(streamingContext, localOutputFilePath);
        PrintStream printStream = new PrintStream(outputStream);
        AqueryOutputHandler aqueryOutputHandler =
            ActionGraphProtoOutputFormatterCallback.constructAqueryOutputHandler(
                OutputType.fromString(format), outputStream, printStream)) {
      // These options are fixed for simplicity. We'll add more configurability if the need arises.
      ActionGraphDump actionGraphDump =
          new ActionGraphDump(
              /* includeActionCmdLine= */ false,
              /* includeArtifacts= */ true,
              /* includePrunedInputs= */ true,
              /* actionFilters= */ null,
              /* includeParamFiles= */ false,
              /* includeFileWriteContents= */ false,
              aqueryOutputHandler,
              getReporter());
      AqueryProcessor.dumpActionGraph(env, aqueryOutputHandler, actionGraphDump);
    }
  }

  private static String getDefaultOutputFileName(String format) {
    switch (format) {
      case "proto":
        return "aquery_dump.proto";
      case "streamed_proto":
        return "aquery_dump.pb";
      case "textproto":
        return "aquery_dump.textproto";
      case "jsonproto":
        return "aquery_dump.json";
      default:
        throw new IllegalArgumentException("Unsupported format type: " + format);
    }
  }

  private static OutputStream initOutputStream(
      @Nullable UploadContext streamingContext, Path outputFilePath) throws IOException {
    if (streamingContext != null) {
      return new BufferedOutputStream(streamingContext.getOutputStream());
    }
    return new BufferedOutputStream(outputFilePath.getOutputStream());
  }

  private void reportExceptionError(Exception e) {
    if (e.getMessage() != null) {
      getReporter().handle(Event.error(e.getMessage()));
    }
  }

  public BuildResult processRequest(BuildRequest request, TargetValidator validator) {
    return processRequest(request, validator, /* postBuildCallback= */ null);
  }

  /**
   * The crux of the build system. Builds the targets specified in the request using the specified
   * Executor.
   *
   * <p>Performs loading, analysis and execution for the specified set of targets, honoring the
   * configuration options in the BuildRequest. Returns normally iff successful, throws an exception
   * otherwise.
   *
   * <p>The caller is responsible for setting up and syncing the package cache.
   *
   * <p>During this function's execution, the actualTargets and successfulTargets fields of the
   * request object are set.
   *
   * @param request the build request that this build tool is servicing, which specifies various
   *     options; during this method's execution, the actualTargets and successfulTargets fields of
   *     the request object are populated
   * @param validator an optional target validator
   * @param postBuildCallback an optional callback called after the build has been completed
   *     successfully
   * @return the result as a {@link BuildResult} object
   */
  public BuildResult processRequest(
      BuildRequest request, TargetValidator validator, PostBuildCallback postBuildCallback) {
    BuildResult result = new BuildResult(request.getStartTime());
    maybeSetStopOnFirstFailure(request, result);
    Throwable crash = null;
    DetailedExitCode detailedExitCode = null;
    try {
      try (SilentCloseable c = Profiler.instance().profile("buildTargets")) {
        buildTargets(request, result, validator);
      }
      detailedExitCode = DetailedExitCode.success();
      if (postBuildCallback != null) {
        try (SilentCloseable c = Profiler.instance().profile("postBuildCallback.process")) {
          result.setPostBuildCallbackFailureDetail(
              postBuildCallback.process(result.getSuccessfulTargets()));
        } catch (InterruptedException e) {
          detailedExitCode =
              InterruptedFailureDetails.detailedExitCode("post build callback interrupted");
        }
      }

      if (env.getSkyframeExecutor() instanceof SequencedSkyframeExecutor
          && request.getBuildOptions().skyframeMemoryDump != null) {
        try (SilentCloseable c = Profiler.instance().profile("BuildTool.dumpSkyframeMemory")) {
          dumpSkyframeMemory(
              result,
              request.getOptions(BuildEventProtocolOptions.class),
              request.getBuildOptions().skyframeMemoryDump);
        }
      }
    } catch (BuildFailedException e) {
      if (!e.isErrorAlreadyShown()) {
        // The actual error has not already been reported by the Builder.
        // TODO(janakr): This is wrong: --keep_going builds with errors don't have a message in
        //  this BuildFailedException, so any error message that is only reported here will be
        //  missing for --keep_going builds. All error reporting should be done at the site of the
        //  error, if only for clearer behavior.
        reportExceptionError(e);
      }
      if (e.isCatastrophic()) {
        result.setCatastrophe();
      }
      detailedExitCode = e.getDetailedExitCode();
    } catch (InterruptedException e) {
      // We may have been interrupted by an error, or the user's interruption may have raced with
      // an error, so check to see if we should report that error code instead.
      detailedExitCode = env.getRuntime().getCrashExitCode();
      AbruptExitException environmentPendingAbruptExitException = env.getPendingException();
      if (detailedExitCode == null && environmentPendingAbruptExitException != null) {
        detailedExitCode = environmentPendingAbruptExitException.getDetailedExitCode();
        // Report the exception from the environment - the exception we're handling here is just an
        // interruption.
        reportExceptionError(environmentPendingAbruptExitException);
      }
      if (detailedExitCode == null) {
        String message = "build interrupted";
        detailedExitCode = InterruptedFailureDetails.detailedExitCode(message);
        env.getReporter().handle(Event.error(message));
        env.getEventBus().post(new BuildInterruptedEvent());
      } else {
        result.setCatastrophe();
      }
    } catch (TargetParsingException | LoadingFailedException e) {
      detailedExitCode = e.getDetailedExitCode();
      reportExceptionError(e);
    } catch (RepositoryMappingResolutionException e) {
      detailedExitCode = e.getDetailedExitCode();
      reportExceptionError(e);
    } catch (ViewCreationFailedException e) {
      detailedExitCode = DetailedExitCode.of(ExitCode.PARSING_FAILURE, e.getFailureDetail());
      reportExceptionError(e);
    } catch (ExitException e) {
      detailedExitCode = e.getDetailedExitCode();
      reportExceptionError(e);
    } catch (TestExecException e) {
      // ExitCode.SUCCESS means that build was successful. Real return code of program
      // is going to be calculated in TestCommand.doTest().
      detailedExitCode = DetailedExitCode.success();
      reportExceptionError(e);
    } catch (InvalidConfigurationException e) {
      detailedExitCode = e.getDetailedExitCode();
      reportExceptionError(e);
      // TODO(gregce): With "global configurations" we cannot tie a configuration creation failure
      // to a single target and have to halt the entire build. Once configurations are genuinely
      // created as part of the analysis phase they should report their error on the level of the
      // target(s) that triggered them.
      result.setCatastrophe();
    } catch (AbruptExitException e) {
      detailedExitCode = e.getDetailedExitCode();
      reportExceptionError(e);
      result.setCatastrophe();
    } catch (PostExecutionDumpException e) {
      detailedExitCode =
          DetailedExitCode.of(
              FailureDetail.newBuilder()
                  .setMessage(e.getMessage())
                  .setActionQuery(
                      ActionQuery.newBuilder()
                          .setCode(ActionQuery.Code.SKYFRAME_STATE_AFTER_EXECUTION)
                          .build())
                  .build());
      reportExceptionError(e);
    } catch (Throwable throwable) {
      crash = throwable;
      detailedExitCode = CrashFailureDetails.detailedExitCodeForThrowable(crash);
      Throwables.throwIfUnchecked(throwable);
      throw new IllegalStateException(throwable);
    } finally {
      if (detailedExitCode == null) {
        detailedExitCode =
            CrashFailureDetails.detailedExitCodeForThrowable(
                new IllegalStateException("Unspecified DetailedExitCode"));
      }
      try (SilentCloseable c = Profiler.instance().profile("stopRequest")) {
        stopRequest(result, crash, detailedExitCode);
      }
    }

    return result;
  }

  /**
   * Handles post-build analysis caching operations.
   *
   * <ol>
   *   <li>If this is a cache-writing build, then this will serialize and upload the frontier
   *       Skyframe values.
   *   <li>If this is a cache-reading build, then this will report the cache hit stats while
   *       downloading the frontier Skyframe values during analysis.
   * </ol>
   */
  private void logAnalysisCachingStatsAndMaybeUploadFrontier(
      BuildRequest request, Optional<PathFragmentPrefixTrie> activeDirectoriesMatcher)
      throws InterruptedException, AbruptExitException {
    if (!(env.getSkyframeExecutor() instanceof SequencedSkyframeExecutor)) {
      return;
    }

    RemoteAnalysisCachingOptions options = request.getOptions(RemoteAnalysisCachingOptions.class);
    if (options == null) {
      return;
    }

    switch (options.mode) {
      case UPLOAD -> {
        try (SilentCloseable closeable =
            Profiler.instance().profile("serializeAndUploadFrontier")) {
          Optional<FailureDetail> maybeFailureDetail =
              FrontierSerializer.serializeAndUploadFrontier(
                  new RemoteAnalysisCachingDependenciesProviderImpl(
                      env, activeDirectoriesMatcher.get()),
                  env.getSkyframeExecutor(),
                  env.getReporter(),
                  env.getEventBus(),
                  options.serializedFrontierProfile);
          if (maybeFailureDetail.isPresent()) {
            throw new AbruptExitException(DetailedExitCode.of(maybeFailureDetail.get()));
          }
        }
      }
      case DOWNLOAD -> {
        var listener = env.getRemoteAnalysisCachingEventListener();
        env.getReporter()
            .handle(
                Event.info(
                    String.format(
                        "Remote analysis caching stats: %s cache hits (%s analysis, %s execution),"
                            + " %s cache misses (%s analysis, %s execution).",
                        listener.getAnalysisNodeCacheHits() + listener.getExecutionNodeCacheHits(),
                        listener.getAnalysisNodeCacheHits(),
                        listener.getExecutionNodeCacheHits(),
                        listener.getAnalysisNodeCacheMisses()
                            + listener.getExecutionNodeCacheMisses(),
                        listener.getAnalysisNodeCacheMisses(),
                        listener.getExecutionNodeCacheMisses())));
      }
      case OFF -> {}
    }
  }

  private static void maybeSetStopOnFirstFailure(BuildRequest request, BuildResult result) {
    if (shouldStopOnFailure(request)) {
      result.setStopOnFirstFailure(true);
    }
  }

  private static boolean shouldStopOnFailure(BuildRequest request) {
    return !(request.getKeepGoing() && request.getExecutionOptions().testKeepGoing);
  }

  /** Initializes the output filter to the value given with {@code --output_filter}. */
  private void initializeOutputFilter(BuildRequest request) {
    RegexPatternOption outputFilterOption = request.getBuildOptions().outputFilter;
    if (outputFilterOption != null) {
      getReporter()
          .setOutputFilter(
              OutputFilter.RegexOutputFilter.forPattern(outputFilterOption.regexPattern()));
    }
  }

  private static boolean needsExecutionPhase(BuildRequestOptions options) {
    return options.performAnalysisPhase && options.performExecutionPhase;
  }

  /**
   * Stops processing the specified request.
   *
   * <p>This logs the build result, cleans up and stops the clock.
   *
   * @param result result to update
   * @param crash any unexpected {@link RuntimeException} or {@link Error}, may be null
   * @param detailedExitCode describes the exit code and an optional detailed failure value to add
   *     to {@code result}
   */
  public void stopRequest(
      BuildResult result, @Nullable Throwable crash, DetailedExitCode detailedExitCode) {
    Preconditions.checkState((crash == null) || !detailedExitCode.isSuccess());
    result.setUnhandledThrowable(crash);
    result.setDetailedExitCode(detailedExitCode);
    if (!detailedExitCode.isSuccess()) {
      logger.atInfo().log(
          "Unsuccessful command ended with FailureDetail: %s", detailedExitCode.getFailureDetail());
    }

    InterruptedException ie = null;

    // The stop time has to be captured before we send the BuildCompleteEvent.
    result.setStopTime(runtime.getClock().currentTimeMillis());

    // Skip the build complete events so that modules can run blazeShutdownOnCrash without thinking
    // that the build completed normally. BlazeCommandDispatcher will call handleCrash.
    if (crash == null) {
      try {
        Profiler.instance().markPhase(ProfilePhase.FINISH);
      } catch (InterruptedException e) {
        env.getReporter().handle(Event.error("Build interrupted during command completion"));
        ie = e;
      }

      env.getEventBus()
          .post(
              new BuildCompleteEvent(
                  result,
                  ImmutableList.of(
                      BuildEventIdUtil.buildToolLogs(), BuildEventIdUtil.buildMetrics())));
    }
    // Post the build tool logs event; the corresponding local files may be contributed from
    // modules, and this has to happen after posting the BuildCompleteEvent because that's when
    // modules add their data to the collection.
    env.getEventBus().post(result.getBuildToolLogCollection().freeze().toEvent());
    if (ie != null) {
      if (detailedExitCode.isSuccess()) {
        result.setDetailedExitCode(
            InterruptedFailureDetails.detailedExitCode(
                "Build interrupted during command completion"));
      } else if (!detailedExitCode.getExitCode().equals(ExitCode.INTERRUPTED)) {
        logger.atWarning().withCause(ie).log(
            "Suppressed interrupted exception during stop request because already failing with: %s",
            detailedExitCode);
      }
    }
  }

  /**
   * Validates the options for this BuildRequest.
   *
   * <p>Issues warnings for the use of deprecated options, and warnings or errors for any option
   * settings that conflict.
   */
  @VisibleForTesting
  public void validateOptions(BuildRequest request) {
    for (String issue : request.validateOptions()) {
      getReporter().handle(Event.warn(issue));
    }
  }

  /**
   * Returns the project file suitable for this build, or null if there's no project file.
   *
   * @param topLevelTargets this build's top-level targets
   * @param skyframeExecutor support for SkyFunctions that look up project files
   * @param eventHandler event handler
   * @throws LoadingFailedException if this build doesn't cleanly resolve to a single project file.
   *     Builds are valid if either a) all top-level targets resolve to the same project file or b)
   *     none of the top-level targets resolve to any project file. Builds are invalid if a) any
   *     top-level targets resolve to different project files or b) any top-level target has
   *     multiple project files (i.e. foo/project.scl and foo/bar/project.scl).
   */
  // TODO: b/324127375 - Support hierarchical project files: [foo/project.scl, foo/bar/project.scl].
  @Nullable
  @VisibleForTesting
  public static Label getProjectFile(
      Collection<Label> topLevelTargets,
      SkyframeExecutor skyframeExecutor,
      ExtendedEventHandler eventHandler)
      throws LoadingFailedException {
    ImmutableMultimap<Label, Label> projectFiles;
    try {
      projectFiles = Project.findProjectFiles(topLevelTargets, skyframeExecutor, eventHandler);
    } catch (ProjectParseException e) {
      throw new LoadingFailedException(
          "Error finding project files: " + e.getCause().getMessage(),
          DetailedExitCode.of(ExitCode.PARSING_FAILURE, FailureDetail.getDefaultInstance()));
    }

    ImmutableSet<Label> distinct = ImmutableSet.copyOf(projectFiles.values());
    if (distinct.size() == 1) {
      Label projectFile = Iterables.getOnlyElement(distinct);
      eventHandler.handle(
          Event.info(String.format("Reading project settings from %s.", projectFile)));
      return projectFile;
    } else if (distinct.isEmpty()) {
      return null;
    } else {
      ListMultimap<Collection<Label>, Label> projectFilesToTargets = LinkedListMultimap.create();
      Multimaps.invertFrom(Multimaps.forMap(projectFiles.asMap()), projectFilesToTargets);
      StringBuilder msgBuilder =
          new StringBuilder("This build doesn't support automatic project resolution. ");
      if (projectFilesToTargets.size() == 1) {
        msgBuilder
            .append("Multiple project files found: ")
            .append(
                projectFilesToTargets.keys().stream().map(Object::toString).collect(joining(",")));
      } else {
        msgBuilder.append("Targets have different project settings. For example: ");
        for (var entry : projectFilesToTargets.asMap().entrySet()) {
          msgBuilder.append(
              String.format(
                  " [%s]: %s",
                  entry.getKey().stream().map(l -> l.toString()).collect(joining(",")),
                  entry.getValue().iterator().next()));
        }
      }
      String errorMsg = msgBuilder.toString();
      throw new LoadingFailedException(
          errorMsg,
          DetailedExitCode.of(ExitCode.BUILD_FAILURE, FailureDetail.getDefaultInstance()));
    }
  }

  /** Returns the project directories found in a project file. */
  public static PathFragmentPrefixTrie getWorkingSetMatcherForSkyfocus(
      Label projectFile, SkyframeExecutor skyframeExecutor, ExtendedEventHandler eventHandler)
      throws InvalidConfigurationException {
    ProjectValue.Key key = new ProjectValue.Key(projectFile);
    EvaluationResult<SkyValue> result =
        skyframeExecutor.evaluateSkyKeys(
            eventHandler, ImmutableList.of(key), /* keepGoing= */ false);

    if (result.hasError()) {
      // InvalidConfigurationException is chosen for convenience, and it's distinguished from
      // the other InvalidConfigurationException cases by Code.INVALID_PROJECT.
      throw new InvalidConfigurationException(
          "unexpected error reading project configuration: " + result.getError(),
          Code.INVALID_PROJECT);
    }

    return PathFragmentPrefixTrie.of(((ProjectValue) result.get(key)).getDefaultActiveDirectory());
  }

  /** Creates a BuildOptions class for the given options taken from an {@link OptionsProvider}. */
  public static BuildOptions applySclConfigs(
      BuildOptions buildOptionsBeforeFlagSets,
      Label projectFile,
      boolean enforceCanonicalConfigs,
      SkyframeExecutor skyframeExecutor,
      ExtendedEventHandler eventHandler)
      throws InvalidConfigurationException {

    FlagSetValue flagSetValue =
        Project.modifyBuildOptionsWithFlagSets(
            projectFile,
            buildOptionsBeforeFlagSets,
            enforceCanonicalConfigs,
            eventHandler,
            skyframeExecutor);

    // BuildOptions after Flagsets
    return flagSetValue.getTopLevelBuildOptions();
  }

  private Reporter getReporter() {
    return env.getReporter();
  }

  /** Describes a failure that isn't severe enough to halt the command in keep_going mode. */
  // TODO(mschaller): consider promoting this to be a sibling of AbruptExitException.
  public static class ExitException extends Exception {

    private final DetailedExitCode detailedExitCode;

    ExitException(DetailedExitCode detailedExitCode) {
      super(checkNotNull(detailedExitCode.getFailureDetail(), "failure detail").getMessage());
      this.detailedExitCode = detailedExitCode;
    }

    DetailedExitCode getDetailedExitCode() {
      return detailedExitCode;
    }
  }

  private static final class RemoteAnalysisCachingDependenciesProviderImpl
      implements RemoteAnalysisCachingDependenciesProvider {
    private final Supplier<ObjectCodecs> analysisObjectCodecsSupplier;
    private final FingerprintValueService fingerprintValueService;
    private final PathFragmentPrefixTrie activeDirectoriesMatcher;
    private final RemoteAnalysisCachingEventListener listener;
    private final HashCode blazeInstallMD5;

    // Non-final because the top level BuildConfigurationValue is determined just before analysis
    // begins in BuildView for the download/deserialization pass, which is later than when this
    // object was created in BuildTool.
    private BuildConfigurationValue topLevelConfig;

    public static RemoteAnalysisCachingDependenciesProvider forAnalysis(
        CommandEnvironment env, Optional<PathFragmentPrefixTrie> activeDirectoriesMatcher) {
      var options = env.getOptions().getOptions(RemoteAnalysisCachingOptions.class);
      if (options == null || options.mode != DOWNLOAD || activeDirectoriesMatcher.isEmpty()) {
        return DisabledDependenciesProvider.INSTANCE;
      }
      return new RemoteAnalysisCachingDependenciesProviderImpl(env, activeDirectoriesMatcher.get());
    }

    private RemoteAnalysisCachingDependenciesProviderImpl(
        CommandEnvironment env, PathFragmentPrefixTrie activeDirectoriesMatcher) {
      this.analysisObjectCodecsSupplier =
          Suppliers.memoize(
              () ->
                  initAnalysisObjectCodecs(
                      requireNonNull(
                              env.getBlazeWorkspace().getAnalysisObjectCodecRegistrySupplier())
                          .get(),
                      env.getRuntime().getRuleClassProvider(),
                      env.getBlazeWorkspace().getSkyframeExecutor()));
      this.fingerprintValueService =
          env.getBlazeWorkspace().getFingerprintValueServiceFactory().create(env.getOptions());
      this.activeDirectoriesMatcher = activeDirectoriesMatcher;
      this.listener = env.getRemoteAnalysisCachingEventListener();
      this.topLevelConfig = env.getSkyframeBuildView().getBuildConfiguration();
      this.blazeInstallMD5 = requireNonNull(env.getDirectories().getInstallMD5());
    }

    private static ObjectCodecs initAnalysisObjectCodecs(
        ObjectCodecRegistry registry,
        RuleClassProvider ruleClassProvider,
        SkyframeExecutor skyframeExecutor) {
      ImmutableClassToInstanceMap.Builder<Object> serializationDeps =
          ImmutableClassToInstanceMap.builder()
              .put(
                  ArtifactSerializationContext.class,
                  skyframeExecutor.getSkyframeBuildView().getArtifactFactory()::getSourceArtifact)
              .put(RuleClassProvider.class, ruleClassProvider)
              // We need a RootCodecDependencies but don't care about the likely roots.
              .put(Root.RootCodecDependencies.class, new Root.RootCodecDependencies())
              // This is needed to determine TargetData for a ConfiguredTarget during serialization.
              .put(PrerequisitePackageFunction.class, skyframeExecutor::getExistingPackage);

      return new ObjectCodecs(registry, serializationDeps.build());
    }

    @Override
    public boolean withinActiveDirectories(PackageIdentifier pkg) {
      return activeDirectoriesMatcher.includes(pkg.getPackageFragment());
    }

    private FrontierNodeVersion frontierNodeVersionSingleton = null;

    @Override
    public FrontierNodeVersion getSkyValueVersion() throws SerializationException {
      if (frontierNodeVersionSingleton == null) {
        synchronized (this) {
          frontierNodeVersionSingleton =
              new FrontierNodeVersion(
                  topLevelConfig.checksum(),
                  getObjectCodecs().serializeMemoized(activeDirectoriesMatcher.toString()),
                  blazeInstallMD5);
        }
      }
      return frontierNodeVersionSingleton;
    }

    @Override
    public ObjectCodecs getObjectCodecs() {
      return analysisObjectCodecsSupplier.get();
    }

    @Override
    public FingerprintValueService getFingerprintValueService() {
      return fingerprintValueService;
    }

    @Override
    public void recordRetrievalResult(RetrievalResult retrievalResult, SkyKey key) {
      listener.recordRetrievalResult(retrievalResult, key);
    }

    @Override
    public void recordSerializationException(SerializationException e) {
      listener.recordSerializationException(e);
    }

    @Override
    public void setTopLevelConfig(BuildConfigurationValue topLevelConfig) {
      this.topLevelConfig = topLevelConfig;
    }
  }
}
