Merge "Copy android-info.txt to PRODUCT_OUT" into main
diff --git a/android/androidmk.go b/android/androidmk.go
index 590cce3..9e721e1 100644
--- a/android/androidmk.go
+++ b/android/androidmk.go
@@ -701,11 +701,6 @@
 type androidMkSingleton struct{}
 
 func (c *androidMkSingleton) GenerateBuildActions(ctx SingletonContext) {
-	// Skip if Soong wasn't invoked from Make.
-	if !ctx.Config().KatiEnabled() {
-		return
-	}
-
 	var androidMkModulesList []blueprint.Module
 
 	ctx.VisitAllModulesBlueprint(func(module blueprint.Module) {
@@ -718,6 +713,12 @@
 		return ctx.ModuleName(androidMkModulesList[i]) < ctx.ModuleName(androidMkModulesList[j])
 	})
 
+	// If running in soong-only mode, do a different, more limited version of this singleton
+	if !ctx.Config().KatiEnabled() {
+		c.soongOnlyBuildActions(ctx, androidMkModulesList)
+		return
+	}
+
 	transMk := PathForOutput(ctx, "Android"+String(ctx.Config().productVariables.Make_suffix)+".mk")
 	if ctx.Failed() {
 		return
@@ -736,6 +737,97 @@
 	})
 }
 
+// In soong-only mode, we don't do most of the androidmk stuff. But disted files are still largely
+// defined through the androidmk mechanisms, so this function is an alternate implementation of
+// the androidmk singleton that just focuses on getting the dist contributions
+func (c *androidMkSingleton) soongOnlyBuildActions(ctx SingletonContext, mods []blueprint.Module) {
+	allDistContributions := getDistContributionsFromMods(ctx, mods)
+
+	distMkFile := absolutePath(filepath.Join(ctx.Config().katiPackageMkDir(), "dist.mk"))
+
+	var goalOutputPairs []string
+	var buf strings.Builder
+	buf.WriteString("DIST_SRC_DST_PAIRS := $(sort")
+	for _, contributions := range allDistContributions {
+		for _, copiesForGoal := range contributions.copiesForGoals {
+			goals := strings.Fields(copiesForGoal.goals)
+			for _, copy := range copiesForGoal.copies {
+				for _, goal := range goals {
+					goalOutputPairs = append(goalOutputPairs, fmt.Sprintf("%s:%s", goal, copy.dest))
+				}
+				buf.WriteString(fmt.Sprintf(" %s:%s", copy.from.String(), copy.dest))
+			}
+		}
+	}
+	buf.WriteString(")\nDIST_GOAL_OUTPUT_PAIRS := $(sort ")
+	buf.WriteString(strings.Join(goalOutputPairs, " "))
+	buf.WriteString(")\n")
+
+	writeValueIfChanged(ctx, distMkFile, buf.String())
+}
+
+func writeValueIfChanged(ctx SingletonContext, path string, value string) {
+	if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
+		ctx.Errorf("%s\n", err)
+		return
+	}
+	previousValue := ""
+	rawPreviousValue, err := os.ReadFile(path)
+	if err == nil {
+		previousValue = string(rawPreviousValue)
+	}
+
+	if previousValue != value {
+		if err = os.WriteFile(path, []byte(value), 0666); err != nil {
+			ctx.Errorf("Failed to write: %v", err)
+		}
+	}
+}
+
+func getDistContributionsFromMods(ctx fillInEntriesContext, mods []blueprint.Module) []distContributions {
+	var allDistContributions []distContributions
+	for _, mod := range mods {
+		if amod, ok := mod.(Module); ok && shouldSkipAndroidMkProcessing(ctx, amod.base()) {
+			continue
+		}
+		if info, ok := OtherModuleProvider(ctx, mod, AndroidMkInfoProvider); ok {
+			if contribution := info.PrimaryInfo.getDistContributions(ctx, mod); contribution != nil {
+				allDistContributions = append(allDistContributions, *contribution)
+			}
+			for _, ei := range info.ExtraInfo {
+				if contribution := ei.getDistContributions(ctx, mod); contribution != nil {
+					allDistContributions = append(allDistContributions, *contribution)
+				}
+			}
+		} else {
+			switch x := mod.(type) {
+			case AndroidMkDataProvider:
+				data := x.AndroidMk()
+
+				if data.Include == "" {
+					data.Include = "$(BUILD_PREBUILT)"
+				}
+
+				data.fillInData(ctx, mod)
+				if contribution := data.Entries.getDistContributions(mod); contribution != nil {
+					allDistContributions = append(allDistContributions, *contribution)
+				}
+			case AndroidMkEntriesProvider:
+				entriesList := x.AndroidMkEntries()
+				for _, entries := range entriesList {
+					entries.fillInEntries(ctx, mod)
+					if contribution := entries.getDistContributions(mod); contribution != nil {
+						allDistContributions = append(allDistContributions, *contribution)
+					}
+				}
+			default:
+				// Not exported to make so no make variables to set.
+			}
+		}
+	}
+	return allDistContributions
+}
+
 func translateAndroidMk(ctx SingletonContext, absMkFile string, moduleInfoJSONPath WritablePath, mods []blueprint.Module) error {
 	buf := &bytes.Buffer{}
 
diff --git a/android/config.go b/android/config.go
index e9cb2cd..87aacd5 100644
--- a/android/config.go
+++ b/android/config.go
@@ -83,6 +83,7 @@
 	OutDir         string
 	SoongOutDir    string
 	SoongVariables string
+	KatiSuffix     string
 
 	ModuleGraphFile   string
 	ModuleActionsFile string
@@ -349,6 +350,7 @@
 	// Changes behavior based on whether Kati runs after soong_build, or if soong_build
 	// runs standalone.
 	katiEnabled bool
+	katiSuffix  string
 
 	captureBuild      bool // true for tests, saves build parameters for each module
 	ignoreEnvironment bool // true for tests, returns empty from all Getenv calls
@@ -620,6 +622,7 @@
 		outDir:            cmdArgs.OutDir,
 		soongOutDir:       cmdArgs.SoongOutDir,
 		runGoTests:        cmdArgs.RunGoTests,
+		katiSuffix:        cmdArgs.KatiSuffix,
 		multilibConflicts: make(map[ArchType]bool),
 
 		moduleListFile: cmdArgs.ModuleListFile,
@@ -1512,6 +1515,10 @@
 	return c.productVariables.GetBuildFlagBool("RELEASE_BOARD_API_LEVEL_FROZEN")
 }
 
+func (c *config) katiPackageMkDir() string {
+	return filepath.Join(c.soongOutDir, "kati_packaging"+c.katiSuffix)
+}
+
 func (c *deviceConfig) Arches() []Arch {
 	var arches []Arch
 	for _, target := range c.config.Targets[Android] {
@@ -2182,7 +2189,7 @@
 		"RELEASE_APEX_CONTRIBUTIONS_ADSERVICES":              "com.android.adservices",
 		"RELEASE_APEX_CONTRIBUTIONS_APPSEARCH":               "com.android.appsearch",
 		"RELEASE_APEX_CONTRIBUTIONS_ART":                     "com.android.art",
-		"RELEASE_APEX_CONTRIBUTIONS_BLUETOOTH":               "com.android.btservices",
+		"RELEASE_APEX_CONTRIBUTIONS_BLUETOOTH":               "com.android.bt",
 		"RELEASE_APEX_CONTRIBUTIONS_CAPTIVEPORTALLOGIN":      "",
 		"RELEASE_APEX_CONTRIBUTIONS_CELLBROADCAST":           "com.android.cellbroadcast",
 		"RELEASE_APEX_CONTRIBUTIONS_CONFIGINFRASTRUCTURE":    "com.android.configinfrastructure",
diff --git a/android/container_violations.go b/android/container_violations.go
index ba8f7d5..bca2b25 100644
--- a/android/container_violations.go
+++ b/android/container_violations.go
@@ -414,10 +414,6 @@
 		"framework", // cts -> unstable
 	},
 
-	"CtsMediaBetterTogetherTestCases": {
-		"framework", // cts -> unstable
-	},
-
 	"CtsMediaCodecTestCases": {
 		"framework", // cts -> unstable
 	},
@@ -478,6 +474,11 @@
 		"framework", // cts -> unstable
 	},
 
+	// TODO(b/387499846): Remove once migrated to sdk_version.
+	"CtsMediaRouterTestCases": {
+		"framework", // cts -> unstable
+	},
+
 	"CtsMediaRouterHostSideTestBluetoothPermissionsApp": {
 		"framework", // cts -> unstable
 	},
@@ -490,6 +491,11 @@
 		"framework", // cts -> unstable
 	},
 
+	// TODO(b/387500109): Remove once migrated to sdk_version.
+	"CtsMediaSessionTestCases": {
+		"framework", // cts -> unstable
+	},
+
 	"CtsMediaV2TestCases": {
 		"framework", // cts -> unstable
 	},
@@ -907,10 +913,6 @@
 		"libnativeloader_vendor_shared_lib", // system -> vendor
 	},
 
-	"MctsMediaBetterTogetherTestCases": {
-		"framework", // cts -> unstable
-	},
-
 	"MctsMediaCodecTestCases": {
 		"framework", // cts -> unstable
 	},
@@ -947,6 +949,16 @@
 		"framework", // cts -> unstable
 	},
 
+	// TODO(b/387499846): Remove once migrated to sdk_version.
+	"MctsMediaRouterTestCases": {
+		"framework", // cts -> unstable
+	},
+
+	// TODO(b/387500109): Remove once migrated to sdk_version.
+	"MctsMediaSessionTestCases": {
+		"framework", // cts -> unstable
+	},
+
 	"MctsMediaTranscodingTestCases": {
 		"framework", // cts -> unstable
 	},
diff --git a/android/module.go b/android/module.go
index b8f2cae..2f505aa 100644
--- a/android/module.go
+++ b/android/module.go
@@ -1679,14 +1679,13 @@
 		}
 	})
 
-	var deps Paths
-
 	var namespacePrefix string
 	nameSpace := ctx.Namespace().Path
 	if nameSpace != "." {
 		namespacePrefix = strings.ReplaceAll(nameSpace, "/", ".") + "-"
 	}
 
+	var deps Paths
 	var info FinalModuleBuildTargetsInfo
 
 	if len(allInstalledFiles) > 0 {
@@ -1853,9 +1852,9 @@
 
 var SourceFilesInfoKey = blueprint.NewProvider[SourceFilesInfo]()
 
+// FinalModuleBuildTargetsInfo is used by buildTargetSingleton to create checkbuild and
+// per-directory build targets. Only set on the final variant of each module
 type FinalModuleBuildTargetsInfo struct {
-	// Used by buildTargetSingleton to create checkbuild and per-directory build targets
-	// Only set on the final variant of each module
 	InstallTarget    WritablePath
 	CheckbuildTarget WritablePath
 	BlueprintDir     string
diff --git a/android/phony.go b/android/phony.go
index f8db88d..7bdd9d3 100644
--- a/android/phony.go
+++ b/android/phony.go
@@ -15,6 +15,7 @@
 package android
 
 import (
+	"strings"
 	"sync"
 
 	"github.com/google/blueprint"
@@ -68,13 +69,25 @@
 	}
 
 	if !ctx.Config().KatiEnabled() {
+		// In soong-only builds, the phonies can conflict with dist targets that will
+		// be generated in the packaging step. Instead of emitting a blueprint/ninja phony directly,
+		// create a makefile that defines the phonies that will be included in the packaging step.
+		// Make will dedup the phonies there.
+		var buildPhonyFileContents strings.Builder
 		for _, phony := range p.phonyList {
-			ctx.Build(pctx, BuildParams{
-				Rule:      blueprint.Phony,
-				Outputs:   []WritablePath{PathForPhony(ctx, phony)},
-				Implicits: p.phonyMap[phony],
-			})
+			buildPhonyFileContents.WriteString(".PHONY: ")
+			buildPhonyFileContents.WriteString(phony)
+			buildPhonyFileContents.WriteString("\n")
+			buildPhonyFileContents.WriteString(phony)
+			buildPhonyFileContents.WriteString(":")
+			for _, dep := range p.phonyMap[phony] {
+				buildPhonyFileContents.WriteString(" ")
+				buildPhonyFileContents.WriteString(dep.String())
+			}
+			buildPhonyFileContents.WriteString("\n")
 		}
+		buildPhonyFile := PathForOutput(ctx, "soong_phony_targets.mk")
+		writeValueIfChanged(ctx, absolutePath(buildPhonyFile.String()), buildPhonyFileContents.String())
 	}
 }
 
diff --git a/apex/apex.go b/apex/apex.go
index 7734391..d1d3d97 100644
--- a/apex/apex.go
+++ b/apex/apex.go
@@ -1056,7 +1056,7 @@
 		"com.android.appsearch",
 		"com.android.art",
 		"com.android.art.debug",
-		"com.android.btservices",
+		"com.android.bt",
 		"com.android.cellbroadcast",
 		"com.android.configinfrastructure",
 		"com.android.conscrypt",
diff --git a/cmd/soong_build/main.go b/cmd/soong_build/main.go
index 6642023..cd4e9bd 100644
--- a/cmd/soong_build/main.go
+++ b/cmd/soong_build/main.go
@@ -64,6 +64,7 @@
 	flag.StringVar(&usedEnvFile, "used_env", "", "File containing used environment variables")
 	flag.StringVar(&cmdlineArgs.OutDir, "out", "", "the ninja builddir directory")
 	flag.StringVar(&cmdlineArgs.ModuleListFile, "l", "", "file that lists filepaths to parse")
+	flag.StringVar(&cmdlineArgs.KatiSuffix, "kati_suffix", "", "the suffix for kati and ninja files, so that different configurations don't clobber each other")
 
 	// Debug flags
 	flag.StringVar(&delveListen, "delve_listen", "", "Delve port to listen on for debugging")
diff --git a/filesystem/android_device.go b/filesystem/android_device.go
index fd8e915..5c3842a 100644
--- a/filesystem/android_device.go
+++ b/filesystem/android_device.go
@@ -15,6 +15,8 @@
 package filesystem
 
 import (
+	"strings"
+
 	"android/soong/android"
 
 	"github.com/google/blueprint"
@@ -79,6 +81,8 @@
 	}
 
 	addDependencyIfDefined(a.partitionProps.Boot_partition_name)
+	addDependencyIfDefined(a.partitionProps.Init_boot_partition_name)
+	addDependencyIfDefined(a.partitionProps.Vendor_boot_partition_name)
 	addDependencyIfDefined(a.partitionProps.System_partition_name)
 	addDependencyIfDefined(a.partitionProps.System_ext_partition_name)
 	addDependencyIfDefined(a.partitionProps.Product_partition_name)
@@ -88,6 +92,7 @@
 	addDependencyIfDefined(a.partitionProps.System_dlkm_partition_name)
 	addDependencyIfDefined(a.partitionProps.Vendor_dlkm_partition_name)
 	addDependencyIfDefined(a.partitionProps.Odm_dlkm_partition_name)
+	addDependencyIfDefined(a.partitionProps.Recovery_partition_name)
 	for _, vbmetaPartition := range a.partitionProps.Vbmeta_partitions {
 		ctx.AddDependency(ctx.Module(), filesystemDepTag, vbmetaPartition)
 	}
@@ -95,6 +100,30 @@
 
 func (a *androidDevice) GenerateAndroidBuildActions(ctx android.ModuleContext) {
 	a.buildTargetFilesZip(ctx)
+	var deps []android.Path
+	ctx.VisitDirectDepsWithTag(filesystemDepTag, func(m android.Module) {
+		imageOutput, ok := android.OtherModuleProvider(ctx, m, android.OutputFilesProvider)
+		if !ok {
+			ctx.ModuleErrorf("Partition module %s doesn't set OutputfilesProvider", m.Name())
+		}
+		if len(imageOutput.DefaultOutputFiles) != 1 {
+			ctx.ModuleErrorf("Partition module %s should provide exact 1 output file", m.Name())
+		}
+		deps = append(deps, imageOutput.DefaultOutputFiles[0])
+	})
+	out := android.PathForModuleOut(ctx, "out")
+	ctx.Build(pctx, android.BuildParams{
+		Rule:      android.Touch,
+		Output:    out,
+		Implicits: deps,
+	})
+	ctx.SetOutputFiles(android.Paths{out}, "")
+	ctx.CheckbuildFile(out)
+}
+
+type targetFilesZipCopy struct {
+	srcModule  *string
+	destSubdir string
 }
 
 func (a *androidDevice) buildTargetFilesZip(ctx android.ModuleContext) {
@@ -104,27 +133,55 @@
 	builder := android.NewRuleBuilder(pctx, ctx)
 	builder.Command().Textf("rm -rf %s", targetFilesDir.String())
 	builder.Command().Textf("mkdir -p %s", targetFilesDir.String())
-	partitionToSubdir := map[*string]string{
-		a.partitionProps.System_partition_name:      "SYSTEM",
-		a.partitionProps.System_ext_partition_name:  "SYSTEM_EXT",
-		a.partitionProps.Product_partition_name:     "PRODUCT",
-		a.partitionProps.Vendor_partition_name:      "VENDOR",
-		a.partitionProps.Odm_partition_name:         "ODM",
-		a.partitionProps.System_dlkm_partition_name: "SYSTEM_DLKM",
-		a.partitionProps.Vendor_dlkm_partition_name: "VENDOR_DLKM",
-		a.partitionProps.Odm_dlkm_partition_name:    "ODM_DLKM",
+	toCopy := []targetFilesZipCopy{
+		targetFilesZipCopy{a.partitionProps.System_partition_name, "SYSTEM"},
+		targetFilesZipCopy{a.partitionProps.System_ext_partition_name, "SYSTEM_EXT"},
+		targetFilesZipCopy{a.partitionProps.Product_partition_name, "PRODUCT"},
+		targetFilesZipCopy{a.partitionProps.Vendor_partition_name, "VENDOR"},
+		targetFilesZipCopy{a.partitionProps.Odm_partition_name, "ODM"},
+		targetFilesZipCopy{a.partitionProps.System_dlkm_partition_name, "SYSTEM_DLKM"},
+		targetFilesZipCopy{a.partitionProps.Vendor_dlkm_partition_name, "VENDOR_DLKM"},
+		targetFilesZipCopy{a.partitionProps.Odm_dlkm_partition_name, "ODM_DLKM"},
+		targetFilesZipCopy{a.partitionProps.Init_boot_partition_name, "BOOT/RAMDISK"},
+		targetFilesZipCopy{a.partitionProps.Init_boot_partition_name, "INIT_BOOT/RAMDISK"},
+		targetFilesZipCopy{a.partitionProps.Vendor_boot_partition_name, "VENDOR_BOOT/RAMDISK"},
 	}
-	for partition, subdir := range partitionToSubdir {
-		if partition == nil {
+	// TODO: Handle cases where recovery files are copied to BOOT/ or RECOVERY/
+	// https://cs.android.com/android/platform/superproject/main/+/main:build/make/core/Makefile;l=6211-6219?q=core%2FMakefile&ss=android%2Fplatform%2Fsuperproject%2Fmain
+	if ctx.DeviceConfig().BoardMoveRecoveryResourcesToVendorBoot() {
+		toCopy = append(toCopy, targetFilesZipCopy{a.partitionProps.Recovery_partition_name, "VENDOR_BOOT/RAMDISK"})
+	}
+
+	for _, zipCopy := range toCopy {
+		if zipCopy.srcModule == nil {
 			continue
 		}
-		fsInfo := a.getFilesystemInfo(ctx, *partition)
+		fsInfo := a.getFilesystemInfo(ctx, *zipCopy.srcModule)
+		subdir := zipCopy.destSubdir
+		rootDirString := fsInfo.RootDir.String()
+		if subdir == "SYSTEM" {
+			rootDirString = rootDirString + "/system"
+		}
 		builder.Command().Textf("mkdir -p %s/%s", targetFilesDir.String(), subdir)
 		builder.Command().
 			BuiltTool("acp").
-			Textf("-rd %s/. %s/%s", fsInfo.RootDir, targetFilesDir, subdir).
+			Textf("-rd %s/. %s/%s", rootDirString, targetFilesDir, subdir).
 			Implicit(fsInfo.Output) // so that the staging dir is built
+
 	}
+	// Copy cmdline files of boot images
+	if a.partitionProps.Vendor_boot_partition_name != nil {
+		bootImg := ctx.GetDirectDepWithTag(proptools.String(a.partitionProps.Vendor_boot_partition_name), filesystemDepTag)
+		bootImgInfo, _ := android.OtherModuleProvider(ctx, bootImg, BootimgInfoProvider)
+		builder.Command().Textf("echo %s > %s/%s/cmdline", proptools.ShellEscape(strings.Join(bootImgInfo.Cmdline, " ")), targetFilesDir, "VENDOR_BOOT")
+		builder.Command().Textf("echo %s > %s/%s/vendor_cmdline", proptools.ShellEscape(strings.Join(bootImgInfo.Cmdline, " ")), targetFilesDir, "VENDOR_BOOT")
+	}
+	if a.partitionProps.Boot_partition_name != nil {
+		bootImg := ctx.GetDirectDepWithTag(proptools.String(a.partitionProps.Boot_partition_name), filesystemDepTag)
+		bootImgInfo, _ := android.OtherModuleProvider(ctx, bootImg, BootimgInfoProvider)
+		builder.Command().Textf("echo %s > %s/%s/cmdline", proptools.ShellEscape(strings.Join(bootImgInfo.Cmdline, " ")), targetFilesDir, "BOOT")
+	}
+
 	builder.Command().
 		BuiltTool("soong_zip").
 		Text("-d").
diff --git a/filesystem/bootimg.go b/filesystem/bootimg.go
index 36b1a18..00398b2 100644
--- a/filesystem/bootimg.go
+++ b/filesystem/bootimg.go
@@ -224,6 +224,25 @@
 
 	ctx.SetOutputFiles([]android.Path{output}, "")
 	b.output = output
+
+	// Set the Filesystem info of the ramdisk dependency.
+	// `android_device` will use this info to package `target_files.zip`
+	if ramdisk := proptools.String(b.properties.Ramdisk_module); ramdisk != "" {
+		ramdiskModule := ctx.GetDirectDepWithTag(ramdisk, bootimgRamdiskDep)
+		fsInfo, _ := android.OtherModuleProvider(ctx, ramdiskModule, FilesystemProvider)
+		android.SetProvider(ctx, FilesystemProvider, fsInfo)
+	}
+
+	// Set BootimgInfo for building target_files.zip
+	android.SetProvider(ctx, BootimgInfoProvider, BootimgInfo{
+		Cmdline: b.properties.Cmdline,
+	})
+}
+
+var BootimgInfoProvider = blueprint.NewProvider[BootimgInfo]()
+
+type BootimgInfo struct {
+	Cmdline []string
 }
 
 func (b *bootimg) buildBootImage(ctx android.ModuleContext, kernel android.Path) android.Path {
@@ -372,6 +391,10 @@
 		cmd.FlagWithArg("--rollback_index ", strconv.FormatInt(*b.properties.Avb_rollback_index, 10))
 	}
 
+	if !ctx.Config().KatiEnabled() {
+		copyImageFileToProductOut(ctx, builder, b.bootImageType.String(), output)
+	}
+
 	builder.Build("add_avb_footer", fmt.Sprintf("Adding avb footer to %s", b.BaseModuleName()))
 	return output
 }
@@ -387,6 +410,10 @@
 		Implicits(toolDeps).
 		Output(output)
 
+	if !ctx.Config().KatiEnabled() {
+		copyImageFileToProductOut(ctx, builder, b.bootImageType.String(), output)
+	}
+
 	builder.Build("sign_bootimg", fmt.Sprintf("Signing %s", b.BaseModuleName()))
 	return output
 }
diff --git a/filesystem/filesystem.go b/filesystem/filesystem.go
index 6dfbfd1..64b7931 100644
--- a/filesystem/filesystem.go
+++ b/filesystem/filesystem.go
@@ -593,11 +593,16 @@
 }
 
 func (f *filesystem) copyFilesToProductOut(ctx android.ModuleContext, builder *android.RuleBuilder, rebasedDir android.OutputPath) {
-	if f.Name() != ctx.Config().SoongDefinedSystemImage() {
+	if !(f.Name() == ctx.Config().SoongDefinedSystemImage() || proptools.Bool(f.properties.Is_auto_generated)) {
 		return
 	}
 	installPath := android.PathForModuleInPartitionInstall(ctx, f.partitionName())
-	builder.Command().Textf("cp -prf %s/* %s", rebasedDir, installPath)
+	builder.Command().Textf("rsync --checksum %s %s", rebasedDir, installPath)
+}
+
+func copyImageFileToProductOut(ctx android.ModuleContext, builder *android.RuleBuilder, partition string, output android.Path) {
+	copyDir := android.PathForModuleInPartitionInstall(ctx, "").Join(ctx, fmt.Sprintf("%s.img", partition))
+	builder.Command().Textf("rsync -a %s %s", output, copyDir)
 }
 
 func (f *filesystem) rootDirString() string {
@@ -651,6 +656,10 @@
 		Output(output).
 		Text(rootDir.String()) // directory where to find fs_config_files|dirs
 
+	if !ctx.Config().KatiEnabled() {
+		copyImageFileToProductOut(ctx, builder, f.partitionName(), output)
+	}
+
 	// rootDir is not deleted. Might be useful for quick inspection.
 	builder.Build("build_filesystem_image", fmt.Sprintf("Creating filesystem %s", f.BaseModuleName()))
 
@@ -1081,7 +1090,10 @@
 	modulesInPackageByName := make(map[string]bool)
 
 	deps := f.gatherFilteredPackagingSpecs(ctx)
-	ctx.WalkDeps(func(child, parent android.Module) bool {
+	ctx.WalkDeps(func(child, _ android.Module) bool {
+		if !child.Enabled(ctx) {
+			return false
+		}
 		for _, ps := range android.OtherModuleProviderOrDefault(
 			ctx, child, android.InstallFilesProvider).PackagingSpecs {
 			if _, ok := deps[ps.RelPathInPackage()]; ok && ps.Partition() == f.PartitionType() {
@@ -1100,6 +1112,9 @@
 
 	var requireModules []android.Module
 	ctx.WalkDeps(func(child, parent android.Module) bool {
+		if !child.Enabled(ctx) {
+			return false
+		}
 		_, parentInPackage := modulesInPackageByModule[parent]
 		_, childInPackageName := modulesInPackageByName[child.Name()]
 
diff --git a/filesystem/vbmeta.go b/filesystem/vbmeta.go
index 6a47859..c8b4675 100644
--- a/filesystem/vbmeta.go
+++ b/filesystem/vbmeta.go
@@ -281,6 +281,10 @@
 		FlagWithArg("-s ", strconv.Itoa(vbmetaMaxSize)).
 		Output(output)
 
+	if !ctx.Config().KatiEnabled() {
+		copyImageFileToProductOut(ctx, builder, v.partitionName(), output)
+	}
+
 	builder.Build("vbmeta", fmt.Sprintf("vbmeta %s", ctx.ModuleName()))
 
 	v.installDir = android.PathForModuleInstall(ctx, "etc")
diff --git a/java/app_import.go b/java/app_import.go
index f593c02..35a054f 100644
--- a/java/app_import.go
+++ b/java/app_import.go
@@ -43,6 +43,12 @@
 		Description: "Uncompress embedded JNI libs",
 	})
 
+	stripEmbeddedJniLibsUnusedArchRule = pctx.AndroidStaticRule("strip-embedded-jni-libs-from-unused-arch", blueprint.RuleParams{
+		Command:     `${config.Zip2ZipCmd} -i $in -o $out -x 'lib/**/*.so' $extraArgs`,
+		CommandDeps: []string{"${config.Zip2ZipCmd}"},
+		Description: "Remove all JNI libs from unused architectures",
+	}, "extraArgs")
+
 	uncompressDexRule = pctx.AndroidStaticRule("uncompress-dex", blueprint.RuleParams{
 		Command: `if (zipinfo $in '*.dex' 2>/dev/null | grep -v ' stor ' >/dev/null) ; then ` +
 			`${config.Zip2ZipCmd} -i $in -o $out -0 'classes*.dex'` +
@@ -150,6 +156,9 @@
 	// the prebuilt is Name() without "prebuilt_" prefix
 	Source_module_name *string
 
+	// Whether stripping all libraries from unused architectures.
+	Strip_unused_jni_arch *bool
+
 	// Path to the .prebuilt_info file of the prebuilt app.
 	// In case of mainline modules, the .prebuilt_info file contains the build_id that was used
 	// to generate the prebuilt.
@@ -292,6 +301,26 @@
 	return shouldUncompressDex(ctx, android.RemoveOptionalPrebuiltPrefix(ctx.ModuleName()), &a.dexpreopter)
 }
 
+func (a *AndroidAppImport) stripEmbeddedJniLibsUnusedArch(
+	ctx android.ModuleContext, inputPath android.Path, outputPath android.WritablePath) {
+	var wantedJniLibSlice []string
+	for _, target := range ctx.MultiTargets() {
+		supported_abis := target.Arch.Abi
+		for _, arch := range supported_abis {
+			wantedJniLibSlice = append(wantedJniLibSlice, " -X lib/"+arch+"/*.so")
+		}
+	}
+	wantedJniLibString := strings.Join(wantedJniLibSlice, " ")
+	ctx.Build(pctx, android.BuildParams{
+		Rule:   stripEmbeddedJniLibsUnusedArchRule,
+		Input:  inputPath,
+		Output: outputPath,
+		Args: map[string]string{
+			"extraArgs": wantedJniLibString,
+		},
+	})
+}
+
 func (a *AndroidAppImport) GenerateAndroidBuildActions(ctx android.ModuleContext) {
 	a.generateAndroidBuildActions(ctx)
 }
@@ -347,6 +376,13 @@
 	jnisUncompressed := android.PathForModuleOut(ctx, "jnis-uncompressed", ctx.ModuleName()+".apk")
 	a.uncompressEmbeddedJniLibs(ctx, srcApk, jnisUncompressed)
 
+	// Strip all embedded JNI libs and include only required ones accordingly to the module's compile_multilib
+	if Bool(a.properties.Strip_unused_jni_arch) {
+		jnisStripped := android.PathForModuleOut(ctx, "jnis-stripped", ctx.ModuleName()+".apk")
+		a.stripEmbeddedJniLibsUnusedArch(ctx, jnisUncompressed, jnisStripped)
+		jnisUncompressed = jnisStripped
+	}
+
 	var pathFragments []string
 	relInstallPath := String(a.properties.Relative_install_path)
 
diff --git a/java/platform_bootclasspath.go b/java/platform_bootclasspath.go
index d09a02e..152eb1e 100644
--- a/java/platform_bootclasspath.go
+++ b/java/platform_bootclasspath.go
@@ -172,7 +172,7 @@
 	// Do not add implLibModule to allModules as the impl lib is only used to collect the
 	// transitive source files
 	var implLibModule []android.Module
-	ctx.VisitDirectDepsWithTag(implLibraryTag, func(m android.Module) {
+	ctx.VisitDirectDepsWithTag(platformBootclasspathImplLibDepTag, func(m android.Module) {
 		implLibModule = append(implLibModule, m)
 	})
 
diff --git a/java/platform_bootclasspath_test.go b/java/platform_bootclasspath_test.go
index f2768db..1f691a0 100644
--- a/java/platform_bootclasspath_test.go
+++ b/java/platform_bootclasspath_test.go
@@ -30,18 +30,23 @@
 	preparer := android.GroupFixturePreparers(
 		prepareForTestWithPlatformBootclasspath,
 		FixtureConfigureBootJars("platform:foo", "system_ext:bar"),
+		android.FixtureMergeMockFs(android.MockFS{
+			"api/current.txt": nil,
+			"api/removed.txt": nil,
+		}),
 		android.FixtureWithRootAndroidBp(`
 			platform_bootclasspath {
 				name: "platform-bootclasspath",
 			}
 
-			java_library {
+			java_sdk_library {
 				name: "bar",
 				srcs: ["a.java"],
 				system_modules: "none",
 				sdk_version: "none",
 				compile_dex: true,
 				system_ext_specific: true,
+				unsafe_ignore_missing_latest_api: true,
 			}
 		`),
 	)
diff --git a/java/platform_compat_config.go b/java/platform_compat_config.go
index 5b145c6..bb98944 100644
--- a/java/platform_compat_config.go
+++ b/java/platform_compat_config.go
@@ -51,6 +51,10 @@
 
 type platformCompatConfigProperties struct {
 	Src *string `android:"path"`
+
+	// If true, we include it in the "merged" XML (merged_compat_config.xml).
+	// Default is true.
+	Include_in_merged_xml *bool
 }
 
 type platformCompatConfig struct {
@@ -60,6 +64,7 @@
 	installDirPath android.InstallPath
 	configFile     android.OutputPath
 	metadataFile   android.OutputPath
+	doMerge        bool
 
 	installConfigFile android.InstallPath
 }
@@ -68,6 +73,10 @@
 	return p.metadataFile
 }
 
+func (p *platformCompatConfig) includeInMergedXml() bool {
+	return p.doMerge
+}
+
 func (p *platformCompatConfig) CompatConfig() android.OutputPath {
 	return p.configFile
 }
@@ -78,6 +87,9 @@
 
 type platformCompatConfigMetadataProvider interface {
 	compatConfigMetadata() android.Path
+
+	// Whether to include it in the "merged" XML (merged_compat_config.xml) or not.
+	includeInMergedXml() bool
 }
 
 type PlatformCompatConfigIntf interface {
@@ -98,6 +110,7 @@
 	metadataFileName := p.Name() + "_meta.xml"
 	p.configFile = android.PathForModuleOut(ctx, configFileName).OutputPath
 	p.metadataFile = android.PathForModuleOut(ctx, metadataFileName).OutputPath
+	p.doMerge = proptools.BoolDefault(p.properties.Include_in_merged_xml, true)
 	path := android.PathForModuleSrc(ctx, String(p.properties.Src))
 
 	rule.Command().
@@ -201,6 +214,10 @@
 	return module.metadataFile
 }
 
+func (module *prebuiltCompatConfigModule) includeInMergedXml() bool {
+	return true // Always include in merged.xml
+}
+
 func (module *prebuiltCompatConfigModule) BaseModuleName() string {
 	return proptools.StringDefault(module.properties.Source_module_name, module.ModuleBase.Name())
 }
@@ -237,6 +254,9 @@
 			if !android.IsModulePreferred(module) {
 				return
 			}
+			if !c.includeInMergedXml() {
+				return
+			}
 			metadata := c.compatConfigMetadata()
 			compatConfigMetadata = append(compatConfigMetadata, metadata)
 		}
diff --git a/java/platform_compat_config_test.go b/java/platform_compat_config_test.go
index 80d991c..f7529a7 100644
--- a/java/platform_compat_config_test.go
+++ b/java/platform_compat_config_test.go
@@ -26,6 +26,7 @@
 		android.FixtureWithRootAndroidBp(`
 			platform_compat_config {
 				name: "myconfig2",
+				include_in_merged_xml: false,
 			}
 			platform_compat_config {
 				name: "myconfig1",
@@ -38,7 +39,6 @@
 
 	CheckMergedCompatConfigInputs(t, result, "myconfig",
 		"out/soong/.intermediates/myconfig1/myconfig1_meta.xml",
-		"out/soong/.intermediates/myconfig2/myconfig2_meta.xml",
 		"out/soong/.intermediates/myconfig3/myconfig3_meta.xml",
 	)
 }
diff --git a/ui/build/build.go b/ui/build/build.go
index 1dc6dbd..26f5969 100644
--- a/ui/build/build.go
+++ b/ui/build/build.go
@@ -107,8 +107,11 @@
 {{end -}}
 pool highmem_pool
  depth = {{.HighmemParallel}}
-{{if and (not .SkipKatiNinja) .HasKatiSuffix}}subninja {{.KatiBuildNinjaFile}}
+{{if and (not .SkipKatiNinja) .HasKatiSuffix}}
+subninja {{.KatiBuildNinjaFile}}
 subninja {{.KatiPackageNinjaFile}}
+{{else}}
+subninja {{.KatiSoongOnlyPackageNinjaFile}}
 {{end -}}
 subninja {{.SoongNinjaFile}}
 `))
@@ -346,25 +349,31 @@
 		return
 	}
 
+	// Still generate the kati suffix in soong-only builds because soong-only still uses kati for
+	// the packaging step. Also, the kati suffix is used for the combined ninja file.
+	genKatiSuffix(ctx, config)
+
 	if what&RunSoong != 0 {
 		runSoong(ctx, config)
 	}
 
 	if what&RunKati != 0 {
-		genKatiSuffix(ctx, config)
 		runKatiCleanSpec(ctx, config)
 		runKatiBuild(ctx, config)
-		runKatiPackage(ctx, config)
+		runKatiPackage(ctx, config, false)
 
-		ioutil.WriteFile(config.LastKatiSuffixFile(), []byte(config.KatiSuffix()), 0666) // a+rw
 	} else if what&RunKatiNinja != 0 {
 		// Load last Kati Suffix if it exists
-		if katiSuffix, err := ioutil.ReadFile(config.LastKatiSuffixFile()); err == nil {
+		if katiSuffix, err := os.ReadFile(config.LastKatiSuffixFile()); err == nil {
 			ctx.Verboseln("Loaded previous kati config:", string(katiSuffix))
 			config.SetKatiSuffix(string(katiSuffix))
 		}
+	} else if what&RunSoong != 0 {
+		runKatiPackage(ctx, config, true)
 	}
 
+	os.WriteFile(config.LastKatiSuffixFile(), []byte(config.KatiSuffix()), 0666) // a+rw
+
 	// Write combined ninja file
 	createCombinedBuildNinjaFile(ctx, config)
 
diff --git a/ui/build/config.go b/ui/build/config.go
index dc468c2..4f2d213 100644
--- a/ui/build/config.go
+++ b/ui/build/config.go
@@ -1592,6 +1592,10 @@
 	return filepath.Join(c.OutDir(), "build"+c.KatiSuffix()+katiPackageSuffix+".ninja")
 }
 
+func (c *configImpl) KatiSoongOnlyPackageNinjaFile() string {
+	return filepath.Join(c.OutDir(), "build"+c.KatiSuffix()+katiSoongOnlyPackageSuffix+".ninja")
+}
+
 func (c *configImpl) SoongVarsFile() string {
 	targetProduct, err := c.TargetProductOrErr()
 	if err != nil {
@@ -1647,7 +1651,7 @@
 }
 
 func (c *configImpl) KatiPackageMkDir() string {
-	return filepath.Join(c.ProductOut(), "obj", "CONFIG", "kati_packaging")
+	return filepath.Join(c.SoongOutDir(), "kati_packaging"+c.KatiSuffix())
 }
 
 func (c *configImpl) hostOutRoot() string {
diff --git a/ui/build/kati.go b/ui/build/kati.go
index acd5254..6519573 100644
--- a/ui/build/kati.go
+++ b/ui/build/kati.go
@@ -31,6 +31,7 @@
 const katiBuildSuffix = ""
 const katiCleanspecSuffix = "-cleanspec"
 const katiPackageSuffix = "-package"
+const katiSoongOnlyPackageSuffix = "-soong-only-package"
 
 // genKatiSuffix creates a filename suffix for kati-generated files so that we
 // can cache them based on their inputs. Such files include the generated Ninja
@@ -40,8 +41,12 @@
 // Currently that includes the TARGET_PRODUCT and kati-processed command line
 // arguments.
 func genKatiSuffix(ctx Context, config Config) {
+	targetProduct := "unknown"
+	if p, err := config.TargetProductOrErr(); err == nil {
+		targetProduct = p
+	}
 	// Construct the base suffix.
-	katiSuffix := "-" + config.TargetProduct() + config.CoverageSuffix()
+	katiSuffix := "-" + targetProduct + config.CoverageSuffix()
 
 	// Append kati arguments to the suffix.
 	if args := config.KatiArgs(); len(args) > 0 {
@@ -68,13 +73,13 @@
 func writeValueIfChanged(ctx Context, config Config, dir string, filename string, value string) {
 	filePath := filepath.Join(dir, filename)
 	previousValue := ""
-	rawPreviousValue, err := ioutil.ReadFile(filePath)
+	rawPreviousValue, err := os.ReadFile(filePath)
 	if err == nil {
 		previousValue = string(rawPreviousValue)
 	}
 
 	if previousValue != value {
-		if err = ioutil.WriteFile(filePath, []byte(value), 0666); err != nil {
+		if err = os.WriteFile(filePath, []byte(value), 0666); err != nil {
 			ctx.Fatalf("Failed to write: %v", err)
 		}
 	}
@@ -334,10 +339,19 @@
 
 // Generate the Ninja file containing the packaging command lines for the dist
 // dir.
-func runKatiPackage(ctx Context, config Config) {
+func runKatiPackage(ctx Context, config Config, soongOnly bool) {
 	ctx.BeginTrace(metrics.RunKati, "kati package")
 	defer ctx.EndTrace()
 
+	entryPoint := "build/make/packaging/main.mk"
+	suffix := katiPackageSuffix
+	ninjaFile := config.KatiPackageNinjaFile()
+	if soongOnly {
+		entryPoint = "build/make/packaging/main_soong_only.mk"
+		suffix = katiSoongOnlyPackageSuffix
+		ninjaFile = config.KatiSoongOnlyPackageNinjaFile()
+	}
+
 	args := []string{
 		// Mark the dist dir as writable.
 		"--writable", config.DistDir() + "/",
@@ -346,14 +360,14 @@
 		// Fail when redefining / duplicating a target.
 		"--werror_overriding_commands",
 		// Entry point.
-		"-f", "build/make/packaging/main.mk",
+		"-f", entryPoint,
 		// Directory containing .mk files for packaging purposes, such as
 		// the dist.mk file, containing dist-for-goals data.
 		"KATI_PACKAGE_MK_DIR=" + config.KatiPackageMkDir(),
 	}
 
 	// Run Kati against a restricted set of environment variables.
-	runKati(ctx, config, katiPackageSuffix, args, func(env *Environment) {
+	runKati(ctx, config, suffix, args, func(env *Environment) {
 		env.Allow([]string{
 			// Some generic basics
 			"LANG",
@@ -381,7 +395,7 @@
 	})
 
 	// Compress and dist the packaging Ninja file.
-	distGzipFile(ctx, config, config.KatiPackageNinjaFile())
+	distGzipFile(ctx, config, ninjaFile)
 }
 
 // Run Kati on the cleanspec files to clean the build.
diff --git a/ui/build/soong.go b/ui/build/soong.go
index 82e5c96..58334a9 100644
--- a/ui/build/soong.go
+++ b/ui/build/soong.go
@@ -197,6 +197,8 @@
 func (pb PrimaryBuilderFactory) primaryBuilderInvocation(config Config) bootstrap.PrimaryBuilderInvocation {
 	commonArgs := make([]string, 0, 0)
 
+	commonArgs = append(commonArgs, "--kati_suffix", config.KatiSuffix())
+
 	if !pb.config.skipSoongTests {
 		commonArgs = append(commonArgs, "-t")
 	}