Write module dexpreopt.config for Make.

This is needed for Java libraries that are <uses-library> dependencies
of Java libraries and apps defined as Make modules. Each dexpreopted
module in Make generates a dexpreopt.config file, which incorporates
information from its dependencies' dexpreopt.config files. For
dependencies that are Make modules their dexpreopt.config files are
generated by Make, and for Soong modules they are generated by Soong.
Since Soong doesn't know which libraries are used by Make, it generates
build rules for a superset of the necessary libraries.

Bug: 132357300
Test: lunch aosp_cf_x86_phone-userdebug && m
Change-Id: I325b1037658736ee3c02450b08c00eca1a175962
diff --git a/dexpreopt/class_loader_context.go b/dexpreopt/class_loader_context.go
index bf610ef..ec62eb3 100644
--- a/dexpreopt/class_loader_context.go
+++ b/dexpreopt/class_loader_context.go
@@ -534,3 +534,26 @@
 	}
 	return clcs
 }
+
+// Convert Soong CLC map to JSON representation for Make.
+func toJsonClassLoaderContext(clcMap ClassLoaderContextMap) jsonClassLoaderContextMap {
+	jClcMap := make(jsonClassLoaderContextMap)
+	for sdkVer, clcs := range clcMap {
+		sdkVerStr := fmt.Sprintf("%d", sdkVer)
+		jClcMap[sdkVerStr] = toJsonClassLoaderContextRec(clcs)
+	}
+	return jClcMap
+}
+
+// Recursive helper for toJsonClassLoaderContext.
+func toJsonClassLoaderContextRec(clcs []*ClassLoaderContext) map[string]*jsonClassLoaderContext {
+	jClcs := make(map[string]*jsonClassLoaderContext, len(clcs))
+	for _, clc := range clcs {
+		jClcs[clc.Name] = &jsonClassLoaderContext{
+			Host:        clc.Host.String(),
+			Device:      clc.Device,
+			Subcontexts: toJsonClassLoaderContextRec(clc.Subcontexts),
+		}
+	}
+	return jClcs
+}
diff --git a/dexpreopt/config.go b/dexpreopt/config.go
index 867ece6..d55204b 100644
--- a/dexpreopt/config.go
+++ b/dexpreopt/config.go
@@ -114,6 +114,7 @@
 	ProfileBootListing   android.OptionalPath
 
 	EnforceUsesLibraries bool
+	ProvidesUsesLibrary  string // the name of the <uses-library> (usually the same as its module)
 	ClassLoaderContexts  ClassLoaderContextMap
 
 	Archs                   []android.ArchType
@@ -290,6 +291,42 @@
 	return config.ModuleConfig, nil
 }
 
+// WriteSlimModuleConfigForMake serializes a subset of ModuleConfig into a per-module
+// dexpreopt.config JSON file. It is a way to pass dexpreopt information about Soong modules to
+// Make, which is needed when a Make module has a <uses-library> dependency on a Soong module.
+func WriteSlimModuleConfigForMake(ctx android.ModuleContext, config *ModuleConfig, path android.WritablePath) {
+	if path == nil {
+		return
+	}
+
+	// JSON representation of the slim module dexpreopt.config.
+	type slimModuleJSONConfig struct {
+		Name                 string
+		DexLocation          string
+		BuildPath            string
+		EnforceUsesLibraries bool
+		ProvidesUsesLibrary  string
+		ClassLoaderContexts  jsonClassLoaderContextMap
+	}
+
+	jsonConfig := &slimModuleJSONConfig{
+		Name:                 config.Name,
+		DexLocation:          config.DexLocation,
+		BuildPath:            config.BuildPath.String(),
+		EnforceUsesLibraries: config.EnforceUsesLibraries,
+		ProvidesUsesLibrary:  config.ProvidesUsesLibrary,
+		ClassLoaderContexts:  toJsonClassLoaderContext(config.ClassLoaderContexts),
+	}
+
+	data, err := json.MarshalIndent(jsonConfig, "", "    ")
+	if err != nil {
+		ctx.ModuleErrorf("failed to JSON marshal module dexpreopt.config: %v", err)
+		return
+	}
+
+	android.WriteFileRule(ctx, path, string(data))
+}
+
 // dex2oatModuleName returns the name of the module to use for the dex2oat host
 // tool. It should be a binary module with public visibility that is compiled
 // and installed for host.
diff --git a/java/androidmk.go b/java/androidmk.go
index cc454b0..21f3012 100644
--- a/java/androidmk.go
+++ b/java/androidmk.go
@@ -125,6 +125,10 @@
 					entries.SetString("LOCAL_MODULE_STEM", library.Stem())
 
 					entries.SetOptionalPaths("LOCAL_SOONG_LINT_REPORTS", library.linter.reports)
+
+					if library.dexpreopter.configPath != nil {
+						entries.SetPath("LOCAL_SOONG_DEXPREOPT_CONFIG", library.dexpreopter.configPath)
+					}
 				},
 			},
 		})
diff --git a/java/app.go b/java/app.go
index e6c9a2d..ce89e9b 100755
--- a/java/app.go
+++ b/java/app.go
@@ -455,6 +455,7 @@
 
 func (a *AndroidApp) dexBuildActions(ctx android.ModuleContext) android.Path {
 	a.dexpreopter.installPath = a.installPath(ctx)
+	a.dexpreopter.isApp = true
 	if a.dexProperties.Uncompress_dex == nil {
 		// If the value was not force-set by the user, use reasonable default based on the module.
 		a.dexProperties.Uncompress_dex = proptools.BoolPtr(a.shouldUncompressDex(ctx))
diff --git a/java/app_import.go b/java/app_import.go
index df940f1..6f21bfb 100644
--- a/java/app_import.go
+++ b/java/app_import.go
@@ -255,6 +255,7 @@
 		installDir = android.PathForModuleInstall(ctx, "app", a.BaseModuleName())
 	}
 
+	a.dexpreopter.isApp = true
 	a.dexpreopter.installPath = installDir.Join(ctx, a.BaseModuleName()+".apk")
 	a.dexpreopter.isPresignedPrebuilt = Bool(a.properties.Presigned)
 	a.dexpreopter.uncompressedDex = a.shouldUncompressDex(ctx)
diff --git a/java/dexpreopt.go b/java/dexpreopt.go
index b5830c7..ac00592 100644
--- a/java/dexpreopt.go
+++ b/java/dexpreopt.go
@@ -30,6 +30,7 @@
 	installPath         android.InstallPath
 	uncompressedDex     bool
 	isSDKLibrary        bool
+	isApp               bool
 	isTest              bool
 	isPresignedPrebuilt bool
 
@@ -38,6 +39,11 @@
 	classLoaderContexts dexpreopt.ClassLoaderContextMap
 
 	builtInstalled string
+
+	// A path to a dexpreopt.config file generated by Soong for libraries that may be used as a
+	// <uses-library> by Make modules. The path is passed to Make via LOCAL_SOONG_DEXPREOPT_CONFIG
+	// variable. If the path is nil, no config is generated (which is the case for apps and tests).
+	configPath android.WritablePath
 }
 
 type DexpreoptProperties struct {
@@ -117,7 +123,40 @@
 	// the dexpreopter struct hasn't been fully initialized before we're called,
 	// e.g. in aar.go. This keeps the behaviour that dexpreopting is effectively
 	// disabled, even if installable is true.
-	if d.dexpreoptDisabled(ctx) || d.installPath.Base() == "." {
+	if d.installPath.Base() == "." {
+		return
+	}
+
+	dexLocation := android.InstallPathToOnDevicePath(ctx, d.installPath)
+
+	buildPath := android.PathForModuleOut(ctx, "dexpreopt", ctx.ModuleName()+".jar").OutputPath
+
+	providesUsesLib := ctx.ModuleName()
+	if ulib, ok := ctx.Module().(ProvidesUsesLib); ok {
+		name := ulib.ProvidesUsesLib()
+		if name != nil {
+			providesUsesLib = *name
+		}
+	}
+
+	if !d.isApp && !d.isTest {
+		// Slim dexpreopt config is serialized to dexpreopt.config files and used by
+		// dex_preopt_config_merger.py to get information about <uses-library> dependencies.
+		// Note that it might be needed even if dexpreopt is disabled for this module.
+		slimDexpreoptConfig := &dexpreopt.ModuleConfig{
+			Name:                 ctx.ModuleName(),
+			DexLocation:          dexLocation,
+			BuildPath:            buildPath,
+			EnforceUsesLibraries: d.enforceUsesLibs,
+			ProvidesUsesLibrary:  providesUsesLib,
+			ClassLoaderContexts:  d.classLoaderContexts,
+			// The rest of the fields are not needed.
+		}
+		d.configPath = android.PathForModuleOut(ctx, "dexpreopt", "dexpreopt.config")
+		dexpreopt.WriteSlimModuleConfigForMake(ctx, slimDexpreoptConfig, d.configPath)
+	}
+
+	if d.dexpreoptDisabled(ctx) {
 		return
 	}
 
@@ -157,8 +196,6 @@
 	// The image locations for all Android variants are identical.
 	imageLocations := bootImage.getAnyAndroidVariant().imageLocations()
 
-	dexLocation := android.InstallPathToOnDevicePath(ctx, d.installPath)
-
 	var profileClassListing android.OptionalPath
 	var profileBootListing android.OptionalPath
 	profileIsTextListing := false
@@ -177,10 +214,11 @@
 		}
 	}
 
+	// Full dexpreopt config, used to create dexpreopt build rules.
 	dexpreoptConfig := &dexpreopt.ModuleConfig{
 		Name:            ctx.ModuleName(),
 		DexLocation:     dexLocation,
-		BuildPath:       android.PathForModuleOut(ctx, "dexpreopt", ctx.ModuleName()+".jar").OutputPath,
+		BuildPath:       buildPath,
 		DexPath:         dexJarFile,
 		ManifestPath:    d.manifestFile,
 		UncompressedDex: d.uncompressedDex,
@@ -192,6 +230,7 @@
 		ProfileBootListing:   profileBootListing,
 
 		EnforceUsesLibraries: d.enforceUsesLibs,
+		ProvidesUsesLibrary:  providesUsesLib,
 		ClassLoaderContexts:  d.classLoaderContexts,
 
 		Archs:                   archs,
diff --git a/java/dexpreopt_test.go b/java/dexpreopt_test.go
index 5550a4c..a9e0773 100644
--- a/java/dexpreopt_test.go
+++ b/java/dexpreopt_test.go
@@ -148,7 +148,7 @@
 		t.Run(test.name, func(t *testing.T) {
 			ctx, _ := testJava(t, test.bp)
 
-			dexpreopt := ctx.ModuleForTests("foo", "android_common").MaybeDescription("dexpreopt")
+			dexpreopt := ctx.ModuleForTests("foo", "android_common").MaybeRule("dexpreopt")
 			enabled := dexpreopt.Rule != nil
 
 			if enabled != test.enabled {