blob: 037f43e26792b73026f13d778d34b859e42291ad [file] [log] [blame]
// Copyright 2022 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.constraints;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.ConfiguredTargetValue;
import com.google.devtools.build.lib.analysis.DependencyKind;
import com.google.devtools.build.lib.analysis.FileProvider;
import com.google.devtools.build.lib.analysis.FilesToRunProvider;
import com.google.devtools.build.lib.analysis.IncompatiblePlatformProvider;
import com.google.devtools.build.lib.analysis.Runfiles;
import com.google.devtools.build.lib.analysis.RunfilesProvider;
import com.google.devtools.build.lib.analysis.TargetAndConfiguration;
import com.google.devtools.build.lib.analysis.TransitiveInfoProviderMapBuilder;
import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
import com.google.devtools.build.lib.analysis.config.ConfigConditions;
import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget;
import com.google.devtools.build.lib.analysis.platform.ConstraintValueInfo;
import com.google.devtools.build.lib.analysis.platform.PlatformInfo;
import com.google.devtools.build.lib.analysis.platform.PlatformProviderUtils;
import com.google.devtools.build.lib.analysis.test.TestActionBuilder;
import com.google.devtools.build.lib.analysis.test.TestConfiguration;
import com.google.devtools.build.lib.analysis.test.TestProvider;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.packages.ConfiguredAttributeMapper;
import com.google.devtools.build.lib.packages.ConfiguredAttributeMapper.ValidationException;
import com.google.devtools.build.lib.packages.Package;
import com.google.devtools.build.lib.packages.PackageSpecification;
import com.google.devtools.build.lib.packages.PackageSpecification.PackageGroupContents;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetKey;
import com.google.devtools.build.lib.skyframe.RuleConfiguredTargetValue;
import com.google.devtools.build.lib.util.OrderedSetMultimap;
import com.google.devtools.build.skyframe.SkyValue;
import com.google.devtools.build.skyframe.state.StateMachine;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import javax.annotation.Nullable;
/**
* Helpers for creating configured targets that are incompatible.
*
* <p>A target is considered incompatible if any of the following applies:
*
* <ol>
* <li>The target's <code>target_compatible_with</code> attribute specifies a constraint that is
* not present in the target platform. The target is said to be "directly incompatible".
* <li>One or more of the target's dependencies is incompatible. The target is said to be
* "indirectly incompatible."
* </ol>
*
* The intent of these helpers is that they get called as early in the analysis phase as possible.
* That's why there are two helpers instead of just one. The first helper determines direct
* incompatibility very early in the analysis phase. If a target is not directly incompatible, the
* dependencies need to be analysed and then we can check for indirect incompatibility. Doing these
* checks as early as possible allows us to skip analysing unused dependencies and ignore unused
* toolchains.
*
* <p>See https://bazel.build/docs/platforms#skipping-incompatible-targets for more information on
* incompatible target skipping.
*/
public class IncompatibleTargetChecker {
/**
* Creates an incompatible configured target if it is "directly incompatible".
*
* <p>In other words, this state machine checks if a target is incompatible because of its
* "target_compatible_with" attribute.
*
* <p>Outputs an {@code Optional} {@link RuleConfiguredTargetValue} as follows.
*
* <ul>
* <li>{@code Optional.empty()}: The target is not directly incompatible. Analysis can continue.
* <li>{@code !Optional.empty()}: The target is directly incompatible. Analysis should not
* continue.
* </ul>
*/
public static class IncompatibleTargetProducer implements StateMachine, Consumer<SkyValue> {
private final TargetAndConfiguration targetAndConfiguration;
private final ConfiguredTargetKey configuredTargetKey;
private final ConfigConditions configConditions;
// Non-null when the target has an associated rule and does not opt out of toolchain resolution.
@Nullable private final PlatformInfo platformInfo;
@Nullable private final NestedSetBuilder<Package> transitivePackages;
private final ResultSink sink;
private final StateMachine runAfter;
private final ImmutableList.Builder<ConstraintValueInfo> invalidConstraintValuesBuilder =
new ImmutableList.Builder<>();
/** Sink for the output of this state machine. */
public interface ResultSink {
void acceptIncompatibleTarget(Optional<RuleConfiguredTargetValue> incompatibleTarget);
void acceptValidationException(ValidationException e);
}
public IncompatibleTargetProducer(
TargetAndConfiguration targetAndConfiguration,
ConfiguredTargetKey configuredTargetKey,
ConfigConditions configConditions,
@Nullable PlatformInfo platformInfo,
@Nullable NestedSetBuilder<Package> transitivePackages,
ResultSink sink,
StateMachine runAfter) {
this.targetAndConfiguration = targetAndConfiguration;
this.configuredTargetKey = configuredTargetKey;
this.configConditions = configConditions;
this.platformInfo = platformInfo;
this.transitivePackages = transitivePackages;
this.sink = sink;
this.runAfter = runAfter;
}
@Override
public StateMachine step(Tasks tasks, ExtendedEventHandler listener) {
Rule rule = targetAndConfiguration.getTarget().getAssociatedRule();
if (rule == null || !rule.useToolchainResolution() || platformInfo == null) {
sink.acceptIncompatibleTarget(Optional.empty());
return runAfter;
}
BuildConfigurationValue configuration = targetAndConfiguration.getConfiguration();
// Retrieves the label list for the target_compatible_with attribute.
ConfiguredAttributeMapper attrs =
ConfiguredAttributeMapper.of(rule, configConditions.asProviders(), configuration);
if (!attrs.has("target_compatible_with", BuildType.LABEL_LIST)) {
sink.acceptIncompatibleTarget(Optional.empty());
return runAfter;
}
// Resolves the constraint labels, checking for invalid configured attributes.
List<Label> targetCompatibleWith;
try {
targetCompatibleWith = attrs.getAndValidate("target_compatible_with", BuildType.LABEL_LIST);
} catch (ValidationException e) {
sink.acceptValidationException(e);
return runAfter;
}
for (Label label : targetCompatibleWith) {
tasks.lookUp(
ConfiguredTargetKey.builder()
.setLabel(label)
.setConfiguration(configuration)
.build()
.toKey(),
this);
}
return this::processResult;
}
@Override
public void accept(SkyValue value) {
var configuredTarget = ((ConfiguredTargetValue) value).getConfiguredTarget();
@Nullable ConstraintValueInfo info = PlatformProviderUtils.constraintValue(configuredTarget);
if (info == null || platformInfo.constraints().hasConstraintValue(info)) {
return;
}
invalidConstraintValuesBuilder.add(info);
}
private StateMachine processResult(Tasks tasks, ExtendedEventHandler listener) {
var invalidConstraintValues = invalidConstraintValuesBuilder.build();
if (!invalidConstraintValues.isEmpty()) {
sink.acceptIncompatibleTarget(
Optional.of(
createIncompatibleRuleConfiguredTarget(
configuredTargetKey,
targetAndConfiguration.getConfiguration(),
configConditions,
IncompatiblePlatformProvider.incompatibleDueToConstraints(
platformInfo.label(), invalidConstraintValues),
targetAndConfiguration.getTarget().getAssociatedRule().getRuleClass(),
transitivePackages)));
return runAfter;
}
sink.acceptIncompatibleTarget(Optional.empty());
return runAfter;
}
}
/**
* Creates an incompatible target if it is "indirectly incompatible".
*
* <p>In other words, this function checks if a target is incompatible because of one of its
* dependencies. If a dependency is incompatible, then this target is also incompatible.
*
* <p>This function returns an {@code Optional} of a {@link RuleConfiguredTargetValue}. This
* provides two states of return values:
*
* <ul>
* <li>{@code Optional.empty()}: The target is not indirectly incompatible. Analysis can
* continue.
* <li>{@code !Optional.empty()}: The target is indirectly incompatible. Analysis should not
* continue.
* </ul>
*/
public static Optional<RuleConfiguredTargetValue> createIndirectlyIncompatibleTarget(
TargetAndConfiguration targetAndConfiguration,
ConfiguredTargetKey configuredTargetKey,
OrderedSetMultimap<DependencyKind, ConfiguredTargetAndData> depValueMap,
ConfigConditions configConditions,
@Nullable PlatformInfo platformInfo,
@Nullable NestedSetBuilder<Package> transitivePackages) {
Target target = targetAndConfiguration.getTarget();
Rule rule = target.getAssociatedRule();
if (rule == null || rule.getRuleClass().equals("toolchain")) {
return Optional.empty();
}
// Find all the incompatible dependencies.
ImmutableList<ConfiguredTarget> incompatibleDeps =
depValueMap.values().stream()
.map(ConfiguredTargetAndData::getConfiguredTarget)
.filter(
dep -> RuleContextConstraintSemantics.checkForIncompatibility(dep).isIncompatible())
.collect(toImmutableList());
if (incompatibleDeps.isEmpty()) {
return Optional.empty();
}
BuildConfigurationValue configuration = targetAndConfiguration.getConfiguration();
Label platformLabel = platformInfo != null ? platformInfo.label() : null;
return Optional.of(
createIncompatibleRuleConfiguredTarget(
configuredTargetKey,
configuration,
configConditions,
IncompatiblePlatformProvider.incompatibleDueToTargets(platformLabel, incompatibleDeps),
rule.getRuleClass(),
transitivePackages));
}
/** Thrown if this target is platform-incompatible with the current build. */
public static class IncompatibleTargetException extends Exception {
private final RuleConfiguredTargetValue target;
public IncompatibleTargetException(RuleConfiguredTargetValue target) {
this.target = target;
}
public RuleConfiguredTargetValue target() {
return target;
}
}
/** Creates an incompatible target. */
private static RuleConfiguredTargetValue createIncompatibleRuleConfiguredTarget(
ConfiguredTargetKey configuredTargetKey,
BuildConfigurationValue configuration,
ConfigConditions configConditions,
IncompatiblePlatformProvider incompatiblePlatformProvider,
String ruleClassString,
@Nullable NestedSetBuilder<Package> transitivePackages) {
// Create dummy instances of the necessary data for a configured target. None of this data will
// actually be used because actions associated with incompatible targets must not be evaluated.
NestedSet<Artifact> filesToBuild = NestedSetBuilder.emptySet(Order.STABLE_ORDER);
FileProvider fileProvider = new FileProvider(filesToBuild);
FilesToRunProvider filesToRunProvider = new FilesToRunProvider(filesToBuild, null, null);
TransitiveInfoProviderMapBuilder providerBuilder =
new TransitiveInfoProviderMapBuilder()
.put(incompatiblePlatformProvider)
.add(RunfilesProvider.simple(Runfiles.EMPTY))
.add(fileProvider)
.add(filesToRunProvider)
.add(
new SupportedEnvironments(
EnvironmentCollection.EMPTY, EnvironmentCollection.EMPTY, ImmutableMap.of()));
if (configuration.hasFragment(TestConfiguration.class)) {
// Create a dummy TestProvider instance so that other parts of the code base stay happy. Even
// though this test will never execute, some code still expects the provider.
TestProvider.TestParams testParams = TestActionBuilder.createEmptyTestParams();
providerBuilder.put(TestProvider.class, new TestProvider(testParams));
}
RuleConfiguredTarget configuredTarget =
new RuleConfiguredTarget(
configuredTargetKey,
convertVisibility(),
providerBuilder.build(),
configConditions.asProviders(),
ruleClassString);
return new RuleConfiguredTargetValue(
configuredTarget, transitivePackages == null ? null : transitivePackages.build());
}
/**
* Generates visibility for an incompatible target.
*
* <p>The intent is for this function is to match ConfiguredTargetFactory.convertVisibility().
* Since visibility is currently validated after incompatibility is evaluated, however, it doesn't
* matter what visibility we set here. To keep it simple, we pretend that all incompatible targets
* are public.
*
* <p>TODO(#16044): Set up properly validated visibility here.
*/
private static NestedSet<PackageGroupContents> convertVisibility() {
return NestedSetBuilder.create(
Order.STABLE_ORDER,
PackageGroupContents.create(ImmutableList.of(PackageSpecification.everything())));
}
}