Generic configuration for generic system modules.

Config object includes a copy of itself for the generic configuration.
The generic configuration replaces any product-specific configurations
with the generic information.
When a module context gets Config() object, it returns the generic
configuration if the module sets use_generic_config to true. Otherwise,
Config() returns the original config object as before.

By adding `generic:"<value>"` annotation to the product variable, the
variables will be initialized with the <value> for the generic configs.
If the <value> is "unset", the variable will be unset.

The generic modules can be included in the shared system image to be
installed in multiple targets.

Bug: 361816274
Test: m nothing --no-skip-soong-tests
Change-Id: I15e4ade17ad1a8969f8e0e91d994b60545dc412f
diff --git a/android/config.go b/android/config.go
index d47f0d4..e026782 100644
--- a/android/config.go
+++ b/android/config.go
@@ -22,6 +22,7 @@
 	"fmt"
 	"os"
 	"path/filepath"
+	"reflect"
 	"runtime"
 	"strconv"
 	"strings"
@@ -108,6 +109,10 @@
 
 const testKeyDir = "build/make/target/product/security"
 
+func (c Config) genericConfig() Config {
+	return Config{c.config.genericConfig}
+}
+
 // SoongOutDir returns the build output directory for the configuration.
 func (c Config) SoongOutDir() string {
 	return c.soongOutDir
@@ -372,7 +377,7 @@
 	// regenerate build.ninja.
 	ninjaFileDepsSet sync.Map
 
-	OncePer
+	*OncePer
 
 	// If buildFromSourceStub is true then the Java API stubs are
 	// built from the source Java files, not the signature text files.
@@ -382,6 +387,17 @@
 	// modules that aren't mixed-built for at least one variant will cause a build
 	// failure
 	ensureAllowlistIntegrity bool
+
+	// If isGeneric is true, this config is the generic config.
+	isGeneric bool
+
+	// InstallPath requires the device name.
+	// This is only for the installPath.
+	deviceNameToInstall *string
+
+	// Copy of this config struct but some product-specific variables are
+	// replaced with the generic configuration values.
+	genericConfig *config
 }
 
 type partialCompileFlags struct {
@@ -639,11 +655,9 @@
 	}
 }
 
-// NewConfig creates a new Config object. The srcDir argument specifies the path
-// to the root source directory. It also loads the config file, if found.
-func NewConfig(cmdArgs CmdArgs, availableEnv map[string]string) (Config, error) {
+func initConfig(cmdArgs CmdArgs, availableEnv map[string]string) (*config, error) {
 	// Make a config with default options.
-	config := &config{
+	newConfig := &config{
 		ProductVariablesFileName: cmdArgs.SoongVariables,
 
 		env: availableEnv,
@@ -657,70 +671,72 @@
 		moduleListFile: cmdArgs.ModuleListFile,
 		fs:             pathtools.NewOsFs(absSrcDir),
 
+		OncePer: &OncePer{},
+
 		buildFromSourceStub: cmdArgs.BuildFromSourceStub,
 	}
 	variant, ok := os.LookupEnv("TARGET_BUILD_VARIANT")
 	isEngBuild := !ok || variant == "eng"
 
-	config.deviceConfig = &deviceConfig{
-		config: config,
+	newConfig.deviceConfig = &deviceConfig{
+		config: newConfig,
 	}
 
 	// Soundness check of the build and source directories. This won't catch strange
 	// configurations with symlinks, but at least checks the obvious case.
 	absBuildDir, err := filepath.Abs(cmdArgs.SoongOutDir)
 	if err != nil {
-		return Config{}, err
+		return &config{}, err
 	}
 
 	absSrcDir, err := filepath.Abs(".")
 	if err != nil {
-		return Config{}, err
+		return &config{}, err
 	}
 
 	if strings.HasPrefix(absSrcDir, absBuildDir) {
-		return Config{}, fmt.Errorf("Build dir must not contain source directory")
+		return &config{}, fmt.Errorf("Build dir must not contain source directory")
 	}
 
 	// Load any configurable options from the configuration file
-	err = loadConfig(config)
+	err = loadConfig(newConfig)
 	if err != nil {
-		return Config{}, err
+		return &config{}, err
 	}
 
 	KatiEnabledMarkerFile := filepath.Join(cmdArgs.SoongOutDir, ".soong.kati_enabled")
 	if _, err := os.Stat(absolutePath(KatiEnabledMarkerFile)); err == nil {
-		config.katiEnabled = true
+		newConfig.katiEnabled = true
 	}
 
-	determineBuildOS(config)
+	determineBuildOS(newConfig)
 
 	// Sets up the map of target OSes to the finer grained compilation targets
 	// that are configured from the product variables.
-	targets, err := decodeTargetProductVariables(config)
+	targets, err := decodeTargetProductVariables(newConfig)
 	if err != nil {
-		return Config{}, err
+		return &config{}, err
 	}
 
-	config.partialCompileFlags, err = config.parsePartialCompileFlags(isEngBuild)
+	newConfig.partialCompileFlags, err = newConfig.parsePartialCompileFlags(isEngBuild)
 	if err != nil {
-		return Config{}, err
+		return &config{}, err
 	}
 
 	// Make the CommonOS OsType available for all products.
 	targets[CommonOS] = []Target{commonTargetMap[CommonOS.Name]}
 
 	var archConfig []archConfig
-	if config.NdkAbis() {
+	if newConfig.NdkAbis() {
 		archConfig = getNdkAbisConfig()
-	} else if config.AmlAbis() {
+	} else if newConfig.AmlAbis() {
 		archConfig = getAmlAbisConfig()
 	}
 
 	if archConfig != nil {
 		androidTargets, err := decodeAndroidArchSettings(archConfig)
 		if err != nil {
-			return Config{}, err
+			return &config{}, err
 		}
 		targets[Android] = androidTargets
 	}
@@ -728,37 +744,113 @@
 	multilib := make(map[string]bool)
 	for _, target := range targets[Android] {
 		if seen := multilib[target.Arch.ArchType.Multilib]; seen {
-			config.multilibConflicts[target.Arch.ArchType] = true
+			newConfig.multilibConflicts[target.Arch.ArchType] = true
 		}
 		multilib[target.Arch.ArchType.Multilib] = true
 	}
 
 	// Map of OS to compilation targets.
-	config.Targets = targets
+	newConfig.Targets = targets
 
 	// Compilation targets for host tools.
-	config.BuildOSTarget = config.Targets[config.BuildOS][0]
-	config.BuildOSCommonTarget = getCommonTargets(config.Targets[config.BuildOS])[0]
+	newConfig.BuildOSTarget = newConfig.Targets[newConfig.BuildOS][0]
+	newConfig.BuildOSCommonTarget = getCommonTargets(newConfig.Targets[newConfig.BuildOS])[0]
 
 	// Compilation targets for Android.
-	if len(config.Targets[Android]) > 0 {
-		config.AndroidCommonTarget = getCommonTargets(config.Targets[Android])[0]
-		config.AndroidFirstDeviceTarget = FirstTarget(config.Targets[Android], "lib64", "lib32")[0]
+	if len(newConfig.Targets[Android]) > 0 {
+		newConfig.AndroidCommonTarget = getCommonTargets(newConfig.Targets[Android])[0]
+		newConfig.AndroidFirstDeviceTarget = FirstTarget(newConfig.Targets[Android], "lib64", "lib32")[0]
 	}
 
 	setBuildMode := func(arg string, mode SoongBuildMode) {
 		if arg != "" {
-			if config.BuildMode != AnalysisNoBazel {
+			if newConfig.BuildMode != AnalysisNoBazel {
 				fmt.Fprintf(os.Stderr, "buildMode is already set, illegal argument: %s", arg)
 				os.Exit(1)
 			}
-			config.BuildMode = mode
+			newConfig.BuildMode = mode
 		}
 	}
 	setBuildMode(cmdArgs.ModuleGraphFile, GenerateModuleGraph)
 	setBuildMode(cmdArgs.DocFile, GenerateDocFile)
 
-	config.productVariables.Build_from_text_stub = boolPtr(config.BuildFromTextStub())
+	newConfig.productVariables.Build_from_text_stub = boolPtr(newConfig.BuildFromTextStub())
+
+	newConfig.deviceNameToInstall = newConfig.productVariables.DeviceName
+
+	return newConfig, err
+}
+
+// Replace variables in config.productVariables that have tags with "generic" key.
+// A generic tag may have a string or an int value for the generic configuration.
+// If the value is "unset", generic configuration will unset the variable.
+func overrideGenericConfig(config *config) {
+	config.genericConfig.isGeneric = true
+	type_pv := reflect.TypeOf(config.genericConfig.productVariables)
+	value_pv := reflect.ValueOf(&config.genericConfig.productVariables)
+	for i := range type_pv.NumField() {
+		type_pv_field := type_pv.Field(i)
+		generic_value := type_pv_field.Tag.Get("generic")
+		// If a product variable has an annotation of "generic" tag, use the
+		// value of the tag to set the generic variable.
+		if generic_value != "" {
+			value_pv_field := value_pv.Elem().Field(i)
+
+			if generic_value == "unset" {
+				// unset the product variable
+				value_pv_field.SetZero()
+				continue
+			}
+
+			kind_of_type_pv := type_pv_field.Type.Kind()
+			is_pointer := false
+			if kind_of_type_pv == reflect.Pointer {
+				is_pointer = true
+				kind_of_type_pv = type_pv_field.Type.Elem().Kind()
+			}
+
+			switch kind_of_type_pv {
+			case reflect.String:
+				if is_pointer {
+					value_pv_field.Set(reflect.ValueOf(stringPtr(generic_value)))
+				} else {
+					value_pv_field.Set(reflect.ValueOf(generic_value))
+				}
+			case reflect.Int:
+				generic_int, err := strconv.Atoi(generic_value)
+				if err != nil {
+					panic(fmt.Errorf("Only an int value can be assigned to int variable: %s", err))
+				}
+				if is_pointer {
+					value_pv_field.Set(reflect.ValueOf(intPtr(generic_int)))
+				} else {
+					value_pv_field.Set(reflect.ValueOf(generic_int))
+				}
+			default:
+				panic(fmt.Errorf("Unknown type to replace for generic variable: %s", &kind_of_type_pv))
+			}
+		}
+	}
+
+	// OncePer must be a singleton.
+	config.genericConfig.OncePer = config.OncePer
+	// keep the device name to get the install path.
+	config.genericConfig.deviceNameToInstall = config.deviceNameToInstall
+}
+
+// NewConfig creates a new Config object. It also loads the config file, if
+// found. The Config object includes a duplicated Config object in it for the
+// generic configuration that does not provide any product specific information.
+func NewConfig(cmdArgs CmdArgs, availableEnv map[string]string) (Config, error) {
+	config, err := initConfig(cmdArgs, availableEnv)
+	if err != nil {
+		return Config{}, err
+	}
+
+	// Initialize generic configuration.
+	config.genericConfig, err = initConfig(cmdArgs, availableEnv)
+	// Update product specific variables with the generic configuration.
+	overrideGenericConfig(config)
 
 	return Config{config}, err
 }
@@ -946,7 +1038,7 @@
 // require them to run and get the current build fingerprint. This ensures they
 // don't rebuild on every incremental build when the build number changes.
 func (c *config) BuildFingerprintFile(ctx PathContext) Path {
-	return PathForArbitraryOutput(ctx, "target", "product", c.DeviceName(), String(c.productVariables.BuildFingerprintFile))
+	return PathForArbitraryOutput(ctx, "target", "product", *c.deviceNameToInstall, String(c.productVariables.BuildFingerprintFile))
 }
 
 // BuildNumberFile returns the path to a text file containing metadata
@@ -974,7 +1066,7 @@
 // require them to run and get the current build thumbprint. This ensures they
 // don't rebuild on every incremental build when the build thumbprint changes.
 func (c *config) BuildThumbprintFile(ctx PathContext) Path {
-	return PathForArbitraryOutput(ctx, "target", "product", c.DeviceName(), String(c.productVariables.BuildThumbprintFile))
+	return PathForArbitraryOutput(ctx, "target", "product", *c.deviceNameToInstall, String(c.productVariables.BuildThumbprintFile))
 }
 
 // DeviceName returns the name of the current device target.
diff --git a/android/config_test.go b/android/config_test.go
index d1b26c1..81b7c3e 100644
--- a/android/config_test.go
+++ b/android/config_test.go
@@ -281,3 +281,72 @@
 		})
 	}
 }
+
+type configTestProperties struct {
+	Use_generic_config *bool
+}
+
+type configTestModule struct {
+	ModuleBase
+	properties configTestProperties
+}
+
+func (d *configTestModule) GenerateAndroidBuildActions(ctx ModuleContext) {
+	deviceName := ctx.Config().DeviceName()
+	if ctx.ModuleName() == "foo" {
+		if ctx.Module().UseGenericConfig() {
+			ctx.PropertyErrorf("use_generic_config", "must not be set for this test")
+		}
+	} else if ctx.ModuleName() == "bar" {
+		if !ctx.Module().UseGenericConfig() {
+			ctx.ModuleErrorf("\"use_generic_config: true\" must be set for this test")
+		}
+	}
+
+	if ctx.Module().UseGenericConfig() {
+		if deviceName != "generic" {
+			ctx.ModuleErrorf("Device name for this module must be \"generic\" but %q\n", deviceName)
+		}
+	} else {
+		if deviceName == "generic" {
+			ctx.ModuleErrorf("Device name for this module must not be \"generic\"\n")
+		}
+	}
+}
+
+func configTestModuleFactory() Module {
+	module := &configTestModule{}
+	module.AddProperties(&module.properties)
+	InitAndroidModule(module)
+	return module
+}
+
+var prepareForConfigTest = GroupFixturePreparers(
+	FixtureRegisterWithContext(func(ctx RegistrationContext) {
+		ctx.RegisterModuleType("test", configTestModuleFactory)
+	}),
+)
+
+func TestGenericConfig(t *testing.T) {
+	bp := `
+		test {
+			name: "foo",
+		}
+
+		test {
+			name: "bar",
+			use_generic_config: true,
+		}
+	`
+
+	result := GroupFixturePreparers(
+		prepareForConfigTest,
+		FixtureWithRootAndroidBp(bp),
+	).RunTest(t)
+
+	foo := result.Module("foo", "").(*configTestModule)
+	bar := result.Module("bar", "").(*configTestModule)
+
+	AssertBoolEquals(t, "Do not use generic config", false, foo.UseGenericConfig())
+	AssertBoolEquals(t, "Use generic config", true, bar.UseGenericConfig())
+}
diff --git a/android/early_module_context.go b/android/early_module_context.go
index 8d28285..300edf1 100644
--- a/android/early_module_context.go
+++ b/android/early_module_context.go
@@ -146,6 +146,13 @@
 }
 
 func (e *earlyModuleContext) Config() Config {
+	// Only the system image may use the generic config.
+	// If a module builds multiple image variations, provide the generic config only for the core
+	// variant which is installed in the system partition. Other image variant may still read the
+	// original configurations.
+	if e.Module().base().UseGenericConfig() && e.Module().base().commonProperties.ImageVariation == "" {
+		return e.EarlyModuleContext.Config().(Config).genericConfig()
+	}
 	return e.EarlyModuleContext.Config().(Config)
 }
 
diff --git a/android/module.go b/android/module.go
index a3fe837..87377cc 100644
--- a/android/module.go
+++ b/android/module.go
@@ -128,6 +128,9 @@
 	// WARNING: This should not be used outside build/soong/fsgen
 	// Overrides returns the list of modules which should not be installed if this module is installed.
 	Overrides() []string
+
+	// If this is true, the module must not read product-specific configurations.
+	UseGenericConfig() bool
 }
 
 // Qualified id for a module
@@ -507,6 +510,10 @@
 	// List of module names that are prevented from being installed when this module gets
 	// installed.
 	Overrides []string
+
+	// Set to true if this module must be generic and does not require product-specific information.
+	// To be included in the system image, this property must be set to true.
+	Use_generic_config *bool
 }
 
 // Properties common to all modules inheriting from ModuleBase. Unlike commonProperties, these
@@ -2588,6 +2595,10 @@
 	return m.commonProperties.Overrides
 }
 
+func (m *ModuleBase) UseGenericConfig() bool {
+	return proptools.Bool(m.commonProperties.Use_generic_config)
+}
+
 type ConfigContext interface {
 	Config() Config
 }
diff --git a/android/module_proxy.go b/android/module_proxy.go
index 77abc11..561c477 100644
--- a/android/module_proxy.go
+++ b/android/module_proxy.go
@@ -231,3 +231,7 @@
 func (m ModuleProxy) VintfFragments(ctx ConfigurableEvaluatorContext) []string {
 	panic("method is not implemented on ModuleProxy")
 }
+
+func (m ModuleProxy) UseGenericConfig() bool {
+	panic("method is not implemented on ModuleProxy")
+}
diff --git a/android/paths.go b/android/paths.go
index 977473f..6612d37 100644
--- a/android/paths.go
+++ b/android/paths.go
@@ -2054,7 +2054,7 @@
 	var partitionPaths []string
 
 	if os.Class == Device {
-		partitionPaths = []string{"target", "product", ctx.Config().DeviceName(), partition}
+		partitionPaths = []string{"target", "product", *ctx.Config().deviceNameToInstall, partition}
 	} else {
 		osName := os.String()
 		if os == Linux {
diff --git a/android/test_config.go b/android/test_config.go
index 3609e6b..5d79df0 100644
--- a/android/test_config.go
+++ b/android/test_config.go
@@ -23,8 +23,7 @@
 	"github.com/google/blueprint/proptools"
 )
 
-// TestConfig returns a Config object for testing.
-func TestConfig(buildDir string, env map[string]string, bp string, fs map[string][]byte) Config {
+func initTestConfig(buildDir string, env map[string]string) *config {
 	envCopy := make(map[string]string)
 	for k, v := range env {
 		envCopy[k] = v
@@ -58,6 +57,7 @@
 		soongOutDir:  filepath.Join(buildDir, "soong"),
 		captureBuild: true,
 		env:          envCopy,
+		OncePer:      &OncePer{},
 
 		// Set testAllowNonExistentPaths so that test contexts don't need to specify every path
 		// passed to PathForSource or PathForModuleSrc.
@@ -69,10 +69,21 @@
 		config: config,
 	}
 	config.TestProductVariables = &config.productVariables
+	config.deviceNameToInstall = config.TestProductVariables.DeviceName
+
+	determineBuildOS(config)
+
+	return config
+}
+
+// TestConfig returns a Config object for testing.
+func TestConfig(buildDir string, env map[string]string, bp string, fs map[string][]byte) Config {
+	config := initTestConfig(buildDir, env)
 
 	config.mockFileSystem(bp, fs)
 
-	determineBuildOS(config)
+	config.genericConfig = initTestConfig(buildDir, env)
+	overrideGenericConfig(config)
 
 	return Config{config}
 }
diff --git a/android/variable.go b/android/variable.go
index 3d5a6ae..8c9a0b1 100644
--- a/android/variable.go
+++ b/android/variable.go
@@ -225,8 +225,8 @@
 	Platform_version_last_stable           *string  `json:",omitempty"`
 	Platform_version_known_codenames       *string  `json:",omitempty"`
 
-	DeviceName                            *string  `json:",omitempty"`
-	DeviceProduct                         *string  `json:",omitempty"`
+	DeviceName                            *string  `json:",omitempty" generic:"generic"`
+	DeviceProduct                         *string  `json:",omitempty" generic:"generic"`
 	DeviceArch                            *string  `json:",omitempty"`
 	DeviceArchVariant                     *string  `json:",omitempty"`
 	DeviceCpuVariant                      *string  `json:",omitempty"`
diff --git a/dexpreopt/config.go b/dexpreopt/config.go
index e57384f..af09dbc 100644
--- a/dexpreopt/config.go
+++ b/dexpreopt/config.go
@@ -139,6 +139,11 @@
 
 // Returns all jars that system_server loads.
 func (g *GlobalConfig) AllSystemServerJars(ctx android.PathContext) *android.ConfiguredJarList {
+	// dexpreopt does not initialize the soong config.
+	// Initialize the OncePer here.
+	if ctx.Config().OncePer == nil {
+		ctx.Config().OncePer = &android.OncePer{}
+	}
 	return ctx.Config().Once(allSystemServerJarsKey, func() interface{} {
 		res := g.AllPlatformSystemServerJars(ctx).AppendList(g.AllApexSystemServerJars(ctx))
 		return &res