Implement transition mutators.

These are more limited than bottom-up or top-down mutators but in
exchange have some pleasant properties:

- "variant not found" errors are impossible
- The logic is pleasantly split into multiple, mostly orthogonal
  parts
- Theoretically, if every mutator is refactored like this, they
  make it possible to partially cache the module graph
- Are quite close to a "configuration transition" in Bazel.

Bug: 231370928
Test: Presubmits.
Change-Id: Idcdb66b5ea75c0d2838f527aaa988df3b12553d8
This commit is contained in:
Lukacs T. Berki 2022-06-13 20:44:57 +02:00
parent 0ca2d0cb67
commit eb641de659
2 changed files with 257 additions and 26 deletions

View file

@ -272,6 +272,11 @@ type moduleInfo struct {
// set during each runMutator // set during each runMutator
splitModules modulesOrAliases splitModules modulesOrAliases
// Used by TransitionMutator implementations
transitionVariations []string
currentTransitionMutator string
requiredVariationsLock sync.Mutex
// set during PrepareBuildActions // set during PrepareBuildActions
actionDefs localBuildActions actionDefs localBuildActions
@ -619,6 +624,173 @@ func (c *Context) RegisterBottomUpMutator(name string, mutator BottomUpMutator)
return info return info
} }
type IncomingTransitionContext interface {
// Module returns the target of the dependency edge for which the transition
// is being computed
Module() Module
// Config returns the config object that was passed to
// Context.PrepareBuildActions.
Config() interface{}
}
type OutgoingTransitionContext interface {
// Module returns the target of the dependency edge for which the transition
// is being computed
Module() Module
// DepTag() Returns the dependency tag through which this dependency is
// reached
DepTag() DependencyTag
}
type TransitionMutator interface {
// Returns the set of variations that should be created for a module no matter
// who depends on it. Used when Make depends on a particular variation or when
// the module knows its variations just based on information given to it in
// the Blueprint file. This method should not mutate the module it is called
// on.
Split(ctx BaseModuleContext) []string
// Called on a module to determine which variation it wants from its direct
// dependencies. The dependency itself can override this decision. This method
// should not mutate the module itself.
OutgoingTransition(ctx OutgoingTransitionContext, sourceVariation string) string
// Called on a module to determine which variation it should be in based on
// the variation modules that depend on it want. This gives the module a final
// say about its own variations. This method should not mutate the module
// itself.
IncomingTransition(ctx IncomingTransitionContext, incomingVariation string) string
// Called after a module was split into multiple variations on each variation.
// It should not split the module any further but adding new dependencies is
// fine. Unlike all the other methods on TransitionMutator, this method is
// allowed to mutate the module.
Mutate(ctx BottomUpMutatorContext, variation string)
}
type transitionMutatorImpl struct {
name string
mutator TransitionMutator
}
// Adds each argument in items to l if it's not already there.
func addToStringListIfNotPresent(l []string, items ...string) []string {
OUTER:
for _, i := range items {
for _, existing := range l {
if existing == i {
continue OUTER
}
}
l = append(l, i)
}
return l
}
func (t *transitionMutatorImpl) addRequiredVariation(m *moduleInfo, variation string) {
m.requiredVariationsLock.Lock()
defer m.requiredVariationsLock.Unlock()
// This is only a consistency check. Leaking the variations of a transition
// mutator to another one could well lead to issues that are difficult to
// track down.
if m.currentTransitionMutator != "" && m.currentTransitionMutator != t.name {
panic(fmt.Errorf("transition mutator is %s in mutator %s", m.currentTransitionMutator, t.name))
}
m.currentTransitionMutator = t.name
m.transitionVariations = addToStringListIfNotPresent(m.transitionVariations, variation)
}
func (t *transitionMutatorImpl) topDownMutator(mctx TopDownMutatorContext) {
module := mctx.(*mutatorContext).module
mutatorSplits := t.mutator.Split(mctx)
if mutatorSplits == nil || len(mutatorSplits) == 0 {
panic(fmt.Errorf("transition mutator %s returned no splits for module %s", t.name, mctx.ModuleName()))
}
// transitionVariations for given a module can be mutated by the module itself
// and modules that directly depend on it. Since this is a top-down mutator,
// all modules that directly depend on this module have already been processed
// so no locking is necessary.
module.transitionVariations = addToStringListIfNotPresent(module.transitionVariations, mutatorSplits...)
sort.Strings(module.transitionVariations)
for _, srcVariation := range module.transitionVariations {
for _, dep := range module.directDeps {
finalVariation := t.transition(mctx)(mctx.Module(), srcVariation, dep.module.logicModule, dep.tag)
t.addRequiredVariation(dep.module, finalVariation)
}
}
}
type transitionContextImpl struct {
module Module
depTag DependencyTag
config interface{}
}
func (c *transitionContextImpl) Module() Module {
return c.module
}
func (c *transitionContextImpl) DepTag() DependencyTag {
return c.depTag
}
func (c *transitionContextImpl) Config() interface{} {
return c.config
}
func (t *transitionMutatorImpl) transition(mctx BaseMutatorContext) Transition {
return func(source Module, sourceVariation string, dep Module, depTag DependencyTag) string {
tc := &transitionContextImpl{module: dep, depTag: depTag, config: mctx.Config()}
outgoingVariation := t.mutator.OutgoingTransition(tc, sourceVariation)
finalVariation := t.mutator.IncomingTransition(tc, outgoingVariation)
return finalVariation
}
}
func (t *transitionMutatorImpl) bottomUpMutator(mctx BottomUpMutatorContext) {
mc := mctx.(*mutatorContext)
// Fetch and clean up transition mutator state. No locking needed since the
// only time interaction between multiple modules is required is during the
// computation of the variations required by a given module.
variations := mc.module.transitionVariations
mc.module.transitionVariations = nil
mc.module.currentTransitionMutator = ""
if len(variations) < 1 {
panic(fmt.Errorf("no variations found for module %s by mutator %s",
mctx.ModuleName(), t.name))
}
if len(variations) == 1 && variations[0] == "" {
// Module is not split, just apply the transition
mc.applyTransition(t.transition(mctx))
} else {
mc.createVariationsWithTransition(t.transition(mctx), variations...)
}
}
func (t *transitionMutatorImpl) mutateMutator(mctx BottomUpMutatorContext) {
module := mctx.(*mutatorContext).module
currentVariation := module.variant.variations[t.name]
t.mutator.Mutate(mctx, currentVariation)
}
func (c *Context) RegisterTransitionMutator(name string, mutator TransitionMutator) {
impl := &transitionMutatorImpl{name: name, mutator: mutator}
c.RegisterTopDownMutator(name+"_deps", impl.topDownMutator).Parallel()
c.RegisterBottomUpMutator(name, impl.bottomUpMutator).Parallel()
c.RegisterBottomUpMutator(name+"_mutate", impl.mutateMutator).Parallel()
}
type MutatorHandle interface { type MutatorHandle interface {
// Set the mutator to visit modules in parallel while maintaining ordering. Calling any // Set the mutator to visit modules in parallel while maintaining ordering. Calling any
// method on the mutator context is thread-safe, but the mutator must handle synchronization // method on the mutator context is thread-safe, but the mutator must handle synchronization
@ -1303,7 +1475,7 @@ func newVariant(module *moduleInfo, mutatorName string, variationName string,
} }
func (c *Context) createVariations(origModule *moduleInfo, mutatorName string, func (c *Context) createVariations(origModule *moduleInfo, mutatorName string,
defaultVariationName *string, variationNames []string, local bool) (modulesOrAliases, []error) { depChooser depChooser, variationNames []string, local bool) (modulesOrAliases, []error) {
if len(variationNames) == 0 { if len(variationNames) == 0 {
panic(fmt.Errorf("mutator %q passed zero-length variation list for module %q", panic(fmt.Errorf("mutator %q passed zero-length variation list for module %q",
@ -1339,7 +1511,7 @@ func (c *Context) createVariations(origModule *moduleInfo, mutatorName string,
newModules = append(newModules, newModule) newModules = append(newModules, newModule)
newErrs := c.convertDepsToVariation(newModule, mutatorName, variationName, defaultVariationName) newErrs := c.convertDepsToVariation(newModule, depChooser)
if len(newErrs) > 0 { if len(newErrs) > 0 {
errs = append(errs, newErrs...) errs = append(errs, newErrs...)
} }
@ -1355,31 +1527,79 @@ func (c *Context) createVariations(origModule *moduleInfo, mutatorName string,
return newModules, errs return newModules, errs
} }
func (c *Context) convertDepsToVariation(module *moduleInfo, type depChooser func(source *moduleInfo, dep depInfo) (*moduleInfo, string)
mutatorName, variationName string, defaultVariationName *string) (errs []error) {
// This function is called for every dependency edge to determine which
// variation of the dependency is needed. Its inputs are the depending module,
// its variation, the dependency and the dependency tag.
type Transition func(source Module, sourceVariation string, dep Module, depTag DependencyTag) string
func chooseDepByTransition(mutatorName string, transition Transition) depChooser {
return func(source *moduleInfo, dep depInfo) (*moduleInfo, string) {
sourceVariation := source.variant.variations[mutatorName]
depLogicModule := dep.module.logicModule
if depLogicModule == nil {
// This is really a lie because the original dependency before the split
// went away when it was split. We choose an arbitrary split module
// instead and hope that whatever information the transition wants from it
// is the same as in the original one
// TODO(lberki): this can be fixed by calling transition() once and saving
// its results somewhere
depLogicModule = dep.module.splitModules[0].moduleOrAliasTarget().logicModule
}
desiredVariation := transition(source.logicModule, sourceVariation, depLogicModule, dep.tag)
for _, m := range dep.module.splitModules {
if m.moduleOrAliasVariant().variations[mutatorName] == desiredVariation {
return m.moduleOrAliasTarget(), ""
}
}
return nil, desiredVariation
}
}
func chooseDep(candidates modulesOrAliases, mutatorName, variationName string, defaultVariationName *string) (*moduleInfo, string) {
for _, m := range candidates {
if m.moduleOrAliasVariant().variations[mutatorName] == variationName {
return m.moduleOrAliasTarget(), ""
}
}
if defaultVariationName != nil {
// give it a second chance; match with defaultVariationName
for _, m := range candidates {
if m.moduleOrAliasVariant().variations[mutatorName] == *defaultVariationName {
return m.moduleOrAliasTarget(), ""
}
}
}
return nil, variationName
}
func chooseDepExplicit(mutatorName string,
variationName string, defaultVariationName *string) depChooser {
return func(source *moduleInfo, dep depInfo) (*moduleInfo, string) {
return chooseDep(dep.module.splitModules, mutatorName, variationName, defaultVariationName)
}
}
func chooseDepInherit(mutatorName string, defaultVariationName *string) depChooser {
return func(source *moduleInfo, dep depInfo) (*moduleInfo, string) {
sourceVariation := source.variant.variations[mutatorName]
return chooseDep(dep.module.splitModules, mutatorName, sourceVariation, defaultVariationName)
}
}
func (c *Context) convertDepsToVariation(module *moduleInfo, depChooser depChooser) (errs []error) {
for i, dep := range module.directDeps { for i, dep := range module.directDeps {
if dep.module.logicModule == nil { if dep.module.logicModule == nil {
var newDep *moduleInfo newDep, missingVariation := depChooser(module, dep)
for _, m := range dep.module.splitModules {
if m.moduleOrAliasVariant().variations[mutatorName] == variationName {
newDep = m.moduleOrAliasTarget()
break
}
}
if newDep == nil && defaultVariationName != nil {
// give it a second chance; match with defaultVariationName
for _, m := range dep.module.splitModules {
if m.moduleOrAliasVariant().variations[mutatorName] == *defaultVariationName {
newDep = m.moduleOrAliasTarget()
break
}
}
}
if newDep == nil { if newDep == nil {
errs = append(errs, &BlueprintError{ errs = append(errs, &BlueprintError{
Err: fmt.Errorf("failed to find variation %q for module %q needed by %q", Err: fmt.Errorf("failed to find variation %q for module %q needed by %q",
variationName, dep.module.Name(), module.Name()), missingVariation, dep.module.Name(), module.Name()),
Pos: module.pos, Pos: module.pos,
}) })
continue continue

View file

@ -991,11 +991,17 @@ func (mctx *mutatorContext) MutatorName() string {
} }
func (mctx *mutatorContext) CreateVariations(variationNames ...string) []Module { func (mctx *mutatorContext) CreateVariations(variationNames ...string) []Module {
return mctx.createVariations(variationNames, false) depChooser := chooseDepInherit(mctx.name, mctx.defaultVariation)
return mctx.createVariations(variationNames, depChooser, false)
}
func (mctx *mutatorContext) createVariationsWithTransition(transition Transition, variationNames ...string) []Module {
return mctx.createVariations(variationNames, chooseDepByTransition(mctx.name, transition), false)
} }
func (mctx *mutatorContext) CreateLocalVariations(variationNames ...string) []Module { func (mctx *mutatorContext) CreateLocalVariations(variationNames ...string) []Module {
return mctx.createVariations(variationNames, true) depChooser := chooseDepInherit(mctx.name, mctx.defaultVariation)
return mctx.createVariations(variationNames, depChooser, true)
} }
func (mctx *mutatorContext) SetVariationProvider(module Module, provider ProviderKey, value interface{}) { func (mctx *mutatorContext) SetVariationProvider(module Module, provider ProviderKey, value interface{}) {
@ -1008,9 +1014,9 @@ func (mctx *mutatorContext) SetVariationProvider(module Module, provider Provide
panic(fmt.Errorf("module %q is not a newly created variant of %q", module, mctx.module)) panic(fmt.Errorf("module %q is not a newly created variant of %q", module, mctx.module))
} }
func (mctx *mutatorContext) createVariations(variationNames []string, local bool) []Module { func (mctx *mutatorContext) createVariations(variationNames []string, depChooser depChooser, local bool) []Module {
var ret []Module var ret []Module
modules, errs := mctx.context.createVariations(mctx.module, mctx.name, mctx.defaultVariation, variationNames, local) modules, errs := mctx.context.createVariations(mctx.module, mctx.name, depChooser, variationNames, local)
if len(errs) > 0 { if len(errs) > 0 {
mctx.errs = append(mctx.errs, errs...) mctx.errs = append(mctx.errs, errs...)
} }
@ -1091,8 +1097,13 @@ func (mctx *mutatorContext) CreateAliasVariation(aliasVariationName, targetVaria
panic(fmt.Errorf("no %q variation in module variations %q", targetVariationName, foundVariations)) panic(fmt.Errorf("no %q variation in module variations %q", targetVariationName, foundVariations))
} }
func (mctx *mutatorContext) applyTransition(transition Transition) {
mctx.context.convertDepsToVariation(mctx.module, chooseDepByTransition(mctx.name, transition))
}
func (mctx *mutatorContext) SetDependencyVariation(variationName string) { func (mctx *mutatorContext) SetDependencyVariation(variationName string) {
mctx.context.convertDepsToVariation(mctx.module, mctx.name, variationName, nil) mctx.context.convertDepsToVariation(mctx.module, chooseDepExplicit(
mctx.name, variationName, nil))
} }
func (mctx *mutatorContext) SetDefaultDependencyVariation(variationName *string) { func (mctx *mutatorContext) SetDefaultDependencyVariation(variationName *string) {