Skip to content

Commit 8fb5c99

Browse files
authored
[8.8.0] Implement transition.and_then (bazelbuild#30330)
### 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 (cherry picked from commit d089ae2) Closes bazelbuild#29620
1 parent 9b90c28 commit 8fb5c99

16 files changed

Lines changed: 688 additions & 48 deletions

File tree

site/en/extending/config.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,34 @@ hot_chocolate_transition = transition(
649649
)
650650
```
651651

652+
### Composing transitions {:#composing-transitions}
653+
654+
Two transitions can be combined into a single new transition using
655+
[`transition.and_then`](/rules/lib/builtins/transition#and_then):
656+
657+
```python
658+
combined_transition = first_transition.and_then(second_transition)
659+
```
660+
661+
The composition applies `first_transition` to the input configuration and then
662+
runs `second_transition` against each of its output configurations. The original
663+
transitions are not modified, and the result is itself a `transition` object
664+
that can be composed further with another `and_then` call.
665+
666+
A composed transition can be attached to a rule or attribute wherever its
667+
component transitions could be used (subject to the usual restriction that an
668+
[incoming edge transition](#incoming-edge-transitions) must be 1:1). At most one
669+
of the composed transitions may be an exec transition (`"exec"` or
670+
[`config.exec`](/rules/lib/toplevel/config#exec)).
671+
672+
When two of the composed transitions are 1:2+, the composition produces the
673+
cross product of their splits. The key for each combined split is the
674+
comma-separated concatenation of the component keys: if the first transition
675+
produces keys `{a, b}` and the second produces `{x, y}` for each of those, the
676+
composition produces the four keys `a,x`, `a,y`, `b,x`, and `b,y`. Note that
677+
each additional 1:2+ transition multiplies the resulting dependency count, so
678+
chain them with care.
679+
652680
### Accessing attributes with transitions {:#accessing-attributes-with-transitions}
653681

654682
[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
@@ -228,6 +228,7 @@ java_library(
228228
"extra/ExtraAction.java",
229229
"extra/ExtraActionMapProvider.java",
230230
"extra/ExtraActionSpec.java",
231+
"starlark/ComposedTransitionMaterializer.java",
231232
"starlark/StarlarkActionFactory.java",
232233
"starlark/StarlarkAttrModule.java",
233234
"starlark/StarlarkAttributeTransitionProvider.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
@@ -421,7 +421,7 @@ public Builder addTrimmingTransitionFactory(TransitionFactory<RuleTransitionData
421421
trimmingTransitionFactory = factory;
422422
} else {
423423
trimmingTransitionFactory =
424-
ComposingTransitionFactory.of(trimmingTransitionFactory, factory);
424+
ComposingTransitionFactory.ofUnchecked(trimmingTransitionFactory, factory);
425425
}
426426
return this;
427427
}

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
@@ -192,17 +192,19 @@ public static ExecutionPlatformResult getExecutionPlatformLabel(
192192
return ExecutionPlatformResult.ofNullLabel();
193193
}
194194

195-
TransitionFactory<AttributeTransitionData> transitionFactory =
196-
kind.getAttribute().getTransitionFactory();
197-
if (!(transitionFactory instanceof ExecutionTransitionFactory)) {
195+
// The exec transition may be composed with other transitions (via transition.and_then), so
196+
// look for it anywhere in the (possibly composed) transition factory.
197+
ExecutionTransitionFactory execTransitionFactory =
198+
findExecutionTransitionFactory(kind.getAttribute().getTransitionFactory());
199+
if (execTransitionFactory == null) {
198200
return ExecutionPlatformResult.ofLabel(
199201
toolchainContexts
200202
.getToolchainContext(ExecGroup.DEFAULT_EXEC_GROUP_NAME)
201203
.executionPlatform()
202204
.label());
203205
}
204206

205-
String execGroup = ((ExecutionTransitionFactory) transitionFactory).getExecGroup();
207+
String execGroup = execTransitionFactory.getExecGroup();
206208
if (toolchainContexts.hasToolchainContext(execGroup)) {
207209
PlatformInfo platform = toolchainContexts.getToolchainContext(execGroup).executionPlatform();
208210
return platform == null
@@ -226,6 +228,24 @@ public static ExecutionPlatformResult getExecutionPlatformLabel(
226228
kind.getAttribute().getName(), execGroup));
227229
}
228230

231+
/**
232+
* Returns the {@link ExecutionTransitionFactory} within the given transition factory, or {@code
233+
* null} if there is none. A composition contains at most one native transition, so there is at
234+
* most one match.
235+
*/
236+
@Nullable
237+
private static ExecutionTransitionFactory findExecutionTransitionFactory(
238+
TransitionFactory<AttributeTransitionData> transitionFactory) {
239+
var found = new ExecutionTransitionFactory[1];
240+
transitionFactory.visit(
241+
factory -> {
242+
if (factory instanceof ExecutionTransitionFactory execFactory) {
243+
found[0] = execFactory;
244+
}
245+
});
246+
return found[0];
247+
}
248+
229249
/** True if {@code owningAspect} is the main aspect, the last one in {@code aspectsList}. */
230250
private static boolean isMainAspect(
231251
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/TargetAndConfigurationProducer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ public StateMachine computeTransition(Tasks tasks) {
370370
target.getAssociatedRule().getRuleClassObject().getTransitionFactory();
371371
if (trimmingTransitionFactory != null) {
372372
transitionFactory =
373-
ComposingTransitionFactory.of(transitionFactory, trimmingTransitionFactory);
373+
ComposingTransitionFactory.ofUnchecked(transitionFactory, trimmingTransitionFactory);
374374
}
375375

376376
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;
@@ -525,6 +526,12 @@ private static TransitionFactory<AttributeTransitionData> convertCfg(
525526
if (trans instanceof StarlarkDefinedConfigTransition starlarkDefinedTransition) {
526527
return new StarlarkAttributeTransitionProvider(starlarkDefinedTransition);
527528
}
529+
if (trans instanceof ComposedConfigurationTransition composition) {
530+
return ComposedTransitionMaterializer.fold(
531+
composition,
532+
element -> convertCfg(thread, element),
533+
"it contains a native transition that can only be used as a rule transition");
534+
}
528535
if (trans instanceof ConfigurationTransitionApi cta) {
529536
// Every ConfigurationTransitionApi must be a TransitionFactory instance to be usable.
530537
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
@@ -113,6 +113,7 @@
113113
import com.google.devtools.build.lib.starlarkbuildapi.MacroFunctionApi;
114114
import com.google.devtools.build.lib.starlarkbuildapi.StarlarkRuleFunctionsApi;
115115
import com.google.devtools.build.lib.starlarkbuildapi.StarlarkSubruleApi;
116+
import com.google.devtools.build.lib.starlarkbuildapi.config.ComposedConfigurationTransition;
116117
import com.google.devtools.build.lib.starlarkbuildapi.config.ConfigurationTransitionApi;
117118
import com.google.devtools.build.lib.util.FileTypeSet;
118119
import com.google.devtools.build.lib.util.Pair;
@@ -942,7 +943,7 @@ public static StarlarkRuleFunction createRule(
942943
});
943944
if (parent != null) {
944945
transitionFactory =
945-
ComposingTransitionFactory.of(transitionFactory, parent.getTransitionFactory());
946+
ComposingTransitionFactory.ofUnchecked(transitionFactory, parent.getTransitionFactory());
946947
}
947948
// Check if the transition has any Starlark code.
948949
StarlarkTransitionCheckingVisitor visitor = new StarlarkTransitionCheckingVisitor();
@@ -1103,6 +1104,13 @@ private static TransitionFactory<RuleTransitionData> convertConfig(@Nullable Obj
11031104
// defined in Starlark via, cfg = transition
11041105
return new StarlarkRuleTransitionProvider(starlarkDefinedConfigTransition);
11051106
}
1107+
if (cfg instanceof ComposedConfigurationTransition composition) {
1108+
return ComposedTransitionMaterializer.fold(
1109+
composition,
1110+
StarlarkRuleClassFunctions::convertConfig,
1111+
"it contains a native transition that can only be used as an attribute transition, such"
1112+
+ " as the exec transition");
1113+
}
11061114
if (cfg instanceof ConfigurationTransitionApi cta) {
11071115
// Every ConfigurationTransitionApi must be a TransitionFactory instance to be usable.
11081116
if (cta instanceof TransitionFactory<?> tf) {

src/main/java/com/google/devtools/build/lib/query2/cquery/CqueryTransitionResolver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ private ConfigurationTransition getRuleTransition(CqueryNode configuredTarget) {
263263
boolean isAlias = rule.getAssociatedRule().getName().equals("alias");
264264
if (trimmingTransitionFactory != null && !isAlias) {
265265
transitionFactory =
266-
ComposingTransitionFactory.of(transitionFactory, trimmingTransitionFactory);
266+
ComposingTransitionFactory.ofUnchecked(transitionFactory, trimmingTransitionFactory);
267267
}
268268

269269
var transitionData = RuleTransitionData.create(rule, /* configConditions= */ null, "");

0 commit comments

Comments
 (0)