blob: c9395c26f45b963a0d3aa6d82376593951f0f492 [file] [log] [blame]
// 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.common.base.Preconditions.checkState;
import static com.google.devtools.build.lib.buildeventstream.BuildEventIdUtil.configurationId;
import com.google.auto.value.AutoOneOf;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.actions.ActionLookupKey;
import com.google.devtools.build.lib.analysis.ConfiguredTargetValue;
import com.google.devtools.build.lib.analysis.InconsistentNullConfigException;
import com.google.devtools.build.lib.analysis.TargetAndConfiguration;
import com.google.devtools.build.lib.analysis.TransitiveDependencyState;
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.InvalidConfigurationException;
import com.google.devtools.build.lib.analysis.config.StarlarkTransitionCache;
import com.google.devtools.build.lib.analysis.config.transitions.ComposingTransition;
import com.google.devtools.build.lib.analysis.config.transitions.ConfigurationTransition;
import com.google.devtools.build.lib.analysis.config.transitions.TransitionFactory;
import com.google.devtools.build.lib.analysis.starlark.StarlarkTransition.TransitionException;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.packages.NoSuchPackageException;
import com.google.devtools.build.lib.packages.NoSuchTargetException;
import com.google.devtools.build.lib.packages.NoSuchThingException;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.RuleTransitionData;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.packages.TargetUtils;
import com.google.devtools.build.lib.skyframe.BuildConfigurationKey;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetKey;
import com.google.devtools.build.lib.skyframe.ConfiguredValueCreationException;
import com.google.devtools.build.lib.util.DetailedExitCode;
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;
import net.starlark.java.syntax.Location;
/**
* Computes the target and configuration for a configured target key.
*
* <p>If the key has a configuration and the target is configurable, attempts to apply a rule side
* transition. If the configuration changes, delegates to a target with the new configuration. If
* the target is not configurable, directly delegates to the null configuration.
*/
public final class TargetAndConfigurationProducer
implements StateMachine,
StateMachine.ValueOrExceptionSink<InvalidConfigurationException>,
Consumer<SkyValue>,
TargetProducer.ResultSink {
/** Accepts results of this producer. */
public interface ResultSink {
void acceptTargetAndConfiguration(TargetAndConfiguration value, ConfiguredTargetKey fullKey);
void acceptTargetAndConfigurationDelegatedValue(ConfiguredTargetValue value);
void acceptTargetAndConfigurationError(TargetAndConfigurationError error);
}
/** Tagged union of possible errors. */
@AutoOneOf(TargetAndConfigurationError.Kind.class)
public abstract static class TargetAndConfigurationError {
/** Tags the error type. */
public enum Kind {
CONFIGURED_VALUE_CREATION,
NO_SUCH_THING,
INCONSISTENT_NULL_CONFIG
}
public abstract Kind kind();
public abstract ConfiguredValueCreationException configuredValueCreation();
public abstract NoSuchThingException noSuchThing();
public abstract InconsistentNullConfigException inconsistentNullConfig();
private static TargetAndConfigurationError of(ConfiguredValueCreationException e) {
return AutoOneOf_TargetAndConfigurationProducer_TargetAndConfigurationError
.configuredValueCreation(e);
}
private static TargetAndConfigurationError of(NoSuchThingException e) {
return AutoOneOf_TargetAndConfigurationProducer_TargetAndConfigurationError.noSuchThing(e);
}
private static TargetAndConfigurationError of(InconsistentNullConfigException e) {
return AutoOneOf_TargetAndConfigurationProducer_TargetAndConfigurationError
.inconsistentNullConfig(e);
}
}
// -------------------- Input --------------------
private final ConfiguredTargetKey preRuleTransitionKey;
@Nullable private final TransitionFactory<RuleTransitionData> trimmingTransitionFactory;
private final StarlarkTransitionCache transitionCache;
private final TransitiveDependencyState transitiveState;
// -------------------- Output --------------------
private final ResultSink sink;
private final ExtendedEventHandler eventHandler;
// -------------------- Internal State --------------------
private Target target;
public TargetAndConfigurationProducer(
ConfiguredTargetKey preRuleTransitionKey,
@Nullable TransitionFactory<RuleTransitionData> trimmingTransitionFactory,
StarlarkTransitionCache transitionCache,
TransitiveDependencyState transitiveState,
ResultSink sink,
ExtendedEventHandler eventHandler) {
this.preRuleTransitionKey = preRuleTransitionKey;
this.trimmingTransitionFactory = trimmingTransitionFactory;
this.transitionCache = transitionCache;
this.transitiveState = transitiveState;
this.sink = sink;
this.eventHandler = eventHandler;
}
@Override
public StateMachine step(Tasks tasks) {
return new TargetProducer(
preRuleTransitionKey.getLabel(),
transitiveState,
(TargetProducer.ResultSink) this,
/* runAfter= */ this::determineConfiguration);
}
@Override
public void acceptTarget(Target target) {
this.target = target;
}
@Override
public void acceptTargetError(NoSuchPackageException e) {
eventHandler.handle(Event.error(e.getMessage()));
sink.acceptTargetAndConfigurationError(TargetAndConfigurationError.of(e));
}
@Override
public void acceptTargetError(NoSuchTargetException e, Location location) {
eventHandler.handle(Event.error(location, e.getMessage()));
sink.acceptTargetAndConfigurationError(TargetAndConfigurationError.of(e));
}
private StateMachine determineConfiguration(Tasks tasks) {
if (target == null) {
return DONE; // A target could not be determined.
}
BuildConfigurationKey configurationKey = preRuleTransitionKey.getConfigurationKey();
if (configurationKey == null) {
if (target.isConfigurable()) {
// We somehow ended up in a target that requires a non-null configuration but with a key
// that doesn't have a configuration. This is always an error, but we need to bubble this
// up to the parent to provide more context.
sink.acceptTargetAndConfigurationError(
TargetAndConfigurationError.of(new InconsistentNullConfigException()));
return DONE;
}
sink.acceptTargetAndConfiguration(
new TargetAndConfiguration(target, /* configuration= */ null), preRuleTransitionKey);
return DONE;
}
if (!target.isConfigurable()) {
// If target is not configurable, but requested with a configuration. Delegates to a key with
// the null configuration. This is expected to be uncommon. The common case of a
// non-configurable target is an input file, but those are usually package local and requested
// correctly with the null configuration.
delegateTo(
tasks,
ConfiguredTargetKey.builder()
.setLabel(preRuleTransitionKey.getLabel())
.setExecutionPlatformLabel(preRuleTransitionKey.getExecutionPlatformLabel())
.build());
return DONE;
}
if (!preRuleTransitionKey.shouldApplyRuleTransition()) {
lookUpConfigurationValue(tasks);
return DONE;
}
ConfigurationTransition transition =
computeTransition(target.getAssociatedRule(), trimmingTransitionFactory);
if (transition == null) {
lookUpConfigurationValue(tasks);
return DONE;
}
return new RuleTransitionApplier(transition);
}
private void delegateTo(Tasks tasks, ActionLookupKey delegate) {
tasks.lookUp(delegate, (Consumer<SkyValue>) this);
}
@Override
public void accept(SkyValue value) {
sink.acceptTargetAndConfigurationDelegatedValue((ConfiguredTargetValue) value);
}
private void lookUpConfigurationValue(Tasks tasks) {
tasks.lookUp(
preRuleTransitionKey.getConfigurationKey(),
InvalidConfigurationException.class,
(ValueOrExceptionSink<InvalidConfigurationException>) this);
}
@Override
public void acceptValueOrException(
@Nullable SkyValue value, @Nullable InvalidConfigurationException error) {
if (value != null) {
sink.acceptTargetAndConfiguration(
new TargetAndConfiguration(target, (BuildConfigurationValue) value),
preRuleTransitionKey);
return;
}
emitError(
error.getMessage(), TargetUtils.getLocationMaybe(target), error.getDetailedExitCode());
}
/**
* Applies the requested rule transition.
*
* <p>When the rule transition results in a new configuration, performs an idempotency check and
* constructs a delegate {@link ConfiguredTargetKey} with the appropriate {@link
* ConfiguredTargetKey#shouldApplyRuleTransition} value. Otherwise, just looks up the
* configuration.
*/
private class RuleTransitionApplier implements StateMachine, TransitionApplier.ResultSink {
// -------------------- Input --------------------
private final ConfigurationTransition transition;
// -------------------- Internal State --------------------
private BuildConfigurationKey configurationKey;
private RuleTransitionApplier(ConfigurationTransition transition) {
this.transition = transition;
}
@Override
public StateMachine step(Tasks tasks) {
return new TransitionApplier(
preRuleTransitionKey.getConfigurationKey(),
transition,
transitionCache,
(TransitionApplier.ResultSink) this,
eventHandler,
/* runAfter= */ this::processTransitionedKey);
}
@Override
public void acceptTransitionedConfigurations(
ImmutableMap<String, BuildConfigurationKey> transitionResult) {
checkState(transitionResult.size() == 1, "Expected exactly one result: %s", transitionResult);
this.configurationKey =
checkNotNull(
transitionResult.get(ConfigurationTransition.PATCH_TRANSITION_KEY),
"Transition result missing patch transition entry: %s",
transitionResult);
}
@Override
public void acceptTransitionError(TransitionException e) {
emitTransitionErrorMessage(e.getMessage());
}
@Override
public void acceptTransitionError(OptionsParsingException e) {
emitTransitionErrorMessage(e.getMessage());
}
private StateMachine processTransitionedKey(Tasks tasks) {
if (configurationKey == null) {
return DONE; // There was an error.
}
BuildConfigurationKey parentConfiguration = preRuleTransitionKey.getConfigurationKey();
if (configurationKey.equals(parentConfiguration)) {
// This key owns the configuration and the computation completes normally.
lookUpConfigurationValue(tasks);
return DONE;
}
eventHandler.post(
ConfigurationTransitionEvent.create(
parentConfiguration.getOptionsChecksum(), configurationKey.getOptionsChecksum()));
return new IdempotencyChecker();
}
/**
* Checks the transition for idempotency before applying delegation.
*
* <p>If the transition is non-idempotent, marks {@link
* ConfiguredTargetKey#shouldApplyRuleTransition} false in the delegate key.
*/
private class IdempotencyChecker implements StateMachine, TransitionApplier.ResultSink {
/* At first glance, it seems like setting `shouldApplyRuleTransition=false` should be benign
* in both cases, but it would be an error in the idempotent case.
*
* Idempotent Case
*
* If we were to mark the idempotent case with `shouldApplyRuleTransition=false`, it would
* lead to action conflicts. Let `//foo[123]` be a key that rule transitions to `//foo[abc]`
* and suppose the outcome is marked `//foo[abc] shouldApplyRuleTransition=false`.
*
* A different parent might directly request `//foo[abc] shouldApplyRuleTransition=true`.
* Since the rule transition is a idempotent, it would result in the same actions as
* `//foo[abc] shouldApplyRuleTransition=false` with a different key, causing action
* conflicts.
*
* Non-idempotent Case
*
* In the example of //foo[abc] shouldApplyRuleTransition=false and //foo[abc]
* shouldApplyRuleTransition=true, there should be no action conflicts because the
* `shouldApplyRuleTransition=false` is the result of a non-idempotent rule transition and
* `shouldApplyRuleTransition=true` will produce a different configuration. */
// -------------------- Internal State --------------------
private BuildConfigurationKey configurationKey2;
@Override
public StateMachine step(Tasks tasks) {
return new TransitionApplier(
configurationKey,
transition,
transitionCache,
(TransitionApplier.ResultSink) this,
eventHandler,
/* runAfter= */ this::checkIdempotencyAndDelegate);
}
@Override
public void acceptTransitionedConfigurations(
ImmutableMap<String, BuildConfigurationKey> transitionResult) {
checkState(
transitionResult.size() == 1, "Expected exactly one result: %s", transitionResult);
this.configurationKey2 =
checkNotNull(
transitionResult.get(ConfigurationTransition.PATCH_TRANSITION_KEY),
"Transition result missing patch transition entry: %s",
transitionResult);
}
@Override
public void acceptTransitionError(TransitionException e) {
emitTransitionErrorMessage(e.getMessage());
}
@Override
public void acceptTransitionError(OptionsParsingException e) {
emitTransitionErrorMessage(e.getMessage());
}
private StateMachine checkIdempotencyAndDelegate(Tasks tasks) {
if (configurationKey2 == null) {
return DONE; // There was an error.
}
ConfiguredTargetKey.Builder keyBuilder =
ConfiguredTargetKey.builder()
.setLabel(preRuleTransitionKey.getLabel())
.setExecutionPlatformLabel(preRuleTransitionKey.getExecutionPlatformLabel())
.setConfigurationKey(configurationKey);
if (!configurationKey.equals(configurationKey2)) {
// The transition was not idempotent. Explicitly informs the delegate to avoid applying a
// rule transition.
keyBuilder.setShouldApplyRuleTransition(false);
}
delegateTo(tasks, keyBuilder.build());
return DONE;
}
}
private void emitTransitionErrorMessage(String message) {
emitError(message, TargetUtils.getLocationMaybe(target), /* exitCode= */ null);
}
}
private void emitError(
String message, @Nullable Location location, @Nullable DetailedExitCode exitCode) {
sink.acceptTargetAndConfigurationError(
TargetAndConfigurationError.of(
new ConfiguredValueCreationException(
location,
message,
preRuleTransitionKey.getLabel(),
configurationId(preRuleTransitionKey.getConfigurationKey()),
/* rootCauses= */ null,
exitCode)));
}
// Public for Cquery.
@Nullable
public static ConfigurationTransition computeTransition(
Rule rule, @Nullable TransitionFactory<RuleTransitionData> trimmingTransitionFactory) {
var transitionData = RuleTransitionData.create(rule);
ConfigurationTransition transition = null;
TransitionFactory<RuleTransitionData> transitionFactory =
rule.getRuleClassObject().getTransitionFactory();
if (transitionFactory != null) {
transition = transitionFactory.create(transitionData);
}
if (trimmingTransitionFactory != null) {
var trimmingTransition = trimmingTransitionFactory.create(transitionData);
if (transition != null) {
transition = ComposingTransition.of(transition, trimmingTransition);
} else {
transition = trimmingTransition;
}
}
return transition;
}
}