Configuration Transitions
Configuration transition is a mechanism for changing the configuration when depending on a target.
Currently, Buck2 has incoming and outgoing transitions:
- Incoming - (or per-rule transitions) declared on the rule.
- Outgoing - (or per-attribute transitions) declared on the attribute.
Transition rule
Transition rules are defined in .bzl
files using the transition
built-in.
The transition
function creates a configuration-related object. The
transition
object is opaque, it does not have any operations, and can only be
used as an argument to rule
function or attribute constructor. The
transition
function call must be assigned to a global variable (this is
similar to user-defined provider declarations).
The transition
function takes three arguments:
implementation
- a function.refs
- references to configuration rules to be resolved and passed to the implementation function.split
- (optional)bool
flag (defaultFalse
) to indicate whether transition is a split transition (used in per attribute transitions).
The implementation
function takes two arguments:
platform
- a configuration to transition.refs
- resolved references as a struct.
Example transition from ios to watchos (for example, to build a watchOS bundle as part of an iOS build):
def _impl(platform: PlatformInfo.type, refs: struct.type) -> PlatformInfo.type:
# Operating system constraint setting.
os = refs.os[ConstraintSettingInfo]
# Watchos constraint value.
watchos = refs.watchos[ConstraintValueInfo]
# Remove operating system constraint from input platform.
constraints = {
s: v
for (s, v) in platform.configuration.constraints.items()
if s != os.label
}
# Add watchos constraint value.
constraints[watchos.setting.label] = watchos
# Construct configuration structure.
new_cfg = ConfigurationInfo(
# Updated constraints.
constraints = constraints,
# Keep original config values.
values = platform.configuration.values,
)
# And return new configuration,
# or a dict of marker to configuration in case of split transition.
return PlatformInfo(
# ... supplying configuration label.
label = "<transitioned-to-watch>",
configuration = new_cfg,
)
iphone_to_watch_transition = transition(_impl, refs = {
"os": "//constraints:os",
"watchos": "//constraints:watchos",
})
A transition function applied twice must produce the configuration identical to the configuration produced after applying transition once.
assert tr(tr(platform=platform, refs=refs), refs=refs) == tr(platform=platform, refs=refs)
If this invariant is not held, certain operations produce incorrect and possibly infinite graphs. This is not yet enforced.
Per rule transition
The rule
function has an optional cfg
attribute, which takes a reference to
the transition
object (created with the transition
function; not a string).
When such a rule is called, it is instantiated, not with the requested configuration, but with the requested configuration transformed with a given rule transition.
For example, the transition for watchos when the iOS target depends on watchos resource:
watchos_resource = rule(
cfg = iphone_to_watch_transition,
...
)
Per attribute transition
The attrs
object has two attribute constructors:
attrs.transition_dep(cfg)
attrs.split_transition_dep(cfg)
These attributes are similar to the dep
attribute. When dependencies are
resolved for the rule instance, then they are resolved not with the rule
instance configuration, but with the configuration transformed with the given
transition.
For split transition, each dependency is resolved into a dict of marker to providers.
For example:
android_binary = rule(
...
attrs = {
"deps": attrs.list(attrs.split_transition_dep(cfg = cpu_split_transition), default = []),
},
)
When the above is invoked as follows:
android_binary(
deps = ["//foo:bar", "//qux:quux"],
)
Then the rule implementation gets something like the following in the deps
attribute:
{
[
{
# Key in this dict is the marker returned from split transition impl function.
"arm64": "providers for //foo:bar configured for arm64",
"armv7": "providers for //foo:bar configured for armv7",
},
{
"arm64": "providers for //qux:quux configured for arm64",
"armv7": "providers for //qux:quux configured for armv7",
},
]
}
It is an error to pass a split transition object to attrs.transition_dep
and a non-split transition to attrs.split_transition_dep
.
Per target transition
The Buck2 team is considering the implementation of per target transitions (that is, transitions referenced at a rule instantiation site as opposed to rule declaration site). No specific plans or APIs exists at the moment.
It could be something like the following:
cxx_binary(
name = "foo",
cfg = "//transitions:opengl-es-1.0",
...
)
Request transition on command line
For information, see RFC.
Access rule attributes in transition function implementation
It might be useful for the transition function to be able to query rule
attributes (for example, to perform transition to different configurations
depending on java_version
attribute).
Both incoming (per rule) and outgoing (per dependency) transitions can access rule attributes. For outgoing transitions, transition rule implementation accesses the attributes of the target that has dependencies with transitions, not attributes of dependency targets.
def _tr(platform, refs, attrs):
# NB: There are some restrictions on what attrs can be made accessible:
# - Only primitive values for now (providers are not resolved)
# - Only unconfigured attributes for now
attrs.my_list_attribute # == [12345, 67890]
tr = transition(
_tr,
refs = {},
attrs = {
"my_list_attribute": attr.list(...),
},
)
my_rule = rule(..., cfg=tr)
my_rule(
...,
my_list_attribute = [12345, 67890],
)