Simplify vendor conditionals
Support vendor conditionals with no Go code.
Test: TestSoongConfigModule
Change-Id: I42546e7f17324921ada80f4d8e1cd399830f8dfc
diff --git a/android/config.go b/android/config.go
index 5c4f0a8..769119a 100644
--- a/android/config.go
+++ b/android/config.go
@@ -29,6 +29,8 @@
"github.com/google/blueprint/bootstrap"
"github.com/google/blueprint/pathtools"
"github.com/google/blueprint/proptools"
+
+ "android/soong/android/soongconfig"
)
var Bool = proptools.Bool
@@ -66,19 +68,7 @@
*deviceConfig
}
-type VendorConfig interface {
- // Bool interprets the variable named `name` as a boolean, returning true if, after
- // lowercasing, it matches one of "1", "y", "yes", "on", or "true". Unset, or any other
- // value will return false.
- Bool(name string) bool
-
- // String returns the string value of `name`. If the variable was not set, it will
- // return the empty string.
- String(name string) string
-
- // IsSet returns whether the variable `name` was set by Make.
- IsSet(name string) bool
-}
+type VendorConfig soongconfig.SoongConfig
type config struct {
FileConfigurableOptions
@@ -128,8 +118,6 @@
OncePer
}
-type vendorConfig map[string]string
-
type jsonConfigurable interface {
SetDefaultConfig()
}
@@ -1137,21 +1125,7 @@
}
func (c *config) VendorConfig(name string) VendorConfig {
- return vendorConfig(c.productVariables.VendorVars[name])
-}
-
-func (c vendorConfig) Bool(name string) bool {
- v := strings.ToLower(c[name])
- return v == "1" || v == "y" || v == "yes" || v == "on" || v == "true"
-}
-
-func (c vendorConfig) String(name string) string {
- return c[name]
-}
-
-func (c vendorConfig) IsSet(name string) bool {
- _, ok := c[name]
- return ok
+ return soongconfig.Config(c.productVariables.VendorVars[name])
}
func (c *config) NdkAbis() bool {
diff --git a/android/hooks.go b/android/hooks.go
index 04ba69e..e8cd81b 100644
--- a/android/hooks.go
+++ b/android/hooks.go
@@ -34,6 +34,9 @@
AppendProperties(...interface{})
PrependProperties(...interface{})
CreateModule(ModuleFactory, ...interface{}) Module
+
+ registerScopedModuleType(name string, factory blueprint.ModuleFactory)
+ moduleFactories() map[string]blueprint.ModuleFactory
}
func AddLoadHook(m blueprint.Module, hook func(LoadHookContext)) {
@@ -52,6 +55,10 @@
module Module
}
+func (l *loadHookContext) moduleFactories() map[string]blueprint.ModuleFactory {
+ return l.bp.ModuleFactories()
+}
+
func (l *loadHookContext) AppendProperties(props ...interface{}) {
for _, p := range props {
err := proptools.AppendMatchingProperties(l.Module().base().customizableProperties,
@@ -101,6 +108,10 @@
return module
}
+func (l *loadHookContext) registerScopedModuleType(name string, factory blueprint.ModuleFactory) {
+ l.bp.RegisterScopedModuleType(name, factory)
+}
+
type InstallHookContext interface {
ModuleContext
Path() InstallPath
diff --git a/android/module.go b/android/module.go
index 05115d6..0ac2135 100644
--- a/android/module.go
+++ b/android/module.go
@@ -62,6 +62,7 @@
ModuleName() string
ModuleDir() string
ModuleType() string
+ BlueprintsFile() string
ContainsProperty(name string) bool
Errorf(pos scanner.Position, fmt string, args ...interface{})
@@ -519,9 +520,13 @@
}
}
+func initAndroidModuleBase(m Module) {
+ m.base().module = m
+}
+
func InitAndroidModule(m Module) {
+ initAndroidModuleBase(m)
base := m.base()
- base.module = m
m.AddProperties(
&base.nameProperties,
diff --git a/android/namespace.go b/android/namespace.go
index 64ad7e9..9d7e8ac 100644
--- a/android/namespace.go
+++ b/android/namespace.go
@@ -162,6 +162,12 @@
return namespace
}
+// A NamelessModule can never be looked up by name. It must still implement Name(), but the return
+// value doesn't have to be unique.
+type NamelessModule interface {
+ Nameless()
+}
+
func (r *NameResolver) NewModule(ctx blueprint.NamespaceContext, moduleGroup blueprint.ModuleGroup, module blueprint.Module) (namespace blueprint.Namespace, errs []error) {
// if this module is a namespace, then save it to our list of namespaces
newNamespace, ok := module.(*NamespaceModule)
@@ -173,6 +179,10 @@
return nil, nil
}
+ if _, ok := module.(NamelessModule); ok {
+ return nil, nil
+ }
+
// if this module is not a namespace, then save it into the appropriate namespace
ns := r.findNamespaceFromCtx(ctx)
diff --git a/android/soong_config_modules.go b/android/soong_config_modules.go
new file mode 100644
index 0000000..bdec64b
--- /dev/null
+++ b/android/soong_config_modules.go
@@ -0,0 +1,369 @@
+// Copyright 2019 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 android
+
+// This file provides module types that implement wrapper module types that add conditionals on
+// Soong config variables.
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+ "text/scanner"
+
+ "github.com/google/blueprint"
+ "github.com/google/blueprint/parser"
+ "github.com/google/blueprint/proptools"
+
+ "android/soong/android/soongconfig"
+)
+
+func init() {
+ RegisterModuleType("soong_config_module_type_import", soongConfigModuleTypeImportFactory)
+ RegisterModuleType("soong_config_module_type", soongConfigModuleTypeFactory)
+ RegisterModuleType("soong_config_string_variable", soongConfigStringVariableDummyFactory)
+ RegisterModuleType("soong_config_bool_variable", soongConfigBoolVariableDummyFactory)
+}
+
+type soongConfigModuleTypeImport struct {
+ ModuleBase
+ properties soongConfigModuleTypeImportProperties
+}
+
+type soongConfigModuleTypeImportProperties struct {
+ From string
+ Module_types []string
+}
+
+// soong_config_module_type_import imports module types with conditionals on Soong config
+// variables from another Android.bp file. The imported module type will exist for all
+// modules after the import in the Android.bp file.
+//
+// For example, an Android.bp file could have:
+//
+// soong_config_module_type_import {
+// from: "device/acme/Android.bp.bp",
+// module_types: ["acme_cc_defaults"],
+// }
+//
+// acme_cc_defaults {
+// name: "acme_defaults",
+// cflags: ["-DGENERIC"],
+// soong_config_variables: {
+// board: {
+// soc_a: {
+// cflags: ["-DSOC_A"],
+// },
+// soc_b: {
+// cflags: ["-DSOC_B"],
+// },
+// },
+// feature: {
+// cflags: ["-DFEATURE"],
+// },
+// },
+// }
+//
+// cc_library {
+// name: "libacme_foo",
+// defaults: ["acme_defaults"],
+// srcs: ["*.cpp"],
+// }
+//
+// And device/acme/Android.bp could have:
+//
+// soong_config_module_type {
+// name: "acme_cc_defaults",
+// module_type: "cc_defaults",
+// config_namespace: "acme",
+// variables: ["board", "feature"],
+// properties: ["cflags", "srcs"],
+// }
+//
+// soong_config_string_variable {
+// name: "board",
+// values: ["soc_a", "soc_b"],
+// }
+//
+// soong_config_bool_variable {
+// name: "feature",
+// }
+//
+// If an acme BoardConfig.mk file contained:
+//
+// SOONG_CONFIG_NAMESPACES += acme
+// SOONG_CONFIG_acme += \
+// board \
+// feature \
+//
+// SOONG_CONFIG_acme_board := soc_a
+// SOONG_CONFIG_acme_feature := true
+//
+// Then libacme_foo would build with cflags "-DGENERIC -DSOC_A -DFEATURE".
+func soongConfigModuleTypeImportFactory() Module {
+ module := &soongConfigModuleTypeImport{}
+
+ module.AddProperties(&module.properties)
+ AddLoadHook(module, func(ctx LoadHookContext) {
+ importModuleTypes(ctx, module.properties.From, module.properties.Module_types...)
+ })
+
+ initAndroidModuleBase(module)
+ return module
+}
+
+func (m *soongConfigModuleTypeImport) Name() string {
+ return "soong_config_module_type_import_" + soongconfig.CanonicalizeToProperty(m.properties.From)
+}
+
+func (*soongConfigModuleTypeImport) Nameless() {}
+func (*soongConfigModuleTypeImport) GenerateAndroidBuildActions(ModuleContext) {}
+
+// Create dummy modules for soong_config_module_type and soong_config_*_variable
+
+type soongConfigModuleTypeModule struct {
+ ModuleBase
+ properties soongconfig.ModuleTypeProperties
+}
+
+// soong_config_module_type defines module types with conditionals on Soong config
+// variables from another Android.bp file. The new module type will exist for all
+// modules after the definition in an Android.bp file, and can be imported into other
+// Android.bp files using soong_config_module_type_import.
+//
+// For example, an Android.bp file could have:
+//
+// soong_config_module_type {
+// name: "acme_cc_defaults",
+// module_type: "cc_defaults",
+// config_namespace: "acme",
+// variables: ["board", "feature"],
+// properties: ["cflags", "srcs"],
+// }
+//
+// soong_config_string_variable {
+// name: "board",
+// values: ["soc_a", "soc_b"],
+// }
+//
+// soong_config_bool_variable {
+// name: "feature",
+// }
+//
+// acme_cc_defaults {
+// name: "acme_defaults",
+// cflags: ["-DGENERIC"],
+// soong_config_variables: {
+// board: {
+// soc_a: {
+// cflags: ["-DSOC_A"],
+// },
+// soc_b: {
+// cflags: ["-DSOC_B"],
+// },
+// },
+// feature: {
+// cflags: ["-DFEATURE"],
+// },
+// },
+// }
+//
+// cc_library {
+// name: "libacme_foo",
+// defaults: ["acme_defaults"],
+// srcs: ["*.cpp"],
+// }
+//
+// And device/acme/Android.bp could have:
+//
+// If an acme BoardConfig.mk file contained:
+//
+// SOONG_CONFIG_NAMESPACES += acme
+// SOONG_CONFIG_acme += \
+// board \
+// feature \
+//
+// SOONG_CONFIG_acme_board := soc_a
+// SOONG_CONFIG_acme_feature := true
+//
+// Then libacme_foo would build with cflags "-DGENERIC -DSOC_A -DFEATURE".
+func soongConfigModuleTypeFactory() Module {
+ module := &soongConfigModuleTypeModule{}
+
+ module.AddProperties(&module.properties)
+
+ AddLoadHook(module, func(ctx LoadHookContext) {
+ // A soong_config_module_type module should implicitly import itself.
+ importModuleTypes(ctx, ctx.BlueprintsFile(), module.properties.Name)
+ })
+
+ initAndroidModuleBase(module)
+
+ return module
+}
+
+func (m *soongConfigModuleTypeModule) Name() string {
+ return m.properties.Name
+}
+func (*soongConfigModuleTypeModule) Nameless() {}
+func (*soongConfigModuleTypeModule) GenerateAndroidBuildActions(ctx ModuleContext) {}
+
+type soongConfigStringVariableDummyModule struct {
+ ModuleBase
+ properties soongconfig.VariableProperties
+ stringProperties soongconfig.StringVariableProperties
+}
+
+type soongConfigBoolVariableDummyModule struct {
+ ModuleBase
+ properties soongconfig.VariableProperties
+}
+
+// soong_config_string_variable defines a variable and a set of possible string values for use
+// in a soong_config_module_type definition.
+func soongConfigStringVariableDummyFactory() Module {
+ module := &soongConfigStringVariableDummyModule{}
+ module.AddProperties(&module.properties, &module.stringProperties)
+ initAndroidModuleBase(module)
+ return module
+}
+
+// soong_config_string_variable defines a variable with true or false values for use
+// in a soong_config_module_type definition.
+func soongConfigBoolVariableDummyFactory() Module {
+ module := &soongConfigBoolVariableDummyModule{}
+ module.AddProperties(&module.properties)
+ initAndroidModuleBase(module)
+ return module
+}
+
+func (m *soongConfigStringVariableDummyModule) Name() string {
+ return m.properties.Name
+}
+func (*soongConfigStringVariableDummyModule) Nameless() {}
+func (*soongConfigStringVariableDummyModule) GenerateAndroidBuildActions(ctx ModuleContext) {}
+
+func (m *soongConfigBoolVariableDummyModule) Name() string {
+ return m.properties.Name
+}
+func (*soongConfigBoolVariableDummyModule) Nameless() {}
+func (*soongConfigBoolVariableDummyModule) GenerateAndroidBuildActions(ctx ModuleContext) {}
+
+func importModuleTypes(ctx LoadHookContext, from string, moduleTypes ...string) {
+ from = filepath.Clean(from)
+ if filepath.Ext(from) != ".bp" {
+ ctx.PropertyErrorf("from", "%q must be a file with extension .bp", from)
+ return
+ }
+
+ if strings.HasPrefix(from, "../") {
+ ctx.PropertyErrorf("from", "%q must not use ../ to escape the source tree",
+ from)
+ return
+ }
+
+ moduleTypeDefinitions := loadSoongConfigModuleTypeDefinition(ctx, from)
+ if moduleTypeDefinitions == nil {
+ return
+ }
+ for _, moduleType := range moduleTypes {
+ if factory, ok := moduleTypeDefinitions[moduleType]; ok {
+ ctx.registerScopedModuleType(moduleType, factory)
+ } else {
+ ctx.PropertyErrorf("module_types", "module type %q not defined in %q",
+ moduleType, from)
+ }
+ }
+}
+
+// loadSoongConfigModuleTypeDefinition loads module types from an Android.bp file. It caches the
+// result so each file is only parsed once.
+func loadSoongConfigModuleTypeDefinition(ctx LoadHookContext, from string) map[string]blueprint.ModuleFactory {
+ type onceKeyType string
+ key := NewCustomOnceKey(onceKeyType(filepath.Clean(from)))
+
+ reportErrors := func(ctx LoadHookContext, filename string, errs ...error) {
+ for _, err := range errs {
+ if parseErr, ok := err.(*parser.ParseError); ok {
+ ctx.Errorf(parseErr.Pos, "%s", parseErr.Err)
+ } else {
+ ctx.Errorf(scanner.Position{Filename: filename}, "%s", err)
+ }
+ }
+ }
+
+ return ctx.Config().Once(key, func() interface{} {
+ r, err := ctx.Config().fs.Open(from)
+ if err != nil {
+ ctx.PropertyErrorf("from", "failed to open %q: %s", from, err)
+ return (map[string]blueprint.ModuleFactory)(nil)
+ }
+
+ mtDef, errs := soongconfig.Parse(r, from)
+
+ if len(errs) > 0 {
+ reportErrors(ctx, from, errs...)
+ return (map[string]blueprint.ModuleFactory)(nil)
+ }
+
+ globalModuleTypes := ctx.moduleFactories()
+
+ factories := make(map[string]blueprint.ModuleFactory)
+
+ for name, moduleType := range mtDef.ModuleTypes {
+ factory := globalModuleTypes[moduleType.BaseModuleType]
+ if factory != nil {
+ factories[name] = soongConfigModuleFactory(factory, moduleType)
+ } else {
+ reportErrors(ctx, from,
+ fmt.Errorf("missing global module type factory for %q", moduleType.BaseModuleType))
+ }
+ }
+
+ if ctx.Failed() {
+ return (map[string]blueprint.ModuleFactory)(nil)
+ }
+
+ return factories
+ }).(map[string]blueprint.ModuleFactory)
+}
+
+// soongConfigModuleFactory takes an existing soongConfigModuleFactory and a ModuleType and returns
+// a new soongConfigModuleFactory that wraps the existing soongConfigModuleFactory and adds conditional on Soong config
+// variables.
+func soongConfigModuleFactory(factory blueprint.ModuleFactory,
+ moduleType *soongconfig.ModuleType) blueprint.ModuleFactory {
+
+ conditionalFactoryProps := soongconfig.CreateProperties(factory, moduleType)
+ if conditionalFactoryProps.IsValid() {
+ return func() (blueprint.Module, []interface{}) {
+ module, props := factory()
+
+ conditionalProps := proptools.CloneEmptyProperties(conditionalFactoryProps.Elem())
+ props = append(props, conditionalProps.Interface())
+
+ AddLoadHook(module, func(ctx LoadHookContext) {
+ config := ctx.Config().VendorConfig(moduleType.ConfigNamespace)
+ for _, ps := range soongconfig.PropertiesToApply(moduleType, conditionalProps, config) {
+ ctx.AppendProperties(ps)
+ }
+ })
+
+ return module, props
+ }
+ } else {
+ return factory
+ }
+}
diff --git a/android/soong_config_modules_test.go b/android/soong_config_modules_test.go
new file mode 100644
index 0000000..66feba8
--- /dev/null
+++ b/android/soong_config_modules_test.go
@@ -0,0 +1,141 @@
+// Copyright 2019 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 android
+
+import (
+ "reflect"
+ "testing"
+)
+
+type soongConfigTestModule struct {
+ ModuleBase
+ props soongConfigTestModuleProperties
+}
+
+type soongConfigTestModuleProperties struct {
+ Cflags []string
+}
+
+func soongConfigTestModuleFactory() Module {
+ m := &soongConfigTestModule{}
+ m.AddProperties(&m.props)
+ InitAndroidModule(m)
+ return m
+}
+
+func (t soongConfigTestModule) GenerateAndroidBuildActions(ModuleContext) {}
+
+func TestSoongConfigModule(t *testing.T) {
+ configBp := `
+ soong_config_module_type {
+ name: "acme_test_defaults",
+ module_type: "test_defaults",
+ config_namespace: "acme",
+ variables: ["board", "feature1", "feature2", "feature3"],
+ properties: ["cflags", "srcs"],
+ }
+
+ soong_config_string_variable {
+ name: "board",
+ values: ["soc_a", "soc_b"],
+ }
+
+ soong_config_bool_variable {
+ name: "feature1",
+ }
+
+ soong_config_bool_variable {
+ name: "feature2",
+ }
+
+ soong_config_bool_variable {
+ name: "feature3",
+ }
+ `
+
+ importBp := `
+ soong_config_module_type_import {
+ from: "SoongConfig.bp",
+ module_types: ["acme_test_defaults"],
+ }
+ `
+
+ bp := `
+ acme_test_defaults {
+ name: "foo",
+ cflags: ["-DGENERIC"],
+ soong_config_variables: {
+ board: {
+ soc_a: {
+ cflags: ["-DSOC_A"],
+ },
+ soc_b: {
+ cflags: ["-DSOC_B"],
+ },
+ },
+ feature1: {
+ cflags: ["-DFEATURE1"],
+ },
+ feature2: {
+ cflags: ["-DFEATURE2"],
+ },
+ feature3: {
+ cflags: ["-DFEATURE3"],
+ },
+ },
+ }
+ `
+
+ run := func(t *testing.T, bp string, fs map[string][]byte) {
+ config := TestConfig(buildDir, nil, bp, fs)
+
+ config.TestProductVariables.VendorVars = map[string]map[string]string{
+ "acme": map[string]string{
+ "board": "soc_a",
+ "feature1": "true",
+ "feature2": "false",
+ // FEATURE3 unset
+ },
+ }
+
+ ctx := NewTestContext()
+ ctx.RegisterModuleType("soong_config_module_type_import", soongConfigModuleTypeImportFactory)
+ ctx.RegisterModuleType("soong_config_module_type", soongConfigModuleTypeFactory)
+ ctx.RegisterModuleType("soong_config_string_variable", soongConfigStringVariableDummyFactory)
+ ctx.RegisterModuleType("soong_config_bool_variable", soongConfigBoolVariableDummyFactory)
+ ctx.RegisterModuleType("test_defaults", soongConfigTestModuleFactory)
+ ctx.Register(config)
+
+ _, errs := ctx.ParseBlueprintsFiles("Android.bp")
+ FailIfErrored(t, errs)
+ _, errs = ctx.PrepareBuildActions(config)
+ FailIfErrored(t, errs)
+
+ foo := ctx.ModuleForTests("foo", "").Module().(*soongConfigTestModule)
+ if g, w := foo.props.Cflags, []string{"-DGENERIC", "-DSOC_A", "-DFEATURE1"}; !reflect.DeepEqual(g, w) {
+ t.Errorf("wanted foo cflags %q, got %q", w, g)
+ }
+ }
+
+ t.Run("single file", func(t *testing.T) {
+ run(t, configBp+bp, nil)
+ })
+
+ t.Run("import", func(t *testing.T) {
+ run(t, importBp+bp, map[string][]byte{
+ "SoongConfig.bp": []byte(configBp),
+ })
+ })
+}
diff --git a/android/soongconfig/config.go b/android/soongconfig/config.go
new file mode 100644
index 0000000..39a776c
--- /dev/null
+++ b/android/soongconfig/config.go
@@ -0,0 +1,51 @@
+// Copyright 2020 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 soongconfig
+
+import "strings"
+
+type SoongConfig interface {
+ // Bool interprets the variable named `name` as a boolean, returning true if, after
+ // lowercasing, it matches one of "1", "y", "yes", "on", or "true". Unset, or any other
+ // value will return false.
+ Bool(name string) bool
+
+ // String returns the string value of `name`. If the variable was not set, it will
+ // return the empty string.
+ String(name string) string
+
+ // IsSet returns whether the variable `name` was set by Make.
+ IsSet(name string) bool
+}
+
+func Config(vars map[string]string) SoongConfig {
+ return soongConfig(vars)
+}
+
+type soongConfig map[string]string
+
+func (c soongConfig) Bool(name string) bool {
+ v := strings.ToLower(c[name])
+ return v == "1" || v == "y" || v == "yes" || v == "on" || v == "true"
+}
+
+func (c soongConfig) String(name string) string {
+ return c[name]
+}
+
+func (c soongConfig) IsSet(name string) bool {
+ _, ok := c[name]
+ return ok
+}
diff --git a/android/soongconfig/modules.go b/android/soongconfig/modules.go
new file mode 100644
index 0000000..aa4f5c5
--- /dev/null
+++ b/android/soongconfig/modules.go
@@ -0,0 +1,517 @@
+// Copyright 2020 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 soongconfig
+
+import (
+ "fmt"
+ "io"
+ "reflect"
+ "sort"
+ "strings"
+
+ "github.com/google/blueprint"
+ "github.com/google/blueprint/parser"
+ "github.com/google/blueprint/proptools"
+)
+
+var soongConfigProperty = proptools.FieldNameForProperty("soong_config_variables")
+
+// loadSoongConfigModuleTypeDefinition loads module types from an Android.bp file. It caches the
+// result so each file is only parsed once.
+func Parse(r io.Reader, from string) (*SoongConfigDefinition, []error) {
+ scope := parser.NewScope(nil)
+ file, errs := parser.ParseAndEval(from, r, scope)
+
+ if len(errs) > 0 {
+ return nil, errs
+ }
+
+ mtDef := &SoongConfigDefinition{
+ ModuleTypes: make(map[string]*ModuleType),
+ variables: make(map[string]soongConfigVariable),
+ }
+
+ for _, def := range file.Defs {
+ switch def := def.(type) {
+ case *parser.Module:
+ newErrs := processImportModuleDef(mtDef, def)
+
+ if len(newErrs) > 0 {
+ errs = append(errs, newErrs...)
+ }
+
+ case *parser.Assignment:
+ // Already handled via Scope object
+ default:
+ panic("unknown definition type")
+ }
+ }
+
+ if len(errs) > 0 {
+ return nil, errs
+ }
+
+ for name, moduleType := range mtDef.ModuleTypes {
+ for _, varName := range moduleType.variableNames {
+ if v, ok := mtDef.variables[varName]; ok {
+ moduleType.Variables = append(moduleType.Variables, v)
+ } else {
+ return nil, []error{
+ fmt.Errorf("unknown variable %q in module type %q", varName, name),
+ }
+ }
+ }
+ }
+
+ return mtDef, nil
+}
+
+func processImportModuleDef(v *SoongConfigDefinition, def *parser.Module) (errs []error) {
+ switch def.Type {
+ case "soong_config_module_type":
+ return processModuleTypeDef(v, def)
+ case "soong_config_string_variable":
+ return processStringVariableDef(v, def)
+ case "soong_config_bool_variable":
+ return processBoolVariableDef(v, def)
+ default:
+ // Unknown module types will be handled when the file is parsed as a normal
+ // Android.bp file.
+ }
+
+ return nil
+}
+
+type ModuleTypeProperties struct {
+ // the name of the new module type. Unlike most modules, this name does not need to be unique,
+ // although only one module type with any name will be importable into an Android.bp file.
+ Name string
+
+ // the module type that this module type will extend.
+ Module_type string
+
+ // the SOONG_CONFIG_NAMESPACE value from a BoardConfig.mk that this module type will read
+ // configuration variables from.
+ Config_namespace string
+
+ // the list of SOONG_CONFIG variables that this module type will read
+ Variables []string
+
+ // the list of properties that this module type will extend.
+ Properties []string
+}
+
+func processModuleTypeDef(v *SoongConfigDefinition, def *parser.Module) (errs []error) {
+
+ props := &ModuleTypeProperties{}
+
+ _, errs = proptools.UnpackProperties(def.Properties, props)
+ if len(errs) > 0 {
+ return errs
+ }
+
+ if props.Name == "" {
+ errs = append(errs, fmt.Errorf("name property must be set"))
+ }
+
+ if props.Config_namespace == "" {
+ errs = append(errs, fmt.Errorf("config_namespace property must be set"))
+ }
+
+ if props.Module_type == "" {
+ errs = append(errs, fmt.Errorf("module_type property must be set"))
+ }
+
+ if len(errs) > 0 {
+ return errs
+ }
+
+ mt := &ModuleType{
+ affectableProperties: props.Properties,
+ ConfigNamespace: props.Config_namespace,
+ BaseModuleType: props.Module_type,
+ variableNames: props.Variables,
+ }
+ v.ModuleTypes[props.Name] = mt
+
+ return nil
+}
+
+type VariableProperties struct {
+ Name string
+}
+
+type StringVariableProperties struct {
+ Values []string
+}
+
+func processStringVariableDef(v *SoongConfigDefinition, def *parser.Module) (errs []error) {
+ stringProps := &StringVariableProperties{}
+
+ base, errs := processVariableDef(def, stringProps)
+ if len(errs) > 0 {
+ return errs
+ }
+
+ if len(stringProps.Values) == 0 {
+ return []error{fmt.Errorf("values property must be set")}
+ }
+
+ v.variables[base.variable] = &stringVariable{
+ baseVariable: base,
+ values: CanonicalizeToProperties(stringProps.Values),
+ }
+
+ return nil
+}
+
+func processBoolVariableDef(v *SoongConfigDefinition, def *parser.Module) (errs []error) {
+ base, errs := processVariableDef(def)
+ if len(errs) > 0 {
+ return errs
+ }
+
+ v.variables[base.variable] = &boolVariable{
+ baseVariable: base,
+ }
+
+ return nil
+}
+
+func processVariableDef(def *parser.Module,
+ extraProps ...interface{}) (cond baseVariable, errs []error) {
+
+ props := &VariableProperties{}
+
+ allProps := append([]interface{}{props}, extraProps...)
+
+ _, errs = proptools.UnpackProperties(def.Properties, allProps...)
+ if len(errs) > 0 {
+ return baseVariable{}, errs
+ }
+
+ if props.Name == "" {
+ return baseVariable{}, []error{fmt.Errorf("name property must be set")}
+ }
+
+ return baseVariable{
+ variable: props.Name,
+ }, nil
+}
+
+type SoongConfigDefinition struct {
+ ModuleTypes map[string]*ModuleType
+
+ variables map[string]soongConfigVariable
+}
+
+// CreateProperties returns a reflect.Value of a newly constructed type that contains the desired
+// property layout for the Soong config variables, with each possible value an interface{} that
+// contains a nil pointer to another newly constructed type that contains the affectable properties.
+// The reflect.Value will be cloned for each call to the Soong config module type's factory method.
+//
+// For example, the acme_cc_defaults example above would
+// produce a reflect.Value whose type is:
+// *struct {
+// Soong_config_variables struct {
+// Board struct {
+// Soc_a interface{}
+// Soc_b interface{}
+// }
+// }
+// }
+// And whose value is:
+// &{
+// Soong_config_variables: {
+// Board: {
+// Soc_a: (*struct{ Cflags []string })(nil),
+// Soc_b: (*struct{ Cflags []string })(nil),
+// },
+// },
+// }
+func CreateProperties(factory blueprint.ModuleFactory, moduleType *ModuleType) reflect.Value {
+ var fields []reflect.StructField
+
+ _, factoryProps := factory()
+ affectablePropertiesType := createAffectablePropertiesType(moduleType.affectableProperties, factoryProps)
+ if affectablePropertiesType == nil {
+ return reflect.Value{}
+ }
+
+ for _, c := range moduleType.Variables {
+ fields = append(fields, reflect.StructField{
+ Name: proptools.FieldNameForProperty(c.variableProperty()),
+ Type: c.variableValuesType(),
+ })
+ }
+
+ typ := reflect.StructOf([]reflect.StructField{{
+ Name: soongConfigProperty,
+ Type: reflect.StructOf(fields),
+ }})
+
+ props := reflect.New(typ)
+ structConditions := props.Elem().FieldByName(soongConfigProperty)
+
+ for i, c := range moduleType.Variables {
+ c.initializeProperties(structConditions.Field(i), affectablePropertiesType)
+ }
+
+ return props
+}
+
+// createAffectablePropertiesType creates a reflect.Type of a struct that has a field for each affectable property
+// that exists in factoryProps.
+func createAffectablePropertiesType(affectableProperties []string, factoryProps []interface{}) reflect.Type {
+ affectableProperties = append([]string(nil), affectableProperties...)
+ sort.Strings(affectableProperties)
+
+ var recurse func(prefix string, aps []string) ([]string, reflect.Type)
+ recurse = func(prefix string, aps []string) ([]string, reflect.Type) {
+ var fields []reflect.StructField
+
+ for len(affectableProperties) > 0 {
+ p := affectableProperties[0]
+ if !strings.HasPrefix(affectableProperties[0], prefix) {
+ break
+ }
+ affectableProperties = affectableProperties[1:]
+
+ nestedProperty := strings.TrimPrefix(p, prefix)
+ if i := strings.IndexRune(nestedProperty, '.'); i >= 0 {
+ var nestedType reflect.Type
+ nestedPrefix := nestedProperty[:i+1]
+
+ affectableProperties, nestedType = recurse(prefix+nestedPrefix, affectableProperties)
+
+ if nestedType != nil {
+ nestedFieldName := proptools.FieldNameForProperty(strings.TrimSuffix(nestedPrefix, "."))
+
+ fields = append(fields, reflect.StructField{
+ Name: nestedFieldName,
+ Type: nestedType,
+ })
+ }
+ } else {
+ typ := typeForPropertyFromPropertyStructs(factoryProps, p)
+ if typ != nil {
+ fields = append(fields, reflect.StructField{
+ Name: proptools.FieldNameForProperty(nestedProperty),
+ Type: typ,
+ })
+ }
+ }
+ }
+
+ var typ reflect.Type
+ if len(fields) > 0 {
+ typ = reflect.StructOf(fields)
+ }
+ return affectableProperties, typ
+ }
+
+ affectableProperties, typ := recurse("", affectableProperties)
+ if len(affectableProperties) > 0 {
+ panic(fmt.Errorf("didn't handle all affectable properties"))
+ }
+
+ if typ != nil {
+ return reflect.PtrTo(typ)
+ }
+
+ return nil
+}
+
+func typeForPropertyFromPropertyStructs(psList []interface{}, property string) reflect.Type {
+ for _, ps := range psList {
+ if typ := typeForPropertyFromPropertyStruct(ps, property); typ != nil {
+ return typ
+ }
+ }
+
+ return nil
+}
+
+func typeForPropertyFromPropertyStruct(ps interface{}, property string) reflect.Type {
+ v := reflect.ValueOf(ps)
+ for len(property) > 0 {
+ if !v.IsValid() {
+ return nil
+ }
+
+ if v.Kind() == reflect.Interface {
+ if v.IsNil() {
+ return nil
+ } else {
+ v = v.Elem()
+ }
+ }
+
+ if v.Kind() == reflect.Ptr {
+ if v.IsNil() {
+ v = reflect.Zero(v.Type().Elem())
+ } else {
+ v = v.Elem()
+ }
+ }
+
+ if v.Kind() != reflect.Struct {
+ return nil
+ }
+
+ if index := strings.IndexRune(property, '.'); index >= 0 {
+ prefix := property[:index]
+ property = property[index+1:]
+
+ v = v.FieldByName(proptools.FieldNameForProperty(prefix))
+ } else {
+ f := v.FieldByName(proptools.FieldNameForProperty(property))
+ if !f.IsValid() {
+ return nil
+ }
+ return f.Type()
+ }
+ }
+ return nil
+}
+
+// PropertiesToApply returns the applicable properties from a ModuleType that should be applied
+// based on SoongConfig values.
+func PropertiesToApply(moduleType *ModuleType, props reflect.Value, config SoongConfig) []interface{} {
+ var ret []interface{}
+ props = props.Elem().FieldByName(soongConfigProperty)
+ for i, c := range moduleType.Variables {
+ if ps := c.PropertiesToApply(config, props.Field(i)); ps != nil {
+ ret = append(ret, ps)
+ }
+ }
+ return ret
+}
+
+type ModuleType struct {
+ BaseModuleType string
+ ConfigNamespace string
+ Variables []soongConfigVariable
+
+ affectableProperties []string
+ variableNames []string
+}
+
+type soongConfigVariable interface {
+ // variableProperty returns the name of the variable.
+ variableProperty() string
+
+ // conditionalValuesType returns a reflect.Type that contains an interface{} for each possible value.
+ variableValuesType() reflect.Type
+
+ // initializeProperties is passed a reflect.Value of the reflect.Type returned by conditionalValuesType and a
+ // reflect.Type of the affectable properties, and should initialize each interface{} in the reflect.Value with
+ // the zero value of the affectable properties type.
+ initializeProperties(v reflect.Value, typ reflect.Type)
+
+ // PropertiesToApply should return one of the interface{} values set by initializeProperties to be applied
+ // to the module.
+ PropertiesToApply(config SoongConfig, values reflect.Value) interface{}
+}
+
+type baseVariable struct {
+ variable string
+}
+
+func (c *baseVariable) variableProperty() string {
+ return CanonicalizeToProperty(c.variable)
+}
+
+type stringVariable struct {
+ baseVariable
+ values []string
+}
+
+func (s *stringVariable) variableValuesType() reflect.Type {
+ var fields []reflect.StructField
+
+ for _, v := range s.values {
+ fields = append(fields, reflect.StructField{
+ Name: proptools.FieldNameForProperty(v),
+ Type: emptyInterfaceType,
+ })
+ }
+
+ return reflect.StructOf(fields)
+}
+
+func (s *stringVariable) initializeProperties(v reflect.Value, typ reflect.Type) {
+ for i := range s.values {
+ v.Field(i).Set(reflect.Zero(typ))
+ }
+}
+
+func (s *stringVariable) PropertiesToApply(config SoongConfig, values reflect.Value) interface{} {
+ for j, v := range s.values {
+ if config.String(s.variable) == v {
+ return values.Field(j).Interface()
+ }
+ }
+
+ return nil
+}
+
+type boolVariable struct {
+ baseVariable
+}
+
+func (b boolVariable) variableValuesType() reflect.Type {
+ return emptyInterfaceType
+}
+
+func (b boolVariable) initializeProperties(v reflect.Value, typ reflect.Type) {
+ v.Set(reflect.Zero(typ))
+}
+
+func (b boolVariable) PropertiesToApply(config SoongConfig, values reflect.Value) interface{} {
+ if config.Bool(b.variable) {
+ return values.Interface()
+ }
+
+ return nil
+}
+
+func CanonicalizeToProperty(v string) string {
+ return strings.Map(func(r rune) rune {
+ switch {
+ case r >= 'A' && r <= 'Z',
+ r >= 'a' && r <= 'z',
+ r >= '0' && r <= '9',
+ r == '_':
+ return r
+ default:
+ return '_'
+ }
+ }, v)
+}
+
+func CanonicalizeToProperties(values []string) []string {
+ ret := make([]string, len(values))
+ for i, v := range values {
+ ret[i] = CanonicalizeToProperty(v)
+ }
+ return ret
+}
+
+type emptyInterfaceStruct struct {
+ i interface{}
+}
+
+var emptyInterfaceType = reflect.TypeOf(emptyInterfaceStruct{}).Field(0).Type
diff --git a/android/soongconfig/modules_test.go b/android/soongconfig/modules_test.go
new file mode 100644
index 0000000..4190016
--- /dev/null
+++ b/android/soongconfig/modules_test.go
@@ -0,0 +1,249 @@
+// Copyright 2020 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 soongconfig
+
+import (
+ "reflect"
+ "testing"
+)
+
+func Test_CanonicalizeToProperty(t *testing.T) {
+ tests := []struct {
+ name string
+ arg string
+ want string
+ }{
+ {
+ name: "lowercase",
+ arg: "board",
+ want: "board",
+ },
+ {
+ name: "uppercase",
+ arg: "BOARD",
+ want: "BOARD",
+ },
+ {
+ name: "numbers",
+ arg: "BOARD123",
+ want: "BOARD123",
+ },
+ {
+ name: "underscore",
+ arg: "TARGET_BOARD",
+ want: "TARGET_BOARD",
+ },
+ {
+ name: "dash",
+ arg: "TARGET-BOARD",
+ want: "TARGET_BOARD",
+ },
+ {
+ name: "unicode",
+ arg: "boardλ",
+ want: "board_",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := CanonicalizeToProperty(tt.arg); got != tt.want {
+ t.Errorf("canonicalizeToProperty() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func Test_typeForPropertyFromPropertyStruct(t *testing.T) {
+ tests := []struct {
+ name string
+ ps interface{}
+ property string
+ want string
+ }{
+ {
+ name: "string",
+ ps: struct {
+ A string
+ }{},
+ property: "a",
+ want: "string",
+ },
+ {
+ name: "list",
+ ps: struct {
+ A []string
+ }{},
+ property: "a",
+ want: "[]string",
+ },
+ {
+ name: "missing",
+ ps: struct {
+ A []string
+ }{},
+ property: "b",
+ want: "",
+ },
+ {
+ name: "nested",
+ ps: struct {
+ A struct {
+ B string
+ }
+ }{},
+ property: "a.b",
+ want: "string",
+ },
+ {
+ name: "missing nested",
+ ps: struct {
+ A struct {
+ B string
+ }
+ }{},
+ property: "a.c",
+ want: "",
+ },
+ {
+ name: "not a struct",
+ ps: struct {
+ A string
+ }{},
+ property: "a.b",
+ want: "",
+ },
+ {
+ name: "nested pointer",
+ ps: struct {
+ A *struct {
+ B string
+ }
+ }{},
+ property: "a.b",
+ want: "string",
+ },
+ {
+ name: "nested interface",
+ ps: struct {
+ A interface{}
+ }{
+ A: struct {
+ B string
+ }{},
+ },
+ property: "a.b",
+ want: "string",
+ },
+ {
+ name: "nested interface pointer",
+ ps: struct {
+ A interface{}
+ }{
+ A: &struct {
+ B string
+ }{},
+ },
+ property: "a.b",
+ want: "string",
+ },
+ {
+ name: "nested interface nil pointer",
+ ps: struct {
+ A interface{}
+ }{
+ A: (*struct {
+ B string
+ })(nil),
+ },
+ property: "a.b",
+ want: "string",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ typ := typeForPropertyFromPropertyStruct(tt.ps, tt.property)
+ got := ""
+ if typ != nil {
+ got = typ.String()
+ }
+ if got != tt.want {
+ t.Errorf("typeForPropertyFromPropertyStruct() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func Test_createAffectablePropertiesType(t *testing.T) {
+ tests := []struct {
+ name string
+ affectableProperties []string
+ factoryProps interface{}
+ want string
+ }{
+ {
+ name: "string",
+ affectableProperties: []string{"cflags"},
+ factoryProps: struct {
+ Cflags string
+ }{},
+ want: "*struct { Cflags string }",
+ },
+ {
+ name: "list",
+ affectableProperties: []string{"cflags"},
+ factoryProps: struct {
+ Cflags []string
+ }{},
+ want: "*struct { Cflags []string }",
+ },
+ {
+ name: "string pointer",
+ affectableProperties: []string{"cflags"},
+ factoryProps: struct {
+ Cflags *string
+ }{},
+ want: "*struct { Cflags *string }",
+ },
+ {
+ name: "subset",
+ affectableProperties: []string{"cflags"},
+ factoryProps: struct {
+ Cflags string
+ Ldflags string
+ }{},
+ want: "*struct { Cflags string }",
+ },
+ {
+ name: "none",
+ affectableProperties: []string{"cflags"},
+ factoryProps: struct {
+ Ldflags string
+ }{},
+ want: "",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ typ := createAffectablePropertiesType(tt.affectableProperties, []interface{}{tt.factoryProps})
+ got := ""
+ if typ != nil {
+ got = typ.String()
+ }
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("createAffectablePropertiesType() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/android/variable_test.go b/android/variable_test.go
index 451d43d..751677f 100644
--- a/android/variable_test.go
+++ b/android/variable_test.go
@@ -148,7 +148,7 @@
clonedProps := proptools.CloneProperties(reflect.ValueOf(props)).Interface()
m.AddProperties(clonedProps)
- // Set a default variableProperties, this will be used as the input to the property struct filter
+ // Set a default soongConfigVariableProperties, this will be used as the input to the property struct filter
// for this test module.
m.variableProperties = testProductVariableProperties
InitAndroidModule(m)