| // 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.base.Splitter; | |
| 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.List; | |
| 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); | |
| } | |
| public static ActionConflictException create( | |
| Artifact artifact, | |
| ActionAnalysisMetadata attemptedAction, | |
| String message, | |
| boolean isPrefixConflict) { | |
| return new ActionConflictException(artifact, attemptedAction, message, isPrefixConflict); | |
| } | |
| /** | |
| * 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"); | |
| } | |
| /** Appends a line diff for large string values e.g. describeKey which can be 50k chars long. */ | |
| private static void describeKeyDiff( | |
| StringBuilder sb, ActionExecutionMetadata metadataA, ActionExecutionMetadata metadataB) { | |
| sb.append("Action describeKey: "); | |
| String valueA = metadataA.describeKey(); | |
| String valueB = metadataB.describeKey(); | |
| valueA = valueA != null ? valueA : "(null)"; | |
| valueB = valueB != null ? valueB : "(null)"; | |
| // Do not print the values when they are identical. | |
| if (valueA.equals(valueB)) { | |
| sb.append("are equal\n"); | |
| return; | |
| } | |
| sb.append("are different:\n"); | |
| List<String> linesA = Splitter.on('\n').splitToList(valueA.trim()); | |
| List<String> linesB = Splitter.on('\n').splitToList(valueB.trim()); | |
| int maxLen = Math.max(linesA.size(), linesB.size()); | |
| for (int i = 0; i < maxLen; i++) { | |
| String lineA = i < linesA.size() ? linesA.get(i) : "(null)"; | |
| String lineB = i < linesB.size() ? linesB.get(i) : "(null)"; | |
| if (!lineA.equals(lineB)) { | |
| sb.append(" Action A: ").append(lineA).append("\n"); | |
| sb.append(" Action B: ").append(lineB).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()); | |
| addStringDetail( | |
| sb, "IsShareable", String.valueOf(a.isShareable()), String.valueOf(b.isShareable())); | |
| try { | |
| addStringDetail( | |
| sb, | |
| "Action key", | |
| a.getKey(actionKeyContext, /* inputMetadataProvider= */ null), | |
| b.getKey(actionKeyContext, /* inputMetadataProvider= */ 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 metadataA) | |
| && (b instanceof ActionExecutionMetadata metadataB)) { | |
| addStringDetail( | |
| sb, "Progress message", metadataA.getProgressMessage(), metadataB.getProgressMessage()); | |
| describeKeyDiff(sb, metadataA, metadataB); | |
| } | |
| 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; | |
| } | |
| } | |
| } |