| // Copyright 2023 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.analysis.producers; |
| |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static com.google.devtools.build.lib.analysis.DependencyKind.OUTPUT_FILE_RULE_DEPENDENCY; |
| import static com.google.devtools.build.lib.analysis.DependencyKind.VISIBILITY_DEPENDENCY; |
| import static com.google.devtools.build.lib.analysis.DependencyResolutionHelpers.getExecutionPlatformLabel; |
| import static com.google.devtools.build.lib.analysis.config.transitions.ConfigurationTransition.PATCH_TRANSITION_KEY; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.devtools.build.lib.analysis.AnalysisRootCauseEvent; |
| import com.google.devtools.build.lib.analysis.DependencyKind; |
| import com.google.devtools.build.lib.analysis.DependencyKind.ToolchainDependencyKind; |
| import com.google.devtools.build.lib.analysis.DependencyResolutionHelpers; |
| import com.google.devtools.build.lib.analysis.DependencyResolutionHelpers.ExecutionPlatformResult; |
| import com.google.devtools.build.lib.analysis.InvalidVisibilityDependencyException; |
| import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue; |
| import com.google.devtools.build.lib.analysis.config.ConfigurationTransitionEvent; |
| import com.google.devtools.build.lib.analysis.config.DependencyEvaluationException; |
| import com.google.devtools.build.lib.analysis.config.transitions.ConfigurationTransition; |
| import com.google.devtools.build.lib.analysis.config.transitions.TransitionCollector; |
| import com.google.devtools.build.lib.analysis.starlark.StarlarkTransition.TransitionException; |
| import com.google.devtools.build.lib.causes.Cause; |
| import com.google.devtools.build.lib.causes.LoadingFailedCause; |
| import com.google.devtools.build.lib.cmdline.Label; |
| import com.google.devtools.build.lib.events.Event; |
| import com.google.devtools.build.lib.packages.Aspect; |
| import com.google.devtools.build.lib.packages.Attribute; |
| import com.google.devtools.build.lib.packages.AttributeTransitionData; |
| import com.google.devtools.build.lib.packages.NoSuchTargetException; |
| import com.google.devtools.build.lib.packages.NoSuchThingException; |
| import com.google.devtools.build.lib.packages.Target; |
| import com.google.devtools.build.lib.packages.TargetUtils; |
| import com.google.devtools.build.lib.skyframe.AspectCreationException; |
| import com.google.devtools.build.lib.skyframe.BuildConfigurationKey; |
| import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData; |
| import com.google.devtools.build.lib.skyframe.ConfiguredValueCreationException; |
| import com.google.devtools.build.skyframe.SkyValue; |
| import com.google.devtools.build.skyframe.state.StateMachine; |
| import com.google.devtools.common.options.OptionsParsingException; |
| import java.util.function.Consumer; |
| import javax.annotation.Nullable; |
| |
| /** |
| * Evaluates dependencies. |
| * |
| * <p>A dependency is described by a {@link DependencyKind}, a {@link Label} and possibly a list of |
| * {@link Aspect}s. This class determines the {@link AttributeConfiguration}, based on the parent's |
| * configuration. This may include using the {@link TransitionApplier} to perform an attribute |
| * configuration transition. |
| * |
| * <p>It then delegates computation of the {@link ConfiguredTargetAndData} prerequisite values to |
| * {@link PrerequisitesProducer} with the determined configuration(s). |
| */ |
| final class DependencyProducer |
| implements StateMachine, TransitionApplier.ResultSink, PrerequisitesProducer.ResultSink { |
| private static final ConfiguredTargetAndData[] EMPTY_OUTPUT = new ConfiguredTargetAndData[0]; |
| |
| interface ResultSink extends TransitionCollector { |
| /** |
| * Accepts dependency values for a given kind and label. |
| * |
| * <p>Multiple values may occur if there is a split transition. |
| * |
| * <p>For a skipped dependency, outputs an empty array. See comments in {@link |
| * DependencyResolutionHelpers#getExecutionPlatformLabel} for when this happens. |
| */ |
| void acceptDependencyValues(int index, ConfiguredTargetAndData[] values); |
| |
| void acceptDependencyError(DependencyError error); |
| |
| void acceptDependencyError(MissingEdgeError error); |
| } |
| |
| // -------------------- Input -------------------- |
| private final PrerequisiteParameters parameters; |
| private final DependencyKind kind; |
| private final Label toLabel; |
| private final ImmutableList<Aspect> propagatingAspects; |
| |
| // -------------------- Output -------------------- |
| private final ResultSink sink; |
| private final int index; |
| |
| // -------------------- Internal State -------------------- |
| private ImmutableMap<String, BuildConfigurationKey> transitionedConfigurations; |
| |
| DependencyProducer( |
| PrerequisiteParameters parameters, |
| DependencyKind kind, |
| Label toLabel, |
| ImmutableList<Aspect> propagatingAspects, |
| ResultSink sink, |
| int index) { |
| this.parameters = parameters; |
| this.kind = checkNotNull(kind); |
| this.toLabel = toLabel; |
| this.propagatingAspects = propagatingAspects; |
| this.sink = sink; |
| this.index = index; |
| } |
| |
| @Override |
| public StateMachine step(Tasks tasks) { |
| @Nullable Attribute attribute = kind.getAttribute(); |
| |
| if (kind == VISIBILITY_DEPENDENCY |
| || (attribute != null && attribute.getName().equals("visibility"))) { |
| // This is always a null transition because visibility targets are not configurable. |
| return computePrerequisites( |
| AttributeConfiguration.ofVisibility(), /* executionPlatformLabel= */ null); |
| } |
| |
| // The logic of `DependencyResolutionHelpers.computeDependencyLabels` implies that |
| // `parameters.configurationKey()` is non-null for everything that follows. |
| BuildConfigurationKey configurationKey = checkNotNull(parameters.configurationKey()); |
| |
| if (DependencyKind.isToolchain(kind)) { |
| // There's no attribute so no attribute transition. |
| |
| // This dependency is a toolchain. Its package has not been loaded and therefore we can't |
| // determine which aspects and which rule configuration transition we should use, so just |
| // use sensible defaults. Not depending on their package makes the error message reporting |
| // a missing toolchain a bit better. |
| // TODO(lberki): This special-casing is weird. Find a better way to depend on toolchains. |
| // This logic needs to stay in sync with the dep finding logic in |
| // //third_party/bazel/src/main/java/com/google/devtools/build/lib/analysis/Util.java#findImplicitDeps. |
| return computePrerequisites( |
| AttributeConfiguration.ofUnary(configurationKey), |
| parameters.getExecutionPlatformLabel( |
| ((ToolchainDependencyKind) kind).getExecGroupName())); |
| } |
| |
| if (kind == OUTPUT_FILE_RULE_DEPENDENCY) { |
| // There's no attribute so no attribute transition. |
| return computePrerequisites( |
| AttributeConfiguration.ofUnary(configurationKey), /* executionPlatformLabel= */ null); |
| } |
| |
| var transitionData = |
| AttributeTransitionData.builder() |
| .attributes(parameters.attributeMap()) |
| .analysisData(parameters.starlarkTransitionProvider()); |
| ExecutionPlatformResult executionPlatformResult = |
| getExecutionPlatformLabel(kind, parameters.toolchainContexts(), parameters.aspects()); |
| switch (executionPlatformResult.kind()) { |
| case LABEL: |
| transitionData.executionPlatform(executionPlatformResult.label()); |
| break; |
| case NULL_LABEL: |
| transitionData.executionPlatform(null); |
| break; |
| case SKIP: |
| sink.acceptDependencyValues(index, EMPTY_OUTPUT); |
| return DONE; |
| case ERROR: |
| return new ExecGroupErrorEmitter(executionPlatformResult.error()); |
| } |
| ConfigurationTransition attributeTransition = |
| attribute.getTransitionFactory().create(transitionData.build()); |
| sink.acceptTransition(kind, toLabel, attributeTransition); |
| return new TransitionApplier( |
| configurationKey, |
| attributeTransition, |
| parameters.transitionCache(), |
| (TransitionApplier.ResultSink) this, |
| parameters.eventHandler(), |
| /* runAfter= */ this::processTransitionResult); |
| } |
| |
| @Override |
| public void acceptTransitionedConfigurations( |
| ImmutableMap<String, BuildConfigurationKey> transitionedConfigurations) { |
| this.transitionedConfigurations = transitionedConfigurations; |
| } |
| |
| @Override |
| public void acceptTransitionError(TransitionException e) { |
| sink.acceptDependencyError( |
| DependencyError.of(new TransitionException(getMessageWithEdgeTransitionInfo(e), e))); |
| } |
| |
| @Override |
| public void acceptTransitionError(OptionsParsingException e) { |
| sink.acceptDependencyError( |
| DependencyError.of( |
| new OptionsParsingException( |
| getMessageWithEdgeTransitionInfo(e), e.getInvalidArgument(), e))); |
| } |
| |
| private String getMessageWithEdgeTransitionInfo(Throwable e) { |
| return String.format( |
| "On dependency edge %s (%s) -|%s|-> %s: %s", |
| parameters.target().getLabel(), |
| parameters.configurationKey().getOptions().shortId(), |
| kind.getAttribute().getName(), |
| toLabel, |
| e.getMessage()); |
| } |
| |
| private StateMachine processTransitionResult(Tasks tasks) { |
| if (transitionedConfigurations == null) { |
| return DONE; // There was a previously reported error. |
| } |
| |
| if (isNonconfigurableTargetInSamePackage()) { |
| // The target is in the same package as the parent and non-configurable. In the general case |
| // loading a child target would defeat Package-based sharding. However, when the target is in |
| // the same Package, that concern no longer applies. This optimization means that delegation, |
| // and the corresponding creation of additional Skyframe nodes, can be avoided in the very |
| // common case of source file dependencies in the same Package. |
| |
| // Discards transition keys for patch transitions but keeps them otherwise. |
| ImmutableList<String> transitionKeys = |
| transitionedConfigurations.size() == 1 |
| && transitionedConfigurations.containsKey(PATCH_TRANSITION_KEY) |
| ? ImmutableList.of() |
| : transitionedConfigurations.keySet().asList(); |
| return computePrerequisites( |
| AttributeConfiguration.ofNullTransitionKeys(transitionKeys), |
| /* executionPlatformLabel= */ null); |
| } |
| |
| String parentChecksum = parameters.configurationKey().getOptionsChecksum(); |
| for (BuildConfigurationKey configuration : transitionedConfigurations.values()) { |
| String childChecksum = configuration.getOptionsChecksum(); |
| if (!parentChecksum.equals(childChecksum)) { |
| parameters |
| .eventHandler() |
| .post(ConfigurationTransitionEvent.create(parentChecksum, childChecksum)); |
| } |
| } |
| |
| if (transitionedConfigurations.size() == 1) { |
| BuildConfigurationKey patchedConfiguration = |
| transitionedConfigurations.get(PATCH_TRANSITION_KEY); |
| if (patchedConfiguration != null) { |
| // It was a patch transition or no-op split transition. |
| return computePrerequisites( |
| AttributeConfiguration.ofUnary(patchedConfiguration), |
| /* executionPlatformLabel= */ null); |
| } |
| } |
| |
| return computePrerequisites( |
| AttributeConfiguration.ofSplit(transitionedConfigurations), |
| /* executionPlatformLabel= */ null); |
| } |
| |
| private StateMachine computePrerequisites( |
| AttributeConfiguration configuration, @Nullable Label executionPlatformLabel) { |
| return new PrerequisitesProducer( |
| parameters, |
| toLabel, |
| executionPlatformLabel, |
| configuration, |
| propagatingAspects, |
| (PrerequisitesProducer.ResultSink) this); |
| } |
| |
| @Override |
| public void acceptPrerequisitesValue(ConfiguredTargetAndData[] value) { |
| sink.acceptDependencyValues(index, value); |
| } |
| |
| @Override |
| public void acceptPrerequisitesError(NoSuchThingException error) { |
| sink.acceptDependencyError(new MissingEdgeError(kind, toLabel, error)); |
| } |
| |
| @Override |
| public void acceptPrerequisitesError(InvalidVisibilityDependencyException error) { |
| sink.acceptDependencyError(DependencyError.of(error)); |
| } |
| |
| @Override |
| public void acceptPrerequisitesCreationError(ConfiguredValueCreationException error) { |
| // Cases where the child target cannot be loaded at all are propagated as |
| // `NoSuchThingException`. In some cases, child target loading completes with errors. In that |
| // case, the error is propagated as a `ConfiguredValueCreationException` with a |
| // `LoadingFailedCause`. Requests parent-side context to be added to such errors by propagating |
| // a `MissingEdgeError`. |
| for (Cause cause : error.getRootCauses().toList()) { |
| if (cause instanceof LoadingFailedCause) { |
| var loadingFailed = (LoadingFailedCause) cause; |
| if (loadingFailed.getLabel().equals(toLabel)) { |
| sink.acceptDependencyError( |
| new MissingEdgeError( |
| kind, toLabel, NoSuchTargetException.createForParentPropagation(toLabel))); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void acceptPrerequisitesAspectError(DependencyEvaluationException error) { |
| sink.acceptDependencyError(DependencyError.of(error)); |
| } |
| |
| @Override |
| public void acceptPrerequisitesAspectError(AspectCreationException error) { |
| sink.acceptDependencyError(DependencyError.of(error)); |
| } |
| |
| private boolean isNonconfigurableTargetInSamePackage() { |
| Target parentTarget = parameters.target(); |
| if (parentTarget.getLabel().getPackageIdentifier().equals(toLabel.getPackageIdentifier())) { |
| try { |
| Target toTarget = parentTarget.getPackage().getTarget(toLabel.getName()); |
| if (!toTarget.isConfigurable()) { |
| return true; |
| } |
| } catch (NoSuchTargetException e) { |
| parameters |
| .transitiveState() |
| .addTransitiveCause(new LoadingFailedCause(toLabel, e.getDetailedExitCode())); |
| parameters |
| .eventHandler() |
| .handle( |
| Event.error( |
| TargetUtils.getLocationMaybe(parentTarget), |
| TargetUtils.formatMissingEdge(parentTarget, toLabel, e, kind.getAttribute()))); |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Emits errors from {@link ExecutionPlatformResult#error}. |
| * |
| * <p>Exists to fetch the {@link BuildConfigurationValue}, needed to construct {@link |
| * AnalysisRootCauseEvent}. |
| */ |
| private class ExecGroupErrorEmitter implements StateMachine, Consumer<SkyValue> { |
| // -------------------- Input -------------------- |
| private final String message; |
| |
| // -------------------- Internal State -------------------- |
| private BuildConfigurationValue configuration; |
| |
| private ExecGroupErrorEmitter(String message) { |
| this.message = message; |
| } |
| |
| @Override |
| public StateMachine step(Tasks tasks) { |
| // The configuration value should already exist as a dependency so this lookup is safe enough |
| // for error handling. |
| tasks.lookUp(parameters.configurationKey(), (Consumer<SkyValue>) this); |
| return this::postEvent; |
| } |
| |
| @Override |
| public void accept(SkyValue value) { |
| this.configuration = (BuildConfigurationValue) value; |
| } |
| |
| private StateMachine postEvent(Tasks tasks) { |
| parameters |
| .eventHandler() |
| .post(AnalysisRootCauseEvent.withConfigurationValue(configuration, toLabel, message)); |
| sink.acceptDependencyError( |
| DependencyError.of( |
| new DependencyEvaluationException( |
| new ConfiguredValueCreationException( |
| parameters.location(), |
| message, |
| parameters.label(), |
| parameters.eventId(), |
| /* rootCauses= */ null, |
| /* detailedExitCode= */ null), |
| // This error originates in dependency resolution, attached to the current target, |
| // so no dependency has reported the error. |
| /* depReportedOwnError= */ false))); |
| return DONE; |
| } |
| } |
| } |