Skip to content

Commit d089ae2

Browse files
fmeumcopybara-github
authored andcommitted
Implement transition.and_then (bazelbuild#29542)
### Description Implements the proposal https://github.com/bazelbuild/proposals/blob/main/designs/2024-04-16-transition-composition.md. ### Motivation This change in particular allows non-toolchain dependencies of rules to inherit both the execution platform (via `config.exec()`) as well as the current target platform (via a custom transition that records `//command_line_option:platforms`), which is important for certain cross-compilation scenarios. ### Build API Changes Yes, as discussed and approved in https://github.com/bazelbuild/proposals/blob/main/designs/2024-04-16-transition-composition.md. ### Checklist - [x] I have added tests for the new use cases (if any). - [x] I have updated the documentation (if applicable). ### Release Notes RELNOTES[NEW]: The `and_then` method on `transition`s can be used to compose transitions. Both Starlark transitions and native transitions (e.g. `config.exec()`) are supported. Closes bazelbuild#29542. PiperOrigin-RevId: 934095515 Change-Id: I3f36ec0907c4af5bdf43e38323d166422b47051c
1 parent 9d331b9 commit d089ae2

17 files changed

Lines changed: 714 additions & 44 deletions

File tree

docs/extending/config.mdx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,34 @@ hot_chocolate_transition = transition(
665665
)
666666
```
667667

668+
### Composing transitions {#composing-transitions}
669+
670+
Two transitions can be combined into a single new transition using
671+
[`transition.and_then`](/rules/lib/builtins/transition#and_then):
672+
673+
```python
674+
combined_transition = first_transition.and_then(second_transition)
675+
```
676+
677+
The composition applies `first_transition` to the input configuration and then
678+
runs `second_transition` against each of its output configurations. The original
679+
transitions are not modified, and the result is itself a `transition` object
680+
that can be composed further with another `and_then` call.
681+
682+
A composed transition can be attached to a rule or attribute wherever its
683+
component transitions could be used (subject to the usual restriction that an
684+
[incoming edge transition](#incoming-edge-transitions) must be 1:1). At most one
685+
of the composed transitions may be an exec transition (`"exec"` or
686+
[`config.exec`](/rules/lib/toplevel/config#exec)).
687+
688+
When two of the composed transitions are 1:2+, the composition produces the
689+
cross product of their splits. The key for each combined split is the
690+
comma-separated concatenation of the component keys: if the first transition
691+
produces keys `{a, b}` and the second produces `{x, y}` for each of those, the
692+
composition produces the four keys `a,x`, `a,y`, `b,x`, and `b,y`. Note that
693+
each additional 1:2+ transition multiplies the resulting dependency count, so
694+
chain them with care.
695+
668696
### Accessing attributes with transitions {#accessing-attributes-with-transitions}
669697

670698
[End to end example](https://github.com/bazelbuild/examples/tree/HEAD/configurations/read_attr_in_transition)

site/en/extending/config.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,34 @@ hot_chocolate_transition = transition(
668668
)
669669
```
670670

671+
### Composing transitions {:#composing-transitions}
672+
673+
Two transitions can be combined into a single new transition using
674+
[`transition.and_then`](/rules/lib/builtins/transition#and_then):
675+
676+
```python
677+
combined_transition = first_transition.and_then(second_transition)
678+
```
679+
680+
The composition applies `first_transition` to the input configuration and then
681+
runs `second_transition` against each of its output configurations. The original
682+
transitions are not modified, and the result is itself a `transition` object
683+
that can be composed further with another `and_then` call.
684+
685+
A composed transition can be attached to a rule or attribute wherever its
686+
component transitions could be used (subject to the usual restriction that an
687+
[incoming edge transition](#incoming-edge-transitions) must be 1:1). At most one
688+
of the composed transitions may be an exec transition (`"exec"` or
689+
[`config.exec`](/rules/lib/toplevel/config#exec)).
690+
691+
When two of the composed transitions are 1:2+, the composition produces the
692+
cross product of their splits. The key for each combined split is the
693+
comma-separated concatenation of the component keys: if the first transition
694+
produces keys `{a, b}` and the second produces `{x, y}` for each of those, the
695+
composition produces the four keys `a,x`, `a,y`, `b,x`, and `b,y`. Note that
696+
each additional 1:2+ transition multiplies the resulting dependency count, so
697+
chain them with care.
698+
671699
### Accessing attributes with transitions {:#accessing-attributes-with-transitions}
672700

673701
[End to end example](https://github.com/bazelbuild/examples/tree/HEAD/configurations/read_attr_in_transition){: .external}

src/main/java/com/google/devtools/build/lib/analysis/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ java_library(
101101
"extra/ExtraAction.java",
102102
"extra/ExtraActionMapProvider.java",
103103
"extra/ExtraActionSpec.java",
104+
"starlark/ComposedTransitionMaterializer.java",
104105
"starlark/StarlarkActionFactory.java",
105106
"starlark/StarlarkAspectPropagationContext.java",
106107
"starlark/StarlarkAttrModule.java",

src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ public Builder addTrimmingTransitionFactory(TransitionFactory<RuleTransitionData
392392
trimmingTransitionFactory = factory;
393393
} else {
394394
trimmingTransitionFactory =
395-
ComposingTransitionFactory.of(trimmingTransitionFactory, factory);
395+
ComposingTransitionFactory.ofUnchecked(trimmingTransitionFactory, factory);
396396
}
397397
return this;
398398
}

src/main/java/com/google/devtools/build/lib/analysis/DependencyResolutionHelpers.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,17 +228,19 @@ private static ExecutionPlatformResult getExecutionPlatformLabel(
228228
return ExecutionPlatformResult.ofNullLabel();
229229
}
230230

231-
TransitionFactory<AttributeTransitionData> transitionFactory =
232-
kind.getAttribute().getTransitionFactory();
233-
if (!(transitionFactory instanceof ExecutionTransitionFactory)) {
231+
// The exec transition may be composed with other transitions (via transition.and_then), so
232+
// look for it anywhere in the (possibly composed) transition factory.
233+
ExecutionTransitionFactory execTransitionFactory =
234+
findExecutionTransitionFactory(kind.getAttribute().getTransitionFactory());
235+
if (execTransitionFactory == null) {
234236
return ExecutionPlatformResult.ofLabel(
235237
toolchainContexts
236238
.getToolchainContext(DeclaredExecGroup.DEFAULT_EXEC_GROUP_NAME)
237239
.executionPlatform()
238240
.label());
239241
}
240242

241-
String execGroup = ((ExecutionTransitionFactory) transitionFactory).getExecGroup();
243+
String execGroup = execTransitionFactory.getExecGroup();
242244
if (toolchainContexts.hasToolchainContext(execGroup)) {
243245
PlatformInfo platform = toolchainContexts.getToolchainContext(execGroup).executionPlatform();
244246
return platform == null
@@ -252,6 +254,24 @@ private static ExecutionPlatformResult getExecutionPlatformLabel(
252254
kind.getAttribute().getName(), execGroup));
253255
}
254256

257+
/**
258+
* Returns the {@link ExecutionTransitionFactory} within the given transition factory, or {@code
259+
* null} if there is none. A composition contains at most one native transition, so there is at
260+
* most one match.
261+
*/
262+
@Nullable
263+
private static ExecutionTransitionFactory findExecutionTransitionFactory(
264+
TransitionFactory<AttributeTransitionData> transitionFactory) {
265+
var found = new ExecutionTransitionFactory[1];
266+
transitionFactory.visit(
267+
factory -> {
268+
if (factory instanceof ExecutionTransitionFactory execFactory) {
269+
found[0] = execFactory;
270+
}
271+
});
272+
return found[0];
273+
}
274+
255275
/** True if {@code owningAspect} is the main aspect, the last one in {@code aspectsList}. */
256276
private static boolean isMainAspect(
257277
ImmutableList<Aspect> aspectsList, @Nullable AspectClass owningAspect) {

src/main/java/com/google/devtools/build/lib/analysis/config/transitions/ComposingTransitionFactory.java

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,20 @@ public abstract class ComposingTransitionFactory<T extends TransitionFactory.Dat
4848
* one of the transitions is {@link NoTransition}, and returns an efficiently composed transition.
4949
*/
5050
public static <T extends TransitionFactory.Data> TransitionFactory<T> of(
51-
TransitionFactory<T> transitionFactory1, TransitionFactory<T> transitionFactory2) {
52-
51+
TransitionFactory<T> transitionFactory1, TransitionFactory<T> transitionFactory2)
52+
throws IncompatibleTransitionsException {
5353
Preconditions.checkNotNull(transitionFactory1);
5454
Preconditions.checkNotNull(transitionFactory2);
55-
Preconditions.checkArgument(
56-
transitionFactory1.transitionType().isCompatibleWith(transitionFactory2.transitionType()),
57-
"transition factory types must be compatible");
58-
Preconditions.checkArgument(
59-
!transitionFactory1.isSplit() || !transitionFactory2.isSplit(),
60-
"can't compose two split transition factories");
55+
if (!transitionFactory1
56+
.transitionType()
57+
.isCompatibleWith(transitionFactory2.transitionType())) {
58+
throw new IncompatibleTransitionsException(
59+
"transition types must be compatible, got %s and %s"
60+
.formatted(transitionFactory1.transitionType(), transitionFactory2.transitionType()));
61+
}
62+
if (transitionFactory1.isTool() && transitionFactory2.isTool()) {
63+
throw new IncompatibleTransitionsException("can't compose two exec transitions");
64+
}
6165

6266
if (NoTransition.isInstance(transitionFactory1)) {
6367
// Since transitionFactory1 causes no changes, use transitionFactory2 directly.
@@ -70,6 +74,26 @@ public static <T extends TransitionFactory.Data> TransitionFactory<T> of(
7074
return create(transitionFactory1, transitionFactory2);
7175
}
7276

77+
/**
78+
* Use {@link #of} instead unless the two transition factories are statically guaranteed to be
79+
* compatible.
80+
*/
81+
public static <T extends TransitionFactory.Data> TransitionFactory<T> ofUnchecked(
82+
TransitionFactory<T> transitionFactory1, TransitionFactory<T> transitionFactory2) {
83+
try {
84+
return of(transitionFactory1, transitionFactory2);
85+
} catch (IncompatibleTransitionsException e) {
86+
throw new IllegalArgumentException(e);
87+
}
88+
}
89+
90+
/** Thrown when two {@link TransitionFactory} instances cannot be composed. */
91+
public static final class IncompatibleTransitionsException extends Exception {
92+
IncompatibleTransitionsException(String message) {
93+
super(message);
94+
}
95+
}
96+
7397
private static <T extends TransitionFactory.Data> TransitionFactory<T> create(
7498
TransitionFactory<T> transitionFactory1, TransitionFactory<T> transitionFactory2) {
7599
return new AutoValue_ComposingTransitionFactory<T>(transitionFactory1, transitionFactory2);
@@ -84,8 +108,11 @@ public ConfigurationTransition create(T data) {
84108

85109
@Override
86110
public TransitionType transitionType() {
87-
// Both types must match so this is correct.
88-
return transitionFactory1().transitionType();
111+
// The two types are compatible, so at most one of them is non-ANY; return the more specific
112+
// one.
113+
return transitionFactory1().transitionType() == TransitionType.ANY
114+
? transitionFactory2().transitionType()
115+
: transitionFactory1().transitionType();
89116
}
90117

91118
abstract TransitionFactory<T> transitionFactory1();
@@ -186,22 +213,19 @@ public boolean equals(Object other) {
186213
}
187214

188215
/**
189-
* Composes a new key out of two given keys. Composing two split transitions is not allowed at
190-
* the moment, so what this essentially does are (1) make sure not both transitions are split
191-
* and (2) choose one from a split transition, if there's any, or return {@code
192-
* PATCH_TRANSITION_KEY}, if there isn't.
216+
* Composes a new key out of two given keys. If either transition is a patch (1:1) its
217+
* placeholder {@code PATCH_TRANSITION_KEY} is absorbed; if both transitions split, the keys are
218+
* joined by a comma so the composition produces the cross product of their splits (e.g. keys
219+
* {@code "a"} and {@code "x"} compose to {@code "a,x"}).
193220
*/
194-
private String composeKeys(String key1, String key2) {
195-
if (!key1.equals(PATCH_TRANSITION_KEY)) {
196-
if (!key2.equals(PATCH_TRANSITION_KEY)) {
197-
throw new IllegalStateException(
198-
String.format(
199-
"can't compose two split transitions %s and %s",
200-
transition1.getName(), transition2.getName()));
201-
}
221+
private static String composeKeys(String key1, String key2) {
222+
if (key1.equals(PATCH_TRANSITION_KEY)) {
223+
return key2;
224+
}
225+
if (key2.equals(PATCH_TRANSITION_KEY)) {
202226
return key1;
203227
}
204-
return key2;
228+
return key1 + "," + key2;
205229
}
206230
}
207231
}

src/main/java/com/google/devtools/build/lib/analysis/producers/RuleTransitionApplier.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ public StateMachine computeTransition(Tasks tasks) {
254254
targetAndConfigurationData.getTrimmingTransitionFactory();
255255
if (trimmingTransitionFactory != null) {
256256
transitionFactory =
257-
ComposingTransitionFactory.of(transitionFactory, trimmingTransitionFactory);
257+
ComposingTransitionFactory.ofUnchecked(transitionFactory, trimmingTransitionFactory);
258258
}
259259
ConfiguredTargetKey preRuleTransitionKey = targetAndConfigurationData.getPreRuleTransitionKey();
260260
var transitionData =
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2026 The Bazel Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.devtools.build.lib.analysis.starlark;
16+
17+
import com.google.devtools.build.lib.analysis.config.transitions.ComposingTransitionFactory;
18+
import com.google.devtools.build.lib.analysis.config.transitions.ComposingTransitionFactory.IncompatibleTransitionsException;
19+
import com.google.devtools.build.lib.analysis.config.transitions.TransitionFactory;
20+
import com.google.devtools.build.lib.starlarkbuildapi.config.ComposedConfigurationTransition;
21+
import com.google.devtools.build.lib.starlarkbuildapi.config.ConfigurationTransitionApi;
22+
import net.starlark.java.eval.EvalException;
23+
import net.starlark.java.eval.Starlark;
24+
25+
/**
26+
* Helpers for turning a {@link ComposedConfigurationTransition} (the deferred result of {@code
27+
* transition.and_then}) into a single {@link TransitionFactory} for a specific use context (rule
28+
* vs. attribute).
29+
*/
30+
final class ComposedTransitionMaterializer {
31+
32+
private ComposedTransitionMaterializer() {}
33+
34+
/** Converts one element of a composed transition into a {@link TransitionFactory}. */
35+
@FunctionalInterface
36+
interface ElementConverter<T extends TransitionFactory.Data> {
37+
TransitionFactory<T> convert(ConfigurationTransitionApi element) throws EvalException;
38+
}
39+
40+
/**
41+
* Folds {@code composition}'s elements into a single {@link TransitionFactory} by converting each
42+
* with {@code converter} and combining them with {@link ComposingTransitionFactory#of}.
43+
*
44+
* @param incompatibleElementMessage error suffix used when an element can't be converted in this
45+
* context (e.g. a native attribute-only transition used as a rule {@code cfg})
46+
*/
47+
static <T extends TransitionFactory.Data> TransitionFactory<T> fold(
48+
ComposedConfigurationTransition composition,
49+
ElementConverter<T> converter,
50+
String incompatibleElementMessage)
51+
throws EvalException {
52+
TransitionFactory<T> result = null;
53+
for (ConfigurationTransitionApi element : composition.getElements()) {
54+
TransitionFactory<T> factory;
55+
try {
56+
factory = converter.convert(element);
57+
} catch (EvalException unused) {
58+
throw Starlark.errorf(
59+
"invalid composed transition for `cfg`: %s (composed at %s)",
60+
incompatibleElementMessage, composition.getLocation());
61+
}
62+
if (result == null) {
63+
result = factory;
64+
} else {
65+
try {
66+
result = ComposingTransitionFactory.of(result, factory);
67+
} catch (IncompatibleTransitionsException e) {
68+
throw Starlark.errorf(
69+
"invalid composed transition for `cfg`: %s (composed at %s)",
70+
e.getMessage(), composition.getLocation());
71+
}
72+
}
73+
}
74+
return result;
75+
}
76+
}

src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkAttrModule.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions;
5757
import com.google.devtools.build.lib.starlarkbuildapi.NativeComputedDefaultApi;
5858
import com.google.devtools.build.lib.starlarkbuildapi.StarlarkAttrModuleApi;
59+
import com.google.devtools.build.lib.starlarkbuildapi.config.ComposedConfigurationTransition;
5960
import com.google.devtools.build.lib.starlarkbuildapi.config.ConfigurationTransitionApi;
6061
import com.google.devtools.build.lib.starlarkbuildapi.core.StructApi;
6162
import com.google.devtools.build.lib.util.FileType;
@@ -535,6 +536,12 @@ private static TransitionFactory<AttributeTransitionData> convertCfg(
535536
if (trans instanceof StarlarkDefinedConfigTransition starlarkDefinedTransition) {
536537
return new StarlarkAttributeTransitionProvider(starlarkDefinedTransition);
537538
}
539+
if (trans instanceof ComposedConfigurationTransition composition) {
540+
return ComposedTransitionMaterializer.fold(
541+
composition,
542+
element -> convertCfg(thread, element),
543+
"it contains a native transition that can only be used as a rule transition");
544+
}
538545
if (trans instanceof ConfigurationTransitionApi cta) {
539546
// Every ConfigurationTransitionApi must be a TransitionFactory instance to be usable.
540547
if (cta instanceof TransitionFactory<?> tf) {

src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkRuleClassFunctions.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
import com.google.devtools.build.lib.starlarkbuildapi.MacroFunctionApi;
115115
import com.google.devtools.build.lib.starlarkbuildapi.StarlarkRuleFunctionsApi;
116116
import com.google.devtools.build.lib.starlarkbuildapi.StarlarkSubruleApi;
117+
import com.google.devtools.build.lib.starlarkbuildapi.config.ComposedConfigurationTransition;
117118
import com.google.devtools.build.lib.starlarkbuildapi.config.ConfigurationTransitionApi;
118119
import com.google.devtools.build.lib.util.FileTypeSet;
119120
import com.google.devtools.build.lib.util.Pair;
@@ -1098,7 +1099,7 @@ public static StarlarkRuleFunction createRule(
10981099
});
10991100
if (parent != null) {
11001101
transitionFactory =
1101-
ComposingTransitionFactory.of(transitionFactory, parent.getTransitionFactory());
1102+
ComposingTransitionFactory.ofUnchecked(transitionFactory, parent.getTransitionFactory());
11021103
}
11031104
// Check if the transition has any Starlark code.
11041105
StarlarkTransitionCheckingVisitor visitor = new StarlarkTransitionCheckingVisitor();
@@ -1250,6 +1251,13 @@ private static TransitionFactory<RuleTransitionData> convertConfig(@Nullable Obj
12501251
// defined in Starlark via, cfg = transition
12511252
return new StarlarkRuleTransitionProvider(starlarkDefinedConfigTransition);
12521253
}
1254+
if (cfg instanceof ComposedConfigurationTransition composition) {
1255+
return ComposedTransitionMaterializer.fold(
1256+
composition,
1257+
StarlarkRuleClassFunctions::convertConfig,
1258+
"it contains a native transition that can only be used as an attribute transition, such"
1259+
+ " as the exec transition");
1260+
}
12531261
if (cfg instanceof ConfigurationTransitionApi cta) {
12541262
// Every ConfigurationTransitionApi must be a TransitionFactory instance to be usable.
12551263
if (cta instanceof TransitionFactory<?> tf) {

0 commit comments

Comments
 (0)