blob: bf28c9a2f27ba4598ce6256aa2f5b8ad13b9bbda [file] [log] [blame]
// Copyright 2021 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.skyframe;
import static com.google.devtools.build.lib.skyframe.BuildDriverKey.TestType.EXCLUSIVE;
import static com.google.devtools.build.lib.skyframe.BuildDriverKey.TestType.EXCLUSIVE_IF_LOCAL;
import static com.google.devtools.build.lib.skyframe.BuildDriverKey.TestType.NOT_TEST;
import static com.google.devtools.build.lib.skyframe.BuildDriverKey.TestType.PARALLEL;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
import com.google.devtools.build.lib.actions.ActionLookupKey;
import com.google.devtools.build.lib.actions.ActionLookupValue;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.AspectValue;
import com.google.devtools.build.lib.analysis.ConfiguredAspect;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.ConfiguredTargetValue;
import com.google.devtools.build.lib.analysis.ExtraActionArtifactsProvider;
import com.google.devtools.build.lib.analysis.TopLevelArtifactContext;
import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
import com.google.devtools.build.lib.analysis.constraints.RuleContextConstraintSemantics;
import com.google.devtools.build.lib.analysis.constraints.TopLevelConstraintSemantics;
import com.google.devtools.build.lib.analysis.constraints.TopLevelConstraintSemantics.EnvironmentCompatibility;
import com.google.devtools.build.lib.analysis.constraints.TopLevelConstraintSemantics.PlatformCompatibility;
import com.google.devtools.build.lib.analysis.constraints.TopLevelConstraintSemantics.TargetCompatibilityCheckException;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.concurrent.Sharder;
import com.google.devtools.build.lib.packages.NoSuchTargetException;
import com.google.devtools.build.lib.packages.Package;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.profiler.SilentCloseable;
import com.google.devtools.build.lib.server.FailureDetails.Analysis;
import com.google.devtools.build.lib.server.FailureDetails.Analysis.Code;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.skyframe.ArtifactConflictFinder.ConflictException;
import com.google.devtools.build.lib.skyframe.AspectCompletionValue.AspectCompletionKey;
import com.google.devtools.build.lib.skyframe.AspectKeyCreator.AspectKey;
import com.google.devtools.build.lib.skyframe.TopLevelStatusEvents.AspectAnalyzedEvent;
import com.google.devtools.build.lib.skyframe.TopLevelStatusEvents.SomeExecutionStartedEvent;
import com.google.devtools.build.lib.skyframe.TopLevelStatusEvents.TestAnalyzedEvent;
import com.google.devtools.build.lib.skyframe.TopLevelStatusEvents.TopLevelEntityAnalysisConcludedEvent;
import com.google.devtools.build.lib.skyframe.TopLevelStatusEvents.TopLevelTargetAnalyzedEvent;
import com.google.devtools.build.lib.skyframe.TopLevelStatusEvents.TopLevelTargetPendingExecutionEvent;
import com.google.devtools.build.lib.skyframe.TopLevelStatusEvents.TopLevelTargetReadyForSymlinkPlanting;
import com.google.devtools.build.lib.skyframe.TopLevelStatusEvents.TopLevelTargetSkippedEvent;
import com.google.devtools.build.lib.util.RegexFilter;
import com.google.devtools.build.skyframe.SkyFunction;
import com.google.devtools.build.skyframe.SkyFunction.Environment.SkyKeyComputeState;
import com.google.devtools.build.skyframe.SkyFunctionException;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import com.google.devtools.build.skyframe.SkyframeLookupResult;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;
/**
* Drives the analysis & execution of an ActionLookupKey, which is wrapped inside a BuildDriverKey.
*/
public class BuildDriverFunction implements SkyFunction {
private final TransitiveActionLookupValuesHelper transitiveActionLookupValuesHelper;
private final Supplier<IncrementalArtifactConflictFinder> incrementalArtifactConflictFinder;
private final Supplier<RuleContextConstraintSemantics> ruleContextConstraintSemantics;
private final Supplier<RegexFilter> extraActionFilterSupplier;
BuildDriverFunction(
TransitiveActionLookupValuesHelper transitiveActionLookupValuesHelper,
Supplier<IncrementalArtifactConflictFinder> incrementalArtifactConflictFinder,
Supplier<RuleContextConstraintSemantics> ruleContextConstraintSemantics,
Supplier<RegexFilter> extraActionFilterSupplier) {
this.transitiveActionLookupValuesHelper = transitiveActionLookupValuesHelper;
this.incrementalArtifactConflictFinder = incrementalArtifactConflictFinder;
this.ruleContextConstraintSemantics = ruleContextConstraintSemantics;
this.extraActionFilterSupplier = extraActionFilterSupplier;
}
private static class State implements SkyKeyComputeState {
private ImmutableMap<ActionAnalysisMetadata, ConflictException> actionConflicts;
// It's only necessary to do this check once.
private boolean checkedForCompatibility = false;
private boolean checkedForPlatformCompatibility = false;
}
/**
* From the ConfiguredTarget/Aspect keys, get the top-level artifacts. Then evaluate them together
* with the appropriate CompletionFunctions. This is the bridge between the conceptual analysis &
* execution phases.
*
* <p>TODO(b/240944910): implement coverage.
*/
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env)
throws SkyFunctionException, InterruptedException {
BuildDriverKey buildDriverKey = (BuildDriverKey) skyKey;
ActionLookupKey actionLookupKey = buildDriverKey.getActionLookupKey();
TopLevelArtifactContext topLevelArtifactContext = buildDriverKey.getTopLevelArtifactContext();
State state = env.getState(State::new);
// Register a dependency on the BUILD_ID. We do this to make sure BuildDriverFunction is
// reevaluated every build.
PrecomputedValue.BUILD_ID.get(env);
// Why SkyValue and not ActionLookupValue? The evaluation of some ActionLookupKey can result in
// classes that don't implement ActionLookupValue
// (e.g. ConfiguredTargetKey -> NonRuleConfiguredTargetValue).
SkyValue topLevelSkyValue = env.getValue(actionLookupKey);
if (env.valuesMissing()) {
return null;
}
// This code path should not be run during error bubbling for several reasons:
// 1. Correctness: to check for action conflicts, we need access to the transitive
// ConfiguredTargets, which will be null after AnalysisPhaseCompleteEvent in
// --discard_analysis_cache mode.
// 2. Performance: this method is CPU intensive, and it does not offer anything while error
// bubbling.
if (!env.inErrorBubblingForSkyFunctionsThatCanFullyRecoverFromErrors()) {
// Unconditionally check for action conflicts.
// TODO(b/214371092): Only check when necessary.
try (SilentCloseable c =
Profiler.instance().profile("BuildDriverFunction.checkActionConflicts")) {
if (state.actionConflicts == null) {
state.actionConflicts =
checkActionConflicts(actionLookupKey, buildDriverKey.strictActionConflictCheck());
}
if (!state.actionConflicts.isEmpty()) {
throw new BuildDriverFunctionException(
new TopLevelConflictException(
"Action conflict(s) detected while analyzing top-level target "
+ actionLookupKey.getLabel(),
state.actionConflicts));
}
}
}
Preconditions.checkState(
topLevelSkyValue instanceof ConfiguredTargetValue
|| topLevelSkyValue instanceof TopLevelAspectsValue);
if (topLevelSkyValue instanceof ConfiguredTargetValue) {
ConfiguredTargetValue configuredTargetValue = (ConfiguredTargetValue) topLevelSkyValue;
ConfiguredTarget configuredTarget = configuredTargetValue.getConfiguredTarget();
// At this point, the target is considered "analyzed". It's important that this event is sent
// before the TopLevelEntityAnalysisConcludedEvent: when the last of the analysis work is
// concluded, we need to have the *complete* list of analyzed targets ready in
// BuildResultListener.
postTopLevelTargetAnalyzedEvent(env, configuredTargetValue, configuredTarget);
BuildConfigurationValue buildConfigurationValue =
configuredTarget.getConfigurationKey() == null
? null
: (BuildConfigurationValue) env.getValue(configuredTarget.getConfigurationKey());
if (env.valuesMissing()) {
return null;
}
if (!state.checkedForCompatibility) {
try {
Boolean isConfiguredTargetCompatible =
isConfiguredTargetCompatible(
env,
state,
configuredTarget,
buildConfigurationValue,
buildDriverKey.isExplicitlyRequested());
if (isConfiguredTargetCompatible == null) {
return null;
}
state.checkedForCompatibility = true;
if (!isConfiguredTargetCompatible) {
env.getListener().post(TopLevelTargetSkippedEvent.create(configuredTarget));
// We still record analyzed but skipped tests, as this information is needed for the
// result summary.
if (!NOT_TEST.equals(buildDriverKey.getTestType())) {
env.getListener()
.post(
TestAnalyzedEvent.create(
configuredTarget,
Preconditions.checkNotNull(buildConfigurationValue),
/*isSkipped=*/ true));
}
// Only send the event now to include the compatibility check in the measurement for
// time spent on analysis work.
env.getListener().post(TopLevelEntityAnalysisConcludedEvent.success(buildDriverKey));
// We consider the evaluation of this BuildDriverKey successful at this point, even when
// the target is skipped.
return new BuildDriverValue(topLevelSkyValue, /*skipped=*/ true);
}
} catch (TargetCompatibilityCheckException e) {
throw new BuildDriverFunctionException(e);
}
}
env.getListener().post(TopLevelEntityAnalysisConcludedEvent.success(buildDriverKey));
env.getListener()
.post(
TopLevelTargetPendingExecutionEvent.create(
configuredTarget, buildDriverKey.isTest()));
requestConfiguredTargetExecution(
configuredTarget,
buildDriverKey,
actionLookupKey,
buildConfigurationValue,
env,
topLevelArtifactContext);
} else {
announceAspectAnalysisDoneAndRequestExecution(
buildDriverKey, (TopLevelAspectsValue) topLevelSkyValue, env, topLevelArtifactContext);
}
if (env.valuesMissing()) {
return null;
}
// If we get to this point, the execution of this target/aspect succeeded.
if (EXCLUSIVE.equals(buildDriverKey.getTestType())
|| EXCLUSIVE_IF_LOCAL.equals(buildDriverKey.getTestType())) {
Preconditions.checkState(topLevelSkyValue instanceof ConfiguredTargetValue);
return new ExclusiveTestBuildDriverValue(
topLevelSkyValue, ((ConfiguredTargetValue) topLevelSkyValue).getConfiguredTarget());
}
return new BuildDriverValue(topLevelSkyValue, /*skipped=*/ false);
}
private static void postTopLevelTargetAnalyzedEvent(
Environment env,
ConfiguredTargetValue configuredTargetValue,
ConfiguredTarget configuredTarget) {
env.getListener().post(TopLevelTargetAnalyzedEvent.create(configuredTarget));
// It's possible that this code path is triggered AFTER the analysis cache clean up and the
// transitive packages for package root resolution is already cleared. In such a case, the
// symlinks should have already been planted.
if (configuredTargetValue.getTransitivePackages() != null) {
env.getListener()
.post(
TopLevelTargetReadyForSymlinkPlanting.create(
configuredTargetValue.getTransitivePackages()));
}
}
/**
* Checks if a ConfiguredTarget is compatible with the platform/environment. See {@link
* TopLevelConstraintSemantics}.
*
* @return null if a value is missing in the environment.
*/
@Nullable
private Boolean isConfiguredTargetCompatible(
Environment env,
State state,
ConfiguredTarget configuredTarget,
BuildConfigurationValue buildConfigurationValue,
boolean isExplicitlyRequested)
throws InterruptedException, TargetCompatibilityCheckException {
if (!state.checkedForPlatformCompatibility) {
PlatformCompatibility platformCompatibility =
TopLevelConstraintSemantics.compatibilityWithPlatformRestrictions(
configuredTarget,
env.getListener(),
/*eagerlyThrowError=*/ true,
isExplicitlyRequested);
state.checkedForPlatformCompatibility = true;
switch (platformCompatibility) {
case INCOMPATIBLE_EXPLICIT:
case INCOMPATIBLE_IMPLICIT:
return false;
case COMPATIBLE:
break;
}
}
EnvironmentCompatibility environmentCompatibility =
TopLevelConstraintSemantics.compatibilityWithTargetEnvironment(
configuredTarget,
buildConfigurationValue,
label -> getTarget(env, label),
env.getListener());
if (env.valuesMissing() || environmentCompatibility == null) {
return null;
}
if (environmentCompatibility.isCompatible()) {
return true;
}
if (environmentCompatibility.severeMissingEnvironments() == null) {
return false;
}
String badTargetsUserMessage =
TopLevelConstraintSemantics.getErrorMessageForTarget(
ruleContextConstraintSemantics.get(),
configuredTarget,
environmentCompatibility.severeMissingEnvironments());
throw new TargetCompatibilityCheckException(
badTargetsUserMessage,
FailureDetail.newBuilder()
.setMessage(badTargetsUserMessage)
.setAnalysis(Analysis.newBuilder().setCode(Code.TARGETS_MISSING_ENVIRONMENTS))
.build());
}
@Nullable
private static Target getTarget(Environment env, Label label)
throws InterruptedException, NoSuchTargetException {
PackageValue packageValue =
(PackageValue) env.getValue(PackageValue.key(label.getPackageIdentifier()));
if (env.valuesMissing() || packageValue == null) {
return null;
}
Package pkg = packageValue.getPackage();
return pkg.getTarget(label.getName());
}
private void requestConfiguredTargetExecution(
ConfiguredTarget configuredTarget,
BuildDriverKey buildDriverKey,
ActionLookupKey actionLookupKey,
BuildConfigurationValue buildConfigurationValue,
Environment env,
TopLevelArtifactContext topLevelArtifactContext)
throws InterruptedException {
ImmutableSet.Builder<Artifact> artifactsToBuild = ImmutableSet.builder();
addExtraActionsIfRequested(
configuredTarget.getProvider(ExtraActionArtifactsProvider.class), artifactsToBuild);
env.getListener().post(SomeExecutionStartedEvent.create());
if (NOT_TEST.equals(buildDriverKey.getTestType())) {
declareDependenciesAndCheckValues(
env,
Iterables.concat(
Artifact.keys(artifactsToBuild.build()),
Collections.singletonList(
TargetCompletionValue.key(
(ConfiguredTargetKey) actionLookupKey, topLevelArtifactContext, false))));
return;
}
env.getListener()
.post(
TestAnalyzedEvent.create(
configuredTarget,
Preconditions.checkNotNull(buildConfigurationValue),
/*isSkipped=*/ false));
if (PARALLEL.equals(buildDriverKey.getTestType())) {
// Only run non-exclusive tests here. Exclusive tests need to be run sequentially later.
declareDependenciesAndCheckValues(
env,
Iterables.concat(
artifactsToBuild.build(),
Collections.singletonList(
TestCompletionValue.key(
(ConfiguredTargetKey) actionLookupKey,
topLevelArtifactContext,
/*exclusiveTesting=*/ false))));
return;
}
// Exclusive tests will be run with sequential Skyframe evaluations afterwards.
declareDependenciesAndCheckValues(env, artifactsToBuild.build());
}
private void announceAspectAnalysisDoneAndRequestExecution(
BuildDriverKey buildDriverKey,
TopLevelAspectsValue topLevelAspectsValue,
Environment env,
TopLevelArtifactContext topLevelArtifactContext)
throws InterruptedException {
env.getListener().post(SomeExecutionStartedEvent.create());
ImmutableSet.Builder<Artifact> artifactsToBuild = ImmutableSet.builder();
List<SkyKey> aspectCompletionKeys = new ArrayList<>();
for (AspectValue aspectValue : topLevelAspectsValue.getTopLevelAspectsValues()) {
AspectKey aspectKey = aspectValue.getKey();
ConfiguredAspect configuredAspect = aspectValue.getConfiguredAspect();
addExtraActionsIfRequested(
configuredAspect.getProvider(ExtraActionArtifactsProvider.class), artifactsToBuild);
postAspectAnalyzedEvent(env, aspectValue, aspectKey, configuredAspect);
aspectCompletionKeys.add(AspectCompletionKey.create(aspectKey, topLevelArtifactContext));
}
// Send the AspectAnalyzedEvents first to make sure the BuildResultListener is up-to-date before
// signaling that the analysis of this top level aspect has concluded.
env.getListener().post(TopLevelEntityAnalysisConcludedEvent.success(buildDriverKey));
declareDependenciesAndCheckValues(
env, Iterables.concat(Artifact.keys(artifactsToBuild.build()), aspectCompletionKeys));
}
private static void postAspectAnalyzedEvent(
Environment env,
AspectValue aspectValue,
AspectKey aspectKey,
ConfiguredAspect configuredAspect) {
// It's possible that this code path is triggered AFTER the analysis cache clean up and the
// transitive packages for package root resolution is already cleared. In such a case, the
// symlinks should have already been planted.
AspectAnalyzedEvent aspectAnalyzedEvent =
aspectValue.getTransitivePackages() == null
? AspectAnalyzedEvent.createWithoutFurtherSymlinkPlanting(aspectKey, configuredAspect)
: AspectAnalyzedEvent.create(
aspectKey, configuredAspect, aspectValue.getTransitivePackages());
env.getListener().post(aspectAnalyzedEvent);
}
/**
* Declares dependencies and checks values for requested nodes in the graph.
*
* <p>Calls {@link SkyFunction.Environment#getValuesAndExceptions} and iterates over the result.
* If any node is not done, or during iteration any value has exception, {@link
* SkyFunction.Environment#valuesMissing} will return true.
*/
private static void declareDependenciesAndCheckValues(
Environment env, Iterable<? extends SkyKey> skyKeys) throws InterruptedException {
SkyframeLookupResult result = env.getValuesAndExceptions(skyKeys);
for (SkyKey key : skyKeys) {
if (result.get(key) == null) {
return;
}
}
}
@VisibleForTesting
ImmutableMap<ActionAnalysisMetadata, ConflictException> checkActionConflicts(
ActionLookupKey actionLookupKey, boolean strictConflictCheck) throws InterruptedException {
ActionLookupValuesCollectionResult transitiveValueCollectionResult =
transitiveActionLookupValuesHelper.collect(actionLookupKey);
ImmutableMap<ActionAnalysisMetadata, ConflictException> conflicts =
incrementalArtifactConflictFinder
.get()
.findArtifactConflicts(
transitiveValueCollectionResult.collectedValues(), strictConflictCheck)
.getConflicts();
if (conflicts.isEmpty()) {
transitiveActionLookupValuesHelper.registerConflictFreeKeys(
transitiveValueCollectionResult.visitedKeys());
}
return conflicts;
}
private void addExtraActionsIfRequested(
ExtraActionArtifactsProvider provider, ImmutableSet.Builder<Artifact> artifactsToBuild) {
if (provider != null) {
addArtifactsToBuilder(
provider.getTransitiveExtraActionArtifacts().toList(),
artifactsToBuild,
extraActionFilterSupplier.get());
}
}
private static void addArtifactsToBuilder(
List<? extends Artifact> artifacts,
ImmutableSet.Builder<Artifact> builder,
RegexFilter filter) {
for (Artifact artifact : artifacts) {
if (filter.isIncluded(artifact.getOwnerLabel().toString())) {
builder.add(artifact);
}
}
}
/** A SkyFunctionException wrapper for the actual TopLevelConflictException. */
private static final class BuildDriverFunctionException extends SkyFunctionException {
// The exception is transient here since it could be caused by external factors (conflict with
// another target).
BuildDriverFunctionException(TopLevelConflictException cause) {
super(cause, Transience.TRANSIENT);
}
BuildDriverFunctionException(TargetCompatibilityCheckException cause) {
super(cause, Transience.TRANSIENT);
}
}
interface TransitiveActionLookupValuesHelper {
/**
* Perform the traversal of the transitive closure of the {@code key} and collect the
* corresponding ActionLookupValues.
*/
ActionLookupValuesCollectionResult collect(ActionLookupKey key) throws InterruptedException;
/** Register with the helper that the {@code keys} are conflict-free. */
void registerConflictFreeKeys(ImmutableSet<SkyKey> keys);
}
@AutoValue
abstract static class ActionLookupValuesCollectionResult {
abstract Sharder<ActionLookupValue> collectedValues();
abstract ImmutableSet<SkyKey> visitedKeys();
static ActionLookupValuesCollectionResult create(
Sharder<ActionLookupValue> collectedValues, ImmutableSet<SkyKey> visitedKeys) {
return new AutoValue_BuildDriverFunction_ActionLookupValuesCollectionResult(
collectedValues, visitedKeys);
}
}
}