| // 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.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.TransitiveDependencyState; |
| 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.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.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; |
| private final TransitiveDependencyState transitiveState; |
| |
| 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, |
| TransitiveDependencyState transitiveState, |
| ResultSink sink, |
| StateMachine runAfter) { |
| this.targetAndConfiguration = targetAndConfiguration; |
| this.configuredTargetKey = configuredTargetKey; |
| this.configConditions = configConditions; |
| this.platformInfo = platformInfo; |
| this.transitiveState = transitiveState; |
| this.sink = sink; |
| this.runAfter = runAfter; |
| } |
| |
| @Override |
| public StateMachine step(Tasks tasks) { |
| 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(), |
| 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) { |
| 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(), |
| transitiveState))); |
| 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, |
| TransitiveDependencyState transitiveState) { |
| 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(), |
| transitiveState)); |
| } |
| |
| /** 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, |
| TransitiveDependencyState transitiveState) { |
| // 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. |
| TransitiveInfoProviderMapBuilder providerBuilder = |
| new TransitiveInfoProviderMapBuilder() |
| .put(incompatiblePlatformProvider) |
| .add(RunfilesProvider.simple(Runfiles.EMPTY)) |
| .add(FileProvider.EMPTY) |
| .add(FilesToRunProvider.EMPTY) |
| .add(SupportedEnvironments.EMPTY); |
| 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, transitiveState.transitivePackages()); |
| } |
| |
| /** |
| * 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()))); |
| } |
| } |