blob: e14cb234ba9d4302ee931999293cc6246ea36e9d [file] [log] [blame]
// Copyright 2024 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.actions;
import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
import com.google.common.base.Functions;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.packages.AspectDescriptor;
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.AbstractSaneAnalysisException;
import com.google.devtools.build.lib.util.DetailedExitCode;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
/**
* This exception is thrown when a conflict between actions is detected. It contains information
* about the artifact for which the conflict is found, and data about the two conflicting actions
* and their owners. Non-final only for {@link WithAspectKeyInfo}.
*/
public sealed class ActionConflictException extends AbstractSaneAnalysisException {
private final Artifact artifact;
private final ActionAnalysisMetadata attemptedAction;
private final boolean isPrefixConflict;
private static final int MAX_DIFF_ARTIFACTS_TO_REPORT = 5;
public static ActionConflictException create(
ActionKeyContext actionKeyContext,
Artifact artifact,
ActionAnalysisMetadata previousAction,
ActionAnalysisMetadata attemptedAction) {
return new ActionConflictException(
artifact,
attemptedAction,
createDetailedMessage(artifact, actionKeyContext, attemptedAction, previousAction),
/* isPrefixConflict= */ false);
}
/**
* Exception to indicate that one {@link Action} has an output artifact whose path is a prefix of
* an output of another action. Since the first path cannot be both a directory and a file, this
* would lead to an error if both actions were executed in the same build.
*/
public static ActionConflictException createPrefix(
Artifact firstArtifact,
Artifact secondArtifact,
ActionAnalysisMetadata firstAction,
ActionAnalysisMetadata secondAction) {
return new ActionConflictException(
firstArtifact,
firstAction,
createPrefixDetailedMessage(
firstArtifact,
secondArtifact,
firstAction.getOwner().getLabel(),
secondAction.getOwner().getLabel()),
/* isPrefixConflict= */ true);
}
public static ActionConflictException withAspectKeyInfo(
ActionConflictException e, ActionLookupKey aspectKey) {
return new WithAspectKeyInfo(e, aspectKey);
}
private ActionConflictException(
Artifact artifact,
ActionAnalysisMetadata attemptedAction,
String message,
boolean isPrefixConflict) {
super(message);
this.artifact = artifact;
this.attemptedAction = attemptedAction;
this.isPrefixConflict = isPrefixConflict;
}
public Artifact getArtifact() {
return artifact;
}
public ActionAnalysisMetadata getAttemptedAction() {
return attemptedAction;
}
private static String createDetailedMessage(
Artifact artifact,
ActionKeyContext actionKeyContext,
ActionAnalysisMetadata a,
ActionAnalysisMetadata b) {
return "file '"
+ artifact.prettyPrint()
+ "' is generated by these conflicting actions:\n"
+ debugSuffix(actionKeyContext, a, b);
}
private static String createPrefixDetailedMessage(
Artifact firstArtifact, Artifact secondArtifact, Label firstOwner, Label secondOwner) {
return String.format(
"One of the output paths '%s' (belonging to %s) and '%s' (belonging to %s) is a"
+ " prefix of the other. These actions cannot be simultaneously present; please"
+ " rename one of the output files or build just one of them",
firstArtifact.getExecPath(), firstOwner, secondArtifact.getExecPath(), secondOwner);
}
public void reportTo(EventHandler eventListener) {
eventListener.handle(Event.error(this.getMessage()));
}
@Override
public DetailedExitCode getDetailedExitCode() {
return DetailedExitCode.of(
FailureDetail.newBuilder()
.setMessage(getMessage())
.setAnalysis(
Analysis.newBuilder()
.setCode(
isPrefixConflict ? Code.ARTIFACT_PREFIX_CONFLICT : Code.ACTION_CONFLICT))
.build());
}
private static void addStringDetail(StringBuilder sb, String key, String valueA, String valueB) {
valueA = valueA != null ? valueA : "(null)";
valueB = valueB != null ? valueB : "(null)";
sb.append(key).append(": ").append(valueA);
if (!valueA.equals(valueB)) {
sb.append(", ").append(valueB);
}
sb.append("\n");
}
private static void addListDetail(
StringBuilder sb, String key, Iterable<Artifact> valueA, Iterable<Artifact> valueB) {
Set<Artifact> diffA = differenceWithoutOwner(valueA, valueB);
Set<Artifact> diffB = differenceWithoutOwner(valueB, valueA);
sb.append(key).append(": ");
if (diffA.isEmpty() && diffB.isEmpty()) {
sb.append("are equal\n");
} else {
if (!diffA.isEmpty()) {
sb.append(
"Attempted action contains artifacts not in previous action (first "
+ MAX_DIFF_ARTIFACTS_TO_REPORT
+ "): \n");
prettyPrintArtifactDiffs(sb, diffA);
}
if (!diffB.isEmpty()) {
sb.append(
"Previous action contains artifacts not in attempted action (first "
+ MAX_DIFF_ARTIFACTS_TO_REPORT
+ "): \n");
prettyPrintArtifactDiffs(sb, diffB);
}
}
}
/** Returns items in {@code valueA} that are not in {@code valueB}, ignoring the owner. */
private static Set<Artifact> differenceWithoutOwner(
Iterable<Artifact> valueA, Iterable<Artifact> valueB) {
ImmutableSet.Builder<Artifact> diff = new ImmutableSet.Builder<>();
// Group valueB by exec path for easier checks.
ImmutableListMultimap<String, Artifact> mapB =
Streams.stream(valueB)
.collect(toImmutableListMultimap(Artifact::getExecPathString, Functions.identity()));
for (Artifact a : valueA) {
boolean found = false;
for (Artifact b : mapB.get(a.getExecPathString())) {
if (a.equalsWithoutOwner(b)) {
found = true;
break;
}
}
if (!found) {
diff.add(a);
}
}
return diff.build();
}
/** Pretty print action diffs (at most {@code MAX_DIFF_ARTIFACTS_TO_REPORT} lines). */
private static void prettyPrintArtifactDiffs(StringBuilder sb, Set<Artifact> diff) {
for (Artifact artifact : Iterables.limit(diff, MAX_DIFF_ARTIFACTS_TO_REPORT)) {
sb.append('\t').append(artifact.prettyPrint()).append('\n');
}
}
// See also Actions.canBeShared()
private static String debugSuffix(
ActionKeyContext actionKeyContext, ActionAnalysisMetadata a, ActionAnalysisMetadata b) {
// Note: the error message reveals to users the names of intermediate files that are not
// documented in the BUILD language. This error-reporting logic is rather elaborate but it
// does help to diagnose some tricky situations.
StringBuilder sb = new StringBuilder();
ActionOwner aOwner = a.getOwner();
ActionOwner bOwner = b.getOwner();
boolean aNull = aOwner == null;
boolean bNull = bOwner == null;
addStringDetail(
sb,
"Label",
aNull ? null : Label.print(aOwner.getLabel()),
bNull ? null : Label.print(bOwner.getLabel()));
if ((!aNull && !aOwner.getAspectDescriptors().isEmpty())
|| (!bNull && !bOwner.getAspectDescriptors().isEmpty())) {
addStringDetail(sb, "Aspects", aspectDescriptor(aOwner), aspectDescriptor(bOwner));
}
addStringDetail(
sb,
"RuleClass",
aNull ? null : aOwner.getTargetKind(),
bNull ? null : bOwner.getTargetKind());
addStringDetail(
sb,
"JavaActionClass",
aNull ? null : a.getClass().toString(),
bNull ? null : b.getClass().toString());
addStringDetail(
sb,
"Configuration",
aNull ? null : aOwner.getConfigurationChecksum(),
bNull ? null : bOwner.getConfigurationChecksum());
addStringDetail(sb, "Mnemonic", a.getMnemonic(), b.getMnemonic());
try {
addStringDetail(
sb,
"Action key",
a.getKey(actionKeyContext, /* artifactExpander= */ null),
b.getKey(actionKeyContext, /* artifactExpander= */ null));
} catch (InterruptedException e) {
// Only for debugging - skip the key and carry on.
addStringDetail(sb, "Action key", "<elided due to interrupt>", "<elided due to interrupt>");
Thread.currentThread().interrupt();
}
if ((a instanceof ActionExecutionMetadata) && (b instanceof ActionExecutionMetadata)) {
addStringDetail(
sb,
"Progress message",
((ActionExecutionMetadata) a).getProgressMessage(),
((ActionExecutionMetadata) b).getProgressMessage());
addStringDetail(
sb,
"Action describeKey",
((ActionExecutionMetadata) a).describeKey(),
((ActionExecutionMetadata) b).describeKey());
}
Artifact aPrimaryInput = a.getPrimaryInput();
Artifact bPrimaryInput = b.getPrimaryInput();
addStringDetail(
sb,
"PrimaryInput",
aPrimaryInput == null ? null : aPrimaryInput.toString(),
bPrimaryInput == null ? null : bPrimaryInput.toString());
addStringDetail(
sb, "PrimaryOutput", a.getPrimaryOutput().toString(), b.getPrimaryOutput().toString());
// Only add list details if the primary input of A matches the input of B. Otherwise
// the above information is enough and list diff detail is not needed.
if ((aPrimaryInput == null && bPrimaryInput == null)
|| (aPrimaryInput != null
&& bPrimaryInput != null
&& aPrimaryInput.toString().equals(bPrimaryInput.toString()))) {
Artifact aPrimaryOutput = a.getPrimaryOutput();
Artifact bPrimaryOutput = b.getPrimaryOutput();
if (!aPrimaryOutput.equalsWithoutOwner(bPrimaryOutput)) {
sb.append("Primary outputs are different: ")
.append(System.identityHashCode(aPrimaryOutput))
.append(", ")
.append(System.identityHashCode(bPrimaryOutput))
.append('\n');
}
ArtifactOwner aArtifactOwner = aPrimaryOutput.getArtifactOwner();
ArtifactOwner bArtifactOwner = bPrimaryOutput.getArtifactOwner();
addStringDetail(
sb, "Owner information", aArtifactOwner.toString(), bArtifactOwner.toString());
addListDetail(
sb, "MandatoryInputs", a.getMandatoryInputs().toList(), b.getMandatoryInputs().toList());
addListDetail(sb, "Outputs", a.getOutputs(), b.getOutputs());
}
return sb.toString();
}
@Nullable
private static String aspectDescriptor(ActionOwner owner) {
return owner == null
? null
: owner.getAspectDescriptors().stream()
.map(AspectDescriptor::getDescription)
.collect(Collectors.joining(",", "[", "]"));
}
@Nullable
public ActionLookupKey getAspectKey() {
return null;
}
/**
* For skymeld.
*
* <p>We need to forward the AspectKey along so that it's available for the final conflict report.
*/
private static final class WithAspectKeyInfo extends ActionConflictException {
private final ActionLookupKey aspectKey;
private WithAspectKeyInfo(ActionConflictException e, ActionLookupKey aspectKey) {
super(e.artifact, e.attemptedAction, e.getMessage(), e.isPrefixConflict);
this.aspectKey = aspectKey;
}
@Override
public ActionLookupKey getAspectKey() {
return aspectKey;
}
}
}