Support composed dependency transitions with dynamic configs.
--
PiperOrigin-RevId: 149439502
MOS_MIGRATED_REVID=149439502
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
index b16a4a6..f7dd0a9 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
@@ -1298,7 +1298,7 @@
* Sorts fragments by class name. This produces a stable order which, e.g., facilitates
* consistent output from buildMneumonic.
*/
- private final static Comparator lexicalFragmentSorter =
+ private static final Comparator lexicalFragmentSorter =
new Comparator<Class<? extends Fragment>>() {
@Override
public int compare(Class<? extends Fragment> o1, Class<? extends Fragment> o2) {
@@ -1588,7 +1588,7 @@
* Accepts the given split transition. The implementation decides how to turn this into
* actual configurations.
*/
- void split(SplitTransition<?> splitTransition);
+ void split(SplitTransition<BuildOptions> splitTransition);
/**
* Returns whether or not all configuration(s) represented by the current state of this
@@ -1651,7 +1651,7 @@
}
@Override
- public void split(SplitTransition<?> splitTransition) {
+ public void split(SplitTransition<BuildOptions> splitTransition) {
// Split transitions can't be nested, so if we're splitting we must be doing it over
// a single config.
toConfigurations =
@@ -1723,15 +1723,20 @@
* transitions that the caller subsequently creates configurations from.
*/
private static class DynamicTransitionApplier implements TransitionApplier {
+ // TODO(gregce): remove this when Attribute.Configurator is refactored to read BuildOptions
+ // instead of BuildConfiguration.
private final BuildConfiguration originalConfiguration;
- // The transition this applier applies to dep rules. May change multiple times. However,
- // composed transitions (e.g. fromConfig -> FooTransition -> BarTransition) are not currently
- // supported, in the name of keeping the model simple. We can always revisit that assumption
- // if needed.
+ private final Transitions transitionsManager;
+ private boolean splitApplied = false;
+
+ // The transition this applier applies to dep rules. When multiple transitions are requested,
+ // this is a ComposingSplitTransition, which encapsulates the sequence into a single instance
+ // so calling code doesn't need special logic to support combinations.
private Transition currentTransition = Attribute.ConfigurationTransition.NONE;
private DynamicTransitionApplier(BuildConfiguration originalConfiguration) {
this.originalConfiguration = originalConfiguration;
+ this.transitionsManager = originalConfiguration.getTransitions();
}
@Override
@@ -1741,36 +1746,48 @@
@Override
public void applyTransition(Transition transitionToApply) {
- if (transitionToApply == Attribute.ConfigurationTransition.NONE
- // Outside of LIPO, data transitions are a no-op. Since dynamic configs don't yet support
- // LIPO, just return fast as a no-op. This isn't just convenient: evaluateTransition
- // calls configurationHook after standard attribute transitions. If configurationHook
- // triggers a data transition, that undoes the earlier transitions (because of lack of
- // composed transition support). That's dangerous and especially pointless for non-LIPO
- // builds. Hence this check.
- // TODO(gregce): add LIPO support and/or make this special case unnecessary.
- || transitionToApply == Attribute.ConfigurationTransition.DATA
- // This means it's not possible to transition back out of a host transition. We may
- // need to revise this when we properly support multiple host configurations.
+ if (currentTransition == Attribute.ConfigurationTransition.NULL
|| currentTransition == HostTransition.INSTANCE) {
+ return; // HOST and NULL transitions are final.
+ } else if (transitionToApply == Attribute.ConfigurationTransition.NONE) {
+ return;
+ } else if (transitionToApply == Attribute.ConfigurationTransition.NULL) {
+ // A NULL transition can just replace earlier transitions: no need to compose them.
+ currentTransition = Attribute.ConfigurationTransition.NULL;
+ return;
+ } else if (transitionToApply == Attribute.ConfigurationTransition.HOST) {
+ // A HOST transition can just replace earlier transitions: no need to compose them.
+ // But it also improves performance: host transitions are common, and
+ // ConfiguredTargetFunction has special optimized logic to handle them. If they were buried
+ // in the last segment of a ComposingSplitTransition, those optimizations wouldn't trigger.
+ currentTransition = HostTransition.INSTANCE;
return;
}
-
- // Since we don't support composed transitions, we need to be careful applying a transition
- // when another transition has already been applied (the latter will simply overwrite the
- // former). All allowed cases should be explicitly asserted here.
- Verify.verify(currentTransition == Attribute.ConfigurationTransition.NONE
- // LIPO transitions are okay because they're no-ops outside LIPO builds. And dynamic
- // configs don't yet support LIPO builds.
- || currentTransition.toString().contains("LipoDataTransition"));
- currentTransition = getCurrentTransitions().getDynamicTransition(transitionToApply);
+ Transition dynamicTransition = transitionToApply instanceof PatchTransition
+ ? transitionToApply
+ : transitionsManager.getDynamicTransition(transitionToApply);
+ currentTransition = currentTransition == Attribute.ConfigurationTransition.NONE
+ ? dynamicTransition
+ : new ComposingSplitTransition(currentTransition, dynamicTransition);
}
@Override
- public void split(SplitTransition<?> splitTransition) {
- Verify.verify(currentTransition == Attribute.ConfigurationTransition.NONE,
- "split transitions aren't expected to mix with other transitions");
- currentTransition = splitTransition;
+ // TODO(gregce): fold this into applyTransition during the static config code removal cleanup
+ public void split(SplitTransition<BuildOptions> splitTransition) {
+ // This "single split" check doesn't come from any design restriction. Its purpose is to
+ // protect against runaway graph explosion, e.g. applying split[1,2,3] -> split[4,5,6] -> ...
+ // and getting 3^n versions of a dep. So it's fine to loosen or lift this restriction
+ // for a principled use case.
+ Preconditions.checkState(!splitApplied,
+ "dependency edges may apply at most one split transition");
+ Preconditions.checkState(currentTransition != Attribute.ConfigurationTransition.NULL,
+ "cannot apply splits after null transitions (null transitions are expected to be final)");
+ Preconditions.checkState(currentTransition != HostTransition.INSTANCE,
+ "cannot apply splits after host transitions (host transitions are expected to be final)");
+ currentTransition = currentTransition == Attribute.ConfigurationTransition.NONE
+ ? splitTransition
+ : new ComposingSplitTransition(currentTransition, splitTransition);
+ splitApplied = true;
}
@Override
@@ -1797,7 +1814,7 @@
if (isNull()) {
return;
}
- getCurrentTransitions().configurationHook(fromRule, attribute, toTarget, this);
+ transitionsManager.configurationHook(fromRule, attribute, toTarget, this);
Rule associatedRule = toTarget.getAssociatedRule();
PatchTransition ruleClassTransition = (PatchTransition)
@@ -1807,12 +1824,13 @@
if (currentTransition == ConfigurationTransition.NONE) {
currentTransition = ruleClassTransition;
} else {
- currentTransition = new ComposingSplitTransition(ruleClassTransition, currentTransition);
+ currentTransition = new ComposingSplitTransition(ruleClassTransition,
+ currentTransition);
}
}
- // We don't support rule class configurators (which might imply composed transitions).
- // The only current use of that is LIPO, which can't currently be invoked with dynamic
+ // We don't support rule class configurators (which may need intermediate configurations to
+ // apply). The only current use of that is LIPO, which can't currently be invoked with dynamic
// configurations (e.g. this code can never get called for LIPO builds). So check that
// if there is a configurator, it's for LIPO, in which case we can ignore it.
if (associatedRule != null) {
@@ -1823,10 +1841,6 @@
}
}
- private Transitions getCurrentTransitions() {
- return originalConfiguration.getTransitions();
- }
-
@Override
public Iterable<Dependency> getDependencies(
Label label, AspectCollection aspects) {
@@ -1904,9 +1918,12 @@
return;
}
+ // TODO(gregce): make the below transitions composable (i.e. take away the "else" clauses) once
+ // the static config code path is removed. They can be mixed freely with dynamic configurations.
if (attribute.hasSplitConfigurationTransition()) {
Preconditions.checkState(attribute.getConfigurator() == null);
- transitionApplier.split(attribute.getSplitTransition(fromRule));
+ transitionApplier.split(
+ (SplitTransition<BuildOptions>) attribute.getSplitTransition(fromRule));
} else {
// III. Attributes determine configurations. The configuration of a prerequisite is determined
// by the attribute.
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/ComposingSplitTransition.java b/src/main/java/com/google/devtools/build/lib/analysis/config/ComposingSplitTransition.java
index 8a33e23..bcac47e 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/ComposingSplitTransition.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/ComposingSplitTransition.java
@@ -14,6 +14,7 @@
package com.google.devtools.build.lib.analysis.config;
+import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition;
@@ -22,40 +23,65 @@
import java.util.List;
/**
- * A split transition that combines a Transition with a {@link PatchTransition}. The patch is
- * applied first, followed by the Transition.
+ * A configuration transition that composes two other transitions in an ordered sequence.
*
- * <p>We implement a {@link SplitTransition} here since that abstraction can capture all possible
- * composed transitions - both those that produce multiple output configurations and those that
- * do not.
+ * <p>Example:
+ * <pre>
+ * transition1: { someSetting = $oldVal + " foo" }
+ * transition2: { someSetting = $oldVal + " bar" }
+ * ComposingSplitTransition(transition1, transition2): { someSetting = $oldVal + " foo bar" }
+ * </pre>
+ *
+ * <p>Child transitions can be {@link SplitTransition}s, {@link PatchTransition}s, or any
+ * combination thereof. We implement this class as a {@link SplitTransition} since that abstraction
+ * captures all possible combinations.
*/
public class ComposingSplitTransition implements SplitTransition<BuildOptions> {
-
- private PatchTransition patch;
- private Transition transition;
+ private Transition transition1;
+ private Transition transition2;
/**
- * Creates a {@link ComposingSplitTransition} with the given {@link Transition} and
- * {@link PatchTransition}.
+ * Creates a {@link ComposingSplitTransition} that applies the sequence:
+ * {@code fromOptions -> transition1 -> transition2 -> toOptions }.
*/
- public ComposingSplitTransition(PatchTransition patch, Transition transition) {
- this.patch = patch;
- this.transition = transition;
+ public ComposingSplitTransition(Transition transition1, Transition transition2) {
+ this.transition1 = verifySupported(transition1);
+ this.transition2 = verifySupported(transition2);
}
@Override
public List<BuildOptions> split(BuildOptions buildOptions) {
- BuildOptions patchedOptions = patch.apply(buildOptions);
+ ImmutableList.Builder<BuildOptions> toOptions = ImmutableList.builder();
+ for (BuildOptions transition1Options : apply(buildOptions, transition1)) {
+ toOptions.addAll(apply(transition1Options, transition2));
+ }
+ return toOptions.build();
+ }
+
+ /**
+ * Verifies support for the given transition type. Throws an {@link IllegalArgumentException} if
+ * unsupported.
+ */
+ private Transition verifySupported(Transition transition) {
+ Preconditions.checkArgument(transition instanceof PatchTransition
+ || transition instanceof SplitTransition<?>);
+ return transition;
+ }
+
+ /**
+ * Applies the given transition over the given {@link BuildOptions}, returns the result.
+ */
+ private List<BuildOptions> apply(BuildOptions fromOptions, Transition transition) {
if (transition == ConfigurationTransition.NONE) {
- return ImmutableList.<BuildOptions>of(patchedOptions);
+ return ImmutableList.<BuildOptions>of(fromOptions);
} else if (transition instanceof PatchTransition) {
- return ImmutableList.<BuildOptions>of(((PatchTransition) transition).apply(patchedOptions));
+ return ImmutableList.<BuildOptions>of(((PatchTransition) transition).apply(fromOptions));
} else if (transition instanceof SplitTransition) {
SplitTransition split = (SplitTransition<BuildOptions>) transition;
- List<BuildOptions> splitOptions = split.split(patchedOptions);
+ List<BuildOptions> splitOptions = split.split(fromOptions);
if (splitOptions.isEmpty()) {
Verify.verify(split.defaultsToSelf());
- return ImmutableList.of(patchedOptions);
+ return ImmutableList.<BuildOptions>of(fromOptions);
} else {
return splitOptions;
}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/ConfigurationsForTargetsWithDynamicConfigurationsTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/ConfigurationsForTargetsWithDynamicConfigurationsTest.java
index 2e359f3..3e0be4b 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/ConfigurationsForTargetsWithDynamicConfigurationsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/ConfigurationsForTargetsWithDynamicConfigurationsTest.java
@@ -14,14 +14,24 @@
package com.google.devtools.build.lib.skyframe;
+import static com.google.common.base.Strings.nullToEmpty;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.analysis.AspectCollection;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.Dependency;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.PatchTransition;
import com.google.devtools.build.lib.analysis.util.TestAspects;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.packages.Attribute;
import com.google.devtools.build.lib.testutil.Suite;
import com.google.devtools.build.lib.testutil.TestSpec;
+import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -135,4 +145,113 @@
Iterables.getOnlyElement(update("//a:sim").getTargetsToBuild());
assertThat(target.getConfiguration().getCpu()).isNotEqualTo("SET BY PATCH");
}
+
+ /**
+ * Returns a custom {@link PatchTransition} with the given value added to
+ * {@link BuildConfiguration.Options#testFilter}.
+ */
+ private static PatchTransition newPatchTransition(final String value) {
+ return new PatchTransition() {
+ @Override
+ public BuildOptions apply(BuildOptions options) {
+ BuildOptions toOptions = options.clone();
+ BuildConfiguration.Options baseOptions = toOptions.get(BuildConfiguration.Options.class);
+ baseOptions.testFilter = (nullToEmpty(baseOptions.testFilter)) + value;
+ return toOptions;
+ }
+
+ @Override
+ public boolean defaultsToSelf() {
+ return false;
+ }
+ };
+ }
+
+ /**
+ * Returns a custom {@link Attribute.SplitTransition} that splits
+ * {@link BuildConfiguration.Options#testFilter} down two paths: {@code += prefix + "1"}
+ * and {@code += prefix + "2"}.
+ */
+ private static Attribute.SplitTransition<BuildOptions> newSplitTransition(final String prefix) {
+ return new Attribute.SplitTransition<BuildOptions>() {
+ @Override
+ public List<BuildOptions> split(BuildOptions buildOptions) {
+ ImmutableList.Builder<BuildOptions> result = ImmutableList.builder();
+ for (int index = 1; index <= 2; index++) {
+ BuildOptions toOptions = buildOptions.clone();
+ BuildConfiguration.Options baseOptions = toOptions.get(BuildConfiguration.Options.class);
+ baseOptions.testFilter =
+ (baseOptions.testFilter == null ? "" : baseOptions.testFilter) + prefix + index;
+ result.add(toOptions);
+ }
+ return result.build();
+ }
+
+ @Override
+ public boolean defaultsToSelf() {
+ return false;
+ }
+ };
+ }
+
+ @Test
+ public void composedStraightTransitions() throws Exception {
+ update(); // Creates the target configuration.
+ BuildConfiguration.TransitionApplier applier = getTargetConfiguration().getTransitionApplier();
+ applier.applyTransition(newPatchTransition("foo"));
+ applier.applyTransition(newPatchTransition("bar"));
+ Dependency dep = Iterables.getOnlyElement(
+ applier.getDependencies(Label.create("some", "target"), AspectCollection.EMPTY));
+ BuildOptions toOptions = Iterables.getOnlyElement(
+ ConfiguredTargetFunction.getDynamicTransitionOptions(getTargetConfiguration().getOptions(),
+ dep.getTransition(), ruleClassProvider.getAllFragments(), ruleClassProvider, false));
+ assertThat(toOptions.get(BuildConfiguration.Options.class).testFilter).isEqualTo("foobar");
+ }
+
+ @Test
+ public void composedStraightTransitionThenSplitTransition() throws Exception {
+ update(); // Creates the target configuration.
+ BuildConfiguration.TransitionApplier applier = getTargetConfiguration().getTransitionApplier();
+ applier.applyTransition(newPatchTransition("foo"));
+ applier.split(newSplitTransition("split"));
+ Dependency dep = Iterables.getOnlyElement(
+ applier.getDependencies(Label.create("some", "target"), AspectCollection.EMPTY));
+ List<String> outValues = new ArrayList<>();
+ for (BuildOptions toOptions : ConfiguredTargetFunction.getDynamicTransitionOptions(
+ getTargetConfiguration().getOptions(), dep.getTransition(),
+ ruleClassProvider.getAllFragments(), ruleClassProvider, false)) {
+ outValues.add(toOptions.get(BuildConfiguration.Options.class).testFilter);
+ }
+ assertThat(outValues).containsExactly("foosplit1", "foosplit2");
+ }
+
+ @Test
+ public void composedSplitTransitionThenStraightTransition() throws Exception {
+ update(); // Creates the target configuration.
+ BuildConfiguration.TransitionApplier applier = getTargetConfiguration().getTransitionApplier();
+ applier.split(newSplitTransition("split"));
+ applier.applyTransition(newPatchTransition("foo"));
+ Dependency dep = Iterables.getOnlyElement(
+ applier.getDependencies(Label.create("some", "target"), AspectCollection.EMPTY));
+ List<String> outValues = new ArrayList<>();
+ for (BuildOptions toOptions : ConfiguredTargetFunction.getDynamicTransitionOptions(
+ getTargetConfiguration().getOptions(), dep.getTransition(),
+ ruleClassProvider.getAllFragments(), ruleClassProvider, false)) {
+ outValues.add(toOptions.get(BuildConfiguration.Options.class).testFilter);
+ }
+ assertThat(outValues).containsExactly("split1foo", "split2foo");
+ }
+
+ @Test
+ public void composedSplitTransitions() throws Exception {
+ update(); // Creates the target configuration.
+ BuildConfiguration.TransitionApplier applier = getTargetConfiguration().getTransitionApplier();
+ applier.split(newSplitTransition("split"));
+ try {
+ applier.split(newSplitTransition("disallowed second split"));
+ fail("expected failure: deps cannot apply more than one split transition each");
+ } catch (IllegalStateException e) {
+ assertThat(e.getMessage()).contains("dependency edges may apply at most one split");
+ }
+ }
}