Merge "Add support for experimentally enabling RBE support on specific rules."
diff --git a/Android.bp b/Android.bp
index 0f7ef9e..9b55c8c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -36,6 +36,7 @@
         "blueprint",
         "blueprint-bootstrap",
         "soong",
+        "soong-android-soongconfig",
         "soong-env",
         "soong-shared",
     ],
@@ -73,6 +74,7 @@
         "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 @@
         "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",
@@ -109,6 +112,20 @@
 }
 
 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",
     deps: [
diff --git a/README.md b/README.md
index b6fda50..b1bb425 100644
--- a/README.md
+++ b/README.md
@@ -376,36 +376,14 @@
 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 @@
 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:
diff --git a/android/config.go b/android/config.go
index ffd7744..3dae6e2 100644
--- a/android/config.go
+++ b/android/config.go
@@ -29,11 +29,14 @@
 	"github.com/google/blueprint/bootstrap"
 	"github.com/google/blueprint/pathtools"
 	"github.com/google/blueprint/proptools"
+
+	"android/soong/android/soongconfig"
 )
 
 var Bool = proptools.Bool
 var String = proptools.String
-var FutureApiLevel = 10000
+
+const FutureApiLevel = 10000
 
 // The configuration file name
 const configFileName = "soong.config"
@@ -66,19 +69,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 +119,6 @@
 	OncePer
 }
 
-type vendorConfig map[string]string
-
 type jsonConfigurable interface {
 	SetDefaultConfig()
 }
@@ -1153,21 +1142,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 4a72889..96c2e1e 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{})
@@ -524,9 +525,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)
diff --git a/apex/androidmk.go b/apex/androidmk.go
index 5fa5bf0..8cb9896 100644
--- a/apex/androidmk.go
+++ b/apex/androidmk.go
@@ -112,6 +112,11 @@
 			}
 		} else {
 			fmt.Fprintln(w, "LOCAL_MODULE_PATH :=", pathWhenActivated)
+
+			// For non-flattend APEXes, the merged notice file is attached to the APEX itself.
+			// We don't need to have notice file for the individual modules in it. Otherwise,
+			// we will have duplicated notice entries.
+			fmt.Fprintln(w, "LOCAL_NO_NOTICE_FILE := true")
 		}
 		fmt.Fprintln(w, "LOCAL_PREBUILT_MODULE_FILE :=", fi.builtFile.String())
 		fmt.Fprintln(w, "LOCAL_MODULE_CLASS :=", fi.class.NameInMake())
@@ -177,7 +182,7 @@
 			fmt.Fprintln(w, "include $(BUILD_SYSTEM)/soong_cc_prebuilt.mk")
 		} else {
 			fmt.Fprintln(w, "LOCAL_MODULE_STEM :=", fi.builtFile.Base())
-			if fi.builtFile == a.manifestPbOut {
+			if fi.builtFile == a.manifestPbOut && apexType == flattenedApex {
 				if a.primaryApexType {
 					// Make apex_manifest.pb module for this APEX to override all other
 					// modules in the APEXes being overridden by this APEX
@@ -187,7 +192,7 @@
 					}
 					fmt.Fprintln(w, "LOCAL_OVERRIDES_MODULES :=", strings.Join(patterns, " "))
 
-					if apexType == flattenedApex && len(a.compatSymlinks) > 0 {
+					if len(a.compatSymlinks) > 0 {
 						// For flattened apexes, compat symlinks are attached to apex_manifest.json which is guaranteed for every apex
 						postInstallCommands = append(postInstallCommands, a.compatSymlinks...)
 					}
@@ -271,6 +276,11 @@
 				if len(postInstallCommands) > 0 {
 					fmt.Fprintln(w, "LOCAL_POST_INSTALL_CMD :=", strings.Join(postInstallCommands, " && "))
 				}
+
+				if a.mergedNotices.Merged.Valid() {
+					fmt.Fprintln(w, "LOCAL_NOTICE_FILE :=", a.mergedNotices.Merged.Path().String())
+				}
+
 				fmt.Fprintln(w, "include $(BUILD_PREBUILT)")
 
 				if apexType == imageApex {
diff --git a/apex/apex.go b/apex/apex.go
index f925066..ae93790 100644
--- a/apex/apex.go
+++ b/apex/apex.go
@@ -818,6 +818,9 @@
 	// Whether to create symlink to the system file instead of having a file
 	// inside the apex or not
 	linkToSystemLib bool
+
+	// Struct holding the merged notice file paths in different formats
+	mergedNotices android.NoticeOutputs
 }
 
 func addDependenciesForNativeModules(ctx android.BottomUpMutatorContext,
diff --git a/apex/builder.go b/apex/builder.go
index 8ae2b5c..17edf7c 100644
--- a/apex/builder.go
+++ b/apex/builder.go
@@ -211,7 +211,7 @@
 	})
 }
 
-func (a *apexBundle) buildNoticeFile(ctx android.ModuleContext, apexFileName string) android.OptionalPath {
+func (a *apexBundle) buildNoticeFiles(ctx android.ModuleContext, apexFileName string) android.NoticeOutputs {
 	noticeFiles := []android.Path{}
 	for _, f := range a.filesInfo {
 		if f.module != nil {
@@ -227,10 +227,10 @@
 	}
 
 	if len(noticeFiles) == 0 {
-		return android.OptionalPath{}
+		return android.NoticeOutputs{}
 	}
 
-	return android.BuildNoticeOutput(ctx, a.installDir, apexFileName, android.FirstUniquePaths(noticeFiles)).HtmlGzOutput
+	return android.BuildNoticeOutput(ctx, a.installDir, apexFileName, android.FirstUniquePaths(noticeFiles))
 }
 
 func (a *apexBundle) buildInstalledFilesFile(ctx android.ModuleContext, builtApex android.Path, imageDir android.Path) android.OutputPath {
@@ -394,11 +394,11 @@
 		optFlags = append(optFlags, "--target_sdk_version "+targetSdkVersion)
 		optFlags = append(optFlags, "--min_sdk_version "+minSdkVersion)
 
-		noticeFile := a.buildNoticeFile(ctx, a.Name()+suffix)
-		if noticeFile.Valid() {
+		a.mergedNotices = a.buildNoticeFiles(ctx, a.Name()+suffix)
+		if a.mergedNotices.HtmlGzOutput.Valid() {
 			// If there's a NOTICE file, embed it as an asset file in the APEX.
-			implicitInputs = append(implicitInputs, noticeFile.Path())
-			optFlags = append(optFlags, "--assets_dir "+filepath.Dir(noticeFile.String()))
+			implicitInputs = append(implicitInputs, a.mergedNotices.HtmlGzOutput.Path())
+			optFlags = append(optFlags, "--assets_dir "+filepath.Dir(a.mergedNotices.HtmlGzOutput.String()))
 		}
 
 		if ctx.ModuleDir() != "system/apex/apexd/apexd_testdata" && ctx.ModuleDir() != "system/apex/shim/build" && a.testOnlyShouldSkipHashtreeGeneration() {
diff --git a/cmd/merge_zips/merge_zips.go b/cmd/merge_zips/merge_zips.go
index a9be612..a95aca9 100644
--- a/cmd/merge_zips/merge_zips.go
+++ b/cmd/merge_zips/merge_zips.go
@@ -667,8 +667,10 @@
 		return nil
 	}
 	var err error
-	fiz.reader, err = zip.OpenReader(fiz.Name())
-	return err
+	if fiz.reader, err = zip.OpenReader(fiz.Name()); err != nil {
+		return fmt.Errorf("%s: %s", fiz.Name(), err.Error())
+	}
+	return nil
 }
 
 func main() {
diff --git a/java/aar.go b/java/aar.go
index 1ba65dc..d707e36 100644
--- a/java/aar.go
+++ b/java/aar.go
@@ -177,7 +177,10 @@
 	linkDeps = append(linkDeps, assetFiles...)
 
 	// SDK version flags
-	minSdkVersion := sdkVersionOrDefault(ctx, sdkContext.minSdkVersion())
+	minSdkVersion, err := sdkContext.minSdkVersion().effectiveVersionString(ctx)
+	if err != nil {
+		ctx.ModuleErrorf("invalid minSdkVersion: %s", err)
+	}
 
 	linkFlags = append(linkFlags, "--min-sdk-version "+minSdkVersion)
 	linkFlags = append(linkFlags, "--target-sdk-version "+minSdkVersion)
@@ -524,22 +527,22 @@
 	exportedStaticPackages android.Paths
 }
 
-func (a *AARImport) sdkVersion() string {
-	return String(a.properties.Sdk_version)
+func (a *AARImport) sdkVersion() sdkSpec {
+	return sdkSpecFrom(String(a.properties.Sdk_version))
 }
 
 func (a *AARImport) systemModules() string {
 	return ""
 }
 
-func (a *AARImport) minSdkVersion() string {
+func (a *AARImport) minSdkVersion() sdkSpec {
 	if a.properties.Min_sdk_version != nil {
-		return *a.properties.Min_sdk_version
+		return sdkSpecFrom(*a.properties.Min_sdk_version)
 	}
 	return a.sdkVersion()
 }
 
-func (a *AARImport) targetSdkVersion() string {
+func (a *AARImport) targetSdkVersion() sdkSpec {
 	return a.sdkVersion()
 }
 
diff --git a/java/android_manifest.go b/java/android_manifest.go
index dc7a3fc..e3646f5 100644
--- a/java/android_manifest.go
+++ b/java/android_manifest.go
@@ -59,7 +59,7 @@
 	if isLibrary {
 		args = append(args, "--library")
 	} else {
-		minSdkVersion, err := sdkVersionToNumber(ctx, sdkContext.minSdkVersion())
+		minSdkVersion, err := sdkContext.minSdkVersion().effectiveVersion(ctx)
 		if err != nil {
 			ctx.ModuleErrorf("invalid minSdkVersion: %s", err)
 		}
@@ -92,15 +92,28 @@
 	}
 
 	var deps android.Paths
-	targetSdkVersion := sdkVersionOrDefault(ctx, sdkContext.targetSdkVersion())
-	minSdkVersion := sdkVersionOrDefault(ctx, sdkContext.minSdkVersion())
-	if (UseApiFingerprint(ctx, sdkContext.targetSdkVersion()) ||
-		UseApiFingerprint(ctx, sdkContext.minSdkVersion())) {
-			apiFingerprint := ApiFingerprintPath(ctx)
-			deps = append(deps, apiFingerprint)
+	targetSdkVersion, err := sdkContext.targetSdkVersion().effectiveVersionString(ctx)
+	if err != nil {
+		ctx.ModuleErrorf("invalid targetSdkVersion: %s", err)
+	}
+	if UseApiFingerprint(ctx, targetSdkVersion) {
+		targetSdkVersion += fmt.Sprintf(".$$(cat %s)", ApiFingerprintPath(ctx).String())
+		deps = append(deps, ApiFingerprintPath(ctx))
+	}
+
+	minSdkVersion, err := sdkContext.minSdkVersion().effectiveVersionString(ctx)
+	if err != nil {
+		ctx.ModuleErrorf("invalid minSdkVersion: %s", err)
+	}
+	if UseApiFingerprint(ctx, minSdkVersion) {
+		minSdkVersion += fmt.Sprintf(".$$(cat %s)", ApiFingerprintPath(ctx).String())
+		deps = append(deps, ApiFingerprintPath(ctx))
 	}
 
 	fixedManifest := android.PathForModuleOut(ctx, "manifest_fixer", "AndroidManifest.xml")
+	if err != nil {
+		ctx.ModuleErrorf("invalid minSdkVersion: %s", err)
+	}
 	ctx.Build(pctx, android.BuildParams{
 		Rule:        manifestFixerRule,
 		Description: "fix manifest",
diff --git a/java/androidmk.go b/java/androidmk.go
index 7c19180..f28ae10 100644
--- a/java/androidmk.go
+++ b/java/androidmk.go
@@ -93,7 +93,7 @@
 					if len(library.dexpreopter.builtInstalled) > 0 {
 						entries.SetString("LOCAL_SOONG_BUILT_INSTALLED", library.dexpreopter.builtInstalled)
 					}
-					entries.SetString("LOCAL_SDK_VERSION", library.sdkVersion())
+					entries.SetString("LOCAL_SDK_VERSION", library.sdkVersion().raw)
 					entries.SetPath("LOCAL_SOONG_CLASSES_JAR", library.implementationAndResourcesJar)
 					entries.SetPath("LOCAL_SOONG_HEADER_JAR", library.headerJarFile)
 
@@ -174,7 +174,7 @@
 				entries.SetBool("LOCAL_UNINSTALLABLE_MODULE", !Bool(prebuilt.properties.Installable))
 				entries.SetPath("LOCAL_SOONG_HEADER_JAR", prebuilt.combinedClasspathFile)
 				entries.SetPath("LOCAL_SOONG_CLASSES_JAR", prebuilt.combinedClasspathFile)
-				entries.SetString("LOCAL_SDK_VERSION", prebuilt.sdkVersion())
+				entries.SetString("LOCAL_SDK_VERSION", prebuilt.sdkVersion().raw)
 				entries.SetString("LOCAL_MODULE_STEM", prebuilt.Stem())
 			},
 		},
@@ -223,7 +223,7 @@
 				entries.SetPath("LOCAL_SOONG_EXPORT_PROGUARD_FLAGS", prebuilt.proguardFlags)
 				entries.SetPath("LOCAL_SOONG_STATIC_LIBRARY_EXTRA_PACKAGES", prebuilt.extraAaptPackagesFile)
 				entries.SetPath("LOCAL_FULL_MANIFEST_FILE", prebuilt.manifest)
-				entries.SetString("LOCAL_SDK_VERSION", prebuilt.sdkVersion())
+				entries.SetString("LOCAL_SDK_VERSION", prebuilt.sdkVersion().raw)
 			},
 		},
 	}}
diff --git a/java/app.go b/java/app.go
index c59047d..2ed3d4d 100755
--- a/java/app.go
+++ b/java/app.go
@@ -163,7 +163,7 @@
 func (a *AndroidApp) DepsMutator(ctx android.BottomUpMutatorContext) {
 	a.Module.deps(ctx)
 
-	if String(a.appProperties.Stl) == "c++_shared" && a.sdkVersion() == "" {
+	if String(a.appProperties.Stl) == "c++_shared" && !a.sdkVersion().specified() {
 		ctx.PropertyErrorf("stl", "sdk_version must be set in order to use c++_shared")
 	}
 
@@ -212,7 +212,7 @@
 // Returns true if the native libraries should be stored in the APK uncompressed and the
 // extractNativeLibs application flag should be set to false in the manifest.
 func (a *AndroidApp) useEmbeddedNativeLibs(ctx android.ModuleContext) bool {
-	minSdkVersion, err := sdkVersionToNumber(ctx, a.minSdkVersion())
+	minSdkVersion, err := a.minSdkVersion().effectiveVersion(ctx)
 	if err != nil {
 		ctx.PropertyErrorf("min_sdk_version", "invalid value %q: %s", a.minSdkVersion(), err)
 	}
@@ -1271,22 +1271,22 @@
 	ctx.InstallFile(r.installDir, r.outputFile.Base(), r.outputFile)
 }
 
-func (r *RuntimeResourceOverlay) sdkVersion() string {
-	return String(r.properties.Sdk_version)
+func (r *RuntimeResourceOverlay) sdkVersion() sdkSpec {
+	return sdkSpecFrom(String(r.properties.Sdk_version))
 }
 
 func (r *RuntimeResourceOverlay) systemModules() string {
 	return ""
 }
 
-func (r *RuntimeResourceOverlay) minSdkVersion() string {
+func (r *RuntimeResourceOverlay) minSdkVersion() sdkSpec {
 	if r.properties.Min_sdk_version != nil {
-		return *r.properties.Min_sdk_version
+		return sdkSpecFrom(*r.properties.Min_sdk_version)
 	}
 	return r.sdkVersion()
 }
 
-func (r *RuntimeResourceOverlay) targetSdkVersion() string {
+func (r *RuntimeResourceOverlay) targetSdkVersion() sdkSpec {
 	return r.sdkVersion()
 }
 
diff --git a/java/dex.go b/java/dex.go
index cd97563..6afdb6d 100644
--- a/java/dex.go
+++ b/java/dex.go
@@ -73,12 +73,12 @@
 			"--verbose")
 	}
 
-	minSdkVersion, err := sdkVersionToNumberAsString(ctx, j.minSdkVersion())
+	minSdkVersion, err := j.minSdkVersion().effectiveVersion(ctx)
 	if err != nil {
 		ctx.PropertyErrorf("min_sdk_version", "%s", err)
 	}
 
-	flags = append(flags, "--min-api "+minSdkVersion)
+	flags = append(flags, "--min-api "+minSdkVersion.asNumberString())
 	return flags
 }
 
diff --git a/java/dexpreopt_bootjars.go b/java/dexpreopt_bootjars.go
index c6aa7fe..87f6d5e 100644
--- a/java/dexpreopt_bootjars.go
+++ b/java/dexpreopt_bootjars.go
@@ -88,6 +88,9 @@
 	images     map[android.ArchType]android.OutputPath  // first image file
 	imagesDeps map[android.ArchType]android.OutputPaths // all files
 
+	// Only for extensions, paths to the primary boot images (grouped by target).
+	primaryImages map[android.ArchType]android.OutputPath
+
 	// File path to a zip archive with all image files (or nil, if not needed).
 	zip android.WritablePath
 }
@@ -355,7 +358,7 @@
 	}
 
 	if image.extension {
-		artImage := artBootImageConfig(ctx).images[arch]
+		artImage := image.primaryImages[arch]
 		cmd.
 			Flag("--runtime-arg").FlagWithInputList("-Xbootclasspath:", image.dexPathsDeps.Paths(), ":").
 			Flag("--runtime-arg").FlagWithList("-Xbootclasspath-locations:", image.dexLocationsDeps, ":").
diff --git a/java/dexpreopt_config.go b/java/dexpreopt_config.go
index 31bec93..637a32f 100644
--- a/java/dexpreopt_config.go
+++ b/java/dexpreopt_config.go
@@ -246,10 +246,12 @@
 
 		// specific to the framework config
 		frameworkCfg.dexPathsDeps = append(artCfg.dexPathsDeps, frameworkCfg.dexPathsDeps...)
+		frameworkCfg.primaryImages = artCfg.images
 		frameworkCfg.imageLocations = append(artCfg.imageLocations, frameworkCfg.imageLocations...)
 
 		// specific to the jitzygote-framework config
 		frameworkJZCfg.dexPathsDeps = append(artJZCfg.dexPathsDeps, frameworkJZCfg.dexPathsDeps...)
+		frameworkJZCfg.primaryImages = artJZCfg.images
 		frameworkJZCfg.imageLocations = append(artJZCfg.imageLocations, frameworkJZCfg.imageLocations...)
 
 		return configs
diff --git a/java/droiddoc.go b/java/droiddoc.go
index a10ec81..abdceba 100644
--- a/java/droiddoc.go
+++ b/java/droiddoc.go
@@ -424,19 +424,19 @@
 
 var _ android.OutputFileProducer = (*Javadoc)(nil)
 
-func (j *Javadoc) sdkVersion() string {
-	return String(j.properties.Sdk_version)
+func (j *Javadoc) sdkVersion() sdkSpec {
+	return sdkSpecFrom(String(j.properties.Sdk_version))
 }
 
 func (j *Javadoc) systemModules() string {
 	return proptools.String(j.properties.System_modules)
 }
 
-func (j *Javadoc) minSdkVersion() string {
+func (j *Javadoc) minSdkVersion() sdkSpec {
 	return j.sdkVersion()
 }
 
-func (j *Javadoc) targetSdkVersion() string {
+func (j *Javadoc) targetSdkVersion() sdkSpec {
 	return j.sdkVersion()
 }
 
diff --git a/java/java.go b/java/java.go
index 4c6a5a5..794ee68 100644
--- a/java/java.go
+++ b/java/java.go
@@ -87,7 +87,7 @@
 	if j.SocSpecific() || j.DeviceSpecific() ||
 		(j.ProductSpecific() && ctx.Config().EnforceProductPartitionInterface()) {
 		if sc, ok := ctx.Module().(sdkContext); ok {
-			if sc.sdkVersion() == "" {
+			if !sc.sdkVersion().specified() {
 				ctx.PropertyErrorf("sdk_version",
 					"sdk_version must have a value when the module is located at vendor or product(only if PRODUCT_ENFORCE_PRODUCT_PARTITION_INTERFACE is set).")
 			}
@@ -98,12 +98,11 @@
 func (j *Module) checkPlatformAPI(ctx android.ModuleContext) {
 	if sc, ok := ctx.Module().(sdkContext); ok {
 		usePlatformAPI := proptools.Bool(j.deviceProperties.Platform_apis)
-		if usePlatformAPI != (sc.sdkVersion() == "") {
-			if usePlatformAPI {
-				ctx.PropertyErrorf("platform_apis", "platform_apis must be false when sdk_version is not empty.")
-			} else {
-				ctx.PropertyErrorf("platform_apis", "platform_apis must be true when sdk_version is empty.")
-			}
+		sdkVersionSpecified := sc.sdkVersion().specified()
+		if usePlatformAPI && sdkVersionSpecified {
+			ctx.PropertyErrorf("platform_apis", "platform_apis must be false when sdk_version is not empty.")
+		} else if !usePlatformAPI && !sdkVersionSpecified {
+			ctx.PropertyErrorf("platform_apis", "platform_apis must be true when sdk_version is empty.")
 		}
 
 	}
@@ -451,8 +450,8 @@
 }
 
 type SdkLibraryDependency interface {
-	SdkHeaderJars(ctx android.BaseModuleContext, sdkVersion string) android.Paths
-	SdkImplementationJars(ctx android.BaseModuleContext, sdkVersion string) android.Paths
+	SdkHeaderJars(ctx android.BaseModuleContext, sdkVersion sdkSpec) android.Paths
+	SdkImplementationJars(ctx android.BaseModuleContext, sdkVersion sdkSpec) android.Paths
 }
 
 type xref interface {
@@ -553,24 +552,24 @@
 			ctx.Config().UnbundledBuild())
 }
 
-func (j *Module) sdkVersion() string {
-	return String(j.deviceProperties.Sdk_version)
+func (j *Module) sdkVersion() sdkSpec {
+	return sdkSpecFrom(String(j.deviceProperties.Sdk_version))
 }
 
 func (j *Module) systemModules() string {
 	return proptools.String(j.deviceProperties.System_modules)
 }
 
-func (j *Module) minSdkVersion() string {
+func (j *Module) minSdkVersion() sdkSpec {
 	if j.deviceProperties.Min_sdk_version != nil {
-		return *j.deviceProperties.Min_sdk_version
+		return sdkSpecFrom(*j.deviceProperties.Min_sdk_version)
 	}
 	return j.sdkVersion()
 }
 
-func (j *Module) targetSdkVersion() string {
+func (j *Module) targetSdkVersion() sdkSpec {
 	if j.deviceProperties.Target_sdk_version != nil {
-		return *j.deviceProperties.Target_sdk_version
+		return sdkSpecFrom(*j.deviceProperties.Target_sdk_version)
 	}
 	return j.sdkVersion()
 }
@@ -776,26 +775,25 @@
 		name == "stub-annotations" || name == "private-stub-annotations-jar" ||
 		name == "core-lambda-stubs" || name == "core-generated-annotation-stubs":
 		return javaCore, true
-	case ver == "core_current":
+	case ver.kind == sdkCore:
 		return javaCore, false
 	case name == "android_system_stubs_current":
 		return javaSystem, true
-	case strings.HasPrefix(ver, "system_"):
+	case ver.kind == sdkSystem:
 		return javaSystem, false
 	case name == "android_test_stubs_current":
 		return javaSystem, true
-	case strings.HasPrefix(ver, "test_"):
+	case ver.kind == sdkTest:
 		return javaPlatform, false
 	case name == "android_stubs_current":
 		return javaSdk, true
-	case ver == "current":
+	case ver.kind == sdkPublic:
 		return javaSdk, false
-	case ver == "" || ver == "none" || ver == "core_platform":
+	case ver.kind == sdkPrivate || ver.kind == sdkNone || ver.kind == sdkCorePlatform:
 		return javaPlatform, false
+	case !ver.valid():
+		panic(fmt.Errorf("sdk_version is invalid. got %q", ver.raw))
 	default:
-		if _, err := strconv.Atoi(ver); err != nil {
-			panic(fmt.Errorf("expected sdk_version to be a number, got %q", ver))
-		}
 		return javaSdk, false
 	}
 }
@@ -992,19 +990,7 @@
 }
 
 func getJavaVersion(ctx android.ModuleContext, javaVersion string, sdkContext sdkContext) javaVersion {
-	v := sdkContext.sdkVersion()
-	// For PDK builds, use the latest SDK version instead of "current"
-	if ctx.Config().IsPdkBuild() &&
-		(v == "" || v == "none" || v == "core_platform" || v == "current") {
-		sdkVersions := ctx.Config().Get(sdkVersionsKey).([]int)
-		latestSdkVersion := 0
-		if len(sdkVersions) > 0 {
-			latestSdkVersion = sdkVersions[len(sdkVersions)-1]
-		}
-		v = strconv.Itoa(latestSdkVersion)
-	}
-
-	sdk, err := sdkVersionToNumber(ctx, v)
+	sdk, err := sdkContext.sdkVersion().effectiveVersion(ctx)
 	if err != nil {
 		ctx.PropertyErrorf("sdk_version", "%s", err)
 	}
@@ -2282,11 +2268,11 @@
 	exportedSdkLibs       []string
 }
 
-func (j *Import) sdkVersion() string {
-	return String(j.properties.Sdk_version)
+func (j *Import) sdkVersion() sdkSpec {
+	return sdkSpecFrom(String(j.properties.Sdk_version))
 }
 
-func (j *Import) minSdkVersion() string {
+func (j *Import) minSdkVersion() sdkSpec {
 	return j.sdkVersion()
 }
 
diff --git a/java/sdk.go b/java/sdk.go
index 2dbcf4a..f388358 100644
--- a/java/sdk.go
+++ b/java/sdk.go
@@ -38,14 +38,16 @@
 var apiFingerprintPathKey = android.NewOnceKey("apiFingerprintPathKey")
 
 type sdkContext interface {
-	// sdkVersion returns the sdk_version property of the current module, or an empty string if it is not set.
-	sdkVersion() string
+	// sdkVersion returns sdkSpec that corresponds to the sdk_version property of the current module
+	sdkVersion() sdkSpec
 	// systemModules returns the system_modules property of the current module, or an empty string if it is not set.
 	systemModules() string
-	// minSdkVersion returns the min_sdk_version property of the current module, or sdkVersion() if it is not set.
-	minSdkVersion() string
-	// targetSdkVersion returns the target_sdk_version property of the current module, or sdkVersion() if it is not set.
-	targetSdkVersion() string
+	// minSdkVersion returns sdkSpec that corresponds to the min_sdk_version property of the current module,
+	// or from sdk_version if it is not set.
+	minSdkVersion() sdkSpec
+	// targetSdkVersion returns the sdkSpec that corresponds to the target_sdk_version property of the current module,
+	// or from sdk_version if it is not set.
+	targetSdkVersion() sdkSpec
 }
 
 func UseApiFingerprint(ctx android.BaseModuleContext, v string) bool {
@@ -58,79 +60,236 @@
 	return false
 }
 
-func sdkVersionOrDefault(ctx android.BaseModuleContext, v string) string {
-	var sdkVersion string
-	switch v {
-	case "", "none", "current", "test_current", "system_current", "core_current", "core_platform":
-		sdkVersion = ctx.Config().DefaultAppTargetSdk()
+// sdkKind represents a particular category of an SDK spec like public, system, test, etc.
+type sdkKind int
+
+const (
+	sdkInvalid sdkKind = iota
+	sdkNone
+	sdkCore
+	sdkCorePlatform
+	sdkPublic
+	sdkSystem
+	sdkTest
+	sdkPrivate
+)
+
+// String returns the string representation of this sdkKind
+func (k sdkKind) String() string {
+	switch k {
+	case sdkPrivate:
+		return "private"
+	case sdkNone:
+		return "none"
+	case sdkPublic:
+		return "public"
+	case sdkSystem:
+		return "system"
+	case sdkTest:
+		return "test"
+	case sdkCore:
+		return "core"
+	case sdkCorePlatform:
+		return "core_platform"
 	default:
-		sdkVersion = v
+		return "invalid"
 	}
-	if UseApiFingerprint(ctx, sdkVersion) {
-		apiFingerprint := ApiFingerprintPath(ctx)
-		sdkVersion += fmt.Sprintf(".$$(cat %s)", apiFingerprint.String())
-	}
-	return sdkVersion
 }
 
-// Returns a sdk version as a number.  For modules targeting an unreleased SDK (meaning it does not yet have a number)
-// it returns android.FutureApiLevel (10000).
-func sdkVersionToNumber(ctx android.EarlyModuleContext, v string) (int, error) {
-	switch v {
-	case "", "none", "current", "test_current", "system_current", "core_current", "core_platform":
-		return ctx.Config().DefaultAppTargetSdkInt(), nil
-	default:
-		n := android.GetNumericSdkVersion(v)
-		if i, err := strconv.Atoi(n); err != nil {
-			return -1, fmt.Errorf("invalid sdk version %q", n)
-		} else {
-			return i, nil
+// sdkVersion represents a specific version number of an SDK spec of a particular kind
+type sdkVersion int
+
+const (
+	// special version number for a not-yet-frozen SDK
+	sdkVersionCurrent sdkVersion = sdkVersion(android.FutureApiLevel)
+	// special version number to be used for SDK specs where version number doesn't
+	// make sense, e.g. "none", "", etc.
+	sdkVersionNone sdkVersion = sdkVersion(0)
+)
+
+// isCurrent checks if the sdkVersion refers to the not-yet-published version of an sdkKind
+func (v sdkVersion) isCurrent() bool {
+	return v == sdkVersionCurrent
+}
+
+// isNumbered checks if the sdkVersion refers to the published (a.k.a numbered) version of an sdkKind
+func (v sdkVersion) isNumbered() bool {
+	return !v.isCurrent() && v != sdkVersionNone
+}
+
+// String returns the string representation of this sdkVersion.
+func (v sdkVersion) String() string {
+	if v.isCurrent() {
+		return "current"
+	} else if v.isNumbered() {
+		return strconv.Itoa(int(v))
+	}
+	return "(no version)"
+}
+
+// asNumberString directly converts the numeric value of this sdk version as a string.
+// When isNumbered() is true, this method is the same as String(). However, for sdkVersionCurrent
+// and sdkVersionNone, this returns 10000 and 0 while String() returns "current" and "(no version"),
+// respectively.
+func (v sdkVersion) asNumberString() string {
+	return strconv.Itoa(int(v))
+}
+
+// sdkSpec represents the kind and the version of an SDK for a module to build against
+type sdkSpec struct {
+	kind    sdkKind
+	version sdkVersion
+	raw     string
+}
+
+// valid checks if this sdkSpec is well-formed. Note however that true doesn't mean that the
+// specified SDK actually exists.
+func (s sdkSpec) valid() bool {
+	return s.kind != sdkInvalid
+}
+
+// specified checks if this sdkSpec is well-formed and is not "".
+func (s sdkSpec) specified() bool {
+	return s.valid() && s.kind != sdkPrivate
+}
+
+// prebuiltSdkAvailableForUnbundledBuilt tells whether this sdkSpec can have a prebuilt SDK
+// that can be used for unbundled builds.
+func (s sdkSpec) prebuiltSdkAvailableForUnbundledBuild() bool {
+	// "", "none", and "core_platform" are not available for unbundled build
+	// as we don't/can't have prebuilt stub for the versions
+	return s.kind != sdkPrivate && s.kind != sdkNone && s.kind != sdkCorePlatform
+}
+
+// forPdkBuild converts this sdkSpec into another sdkSpec that is for the PDK builds.
+func (s sdkSpec) forPdkBuild(ctx android.EarlyModuleContext) sdkSpec {
+	// For PDK builds, use the latest SDK version instead of "current" or ""
+	if s.kind == sdkPrivate || s.kind == sdkPublic {
+		kind := s.kind
+		if kind == sdkPrivate {
+			// We don't have prebuilt SDK for private APIs, so use the public SDK
+			// instead. This looks odd, but that's how it has been done.
+			// TODO(b/148271073): investigate the need for this.
+			kind = sdkPublic
 		}
+		version := sdkVersion(LatestSdkVersionInt(ctx))
+		return sdkSpec{kind, version, s.raw}
 	}
+	return s
 }
 
-func sdkVersionToNumberAsString(ctx android.EarlyModuleContext, v string) (string, error) {
-	n, err := sdkVersionToNumber(ctx, v)
-	if err != nil {
-		return "", err
+// usePrebuilt determines whether prebuilt SDK should be used for this sdkSpec with the given context.
+func (s sdkSpec) usePrebuilt(ctx android.EarlyModuleContext) bool {
+	if s.version.isCurrent() {
+		// "current" can be built from source and be from prebuilt SDK
+		return ctx.Config().UnbundledBuildUsePrebuiltSdks()
+	} else if s.version.isNumbered() {
+		// sanity check
+		if s.kind != sdkPublic && s.kind != sdkSystem && s.kind != sdkTest {
+			panic(fmt.Errorf("prebuilt SDK is not not available for sdkKind=%q", s.kind))
+			return false
+		}
+		// numbered SDKs are always from prebuilt
+		return true
 	}
-	return strconv.Itoa(n), nil
+	// "", "none", "core_platform" fall here
+	return false
+}
+
+// effectiveVersion converts an sdkSpec into the concrete sdkVersion that the module
+// should use. For modules targeting an unreleased SDK (meaning it does not yet have a number)
+// it returns android.FutureApiLevel(10000).
+func (s sdkSpec) effectiveVersion(ctx android.EarlyModuleContext) (sdkVersion, error) {
+	if !s.valid() {
+		return s.version, fmt.Errorf("invalid sdk version %q", s.raw)
+	}
+	if ctx.Config().IsPdkBuild() {
+		s = s.forPdkBuild(ctx)
+	}
+	if s.version.isNumbered() {
+		return s.version, nil
+	}
+	return sdkVersion(ctx.Config().DefaultAppTargetSdkInt()), nil
+}
+
+// effectiveVersionString converts an sdkSpec into the concrete version string that the module
+// should use. For modules targeting an unreleased SDK (meaning it does not yet have a number)
+// it returns the codename (P, Q, R, etc.)
+func (s sdkSpec) effectiveVersionString(ctx android.EarlyModuleContext) (string, error) {
+	ver, err := s.effectiveVersion(ctx)
+	if err == nil && int(ver) == ctx.Config().DefaultAppTargetSdkInt() {
+		return ctx.Config().DefaultAppTargetSdk(), nil
+	}
+	return ver.String(), err
+}
+
+func sdkSpecFrom(str string) sdkSpec {
+	switch str {
+	// special cases first
+	case "":
+		return sdkSpec{sdkPrivate, sdkVersionNone, str}
+	case "none":
+		return sdkSpec{sdkNone, sdkVersionNone, str}
+	case "core_platform":
+		return sdkSpec{sdkCorePlatform, sdkVersionNone, str}
+	default:
+		// the syntax is [kind_]version
+		sep := strings.LastIndex(str, "_")
+
+		var kindString string
+		if sep == 0 {
+			return sdkSpec{sdkInvalid, sdkVersionNone, str}
+		} else if sep == -1 {
+			kindString = ""
+		} else {
+			kindString = str[0:sep]
+		}
+		versionString := str[sep+1 : len(str)]
+
+		var kind sdkKind
+		switch kindString {
+		case "":
+			kind = sdkPublic
+		case "core":
+			kind = sdkCore
+		case "system":
+			kind = sdkSystem
+		case "test":
+			kind = sdkTest
+		default:
+			return sdkSpec{sdkInvalid, sdkVersionNone, str}
+		}
+
+		var version sdkVersion
+		if versionString == "current" {
+			version = sdkVersionCurrent
+		} else if i, err := strconv.Atoi(versionString); err == nil {
+			version = sdkVersion(i)
+		} else {
+			return sdkSpec{sdkInvalid, sdkVersionNone, str}
+		}
+
+		return sdkSpec{kind, version, str}
+	}
 }
 
 func decodeSdkDep(ctx android.EarlyModuleContext, sdkContext sdkContext) sdkDep {
-	v := sdkContext.sdkVersion()
-
-	// For PDK builds, use the latest SDK version instead of "current"
-	if ctx.Config().IsPdkBuild() && (v == "" || v == "current") {
-		sdkVersions := ctx.Config().Get(sdkVersionsKey).([]int)
-		latestSdkVersion := 0
-		if len(sdkVersions) > 0 {
-			latestSdkVersion = sdkVersions[len(sdkVersions)-1]
-		}
-		v = strconv.Itoa(latestSdkVersion)
-	}
-
-	numericSdkVersion, err := sdkVersionToNumber(ctx, v)
-	if err != nil {
-		ctx.PropertyErrorf("sdk_version", "%s", err)
+	sdkVersion := sdkContext.sdkVersion()
+	if !sdkVersion.valid() {
+		ctx.PropertyErrorf("sdk_version", "invalid version %q", sdkVersion.raw)
 		return sdkDep{}
 	}
 
-	toPrebuilt := func(sdk string) sdkDep {
-		var api, v string
-		if strings.Contains(sdk, "_") {
-			t := strings.Split(sdk, "_")
-			api = t[0]
-			v = t[1]
-		} else {
-			api = "public"
-			v = sdk
-		}
-		dir := filepath.Join("prebuilts", "sdk", v, api)
+	if ctx.Config().IsPdkBuild() {
+		sdkVersion = sdkVersion.forPdkBuild(ctx)
+	}
+
+	if sdkVersion.usePrebuilt(ctx) {
+		dir := filepath.Join("prebuilts", "sdk", sdkVersion.version.String(), sdkVersion.kind.String())
 		jar := filepath.Join(dir, "android.jar")
 		// There's no aidl for other SDKs yet.
 		// TODO(77525052): Add aidl files for other SDKs too.
-		public_dir := filepath.Join("prebuilts", "sdk", v, "public")
+		public_dir := filepath.Join("prebuilts", "sdk", sdkVersion.version.String(), "public")
 		aidl := filepath.Join(public_dir, "framework.aidl")
 		jarPath := android.ExistentPathForSource(ctx, jar)
 		aidlPath := android.ExistentPathForSource(ctx, aidl)
@@ -139,17 +298,17 @@
 		if (!jarPath.Valid() || !aidlPath.Valid()) && ctx.Config().AllowMissingDependencies() {
 			return sdkDep{
 				invalidVersion: true,
-				bootclasspath:  []string{fmt.Sprintf("sdk_%s_%s_android", api, v)},
+				bootclasspath:  []string{fmt.Sprintf("sdk_%s_%s_android", sdkVersion.kind, sdkVersion.version.String())},
 			}
 		}
 
 		if !jarPath.Valid() {
-			ctx.PropertyErrorf("sdk_version", "invalid sdk version %q, %q does not exist", sdk, jar)
+			ctx.PropertyErrorf("sdk_version", "invalid sdk version %q, %q does not exist", sdkVersion.raw, jar)
 			return sdkDep{}
 		}
 
 		if !aidlPath.Valid() {
-			ctx.PropertyErrorf("sdk_version", "invalid sdk version %q, %q does not exist", sdk, aidl)
+			ctx.PropertyErrorf("sdk_version", "invalid sdk version %q, %q does not exist", sdkVersion.raw, aidl)
 			return sdkDep{}
 		}
 
@@ -173,31 +332,26 @@
 
 	// Ensures that the specificed system SDK version is one of BOARD_SYSTEMSDK_VERSIONS (for vendor apks)
 	// or PRODUCT_SYSTEMSDK_VERSIONS (for other apks or when BOARD_SYSTEMSDK_VERSIONS is not set)
-	if strings.HasPrefix(v, "system_") && numericSdkVersion != android.FutureApiLevel {
+	if sdkVersion.kind == sdkSystem && sdkVersion.version.isNumbered() {
 		allowed_versions := ctx.DeviceConfig().PlatformSystemSdkVersions()
 		if ctx.DeviceSpecific() || ctx.SocSpecific() {
 			if len(ctx.DeviceConfig().SystemSdkVersions()) > 0 {
 				allowed_versions = ctx.DeviceConfig().SystemSdkVersions()
 			}
 		}
-		if len(allowed_versions) > 0 && !android.InList(strconv.Itoa(numericSdkVersion), allowed_versions) {
+		if len(allowed_versions) > 0 && !android.InList(sdkVersion.version.String(), allowed_versions) {
 			ctx.PropertyErrorf("sdk_version", "incompatible sdk version %q. System SDK version should be one of %q",
-				v, allowed_versions)
+				sdkVersion.raw, allowed_versions)
 		}
 	}
 
-	if ctx.Config().UnbundledBuildUsePrebuiltSdks() &&
-		v != "" && v != "none" && v != "core_platform" {
-		return toPrebuilt(v)
-	}
-
-	switch v {
-	case "":
+	switch sdkVersion.kind {
+	case sdkPrivate:
 		return sdkDep{
 			useDefaultLibs:     true,
 			frameworkResModule: "framework-res",
 		}
-	case "none":
+	case sdkNone:
 		systemModules := sdkContext.systemModules()
 		if systemModules == "" {
 			ctx.PropertyErrorf("sdk_version",
@@ -214,22 +368,22 @@
 			systemModules:  systemModules,
 			bootclasspath:  []string{systemModules},
 		}
-	case "core_platform":
+	case sdkCorePlatform:
 		return sdkDep{
 			useDefaultLibs:     true,
 			frameworkResModule: "framework-res",
 			noFrameworksLibs:   true,
 		}
-	case "current":
+	case sdkPublic:
 		return toModule("android_stubs_current", "framework-res", sdkFrameworkAidlPath(ctx))
-	case "system_current":
+	case sdkSystem:
 		return toModule("android_system_stubs_current", "framework-res", sdkFrameworkAidlPath(ctx))
-	case "test_current":
+	case sdkTest:
 		return toModule("android_test_stubs_current", "framework-res", sdkFrameworkAidlPath(ctx))
-	case "core_current":
+	case sdkCore:
 		return toModule("core.current.stubs", "", nil)
 	default:
-		return toPrebuilt(v)
+		panic(fmt.Errorf("invalid sdk %q", sdkVersion.raw))
 	}
 }
 
@@ -262,6 +416,15 @@
 	ctx.Config().Once(sdkVersionsKey, func() interface{} { return sdkVersions })
 }
 
+func LatestSdkVersionInt(ctx android.EarlyModuleContext) int {
+	sdkVersions := ctx.Config().Get(sdkVersionsKey).([]int)
+	latestSdkVersion := 0
+	if len(sdkVersions) > 0 {
+		latestSdkVersion = sdkVersions[len(sdkVersions)-1]
+	}
+	return latestSdkVersion
+}
+
 func sdkSingletonFactory() android.Singleton {
 	return sdkSingleton{}
 }
diff --git a/java/sdk_library.go b/java/sdk_library.go
index 72c4a7c..530e02c 100644
--- a/java/sdk_library.go
+++ b/java/sdk_library.go
@@ -650,27 +650,27 @@
 	mctx.CreateModule(android.PrebuiltEtcFactory, &etcProps)
 }
 
-func (module *SdkLibrary) PrebuiltJars(ctx android.BaseModuleContext, sdkVersion string) android.Paths {
-	var api, v string
-	if sdkVersion == "" || sdkVersion == "none" {
-		api = "system"
-		v = "current"
-	} else if strings.Contains(sdkVersion, "_") {
-		t := strings.Split(sdkVersion, "_")
-		api = t[0]
-		v = t[1]
+func (module *SdkLibrary) PrebuiltJars(ctx android.BaseModuleContext, s sdkSpec) android.Paths {
+	var ver sdkVersion
+	var kind sdkKind
+	if s.usePrebuilt(ctx) {
+		ver = s.version
+		kind = s.kind
 	} else {
-		api = "public"
-		v = sdkVersion
+		// We don't have prebuilt SDK for the specific sdkVersion.
+		// Instead of breaking the build, fallback to use "system_current"
+		ver = sdkVersionCurrent
+		kind = sdkSystem
 	}
-	dir := filepath.Join("prebuilts", "sdk", v, api)
+
+	dir := filepath.Join("prebuilts", "sdk", ver.String(), kind.String())
 	jar := filepath.Join(dir, module.BaseModuleName()+".jar")
 	jarPath := android.ExistentPathForSource(ctx, jar)
 	if !jarPath.Valid() {
 		if ctx.Config().AllowMissingDependencies() {
 			return android.Paths{android.PathForSource(ctx, jar)}
 		} else {
-			ctx.PropertyErrorf("sdk_library", "invalid sdk version %q, %q does not exist", sdkVersion, jar)
+			ctx.PropertyErrorf("sdk_library", "invalid sdk version %q, %q does not exist", s.raw, jar)
 		}
 		return nil
 	}
@@ -678,32 +678,34 @@
 }
 
 // to satisfy SdkLibraryDependency interface
-func (module *SdkLibrary) SdkHeaderJars(ctx android.BaseModuleContext, sdkVersion string) android.Paths {
+func (module *SdkLibrary) SdkHeaderJars(ctx android.BaseModuleContext, sdkVersion sdkSpec) android.Paths {
 	// This module is just a wrapper for the stubs.
 	if ctx.Config().UnbundledBuildUsePrebuiltSdks() {
 		return module.PrebuiltJars(ctx, sdkVersion)
 	} else {
-		if strings.HasPrefix(sdkVersion, "system_") {
+		switch sdkVersion.kind {
+		case sdkSystem:
 			return module.systemApiStubsPath
-		} else if sdkVersion == "" {
+		case sdkPrivate:
 			return module.Library.HeaderJars()
-		} else {
+		default:
 			return module.publicApiStubsPath
 		}
 	}
 }
 
 // to satisfy SdkLibraryDependency interface
-func (module *SdkLibrary) SdkImplementationJars(ctx android.BaseModuleContext, sdkVersion string) android.Paths {
+func (module *SdkLibrary) SdkImplementationJars(ctx android.BaseModuleContext, sdkVersion sdkSpec) android.Paths {
 	// This module is just a wrapper for the stubs.
 	if ctx.Config().UnbundledBuildUsePrebuiltSdks() {
 		return module.PrebuiltJars(ctx, sdkVersion)
 	} else {
-		if strings.HasPrefix(sdkVersion, "system_") {
+		switch sdkVersion.kind {
+		case sdkSystem:
 			return module.systemApiStubsImplPath
-		} else if sdkVersion == "" {
+		case sdkPrivate:
 			return module.Library.ImplementationJars()
-		} else {
+		default:
 			return module.publicApiStubsImplPath
 		}
 	}
@@ -923,13 +925,13 @@
 }
 
 // to satisfy SdkLibraryDependency interface
-func (module *sdkLibraryImport) SdkHeaderJars(ctx android.BaseModuleContext, sdkVersion string) android.Paths {
+func (module *sdkLibraryImport) SdkHeaderJars(ctx android.BaseModuleContext, sdkVersion sdkSpec) android.Paths {
 	// This module is just a wrapper for the prebuilt stubs.
 	return module.stubsPath
 }
 
 // to satisfy SdkLibraryDependency interface
-func (module *sdkLibraryImport) SdkImplementationJars(ctx android.BaseModuleContext, sdkVersion string) android.Paths {
+func (module *sdkLibraryImport) SdkImplementationJars(ctx android.BaseModuleContext, sdkVersion sdkSpec) android.Paths {
 	// This module is just a wrapper for the stubs.
 	return module.stubsPath
 }