diff options
author | Colin Cross <ccross@android.com> | 2024-04-16 20:14:53 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2024-04-16 20:14:53 +0000 |
commit | 6ed94b7f852cfbbe96a918bcd2b3a9648dbf3f5e (patch) | |
tree | 8409d51a3ea86409e7e054198d5f7b3b66f53a31 | |
parent | 28357db9d0c33ce1c749371eeeff16e0d658a252 (diff) | |
parent | 95b3627f6f1902312f4a70a3222a659cad4d311f (diff) | |
download | blueprint-6ed94b7f852cfbbe96a918bcd2b3a9648dbf3f5e.tar.gz |
Merge "Move TransitionMutator to transition.go and add tests" into main
-rw-r--r-- | Android.bp | 2 | ||||
-rw-r--r-- | context.go | 283 | ||||
-rw-r--r-- | transition.go | 304 | ||||
-rw-r--r-- | transition_test.go | 198 |
4 files changed, 504 insertions, 283 deletions
@@ -53,6 +53,7 @@ bootstrap_go_package { "scope.go", "singleton_ctx.go", "source_file_provider.go", + "transition.go", ], testSrcs: [ "context_test.go", @@ -63,6 +64,7 @@ bootstrap_go_package { "ninja_writer_test.go", "provider_test.go", "splice_modules_test.go", + "transition_test.go", "visit_test.go", ], } @@ -681,284 +681,6 @@ func (c *Context) RegisterBottomUpMutator(name string, mutator BottomUpMutator) 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{} - - // Provider returns the value for a provider for the target of the dependency edge for which the - // transition is being computed. If the value is not set it returns nil and false. It panics if - // called before the appropriate mutator or GenerateBuildActions pass for the provider. The value - // returned may be a deep copy of the value originally passed to SetProvider. - // - // This method shouldn't be used directly, prefer the type-safe android.ModuleProvider instead. - Provider(provider AnyProviderKey) (any, bool) -} - -type OutgoingTransitionContext interface { - // Module returns the source 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 - - // Config returns the config object that was passed to - // Context.PrepareBuildActions. - Config() interface{} - - // Provider returns the value for a provider for the source of the dependency edge for which the - // transition is being computed. If the value is not set it returns nil and false. It panics if - // called before the appropriate mutator or GenerateBuildActions pass for the provider. The value - // returned may be a deep copy of the value originally passed to SetProvider. - // - // This method shouldn't be used directly, prefer the type-safe android.ModuleProvider instead. - Provider(provider AnyProviderKey) (any, bool) -} - -// TransitionMutator implements a top-down mechanism where a module tells its -// direct dependencies what variation they should be built in but the dependency -// has the final say. -// -// When implementing a transition mutator, one needs to implement four methods: -// - Split() that tells what variations a module has by itself -// - OutgoingTransition() where a module tells what it wants from its -// dependency -// - IncomingTransition() where a module has the final say about its own -// variation -// - Mutate() that changes the state of a module depending on its variation -// -// That the effective variation of module B when depended on by module A is the -// composition the outgoing transition of module A and the incoming transition -// of module B. -// -// The outgoing transition should not take the properties of the dependency into -// account, only those of the module that depends on it. For this reason, the -// dependency is not even passed into it as an argument. Likewise, the incoming -// transition should not take the properties of the depending module into -// account and is thus not informed about it. This makes for a nice -// decomposition of the decision logic. -// -// A given transition mutator only affects its own variation; other variations -// stay unchanged along the dependency edges. -// -// Soong makes sure that all modules are created in the desired variations and -// that dependency edges are set up correctly. This ensures that "missing -// variation" errors do not happen and allows for more flexible changes in the -// value of the variation among dependency edges (as opposed to bottom-up -// mutators where if module A in variation X depends on module B and module B -// has that variation X, A must depend on variation X of B) -// -// The limited power of the context objects passed to individual mutators -// methods also makes it more difficult to shoot oneself in the foot. Complete -// safety is not guaranteed because no one prevents individual transition -// mutators from mutating modules in illegal ways and for e.g. Split() or -// Mutate() to run their own visitations of the transitive dependency of the -// module and both of these are bad ideas, but it's better than no guardrails at -// all. -// -// This model is pretty close to Bazel's configuration transitions. The mapping -// between concepts in Soong and Bazel is as follows: -// - Module == configured target -// - Variant == configuration -// - Variation name == configuration flag -// - Variation == configuration flag value -// - Outgoing transition == attribute transition -// - Incoming transition == rule transition -// -// The Split() method does not have a Bazel equivalent and Bazel split -// transitions do not have a Soong equivalent. -// -// Mutate() does not make sense in Bazel due to the different models of the -// two systems: when creating new variations, Soong clones the old module and -// thus some way is needed to change it state whereas Bazel creates each -// configuration of a given configured target anew. -type TransitionMutator interface { - // Split 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 - - // OutgoingTransition is 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 - - // IncomingTransition is 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 - - // Mutate is 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 { - for _, i := range items { - if !slices.Contains(l, i) { - 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) - - outgoingTransitionCache := make([][]string, len(module.transitionVariations)) - for srcVariationIndex, srcVariation := range module.transitionVariations { - srcVariationTransitionCache := make([]string, len(module.directDeps)) - for depIndex, dep := range module.directDeps { - finalVariation := t.transition(mctx)(mctx.moduleInfo(), srcVariation, dep.module, dep.tag) - srcVariationTransitionCache[depIndex] = finalVariation - t.addRequiredVariation(dep.module, finalVariation) - } - outgoingTransitionCache[srcVariationIndex] = srcVariationTransitionCache - } - module.outgoingTransitionCache = outgoingTransitionCache -} - -type transitionContextImpl struct { - context *Context - source *moduleInfo - dep *moduleInfo - depTag DependencyTag - config interface{} -} - -func (c *transitionContextImpl) DepTag() DependencyTag { - return c.depTag -} - -func (c *transitionContextImpl) Config() interface{} { - return c.config -} - -type outgoingTransitionContextImpl struct { - transitionContextImpl -} - -func (c *outgoingTransitionContextImpl) Module() Module { - return c.source.logicModule -} - -func (c *outgoingTransitionContextImpl) Provider(provider AnyProviderKey) (any, bool) { - return c.context.provider(c.source, provider.provider()) -} - -type incomingTransitionContextImpl struct { - transitionContextImpl -} - -func (c *incomingTransitionContextImpl) Module() Module { - return c.dep.logicModule -} - -func (c *incomingTransitionContextImpl) Provider(provider AnyProviderKey) (any, bool) { - return c.context.provider(c.dep, provider.provider()) -} - -func (t *transitionMutatorImpl) transition(mctx BaseModuleContext) Transition { - return func(source *moduleInfo, sourceVariation string, dep *moduleInfo, depTag DependencyTag) string { - tc := transitionContextImpl{ - context: mctx.base().context, - source: source, - dep: dep, - depTag: depTag, - config: mctx.Config(), - } - outgoingVariation := t.mutator.OutgoingTransition(&outgoingTransitionContextImpl{tc}, sourceVariation) - if mctx.Failed() { - return outgoingVariation - } - finalVariation := t.mutator.IncomingTransition(&incomingTransitionContextImpl{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 - outgoingTransitionCache := mc.module.outgoingTransitionCache - mc.module.transitionVariations = nil - mc.module.outgoingTransitionCache = 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.context.convertDepsToVariation(mc.module, 0, - chooseDepByIndexes(mc.mutator.name, outgoingTransitionCache)) - } else { - mc.createVariationsWithTransition(variations, outgoingTransitionCache) - } -} - -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 { // 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 @@ -1807,11 +1529,6 @@ func (c *Context) createVariations(origModule *moduleInfo, mutator *mutatorInfo, type depChooser func(source *moduleInfo, variationIndex, depIndex int, dep depInfo) (*moduleInfo, string) -// 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 *moduleInfo, sourceVariation string, dep *moduleInfo, depTag DependencyTag) string - func chooseDep(candidates modulesOrAliases, mutatorName, variationName string, defaultVariationName *string) (*moduleInfo, string) { for _, m := range candidates { if m.moduleOrAliasVariant().variations[mutatorName] == variationName { diff --git a/transition.go b/transition.go new file mode 100644 index 0000000..31f785e --- /dev/null +++ b/transition.go @@ -0,0 +1,304 @@ +// Copyright 2024 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package blueprint + +import ( + "fmt" + "slices" + "sort" +) + +// TransitionMutator implements a top-down mechanism where a module tells its +// direct dependencies what variation they should be built in but the dependency +// has the final say. +// +// When implementing a transition mutator, one needs to implement four methods: +// - Split() that tells what variations a module has by itself +// - OutgoingTransition() where a module tells what it wants from its +// dependency +// - IncomingTransition() where a module has the final say about its own +// variation +// - Mutate() that changes the state of a module depending on its variation +// +// That the effective variation of module B when depended on by module A is the +// composition the outgoing transition of module A and the incoming transition +// of module B. +// +// The outgoing transition should not take the properties of the dependency into +// account, only those of the module that depends on it. For this reason, the +// dependency is not even passed into it as an argument. Likewise, the incoming +// transition should not take the properties of the depending module into +// account and is thus not informed about it. This makes for a nice +// decomposition of the decision logic. +// +// A given transition mutator only affects its own variation; other variations +// stay unchanged along the dependency edges. +// +// Soong makes sure that all modules are created in the desired variations and +// that dependency edges are set up correctly. This ensures that "missing +// variation" errors do not happen and allows for more flexible changes in the +// value of the variation among dependency edges (as opposed to bottom-up +// mutators where if module A in variation X depends on module B and module B +// has that variation X, A must depend on variation X of B) +// +// The limited power of the context objects passed to individual mutators +// methods also makes it more difficult to shoot oneself in the foot. Complete +// safety is not guaranteed because no one prevents individual transition +// mutators from mutating modules in illegal ways and for e.g. Split() or +// Mutate() to run their own visitations of the transitive dependency of the +// module and both of these are bad ideas, but it's better than no guardrails at +// all. +// +// This model is pretty close to Bazel's configuration transitions. The mapping +// between concepts in Soong and Bazel is as follows: +// - Module == configured target +// - Variant == configuration +// - Variation name == configuration flag +// - Variation == configuration flag value +// - Outgoing transition == attribute transition +// - Incoming transition == rule transition +// +// The Split() method does not have a Bazel equivalent and Bazel split +// transitions do not have a Soong equivalent. +// +// Mutate() does not make sense in Bazel due to the different models of the +// two systems: when creating new variations, Soong clones the old module and +// thus some way is needed to change it state whereas Bazel creates each +// configuration of a given configured target anew. +type TransitionMutator interface { + // Split 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 + + // OutgoingTransition is 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 + + // IncomingTransition is 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 + + // Mutate is 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 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{} + + // Provider returns the value for a provider for the target of the dependency edge for which the + // transition is being computed. If the value is not set it returns nil and false. It panics if + // called before the appropriate mutator or GenerateBuildActions pass for the provider. The value + // returned may be a deep copy of the value originally passed to SetProvider. + // + // This method shouldn't be used directly, prefer the type-safe android.ModuleProvider instead. + Provider(provider AnyProviderKey) (any, bool) +} + +type OutgoingTransitionContext interface { + // Module returns the source 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 + + // Config returns the config object that was passed to + // Context.PrepareBuildActions. + Config() interface{} + + // Provider returns the value for a provider for the source of the dependency edge for which the + // transition is being computed. If the value is not set it returns nil and false. It panics if + // called before the appropriate mutator or GenerateBuildActions pass for the provider. The value + // returned may be a deep copy of the value originally passed to SetProvider. + // + // This method shouldn't be used directly, prefer the type-safe android.ModuleProvider instead. + Provider(provider AnyProviderKey) (any, bool) +} + +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 { + for _, i := range items { + if !slices.Contains(l, i) { + 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) + + outgoingTransitionCache := make([][]string, len(module.transitionVariations)) + for srcVariationIndex, srcVariation := range module.transitionVariations { + srcVariationTransitionCache := make([]string, len(module.directDeps)) + for depIndex, dep := range module.directDeps { + finalVariation := t.transition(mctx)(mctx.moduleInfo(), srcVariation, dep.module, dep.tag) + srcVariationTransitionCache[depIndex] = finalVariation + t.addRequiredVariation(dep.module, finalVariation) + } + outgoingTransitionCache[srcVariationIndex] = srcVariationTransitionCache + } + module.outgoingTransitionCache = outgoingTransitionCache +} + +type transitionContextImpl struct { + context *Context + source *moduleInfo + dep *moduleInfo + depTag DependencyTag + config interface{} +} + +func (c *transitionContextImpl) DepTag() DependencyTag { + return c.depTag +} + +func (c *transitionContextImpl) Config() interface{} { + return c.config +} + +type outgoingTransitionContextImpl struct { + transitionContextImpl +} + +func (c *outgoingTransitionContextImpl) Module() Module { + return c.source.logicModule +} + +func (c *outgoingTransitionContextImpl) Provider(provider AnyProviderKey) (any, bool) { + return c.context.provider(c.source, provider.provider()) +} + +type incomingTransitionContextImpl struct { + transitionContextImpl +} + +func (c *incomingTransitionContextImpl) Module() Module { + return c.dep.logicModule +} + +func (c *incomingTransitionContextImpl) Provider(provider AnyProviderKey) (any, bool) { + return c.context.provider(c.dep, provider.provider()) +} + +func (t *transitionMutatorImpl) transition(mctx BaseModuleContext) Transition { + return func(source *moduleInfo, sourceVariation string, dep *moduleInfo, depTag DependencyTag) string { + tc := transitionContextImpl{ + context: mctx.base().context, + source: source, + dep: dep, + depTag: depTag, + config: mctx.Config(), + } + outgoingVariation := t.mutator.OutgoingTransition(&outgoingTransitionContextImpl{tc}, sourceVariation) + if mctx.Failed() { + return outgoingVariation + } + finalVariation := t.mutator.IncomingTransition(&incomingTransitionContextImpl{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 + outgoingTransitionCache := mc.module.outgoingTransitionCache + mc.module.transitionVariations = nil + mc.module.outgoingTransitionCache = 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.context.convertDepsToVariation(mc.module, 0, + chooseDepByIndexes(mc.mutator.name, outgoingTransitionCache)) + } else { + mc.createVariationsWithTransition(variations, outgoingTransitionCache) + } +} + +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() +} + +// 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 *moduleInfo, sourceVariation string, dep *moduleInfo, depTag DependencyTag) string diff --git a/transition_test.go b/transition_test.go new file mode 100644 index 0000000..128e7d5 --- /dev/null +++ b/transition_test.go @@ -0,0 +1,198 @@ +// Copyright 2024 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package blueprint + +import ( + "slices" + "testing" +) + +func TestTransition(t *testing.T) { + ctx := newContext() + ctx.MockFileSystem(map[string][]byte{ + "Android.bp": []byte(` + transition_module { + name: "A", + deps: ["B", "C"], + split: ["a", "b"], + } + + transition_module { + name: "B", + deps: ["C"], + outgoing: "c", + } + + transition_module { + name: "C", + deps: ["D"], + } + + transition_module { + name: "D", + incoming: "d", + } + `), + }) + + ctx.RegisterBottomUpMutator("deps", depsMutator) + ctx.RegisterTransitionMutator("transition", transitionTestMutator{}) + + ctx.RegisterModuleType("transition_module", newTransitionModule) + _, errs := ctx.ParseBlueprintsFiles("Android.bp", nil) + if len(errs) > 0 { + t.Errorf("unexpected parse errors:") + for _, err := range errs { + t.Errorf(" %s", err) + } + t.FailNow() + } + + _, errs = ctx.ResolveDependencies(nil) + if len(errs) > 0 { + t.Errorf("unexpected dep errors:") + for _, err := range errs { + t.Errorf(" %s", err) + } + t.FailNow() + } + + getModule := func(name, variant string) *transitionModule { + group := ctx.moduleGroupFromName(name, nil) + module := group.moduleOrAliasByVariantName(variant).module() + return module.logicModule.(*transitionModule) + } + + checkVariants := func(name string, expectedVariants []string) { + t.Helper() + group := ctx.moduleGroupFromName(name, nil) + var gotVariants []string + for _, variant := range group.modules { + gotVariants = append(gotVariants, variant.moduleOrAliasVariant().variations["transition"]) + } + if !slices.Equal(expectedVariants, gotVariants) { + t.Errorf("expected variants of %q to be %q, got %q", name, expectedVariants, gotVariants) + } + } + + // Module A uses Split to create a and b variants + checkVariants("A", []string{"a", "b"}) + // Module B inherits a and b variants from A + checkVariants("B", []string{"", "a", "b"}) + // Module C inherits a and b variants from A, but gets an outgoing c variant from B + checkVariants("C", []string{"", "a", "b", "c"}) + // Module D always has incoming variant d + checkVariants("D", []string{"", "d"}) + + A_a := getModule("A", "a") + A_b := getModule("A", "b") + B_a := getModule("B", "a") + B_b := getModule("B", "b") + C_a := getModule("C", "a") + C_b := getModule("C", "b") + C_c := getModule("C", "c") + D_d := getModule("D", "d") + + checkDeps := func(m Module, expected ...string) { + var got []string + ctx.VisitDirectDeps(m, func(m Module) { + got = append(got, ctx.ModuleName(m)+"("+ctx.ModuleSubDir(m)+")") + }) + if !slices.Equal(got, expected) { + t.Errorf("unexpected %q dependencies, got %q expected %q", + ctx.ModuleName(m), got, expected) + } + } + + checkDeps(A_a, "B(a)", "C(a)") + checkDeps(A_b, "B(b)", "C(b)") + checkDeps(B_a, "C(c)") + checkDeps(B_b, "C(c)") + checkDeps(C_a, "D(d)") + checkDeps(C_b, "D(d)") + checkDeps(C_c, "D(d)") + checkDeps(D_d) + + checkMutate := func(m *transitionModule, variant string) { + t.Helper() + if m.properties.Mutated != variant { + t.Errorf("unexpected mutated property in %q, expected %q got %q", m.Name(), variant, m.properties.Mutated) + } + } + + checkMutate(A_a, "a") + checkMutate(A_b, "b") + checkMutate(B_a, "a") + checkMutate(B_b, "b") + checkMutate(C_a, "a") + checkMutate(C_b, "b") + checkMutate(C_c, "c") + checkMutate(D_d, "d") +} + +type transitionTestMutator struct{} + +func (transitionTestMutator) Split(ctx BaseModuleContext) []string { + if split := ctx.Module().(*transitionModule).properties.Split; len(split) > 0 { + return split + } + return []string{""} +} + +func (transitionTestMutator) OutgoingTransition(ctx OutgoingTransitionContext, sourceVariation string) string { + if outgoing := ctx.Module().(*transitionModule).properties.Outgoing; outgoing != nil { + return *outgoing + } + return sourceVariation +} + +func (transitionTestMutator) IncomingTransition(ctx IncomingTransitionContext, incomingVariation string) string { + if incoming := ctx.Module().(*transitionModule).properties.Incoming; incoming != nil { + return *incoming + } + return incomingVariation +} + +func (transitionTestMutator) Mutate(ctx BottomUpMutatorContext, variation string) { + ctx.Module().(*transitionModule).properties.Mutated = variation +} + +type transitionModule struct { + SimpleName + properties struct { + Deps []string + Split []string + Outgoing *string + Incoming *string + + Mutated string `blueprint:"mutated"` + } +} + +func newTransitionModule() (Module, []interface{}) { + m := &transitionModule{} + return m, []interface{}{&m.properties, &m.SimpleName.Properties} +} + +func (f *transitionModule) GenerateBuildActions(ModuleContext) { +} + +func (f *transitionModule) Deps() []string { + return f.properties.Deps +} + +func (f *transitionModule) IgnoreDeps() []string { + return nil +} |