Merge "Simplify vendor conditionals"

This commit is contained in:
Colin Cross 2020-01-27 19:03:23 +00:00 committed by Gerrit Code Review
commit b7ec426e01
12 changed files with 1479 additions and 57 deletions

View File

@ -36,6 +36,7 @@ bootstrap_go_package {
"blueprint",
"blueprint-bootstrap",
"soong",
"soong-android-soongconfig",
"soong-env",
"soong-shared",
],
@ -73,6 +74,7 @@ bootstrap_go_package {
"android/sdk.go",
"android/sh_binary.go",
"android/singleton.go",
"android/soong_config_modules.go",
"android/testing.go",
"android/util.go",
"android/variable.go",
@ -101,6 +103,7 @@ bootstrap_go_package {
"android/prebuilt_test.go",
"android/prebuilt_etc_test.go",
"android/rule_builder_test.go",
"android/soong_config_modules_test.go",
"android/util_test.go",
"android/variable_test.go",
"android/visibility_test.go",
@ -108,6 +111,20 @@ bootstrap_go_package {
],
}
bootstrap_go_package {
name: "soong-android-soongconfig",
pkgPath: "android/soong/android/soongconfig",
deps: [
"blueprint",
"blueprint-parser",
"blueprint-proptools",
],
srcs: [
"android/soongconfig/config.go",
"android/soongconfig/modules.go",
],
}
bootstrap_go_package {
name: "soong-cc-config",
pkgPath: "android/soong/cc/config",

128
README.md
View File

@ -376,36 +376,14 @@ The androidmk converter will produce multiple conflicting modules, which must
be resolved by hand to a single module with any differences inside
`target: { android: { }, host: { } }` blocks.
## Build logic
### Conditionals
The build logic is written in Go using the
[blueprint](http://godoc.org/github.com/google/blueprint) framework. Build
logic receives module definitions parsed into Go structures using reflection
and produces build rules. The build rules are collected by blueprint and
written to a [ninja](http://ninja-build.org) build file.
## Other documentation
* [Best Practices](docs/best_practices.md)
* [Build Performance](docs/perf.md)
* [Generating CLion Projects](docs/clion.md)
* [Generating YouCompleteMe/VSCode compile\_commands.json file](docs/compdb.md)
* Make-specific documentation: [build/make/README.md](https://android.googlesource.com/platform/build/+/master/README.md)
## FAQ
### How do I write conditionals?
Soong deliberately does not support conditionals in Android.bp files. We
Soong deliberately does not support most conditionals in Android.bp files. We
suggest removing most conditionals from the build. See
[Best Practices](docs/best_practices.md#removing-conditionals) for some
examples on how to remove conditionals.
In cases where build time conditionals are unavoidable, complexity in build
rules that would require conditionals are handled in Go through Soong plugins.
This allows Go language features to be used for better readability and
testability, and implicit dependencies introduced by conditionals can be
tracked. Most conditionals supported natively by Soong are converted to a map
Most conditionals supported natively by Soong are converted to a map
property. When building the module one of the properties in the map will be
selected, and its values appended to the property with the same name at the
top level of the module.
@ -430,6 +408,106 @@ When building the module for arm the `generic.cpp` and `arm.cpp` sources will
be built. When building for x86 the `generic.cpp` and 'x86.cpp' sources will
be built.
#### Soong Config Variables
When converting vendor modules that contain conditionals, simple conditionals
can be supported through Soong config variables using `soong_config_*`
modules that describe the module types, variables and possible values:
```
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",
}
```
This example describes a new `acme_cc_defaults` module type that extends the
`cc_defaults` module type, with two additional conditionals based on variables
`board` and `feature`, which can affect properties `cflags` and `srcs`.
The values of the variables can be set from a product's `BoardConfig.mk` file:
```
SOONG_CONFIG_NAMESPACES += acme
SOONG_CONFIG_acme += \
board \
feature \
SOONG_CONFIG_acme_board := soc_a
SOONG_CONFIG_acme_feature := true
```
The `acme_cc_defaults` module type can be used anywhere after the definition in
the file where it is defined, or can be imported into another file with:
```
soong_config_module_type_import {
from: "device/acme/Android.bp",
module_types: ["acme_cc_defaults"],
}
```
It can used like any other module type:
```
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"],
}
```
With the `BoardConfig.mk` snippet above, libacme_foo would build with
cflags "-DGENERIC -DSOC_A -DFEATURE".
`soong_config_module_type` modules will work best when used to wrap defaults
modules (`cc_defaults`, `java_defaults`, etc.), which can then be referenced
by all of the vendor's other modules using the normal namespace and visibility
rules.
## Build logic
The build logic is written in Go using the
[blueprint](http://godoc.org/github.com/google/blueprint) framework. Build
logic receives module definitions parsed into Go structures using reflection
and produces build rules. The build rules are collected by blueprint and
written to a [ninja](http://ninja-build.org) build file.
## Other documentation
* [Best Practices](docs/best_practices.md)
* [Build Performance](docs/perf.md)
* [Generating CLion Projects](docs/clion.md)
* [Generating YouCompleteMe/VSCode compile\_commands.json file](docs/compdb.md)
* Make-specific documentation: [build/make/README.md](https://android.googlesource.com/platform/build/+/master/README.md)
## Developing for Soong
To load Soong code in a Go-aware IDE, create a directory outside your android tree and then:

View File

@ -29,6 +29,8 @@ import (
"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 @@ type DeviceConfig struct {
*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 @@ type deviceConfig struct {
OncePer
}
type vendorConfig map[string]string
type jsonConfigurable interface {
SetDefaultConfig()
}
@ -1141,21 +1129,7 @@ func (c *config) XOMDisabledForPath(path string) bool {
}
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 {

View File

@ -34,6 +34,9 @@ type LoadHookContext interface {
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 @@ type loadHookContext struct {
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 @@ func (l *loadHookContext) CreateModule(factory ModuleFactory, props ...interface
return module
}
func (l *loadHookContext) registerScopedModuleType(name string, factory blueprint.ModuleFactory) {
l.bp.RegisterScopedModuleType(name, factory)
}
type InstallHookContext interface {
ModuleContext
Path() InstallPath

View File

@ -62,6 +62,7 @@ type EarlyModuleContext interface {
ModuleName() string
ModuleDir() string
ModuleType() string
BlueprintsFile() string
ContainsProperty(name string) bool
Errorf(pos scanner.Position, fmt string, args ...interface{})
@ -524,9 +525,13 @@ func (k moduleKind) String() string {
}
}
func initAndroidModuleBase(m Module) {
m.base().module = m
}
func InitAndroidModule(m Module) {
initAndroidModuleBase(m)
base := m.base()
base.module = m
m.AddProperties(
&base.nameProperties,

View File

@ -162,6 +162,12 @@ func (r *NameResolver) findNamespace(path string) (namespace *Namespace) {
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 @@ func (r *NameResolver) NewModule(ctx blueprint.NamespaceContext, moduleGroup blu
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)

View File

@ -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
}
}

View File

@ -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),
})
})
}

View File

@ -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
}

View File

@ -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

View File

@ -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)
}
})
}
}

View File

@ -148,7 +148,7 @@ func testProductVariableModuleFactoryFactory(props interface{}) func() Module {
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)