Merge "Add python library build rule" into main
diff --git a/Android.bp b/Android.bp
index cbe1c7b..42b7d83 100644
--- a/Android.bp
+++ b/Android.bp
@@ -206,3 +206,30 @@
     relative_install_path: "etc", // odm/etc/build.prop
     visibility: ["//visibility:private"],
 }
+
+build_prop {
+    name: "system_dlkm-build.prop",
+    stem: "build.prop",
+    system_dlkm_specific: true,
+    product_config: ":product_config",
+    relative_install_path: "etc", // system_dlkm/etc/build.prop
+    visibility: ["//visibility:private"],
+}
+
+build_prop {
+    name: "vendor_dlkm-build.prop",
+    stem: "build.prop",
+    vendor_dlkm_specific: true,
+    product_config: ":product_config",
+    relative_install_path: "etc", // vendor_dlkm/etc/build.prop
+    visibility: ["//visibility:private"],
+}
+
+build_prop {
+    name: "odm_dlkm-build.prop",
+    stem: "build.prop",
+    odm_dlkm_specific: true,
+    product_config: ":product_config",
+    relative_install_path: "etc", // odm_dlkm/etc/build.prop
+    visibility: ["//visibility:private"],
+}
diff --git a/android/Android.bp b/android/Android.bp
index 1ed2dba..a9a3564 100644
--- a/android/Android.bp
+++ b/android/Android.bp
@@ -31,6 +31,7 @@
     srcs: [
         "aconfig_providers.go",
         "all_teams.go",
+        "android_info.go",
         "androidmk.go",
         "apex.go",
         "apex_contributions.go",
@@ -111,6 +112,7 @@
         "util.go",
         "variable.go",
         "vintf_fragment.go",
+        "vintf_data.go",
         "visibility.go",
     ],
     testSrcs: [
@@ -120,6 +122,7 @@
         "apex_test.go",
         "arch_test.go",
         "blueprint_e2e_test.go",
+        "build_prop_test.go",
         "config_test.go",
         "configured_jars_test.go",
         "csuite_config_test.go",
diff --git a/android/android_info.go b/android/android_info.go
new file mode 100644
index 0000000..a8d3d4e
--- /dev/null
+++ b/android/android_info.go
@@ -0,0 +1,91 @@
+// Copyright 2024 The Android Open Source Project
+//
+// 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 (
+	"github.com/google/blueprint"
+	"github.com/google/blueprint/proptools"
+)
+
+var (
+	mergeAndRemoveComments = pctx.AndroidStaticRule("merge_and_remove_comments",
+		blueprint.RuleParams{
+			Command: "cat $in | grep -v '#' > $out",
+		},
+	)
+	androidInfoTxtToProp = pctx.AndroidStaticRule("android_info_txt_to_prop",
+		blueprint.RuleParams{
+			Command: "grep 'require version-' $in | sed -e 's/require version-/ro.build.expect./g' > $out",
+		},
+	)
+)
+
+type androidInfoProperties struct {
+	// Name of output file. Defaults to module name
+	Stem *string
+
+	// Paths of board-info.txt files.
+	Board_info_files []string `android:"path"`
+
+	// Name of bootloader board. If board_info_files is empty, `board={bootloader_board_name}` will
+	// be printed to output. Ignored if board_info_files is not empty.
+	Bootloader_board_name *string
+}
+
+type androidInfoModule struct {
+	ModuleBase
+
+	properties androidInfoProperties
+}
+
+func (p *androidInfoModule) GenerateAndroidBuildActions(ctx ModuleContext) {
+	if len(p.properties.Board_info_files) > 0 && p.properties.Bootloader_board_name != nil {
+		ctx.ModuleErrorf("Either Board_info_files or Bootloader_board_name should be set. Please remove one of them\n")
+		return
+	}
+	androidInfoTxtName := proptools.StringDefault(p.properties.Stem, ctx.ModuleName()+".txt")
+	androidInfoTxt := PathForModuleOut(ctx, androidInfoTxtName)
+	androidInfoProp := androidInfoTxt.ReplaceExtension(ctx, "prop")
+
+	if boardInfoFiles := PathsForModuleSrc(ctx, p.properties.Board_info_files); len(boardInfoFiles) > 0 {
+		ctx.Build(pctx, BuildParams{
+			Rule:   mergeAndRemoveComments,
+			Inputs: boardInfoFiles,
+			Output: androidInfoTxt,
+		})
+	} else if bootloaderBoardName := proptools.String(p.properties.Bootloader_board_name); bootloaderBoardName != "" {
+		WriteFileRule(ctx, androidInfoTxt, "board="+bootloaderBoardName)
+	} else {
+		WriteFileRule(ctx, androidInfoTxt, "")
+	}
+
+	// Create android_info.prop
+	ctx.Build(pctx, BuildParams{
+		Rule:   androidInfoTxtToProp,
+		Input:  androidInfoTxt,
+		Output: androidInfoProp,
+	})
+
+	ctx.SetOutputFiles(Paths{androidInfoProp}, "")
+}
+
+// android_info module generate a file named android-info.txt that contains various information
+// about the device we're building for.  This file is typically packaged up with everything else.
+func AndroidInfoFactory() Module {
+	module := &androidInfoModule{}
+	module.AddProperties(&module.properties)
+	InitAndroidModule(module)
+	return module
+}
diff --git a/android/apex.go b/android/apex.go
index 79ab13c..3486350 100644
--- a/android/apex.go
+++ b/android/apex.go
@@ -160,14 +160,6 @@
 		reflect.DeepEqual(i.InApexModules, otherApexInfo.InApexModules)
 }
 
-// ApexTestForInfo stores the contents of APEXes for which this module is a test - although this
-// module is not part of the APEX - and thus has access to APEX internals.
-type ApexTestForInfo struct {
-	ApexContents []*ApexContents
-}
-
-var ApexTestForInfoProvider = blueprint.NewMutatorProvider[ApexTestForInfo]("apex_test_for")
-
 // ApexBundleInfo contains information about the dependencies of an apex
 type ApexBundleInfo struct {
 	Contents *ApexContents
@@ -269,12 +261,6 @@
 	// check-platform-availability mutator in the apex package.
 	SetNotAvailableForPlatform()
 
-	// Returns the list of APEXes that this module is a test for. The module has access to the
-	// private part of the listed APEXes even when it is not included in the APEXes. This by
-	// default returns nil. A module type should override the default implementation. For
-	// example, cc_test module type returns the value of test_for here.
-	TestFor() []string
-
 	// Returns nil (success) if this module should support the given sdk version. Returns an
 	// error if not. No default implementation is provided for this method. A module type
 	// implementing this interface should provide an implementation. A module supports an sdk
@@ -457,13 +443,6 @@
 	return false
 }
 
-// Implements ApexModule
-func (m *ApexModuleBase) TestFor() []string {
-	// If needed, this will be overridden by concrete types inheriting
-	// ApexModuleBase
-	return nil
-}
-
 // Returns the test apexes that this module is included in.
 func (m *ApexModuleBase) TestApexes() []string {
 	return m.ApexProperties.TestApexes
@@ -833,7 +812,7 @@
 
 	// If this is the FinalModule (last visited module) copy
 	// AnyVariantDirectlyInAnyApex to all the other variants
-	if am == mctx.FinalModule().(ApexModule) {
+	if mctx.IsFinalModule(am) {
 		mctx.VisitAllModuleVariants(func(variant Module) {
 			variant.(ApexModule).apexModuleBase().ApexProperties.AnyVariantDirectlyInAnyApex =
 				base.ApexProperties.AnyVariantDirectlyInAnyApex
@@ -1062,12 +1041,6 @@
 	return apiLevel
 }
 
-// Implemented by apexBundle.
-type ApexTestInterface interface {
-	// Return true if the apex bundle is an apex_test
-	IsTestApex() bool
-}
-
 var ApexExportsInfoProvider = blueprint.NewProvider[ApexExportsInfo]()
 
 // ApexExportsInfo contains information about the artifacts provided by apexes to dexpreopt and hiddenapi
diff --git a/android/base_module_context.go b/android/base_module_context.go
index e24ce9d..060fae5 100644
--- a/android/base_module_context.go
+++ b/android/base_module_context.go
@@ -17,6 +17,7 @@
 import (
 	"fmt"
 	"regexp"
+	"slices"
 	"strings"
 
 	"github.com/google/blueprint"
@@ -87,6 +88,10 @@
 	// This method shouldn't be used directly, prefer the type-safe android.OtherModuleProvider instead.
 	otherModuleProvider(m blueprint.Module, provider blueprint.AnyProviderKey) (any, bool)
 
+	// OtherModuleIsAutoGenerated returns true if the module is auto generated by another module
+	// instead of being defined in Android.bp file.
+	OtherModuleIsAutoGenerated(m blueprint.Module) bool
+
 	// Provider returns the value for a provider for the current module.  If the value is
 	// not set it returns nil and false.  It panics if called before the appropriate
 	// mutator or GenerateBuildActions pass for the provider.  The value returned may be a deep
@@ -193,9 +198,15 @@
 	// singleton actions that are only done once for all variants of a module.
 	FinalModule() Module
 
+	// IsFinalModule returns if the current module is the last variant.  Variants of a module are always visited in
+	// order by mutators and GenerateBuildActions, so the data created by the current mutator can be read from all
+	// variants using VisitAllModuleVariants if the current module is the last one. This can be used to perform
+	// singleton actions that are only done once for all variants of a module.
+	IsFinalModule(module Module) bool
+
 	// VisitAllModuleVariants calls visit for each variant of the current module.  Variants of a module are always
 	// visited in order by mutators and GenerateBuildActions, so the data created by the current mutator can be read
-	// from all variants if the current module == FinalModule().  Otherwise, care must be taken to not access any
+	// from all variants if the current module is the last one. Otherwise, care must be taken to not access any
 	// data modified by the current mutator.
 	VisitAllModuleVariants(visit func(Module))
 
@@ -275,6 +286,10 @@
 	return b.bp.OtherModuleProvider(m, provider)
 }
 
+func (b *baseModuleContext) OtherModuleIsAutoGenerated(m blueprint.Module) bool {
+	return b.bp.OtherModuleIsAutoGenerated(m)
+}
+
 func (b *baseModuleContext) provider(provider blueprint.AnyProviderKey) (any, bool) {
 	return b.bp.Provider(provider)
 }
@@ -562,7 +577,7 @@
 }
 
 func (b *baseModuleContext) GetWalkPath() []Module {
-	return b.walkPath
+	return slices.Clone(b.walkPath)
 }
 
 func (b *baseModuleContext) GetTagPath() []blueprint.DependencyTag {
@@ -583,6 +598,10 @@
 	return b.bp.FinalModule().(Module)
 }
 
+func (b *baseModuleContext) IsFinalModule(module Module) bool {
+	return b.bp.IsFinalModule(module)
+}
+
 // IsMetaDependencyTag returns true for cross-cutting metadata dependencies.
 func IsMetaDependencyTag(tag blueprint.DependencyTag) bool {
 	if tag == licenseKindTag {
diff --git a/android/build_prop.go b/android/build_prop.go
index 7c3c506..8389470 100644
--- a/android/build_prop.go
+++ b/android/build_prop.go
@@ -19,8 +19,12 @@
 )
 
 func init() {
-	ctx := InitRegistrationContext
+	registerBuildPropComponents(InitRegistrationContext)
+}
+
+func registerBuildPropComponents(ctx RegistrationContext) {
 	ctx.RegisterModuleType("build_prop", BuildPropFactory)
+	ctx.RegisterModuleType("android_info", AndroidInfoFactory)
 }
 
 type buildPropProperties struct {
@@ -38,6 +42,10 @@
 	// Path to a JSON file containing product configs.
 	Product_config *string `android:"path"`
 
+	// Path to android-info.txt file containing board specific info.
+	// This is empty for build.prop of all partitions except vendor.
+	Android_info *string `android:"path"`
+
 	// Optional subdirectory under which this file is installed into
 	Relative_install_path *string
 }
@@ -66,7 +74,10 @@
 	} else if partition == "odm" {
 		return ctx.Config().OdmPropFiles(ctx)
 	} else if partition == "vendor" {
-		// TODO (b/375500423): Add android-info.txt to prop files
+		if p.properties.Android_info != nil {
+			androidInfo := PathForModuleSrc(ctx, proptools.String(p.properties.Android_info))
+			return append(ctx.Config().VendorPropFiles(ctx), androidInfo)
+		}
 		return ctx.Config().VendorPropFiles(ctx)
 	}
 	return nil
@@ -98,6 +109,12 @@
 		return "product"
 	} else if p.SystemExtSpecific() {
 		return "system_ext"
+	} else if p.InstallInSystemDlkm() {
+		return "system_dlkm"
+	} else if p.InstallInVendorDlkm() {
+		return "vendor_dlkm"
+	} else if p.InstallInOdmDlkm() {
+		return "odm_dlkm"
 	}
 	return "system"
 }
@@ -108,16 +125,18 @@
 	"product",
 	"odm",
 	"vendor",
+	"system_dlkm",
+	"vendor_dlkm",
+	"odm_dlkm",
 }
 
 func (p *buildPropModule) GenerateAndroidBuildActions(ctx ModuleContext) {
-	p.outputFilePath = PathForModuleOut(ctx, "build.prop").OutputPath
-	if !ctx.Config().KatiEnabled() {
-		WriteFileRule(ctx, p.outputFilePath, "# no build.prop if kati is disabled")
-		ctx.SetOutputFiles(Paths{p.outputFilePath}, "")
-		return
+	if !p.SocSpecific() && p.properties.Android_info != nil {
+		ctx.ModuleErrorf("Android_info cannot be set if build.prop is not installed in vendor partition")
 	}
 
+	p.outputFilePath = PathForModuleOut(ctx, "build.prop").OutputPath
+
 	partition := p.partition(ctx.DeviceConfig())
 	if !InList(partition, validPartitions) {
 		ctx.PropertyErrorf("partition", "unsupported partition %q: only %q are supported", partition, validPartitions)
diff --git a/android/build_prop_test.go b/android/build_prop_test.go
new file mode 100644
index 0000000..e75975a
--- /dev/null
+++ b/android/build_prop_test.go
@@ -0,0 +1,41 @@
+// Copyright 2024 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 (
+	"testing"
+)
+
+func TestPropFileInputs(t *testing.T) {
+	bp := `
+build_prop {
+    name: "vendor-build.prop",
+    stem: "build.prop",
+    vendor: true,
+    android_info: ":board-info",
+    //product_config: ":product_config",
+}
+android_info {
+    name: "board-info",
+    stem: "android-info.txt",
+}
+`
+
+	res := GroupFixturePreparers(
+		FixtureRegisterWithContext(registerBuildPropComponents),
+	).RunTestWithBp(t, bp)
+	buildPropCmd := res.ModuleForTests("vendor-build.prop", "").Rule("vendor-build.prop_.vendor-build.prop").RuleParams.Command
+	AssertStringDoesContain(t, "Could not find android-info in prop files of vendor build.prop", buildPropCmd, "--prop-files=out/soong/.intermediates/board-info/android-info.prop")
+}
diff --git a/android/config.go b/android/config.go
index 1739b01..27d3b87 100644
--- a/android/config.go
+++ b/android/config.go
@@ -867,7 +867,7 @@
 }
 
 func (c *config) TargetsJava21() bool {
-	return c.IsEnvTrue("EXPERIMENTAL_TARGET_JAVA_VERSION_21")
+	return c.productVariables.GetBuildFlagBool("RELEASE_TARGET_JAVA_21")
 }
 
 // EnvDeps returns the environment variables this build depends on. The first
@@ -1522,6 +1522,13 @@
 	return "vendor"
 }
 
+func (c *deviceConfig) VendorDlkmPath() string {
+	if c.config.productVariables.VendorDlkmPath != nil {
+		return *c.config.productVariables.VendorDlkmPath
+	}
+	return "vendor_dlkm"
+}
+
 func (c *deviceConfig) BuildingVendorImage() bool {
 	return proptools.Bool(c.config.productVariables.BuildingVendorImage)
 }
@@ -1549,6 +1556,17 @@
 	return "odm"
 }
 
+func (c *deviceConfig) BuildingOdmImage() bool {
+	return proptools.Bool(c.config.productVariables.BuildingOdmImage)
+}
+
+func (c *deviceConfig) OdmDlkmPath() string {
+	if c.config.productVariables.OdmDlkmPath != nil {
+		return *c.config.productVariables.OdmDlkmPath
+	}
+	return "odm_dlkm"
+}
+
 func (c *deviceConfig) ProductPath() string {
 	if c.config.productVariables.ProductPath != nil {
 		return *c.config.productVariables.ProductPath
@@ -1567,6 +1585,31 @@
 	return "system_ext"
 }
 
+func (c *deviceConfig) SystemDlkmPath() string {
+	if c.config.productVariables.SystemDlkmPath != nil {
+		return *c.config.productVariables.SystemDlkmPath
+	}
+	return "system_dlkm"
+}
+
+func (c *deviceConfig) OemPath() string {
+	if c.config.productVariables.OemPath != nil {
+		return *c.config.productVariables.OemPath
+	}
+	return "oem"
+}
+
+func (c *deviceConfig) UserdataPath() string {
+	if c.config.productVariables.UserdataPath != nil {
+		return *c.config.productVariables.UserdataPath
+	}
+	return "data"
+}
+
+func (c *deviceConfig) BuildingUserdataImage() bool {
+	return proptools.Bool(c.config.productVariables.BuildingUserdataImage)
+}
+
 func (c *deviceConfig) BtConfigIncludeDir() string {
 	return String(c.config.productVariables.BtConfigIncludeDir)
 }
@@ -1791,10 +1834,6 @@
 	return Bool(c.productVariables.CompressedApex) && !c.UnbundledBuildApps()
 }
 
-func (c *config) ApexTrimEnabled() bool {
-	return Bool(c.productVariables.TrimmedApex)
-}
-
 func (c *config) UseSoongSystemImage() bool {
 	return Bool(c.productVariables.UseSoongSystemImage)
 }
@@ -2234,3 +2273,27 @@
 func (c Config) InstallApexSystemServerDexpreoptSamePartition() bool {
 	return c.config.productVariables.GetBuildFlagBool("RELEASE_INSTALL_APEX_SYSTEMSERVER_DEXPREOPT_SAME_PARTITION")
 }
+
+func (c *config) DeviceMatrixFile() []string {
+	return c.productVariables.DeviceMatrixFile
+}
+
+func (c *config) ProductManifestFiles() []string {
+	return c.productVariables.ProductManifestFiles
+}
+
+func (c *config) SystemManifestFile() []string {
+	return c.productVariables.SystemManifestFile
+}
+
+func (c *config) SystemExtManifestFiles() []string {
+	return c.productVariables.SystemExtManifestFiles
+}
+
+func (c *config) DeviceManifestFiles() []string {
+	return c.productVariables.DeviceManifestFiles
+}
+
+func (c *config) OdmManifestFiles() []string {
+	return c.productVariables.OdmManifestFiles
+}
diff --git a/android/dirgroup.go b/android/dirgroup.go
index 20c4d13..62fbaa5 100644
--- a/android/dirgroup.go
+++ b/android/dirgroup.go
@@ -39,8 +39,7 @@
 }
 
 type DirInfo struct {
-	// TODO(b/358302178): Use DirectoryPaths instead of Paths
-	Dirs Paths
+	Dirs DirectoryPaths
 }
 
 var DirProvider = blueprint.NewProvider[DirInfo]()
diff --git a/android/filegroup_test.go b/android/filegroup_test.go
index 14e9368..670037d 100644
--- a/android/filegroup_test.go
+++ b/android/filegroup_test.go
@@ -1,55 +1,9 @@
 package android
 
 import (
-	"path/filepath"
 	"testing"
 )
 
-func TestFileGroupWithPathProp(t *testing.T) {
-	// TODO(b/247782695), TODO(b/242847534) Fix mixed builds for filegroups
-	t.Skip("Re-enable once filegroups are corrected for mixed builds")
-	outBaseDir := "outputbase"
-	pathPrefix := outBaseDir + "/execroot/__main__"
-	expectedOutputfile := filepath.Join(pathPrefix, "a/b/c/d/test.aidl")
-
-	testCases := []struct {
-		bp  string
-		rel string
-	}{
-		{
-			bp: `
-	filegroup {
-		name: "baz",
-		srcs: ["a/b/c/d/test.aidl"],
-		path: "a/b",
-		bazel_module: { label: "//:baz" },
-	}
-`,
-			rel: "c/d/test.aidl",
-		},
-		{
-			bp: `
-	filegroup {
-		name: "baz",
-		srcs: ["a/b/c/d/test.aidl"],
-		bazel_module: { label: "//:baz" },
-	}
-`,
-			rel: "a/b/c/d/test.aidl",
-		},
-	}
-
-	for _, testCase := range testCases {
-		result := GroupFixturePreparers(
-			PrepareForTestWithFilegroup,
-		).RunTestWithBp(t, testCase.bp)
-
-		fg := result.Module("baz", "").(*fileGroup)
-		AssertStringEquals(t, "src relativeRoot", testCase.rel, fg.srcs[0].Rel())
-		AssertStringEquals(t, "src full path", expectedOutputfile, fg.srcs[0].String())
-	}
-}
-
 func TestFilegroupDefaults(t *testing.T) {
 	bp := FixtureAddTextFile("p/Android.bp", `
 		filegroup_defaults {
diff --git a/android/init.go b/android/init.go
index 1ace344..d3a13d0 100644
--- a/android/init.go
+++ b/android/init.go
@@ -20,6 +20,7 @@
 	gob.Register(extraFilesZip{})
 	gob.Register(InstallPath{})
 	gob.Register(ModuleGenPath{})
+	gob.Register(ModuleObjPath{})
 	gob.Register(ModuleOutPath{})
 	gob.Register(OutputPath{})
 	gob.Register(PhonyPath{})
diff --git a/android/module.go b/android/module.go
index 1148430..3b30c11 100644
--- a/android/module.go
+++ b/android/module.go
@@ -81,10 +81,14 @@
 	InstallInProduct() bool
 	InstallInVendor() bool
 	InstallInSystemExt() bool
+	InstallInSystemDlkm() bool
+	InstallInVendorDlkm() bool
+	InstallInOdmDlkm() bool
 	InstallForceOS() (*OsType, *ArchType)
 	PartitionTag(DeviceConfig) string
 	HideFromMake()
 	IsHideFromMake() bool
+	SkipInstall()
 	IsSkipInstall() bool
 	MakeUninstallable()
 	ReplacedByPrebuilt()
@@ -385,6 +389,15 @@
 	// Whether this module is installed to debug ramdisk
 	Debug_ramdisk *bool
 
+	// Install to partition system_dlkm when set to true.
+	System_dlkm_specific *bool
+
+	// Install to partition vendor_dlkm when set to true.
+	Vendor_dlkm_specific *bool
+
+	// Install to partition odm_dlkm when set to true.
+	Odm_dlkm_specific *bool
+
 	// Whether this module is built for non-native architectures (also known as native bridge binary)
 	Native_bridge_supported *bool `android:"arch_variant"`
 
@@ -1369,6 +1382,8 @@
 		if config.SystemExtPath() == "system_ext" {
 			partition = "system_ext"
 		}
+	} else if m.InstallInRamdisk() {
+		partition = "ramdisk"
 	}
 	return partition
 }
@@ -1421,6 +1436,7 @@
 func (m *ModuleBase) MakeUninstallable() {
 	m.commonProperties.UninstallableApexPlatformVariant = true
 	m.HideFromMake()
+	m.SkipInstall()
 }
 
 func (m *ModuleBase) ReplacedByPrebuilt() {
@@ -1533,6 +1549,18 @@
 	return false
 }
 
+func (m *ModuleBase) InstallInSystemDlkm() bool {
+	return Bool(m.commonProperties.System_dlkm_specific)
+}
+
+func (m *ModuleBase) InstallInVendorDlkm() bool {
+	return Bool(m.commonProperties.Vendor_dlkm_specific)
+}
+
+func (m *ModuleBase) InstallInOdmDlkm() bool {
+	return Bool(m.commonProperties.Odm_dlkm_specific)
+}
+
 func (m *ModuleBase) InstallForceOS() (*OsType, *ArchType) {
 	return nil, nil
 }
@@ -1820,6 +1848,8 @@
 	Enabled bool
 	// Whether the module has been replaced by a prebuilt
 	ReplacedByPrebuilt bool
+	// The Target of artifacts that this module variant is responsible for creating.
+	CompileTarget Target
 }
 
 var CommonPropertiesProviderKey = blueprint.NewProvider[CommonPropertiesProviderData]()
@@ -2008,7 +2038,7 @@
 		ctx.GetMissingDependencies()
 	}
 
-	if m == ctx.FinalModule().(Module).base() {
+	if ctx.IsFinalModule(m.module) {
 		m.generateModuleTarget(ctx)
 		if ctx.Failed() {
 			return
@@ -2084,6 +2114,7 @@
 
 	commonData := CommonPropertiesProviderData{
 		ReplacedByPrebuilt: m.commonProperties.ReplacedByPrebuilt,
+		CompileTarget:      m.commonProperties.CompileTarget,
 	}
 	if m.commonProperties.ForcedDisabled {
 		commonData.Enabled = false
diff --git a/android/module_context.go b/android/module_context.go
index 9fa3a62..41cb0cc 100644
--- a/android/module_context.go
+++ b/android/module_context.go
@@ -19,6 +19,7 @@
 	"github.com/google/blueprint/depset"
 	"path"
 	"path/filepath"
+	"slices"
 	"strings"
 
 	"github.com/google/blueprint"
@@ -195,6 +196,9 @@
 	InstallInOdm() bool
 	InstallInProduct() bool
 	InstallInVendor() bool
+	InstallInSystemDlkm() bool
+	InstallInVendorDlkm() bool
+	InstallInOdmDlkm() bool
 	InstallForceOS() (*OsType, *ArchType)
 
 	RequiredModuleNames(ctx ConfigurableEvaluatorContext) []string
@@ -492,15 +496,23 @@
 	return m.module.InstallInVendor()
 }
 
+func (m *moduleContext) InstallInSystemDlkm() bool {
+	return m.module.InstallInSystemDlkm()
+}
+
+func (m *moduleContext) InstallInVendorDlkm() bool {
+	return m.module.InstallInVendorDlkm()
+}
+
+func (m *moduleContext) InstallInOdmDlkm() bool {
+	return m.module.InstallInOdmDlkm()
+}
+
 func (m *moduleContext) skipInstall() bool {
 	if m.module.base().commonProperties.SkipInstall {
 		return true
 	}
 
-	if m.module.base().commonProperties.HideFromMake {
-		return true
-	}
-
 	// We'll need a solution for choosing which of modules with the same name in different
 	// namespaces to install.  For now, reuse the list of namespaces exported to Make as the
 	// list of namespaces to install in a Soong-only build.
@@ -519,6 +531,10 @@
 		return false
 	}
 
+	if m.module.base().commonProperties.HideFromMake {
+		return false
+	}
+
 	if proptools.Bool(m.module.base().commonProperties.No_full_install) {
 		return false
 	}
@@ -562,9 +578,22 @@
 	m.aconfigFilePaths = paths
 }
 
+func (m *moduleContext) getOwnerAndOverrides() (string, []string) {
+	owner := m.ModuleName()
+	overrides := slices.Clone(m.Module().base().commonProperties.Overrides)
+	if b, ok := m.Module().(OverridableModule); ok {
+		if b.GetOverriddenBy() != "" {
+			// overriding variant of base module
+			overrides = append(overrides, m.ModuleName()) // com.android.foo
+			owner = m.Module().Name()                     // com.company.android.foo
+		}
+	}
+	return owner, overrides
+}
+
 func (m *moduleContext) packageFile(fullInstallPath InstallPath, srcPath Path, executable bool) PackagingSpec {
 	licenseFiles := m.Module().EffectiveLicenseFiles()
-	overrides := CopyOf(m.Module().base().commonProperties.Overrides)
+	owner, overrides := m.getOwnerAndOverrides()
 	spec := PackagingSpec{
 		relPathInPackage:      Rel(m, fullInstallPath.PartitionDir(), fullInstallPath.String()),
 		srcPath:               srcPath,
@@ -576,7 +605,7 @@
 		aconfigPaths:          m.getAconfigPaths(),
 		archType:              m.target.Arch.ArchType,
 		overrides:             &overrides,
-		owner:                 m.ModuleName(),
+		owner:                 owner,
 	}
 	m.packagingSpecs = append(m.packagingSpecs, spec)
 	return spec
@@ -695,7 +724,7 @@
 		m.installFiles = append(m.installFiles, fullInstallPath)
 	}
 
-	overrides := CopyOf(m.Module().base().commonProperties.Overrides)
+	owner, overrides := m.getOwnerAndOverrides()
 	m.packagingSpecs = append(m.packagingSpecs, PackagingSpec{
 		relPathInPackage: Rel(m, fullInstallPath.PartitionDir(), fullInstallPath.String()),
 		srcPath:          nil,
@@ -706,7 +735,7 @@
 		aconfigPaths:     m.getAconfigPaths(),
 		archType:         m.target.Arch.ArchType,
 		overrides:        &overrides,
-		owner:            m.ModuleName(),
+		owner:            owner,
 	})
 
 	return fullInstallPath
@@ -742,7 +771,7 @@
 		m.installFiles = append(m.installFiles, fullInstallPath)
 	}
 
-	overrides := CopyOf(m.Module().base().commonProperties.Overrides)
+	owner, overrides := m.getOwnerAndOverrides()
 	m.packagingSpecs = append(m.packagingSpecs, PackagingSpec{
 		relPathInPackage: Rel(m, fullInstallPath.PartitionDir(), fullInstallPath.String()),
 		srcPath:          nil,
@@ -753,7 +782,7 @@
 		aconfigPaths:     m.getAconfigPaths(),
 		archType:         m.target.Arch.ArchType,
 		overrides:        &overrides,
-		owner:            m.ModuleName(),
+		owner:            owner,
 	})
 
 	return fullInstallPath
diff --git a/android/module_proxy.go b/android/module_proxy.go
index 2a65072..1f96799 100644
--- a/android/module_proxy.go
+++ b/android/module_proxy.go
@@ -106,6 +106,18 @@
 	panic("method is not implemented on ModuleProxy")
 }
 
+func (m ModuleProxy) InstallInSystemDlkm() bool {
+	panic("method is not implemented on ModuleProxy")
+}
+
+func (m ModuleProxy) InstallInVendorDlkm() bool {
+	panic("method is not implemented on ModuleProxy")
+}
+
+func (m ModuleProxy) InstallInOdmDlkm() bool {
+	panic("method is not implemented on ModuleProxy")
+}
+
 func (m ModuleProxy) InstallForceOS() (*OsType, *ArchType) {
 	panic("method is not implemented on ModuleProxy")
 }
@@ -122,6 +134,10 @@
 	panic("method is not implemented on ModuleProxy")
 }
 
+func (m ModuleProxy) SkipInstall() {
+	panic("method is not implemented on ModuleProxy")
+}
+
 func (m ModuleProxy) IsSkipInstall() bool {
 	panic("method is not implemented on ModuleProxy")
 }
diff --git a/android/mutator.go b/android/mutator.go
index 4ddc606..fdd16a8 100644
--- a/android/mutator.go
+++ b/android/mutator.go
@@ -70,7 +70,7 @@
 	TopDown(name string, m TopDownMutator) MutatorHandle
 	BottomUp(name string, m BottomUpMutator) MutatorHandle
 	BottomUpBlueprint(name string, m blueprint.BottomUpMutator) MutatorHandle
-	Transition(name string, m TransitionMutator)
+	Transition(name string, m TransitionMutator) TransitionMutatorHandle
 }
 
 type RegisterMutatorFunc func(RegisterMutatorsContext)
@@ -579,7 +579,7 @@
 	}
 }
 
-func (x *registerMutatorsContext) Transition(name string, m TransitionMutator) {
+func (x *registerMutatorsContext) Transition(name string, m TransitionMutator) TransitionMutatorHandle {
 	atm := &androidTransitionMutator{
 		finalPhase: x.finalPhase,
 		mutator:    m,
@@ -587,8 +587,10 @@
 	}
 	mutator := &mutator{
 		name:              name,
-		transitionMutator: atm}
+		transitionMutator: atm,
+	}
 	x.mutators = append(x.mutators, mutator)
+	return mutator
 }
 
 func (x *registerMutatorsContext) mutatorName(name string) string {
@@ -625,7 +627,10 @@
 	} else if mutator.topDownMutator != nil {
 		handle = blueprintCtx.RegisterTopDownMutator(mutator.name, mutator.topDownMutator)
 	} else if mutator.transitionMutator != nil {
-		blueprintCtx.RegisterTransitionMutator(mutator.name, mutator.transitionMutator)
+		handle := blueprintCtx.RegisterTransitionMutator(mutator.name, mutator.transitionMutator)
+		if mutator.neverFar {
+			handle.NeverFar()
+		}
 	}
 
 	// Forward booleans set on the MutatorHandle to the blueprint.MutatorHandle.
@@ -681,6 +686,14 @@
 	MutatesGlobalState() MutatorHandle
 }
 
+type TransitionMutatorHandle interface {
+	// NeverFar causes the variations created by this mutator to never be ignored when adding
+	// far variation dependencies. Normally, far variation dependencies ignore all the variants
+	// of the source module, and only use the variants explicitly requested by the
+	// AddFarVariationDependencies call.
+	NeverFar() MutatorHandle
+}
+
 func (mutator *mutator) Parallel() MutatorHandle {
 	return mutator
 }
@@ -715,6 +728,11 @@
 	return mutator
 }
 
+func (mutator *mutator) NeverFar() MutatorHandle {
+	mutator.neverFar = true
+	return mutator
+}
+
 func RegisterComponentsMutator(ctx RegisterMutatorsContext) {
 	ctx.BottomUp("component-deps", componentDepsMutator)
 }
diff --git a/android/neverallow.go b/android/neverallow.go
index 44ac2cd..326150b 100644
--- a/android/neverallow.go
+++ b/android/neverallow.go
@@ -63,6 +63,7 @@
 	AddNeverAllowRules(createLimitDirgroupRule()...)
 	AddNeverAllowRules(createFilesystemIsAutoGeneratedRule())
 	AddNeverAllowRules(createKotlinPluginRule()...)
+	AddNeverAllowRules(createPrebuiltEtcBpDefineRule())
 }
 
 // Add a NeverAllow rule to the set of rules to apply.
@@ -321,6 +322,28 @@
 	}
 }
 
+// These module types are introduced to convert PRODUCT_COPY_FILES to Soong,
+// and is only intended to be used by filesystem_creator.
+func createPrebuiltEtcBpDefineRule() Rule {
+	return NeverAllow().
+		ModuleType(
+			"prebuilt_usr_srec",
+			"prebuilt_priv_app",
+			"prebuilt_rfs",
+			"prebuilt_framework",
+			"prebuilt_res",
+			"prebuilt_wlc_upt",
+			"prebuilt_odm",
+			"prebuilt_vendor_dlkm",
+			"prebuilt_bt_firmware",
+			"prebuilt_tvservice",
+			"prebuilt_optee",
+			"prebuilt_tvconfig",
+		).
+		DefinedInBpFile().
+		Because("module type not allowed to be defined in bp file")
+}
+
 func neverallowMutator(ctx BottomUpMutatorContext) {
 	m, ok := ctx.Module().(Module)
 	if !ok {
@@ -354,6 +377,10 @@
 			continue
 		}
 
+		if !n.appliesToBpDefinedModule(ctx) {
+			continue
+		}
+
 		ctx.ModuleErrorf("violates " + n.String())
 	}
 }
@@ -477,6 +504,8 @@
 
 	WithoutMatcher(properties string, matcher ValueMatcher) Rule
 
+	DefinedInBpFile() Rule
+
 	Because(reason string) Rule
 }
 
@@ -498,6 +527,8 @@
 	unlessProps ruleProperties
 
 	onlyBootclasspathJar bool
+
+	definedInBp bool
 }
 
 // Create a new NeverAllow rule.
@@ -571,6 +602,13 @@
 	return r
 }
 
+// DefinedInBpFile specifies that this rule applies to modules that are defined
+// in bp files, and does not apply to modules that are auto generated by other modules.
+func (r *rule) DefinedInBpFile() Rule {
+	r.definedInBp = true
+	return r
+}
+
 func selectMatcher(expected string) ValueMatcher {
 	if expected == "*" {
 		return anyMatcherInstance
@@ -665,6 +703,13 @@
 	return includeProps && !excludeProps
 }
 
+func (r *rule) appliesToBpDefinedModule(ctx BottomUpMutatorContext) bool {
+	if !r.definedInBp {
+		return true
+	}
+	return !ctx.OtherModuleIsAutoGenerated(ctx.Module()) == r.definedInBp
+}
+
 func StartsWith(prefix string) ValueMatcher {
 	return &startsWithMatcher{prefix}
 }
diff --git a/android/neverallow_test.go b/android/neverallow_test.go
index caec8c7..c74d5ff 100644
--- a/android/neverallow_test.go
+++ b/android/neverallow_test.go
@@ -374,6 +374,20 @@
 			`is_auto_generated property is only allowed for filesystem modules in build/soong/fsgen directory`,
 		},
 	},
+	// Test for the rule restricting use of prebuilt_* module
+	{
+		name: `"prebuilt_usr_srec" defined in Android.bp file`,
+		fs: map[string][]byte{
+			"a/b/Android.bp": []byte(`
+				prebuilt_usr_srec {
+					name: "foo",
+				}
+			`),
+		},
+		expectedErrors: []string{
+			`module type not allowed to be defined in bp file`,
+		},
+	},
 }
 
 var prepareForNeverAllowTest = GroupFixturePreparers(
@@ -383,6 +397,7 @@
 		ctx.RegisterModuleType("java_library_host", newMockJavaLibraryModule)
 		ctx.RegisterModuleType("java_device_for_host", newMockJavaLibraryModule)
 		ctx.RegisterModuleType("filesystem", newMockFilesystemModule)
+		ctx.RegisterModuleType("prebuilt_usr_srec", newMockPrebuiltUsrSrecModule)
 	}),
 )
 
@@ -482,3 +497,16 @@
 
 func (p *mockJavaLibraryModule) GenerateAndroidBuildActions(ModuleContext) {
 }
+
+type mockPrebuiltUsrSrecModule struct {
+	ModuleBase
+}
+
+func (p *mockPrebuiltUsrSrecModule) GenerateAndroidBuildActions(ModuleContext) {
+}
+
+func newMockPrebuiltUsrSrecModule() Module {
+	m := &mockPrebuiltUsrSrecModule{}
+	InitAndroidModule(m)
+	return m
+}
diff --git a/android/packaging.go b/android/packaging.go
index acafcd4..98c85fa 100644
--- a/android/packaging.go
+++ b/android/packaging.go
@@ -207,38 +207,49 @@
 	// If this is set to true by a module type inheriting PackagingBase, the deps property
 	// collects the first target only even with compile_multilib: true.
 	DepsCollectFirstTargetOnly bool
+
+	// If this is set to try by a module type inheriting PackagingBase, the module type is
+	// allowed to utilize High_priority_deps.
+	AllowHighPriorityDeps bool
 }
 
-type depsProperty struct {
+type DepsProperty struct {
+	// Deps that have higher priority in packaging when there is a packaging conflict.
+	// For example, if multiple files are being installed to same filepath, the install file
+	// of the module listed in this property will have a higher priority over those in other
+	// deps properties.
+	High_priority_deps []string `android:"arch_variant"`
+
 	// Modules to include in this package
 	Deps proptools.Configurable[[]string] `android:"arch_variant"`
 }
 
 type packagingMultilibProperties struct {
-	First    depsProperty `android:"arch_variant"`
-	Common   depsProperty `android:"arch_variant"`
-	Lib32    depsProperty `android:"arch_variant"`
-	Lib64    depsProperty `android:"arch_variant"`
-	Both     depsProperty `android:"arch_variant"`
-	Prefer32 depsProperty `android:"arch_variant"`
+	First    DepsProperty `android:"arch_variant"`
+	Common   DepsProperty `android:"arch_variant"`
+	Lib32    DepsProperty `android:"arch_variant"`
+	Lib64    DepsProperty `android:"arch_variant"`
+	Both     DepsProperty `android:"arch_variant"`
+	Prefer32 DepsProperty `android:"arch_variant"`
 }
 
 type packagingArchProperties struct {
-	Arm64  depsProperty
-	Arm    depsProperty
-	X86_64 depsProperty
-	X86    depsProperty
+	Arm64  DepsProperty
+	Arm    DepsProperty
+	X86_64 DepsProperty
+	X86    DepsProperty
 }
 
 type PackagingProperties struct {
-	Deps     proptools.Configurable[[]string] `android:"arch_variant"`
-	Multilib packagingMultilibProperties      `android:"arch_variant"`
+	DepsProperty
+
+	Multilib packagingMultilibProperties `android:"arch_variant"`
 	Arch     packagingArchProperties
 }
 
 func InitPackageModule(p PackageModule) {
 	base := p.packagingBase()
-	p.AddProperties(&base.properties)
+	p.AddProperties(&base.properties, &base.properties.DepsProperty)
 }
 
 func (p *PackagingBase) packagingBase() *PackagingBase {
@@ -249,55 +260,72 @@
 // the current archicture when this module is not configured for multi target. When configured for
 // multi target, deps is selected for each of the targets and is NOT selected for the current
 // architecture which would be Common.
-func (p *PackagingBase) getDepsForArch(ctx BaseModuleContext, arch ArchType) []string {
-	get := func(prop proptools.Configurable[[]string]) []string {
-		return prop.GetOrDefault(ctx, nil)
+// It returns two lists, the normal and high priority deps, respectively.
+func (p *PackagingBase) getDepsForArch(ctx BaseModuleContext, arch ArchType) ([]string, []string) {
+	var normalDeps []string
+	var highPriorityDeps []string
+
+	get := func(prop DepsProperty) {
+		normalDeps = append(normalDeps, prop.Deps.GetOrDefault(ctx, nil)...)
+		highPriorityDeps = append(highPriorityDeps, prop.High_priority_deps...)
+	}
+	has := func(prop DepsProperty) bool {
+		return len(prop.Deps.GetOrDefault(ctx, nil)) > 0 || len(prop.High_priority_deps) > 0
 	}
 
-	var ret []string
 	if arch == ctx.Target().Arch.ArchType && len(ctx.MultiTargets()) == 0 {
-		ret = append(ret, get(p.properties.Deps)...)
+		get(p.properties.DepsProperty)
 	} else if arch.Multilib == "lib32" {
-		ret = append(ret, get(p.properties.Multilib.Lib32.Deps)...)
+		get(p.properties.Multilib.Lib32)
 		// multilib.prefer32.deps are added for lib32 only when they support 32-bit arch
-		for _, dep := range get(p.properties.Multilib.Prefer32.Deps) {
+		for _, dep := range p.properties.Multilib.Prefer32.Deps.GetOrDefault(ctx, nil) {
 			if checkIfOtherModuleSupportsLib32(ctx, dep) {
-				ret = append(ret, dep)
+				normalDeps = append(normalDeps, dep)
+			}
+		}
+		for _, dep := range p.properties.Multilib.Prefer32.High_priority_deps {
+			if checkIfOtherModuleSupportsLib32(ctx, dep) {
+				highPriorityDeps = append(highPriorityDeps, dep)
 			}
 		}
 	} else if arch.Multilib == "lib64" {
-		ret = append(ret, get(p.properties.Multilib.Lib64.Deps)...)
+		get(p.properties.Multilib.Lib64)
 		// multilib.prefer32.deps are added for lib64 only when they don't support 32-bit arch
-		for _, dep := range get(p.properties.Multilib.Prefer32.Deps) {
+		for _, dep := range p.properties.Multilib.Prefer32.Deps.GetOrDefault(ctx, nil) {
 			if !checkIfOtherModuleSupportsLib32(ctx, dep) {
-				ret = append(ret, dep)
+				normalDeps = append(normalDeps, dep)
+			}
+		}
+		for _, dep := range p.properties.Multilib.Prefer32.High_priority_deps {
+			if !checkIfOtherModuleSupportsLib32(ctx, dep) {
+				highPriorityDeps = append(highPriorityDeps, dep)
 			}
 		}
 	} else if arch == Common {
-		ret = append(ret, get(p.properties.Multilib.Common.Deps)...)
+		get(p.properties.Multilib.Common)
 	}
 
 	if p.DepsCollectFirstTargetOnly {
-		if len(get(p.properties.Multilib.First.Deps)) > 0 {
+		if has(p.properties.Multilib.First) {
 			ctx.PropertyErrorf("multilib.first.deps", "not supported. use \"deps\" instead")
 		}
 		for i, t := range ctx.MultiTargets() {
 			if t.Arch.ArchType == arch {
-				ret = append(ret, get(p.properties.Multilib.Both.Deps)...)
+				get(p.properties.Multilib.Both)
 				if i == 0 {
-					ret = append(ret, get(p.properties.Deps)...)
+					get(p.properties.DepsProperty)
 				}
 			}
 		}
 	} else {
-		if len(get(p.properties.Multilib.Both.Deps)) > 0 {
+		if has(p.properties.Multilib.Both) {
 			ctx.PropertyErrorf("multilib.both.deps", "not supported. use \"deps\" instead")
 		}
 		for i, t := range ctx.MultiTargets() {
 			if t.Arch.ArchType == arch {
-				ret = append(ret, get(p.properties.Deps)...)
+				get(p.properties.DepsProperty)
 				if i == 0 {
-					ret = append(ret, get(p.properties.Multilib.First.Deps)...)
+					get(p.properties.Multilib.First)
 				}
 			}
 		}
@@ -306,17 +334,21 @@
 	if ctx.Arch().ArchType == Common {
 		switch arch {
 		case Arm64:
-			ret = append(ret, get(p.properties.Arch.Arm64.Deps)...)
+			get(p.properties.Arch.Arm64)
 		case Arm:
-			ret = append(ret, get(p.properties.Arch.Arm.Deps)...)
+			get(p.properties.Arch.Arm)
 		case X86_64:
-			ret = append(ret, get(p.properties.Arch.X86_64.Deps)...)
+			get(p.properties.Arch.X86_64)
 		case X86:
-			ret = append(ret, get(p.properties.Arch.X86.Deps)...)
+			get(p.properties.Arch.X86)
 		}
 	}
 
-	return FirstUniqueStrings(ret)
+	if len(highPriorityDeps) > 0 && !p.AllowHighPriorityDeps {
+		ctx.ModuleErrorf("Usage of high_priority_deps is not allowed for %s module type", ctx.ModuleType())
+	}
+
+	return FirstUniqueStrings(normalDeps), FirstUniqueStrings(highPriorityDeps)
 }
 
 func getSupportedTargets(ctx BaseModuleContext) []Target {
@@ -360,6 +392,8 @@
 	IsPackagingItem() bool
 }
 
+var _ PackagingItem = (*PackagingItemAlwaysDepTag)(nil)
+
 // DepTag provides default implementation of PackagingItem interface.
 // PackagingBase-derived modules can define their own dependency tag by embedding this, which
 // can be passed to AddDeps() or AddDependencies().
@@ -371,31 +405,52 @@
 	return true
 }
 
+// highPriorityDepTag provides default implementation of HighPriorityPackagingItem interface.
+type highPriorityDepTag struct {
+	blueprint.DependencyTag
+}
+
 // See PackageModule.AddDeps
 func (p *PackagingBase) AddDeps(ctx BottomUpMutatorContext, depTag blueprint.DependencyTag) {
+	addDep := func(t Target, dep string, highPriority bool) {
+		if p.IgnoreMissingDependencies && !ctx.OtherModuleExists(dep) {
+			return
+		}
+		targetVariation := t.Variations()
+		sharedVariation := blueprint.Variation{
+			Mutator:   "link",
+			Variation: "shared",
+		}
+		// If a shared variation exists, use that. Static variants do not provide any standalone files
+		// for packaging.
+		if ctx.OtherModuleFarDependencyVariantExists([]blueprint.Variation{sharedVariation}, dep) {
+			targetVariation = append(targetVariation, sharedVariation)
+		}
+		depTagToUse := depTag
+		if highPriority {
+			depTagToUse = highPriorityDepTag{depTag}
+		}
+
+		ctx.AddFarVariationDependencies(targetVariation, depTagToUse, dep)
+	}
 	for _, t := range getSupportedTargets(ctx) {
-		for _, dep := range p.getDepsForArch(ctx, t.Arch.ArchType) {
-			if p.IgnoreMissingDependencies && !ctx.OtherModuleExists(dep) {
-				continue
-			}
-			targetVariation := t.Variations()
-			sharedVariation := blueprint.Variation{
-				Mutator:   "link",
-				Variation: "shared",
-			}
-			// If a shared variation exists, use that. Static variants do not provide any standalone files
-			// for packaging.
-			if ctx.OtherModuleFarDependencyVariantExists([]blueprint.Variation{sharedVariation}, dep) {
-				targetVariation = append(targetVariation, sharedVariation)
-			}
-			ctx.AddFarVariationDependencies(targetVariation, depTag, dep)
+		normalDeps, highPriorityDeps := p.getDepsForArch(ctx, t.Arch.ArchType)
+		for _, dep := range normalDeps {
+			addDep(t, dep, false)
+		}
+		for _, dep := range highPriorityDeps {
+			addDep(t, dep, true)
 		}
 	}
 }
 
 func (p *PackagingBase) GatherPackagingSpecsWithFilter(ctx ModuleContext, filter func(PackagingSpec) bool) map[string]PackagingSpec {
-	// all packaging specs gathered from the dep.
-	var all []PackagingSpec
+	// packaging specs gathered from the dep that are not high priorities.
+	var regularPriorities []PackagingSpec
+
+	// all packaging specs gathered from the high priority deps.
+	var highPriorities []PackagingSpec
+
 	// Name of the dependency which requested the packaging spec.
 	// If this dep is overridden, the packaging spec will not be installed via this dependency chain.
 	// (the packaging spec might still be installed if there are some other deps which depend on it).
@@ -420,7 +475,8 @@
 	}
 
 	ctx.VisitDirectDeps(func(child Module) {
-		if pi, ok := ctx.OtherModuleDependencyTag(child).(PackagingItem); !ok || !pi.IsPackagingItem() {
+		depTag := ctx.OtherModuleDependencyTag(child)
+		if pi, ok := depTag.(PackagingItem); !ok || !pi.IsPackagingItem() {
 			return
 		}
 		for _, ps := range OtherModuleProviderOrDefault(
@@ -434,7 +490,13 @@
 					continue
 				}
 			}
-			all = append(all, ps)
+
+			if _, ok := depTag.(highPriorityDepTag); ok {
+				highPriorities = append(highPriorities, ps)
+			} else {
+				regularPriorities = append(regularPriorities, ps)
+			}
+
 			depNames = append(depNames, child.Name())
 			if ps.overrides != nil {
 				overridden = append(overridden, *ps.overrides...)
@@ -442,21 +504,26 @@
 		}
 	})
 
-	// all minus packaging specs that are overridden
-	var filtered []PackagingSpec
-	for index, ps := range all {
-		if ps.owner != "" && InList(ps.owner, overridden) {
-			continue
+	filterOverridden := func(input []PackagingSpec) []PackagingSpec {
+		// input minus packaging specs that are overridden
+		var filtered []PackagingSpec
+		for index, ps := range input {
+			if ps.owner != "" && InList(ps.owner, overridden) {
+				continue
+			}
+			// The dependency which requested this packaging spec has been overridden.
+			if InList(depNames[index], overridden) {
+				continue
+			}
+			filtered = append(filtered, ps)
 		}
-		// The dependency which requested this packaging spec has been overridden.
-		if InList(depNames[index], overridden) {
-			continue
-		}
-		filtered = append(filtered, ps)
+		return filtered
 	}
 
+	filteredRegularPriority := filterOverridden(regularPriorities)
+
 	m := make(map[string]PackagingSpec)
-	for _, ps := range filtered {
+	for _, ps := range filteredRegularPriority {
 		dstPath := ps.relPathInPackage
 		if existingPs, ok := m[dstPath]; ok {
 			if !existingPs.Equals(&ps) {
@@ -466,6 +533,21 @@
 		}
 		m[dstPath] = ps
 	}
+
+	filteredHighPriority := filterOverridden(highPriorities)
+	highPriorityPs := make(map[string]PackagingSpec)
+	for _, ps := range filteredHighPriority {
+		dstPath := ps.relPathInPackage
+		if existingPs, ok := highPriorityPs[dstPath]; ok {
+			if !existingPs.Equals(&ps) {
+				ctx.ModuleErrorf("packaging conflict at %v:\n%v\n%v", dstPath, existingPs, ps)
+			}
+			continue
+		}
+		highPriorityPs[dstPath] = ps
+		m[dstPath] = ps
+	}
+
 	return m
 }
 
diff --git a/android/paths.go b/android/paths.go
index bf2c3a0..9cb872d 100644
--- a/android/paths.go
+++ b/android/paths.go
@@ -117,6 +117,9 @@
 	InstallInOdm() bool
 	InstallInProduct() bool
 	InstallInVendor() bool
+	InstallInSystemDlkm() bool
+	InstallInVendorDlkm() bool
+	InstallInOdmDlkm() bool
 	InstallForceOS() (*OsType, *ArchType)
 }
 
@@ -170,6 +173,18 @@
 	return ctx.Module().InstallInVendor()
 }
 
+func (ctx *baseModuleContextToModuleInstallPathContext) InstallInSystemDlkm() bool {
+	return ctx.Module().InstallInSystemDlkm()
+}
+
+func (ctx *baseModuleContextToModuleInstallPathContext) InstallInVendorDlkm() bool {
+	return ctx.Module().InstallInVendorDlkm()
+}
+
+func (ctx *baseModuleContextToModuleInstallPathContext) InstallInOdmDlkm() bool {
+	return ctx.Module().InstallInOdmDlkm()
+}
+
 func (ctx *baseModuleContextToModuleInstallPathContext) InstallForceOS() (*OsType, *ArchType) {
 	return ctx.Module().InstallForceOS()
 }
@@ -551,15 +566,35 @@
 	return ret
 }
 
+type directoryPath struct {
+	basePath
+}
+
+func (d *directoryPath) String() string {
+	return d.basePath.String()
+}
+
+func (d *directoryPath) base() basePath {
+	return d.basePath
+}
+
+// DirectoryPath represents a source path for directories. Incompatible with Path by design.
+type DirectoryPath interface {
+	String() string
+	base() basePath
+}
+
+var _ DirectoryPath = (*directoryPath)(nil)
+
+type DirectoryPaths []DirectoryPath
+
 // DirectoryPathsForModuleSrcExcludes returns a Paths{} containing the resolved references in
 // directory paths. Elements of paths are resolved as:
 //   - filepath, relative to local module directory, resolves as a filepath relative to the local
 //     source directory
 //   - other modules using the ":name" syntax. These modules must implement DirProvider.
-//
-// TODO(b/358302178): Implement DirectoryPath and change the return type.
-func DirectoryPathsForModuleSrc(ctx ModuleMissingDepsPathContext, paths []string) Paths {
-	var ret Paths
+func DirectoryPathsForModuleSrc(ctx ModuleMissingDepsPathContext, paths []string) DirectoryPaths {
+	var ret DirectoryPaths
 
 	for _, path := range paths {
 		if m, t := SrcIsModuleWithTag(path); m != "" {
@@ -588,12 +623,12 @@
 			} else if !isDir {
 				ReportPathErrorf(ctx, "module directory path %q is not a directory", p)
 			} else {
-				ret = append(ret, p)
+				ret = append(ret, &directoryPath{basePath{path: p.path, rel: p.rel}})
 			}
 		}
 	}
 
-	seen := make(map[Path]bool, len(ret))
+	seen := make(map[DirectoryPath]bool, len(ret))
 	for _, path := range ret {
 		if seen[path] {
 			ReportPathErrorf(ctx, "duplicated path %q", path)
@@ -2057,6 +2092,10 @@
 	return base.Join(ctx, paths...)
 }
 
+func PathForSuiteInstall(ctx PathContext, suite string, pathComponents ...string) InstallPath {
+	return pathForPartitionInstallDir(ctx, "test_suites", "test_suites", false).Join(ctx, suite).Join(ctx, pathComponents...)
+}
+
 func InstallPathToOnDevicePath(ctx PathContext, path InstallPath) string {
 	rel := Rel(ctx, strings.TrimSuffix(path.PartitionDir(), path.partition), path.String())
 	return "/" + rel
@@ -2111,6 +2150,12 @@
 			partition = ctx.DeviceConfig().SystemExtPath()
 		} else if ctx.InstallInRoot() {
 			partition = "root"
+		} else if ctx.InstallInSystemDlkm() {
+			partition = ctx.DeviceConfig().SystemDlkmPath()
+		} else if ctx.InstallInVendorDlkm() {
+			partition = ctx.DeviceConfig().VendorDlkmPath()
+		} else if ctx.InstallInOdmDlkm() {
+			partition = ctx.DeviceConfig().OdmDlkmPath()
 		} else {
 			partition = "system"
 		}
@@ -2314,6 +2359,9 @@
 	inOdm           bool
 	inProduct       bool
 	inVendor        bool
+	inSystemDlkm    bool
+	inVendorDlkm    bool
+	inOdmDlkm       bool
 	forceOS         *OsType
 	forceArch       *ArchType
 }
@@ -2368,6 +2416,18 @@
 	return m.inVendor
 }
 
+func (m testModuleInstallPathContext) InstallInSystemDlkm() bool {
+	return m.inSystemDlkm
+}
+
+func (m testModuleInstallPathContext) InstallInVendorDlkm() bool {
+	return m.inVendorDlkm
+}
+
+func (m testModuleInstallPathContext) InstallInOdmDlkm() bool {
+	return m.inOdmDlkm
+}
+
 func (m testModuleInstallPathContext) InstallForceOS() (*OsType, *ArchType) {
 	return m.forceOS, m.forceArch
 }
diff --git a/android/paths_test.go b/android/paths_test.go
index 941f0ca..5e618f9 100644
--- a/android/paths_test.go
+++ b/android/paths_test.go
@@ -1592,6 +1592,12 @@
 	})
 }
 
+func TestDirectoryPathIsIncompatibleWithPath(t *testing.T) {
+	d := (DirectoryPath)(&directoryPath{})
+	_, ok := d.(Path)
+	AssertBoolEquals(t, "directoryPath shouldn't implement Path", ok, false)
+}
+
 func ExampleOutputPath_ReplaceExtension() {
 	ctx := &configErrorWrapper{
 		config: TestConfig("out", nil, "", nil),
diff --git a/android/prebuilt.go b/android/prebuilt.go
index 5d75b62..51e72af 100644
--- a/android/prebuilt.go
+++ b/android/prebuilt.go
@@ -569,6 +569,7 @@
 		for _, moduleInFamily := range allModulesInFamily {
 			if moduleInFamily.Name() != selectedModuleInFamily.Name() {
 				moduleInFamily.HideFromMake()
+				moduleInFamily.SkipInstall()
 				// If this is a prebuilt module, unset properties.UsePrebuilt
 				// properties.UsePrebuilt might evaluate to true via soong config var fallback mechanism
 				// Set it to false explicitly so that the following mutator does not replace rdeps to this unselected prebuilt
@@ -639,6 +640,7 @@
 			}
 		} else {
 			m.HideFromMake()
+			m.SkipInstall()
 		}
 	}
 }
diff --git a/android/register.go b/android/register.go
index bb1ead7..8d2f19e 100644
--- a/android/register.go
+++ b/android/register.go
@@ -98,6 +98,7 @@
 	usesCreateModule        bool
 	mutatesDependencies     bool
 	mutatesGlobalState      bool
+	neverFar                bool
 }
 
 var _ sortableComponent = &mutator{}
diff --git a/android/rule_builder.go b/android/rule_builder.go
index 56de9cd..403c184 100644
--- a/android/rule_builder.go
+++ b/android/rule_builder.go
@@ -575,25 +575,28 @@
 		nsjailCmd.WriteString(r.outDir.String())
 		nsjailCmd.WriteString(":nsjail_build_sandbox/out")
 
-		for _, input := range inputs {
+		addBindMount := func(src, dst string) {
 			nsjailCmd.WriteString(" -R $PWD/")
-			nsjailCmd.WriteString(input.String())
+			nsjailCmd.WriteString(src)
 			nsjailCmd.WriteString(":nsjail_build_sandbox/")
-			nsjailCmd.WriteString(r.nsjailPathForInputRel(input))
+			nsjailCmd.WriteString(dst)
+		}
+
+		for _, input := range inputs {
+			addBindMount(input.String(), r.nsjailPathForInputRel(input))
 		}
 		for _, tool := range tools {
-			nsjailCmd.WriteString(" -R $PWD/")
-			nsjailCmd.WriteString(tool.String())
-			nsjailCmd.WriteString(":nsjail_build_sandbox/")
-			nsjailCmd.WriteString(nsjailPathForToolRel(r.ctx, tool))
+			addBindMount(tool.String(), nsjailPathForToolRel(r.ctx, tool))
 		}
 		inputs = append(inputs, tools...)
 		for _, c := range r.commands {
+			for _, directory := range c.implicitDirectories {
+				addBindMount(directory.String(), directory.String())
+				// TODO(b/375551969): Add implicitDirectories to BuildParams, rather than relying on implicits
+				inputs = append(inputs, SourcePath{basePath: directory.base()})
+			}
 			for _, tool := range c.packagedTools {
-				nsjailCmd.WriteString(" -R $PWD/")
-				nsjailCmd.WriteString(tool.srcPath.String())
-				nsjailCmd.WriteString(":nsjail_build_sandbox/")
-				nsjailCmd.WriteString(nsjailPathForPackagedToolRel(tool))
+				addBindMount(tool.srcPath.String(), nsjailPathForPackagedToolRel(tool))
 				inputs = append(inputs, tool.srcPath)
 			}
 		}
@@ -917,16 +920,17 @@
 type RuleBuilderCommand struct {
 	rule *RuleBuilder
 
-	buf           strings.Builder
-	inputs        Paths
-	implicits     Paths
-	orderOnlys    Paths
-	validations   Paths
-	outputs       WritablePaths
-	depFiles      WritablePaths
-	tools         Paths
-	packagedTools []PackagingSpec
-	rspFiles      []rspFileAndPaths
+	buf                 strings.Builder
+	inputs              Paths
+	implicits           Paths
+	orderOnlys          Paths
+	validations         Paths
+	outputs             WritablePaths
+	depFiles            WritablePaths
+	tools               Paths
+	packagedTools       []PackagingSpec
+	rspFiles            []rspFileAndPaths
+	implicitDirectories DirectoryPaths
 }
 
 type rspFileAndPaths struct {
@@ -951,6 +955,10 @@
 	c.implicits = append(c.implicits, path)
 }
 
+func (c *RuleBuilderCommand) addImplicitDirectory(path DirectoryPath) {
+	c.implicitDirectories = append(c.implicitDirectories, path)
+}
+
 func (c *RuleBuilderCommand) addOrderOnly(path Path) {
 	checkPathNotNil(path)
 	c.orderOnlys = append(c.orderOnlys, path)
@@ -1313,6 +1321,16 @@
 	return c
 }
 
+// ImplicitDirectory adds the specified input directory to the dependencies without modifying the
+// command line. Added directories will be bind-mounted for the nsjail.
+func (c *RuleBuilderCommand) ImplicitDirectory(path DirectoryPath) *RuleBuilderCommand {
+	if !c.rule.nsjail {
+		panic("ImplicitDirectory() must be called after Nsjail()")
+	}
+	c.addImplicitDirectory(path)
+	return c
+}
+
 // GetImplicits returns the command's implicit inputs.
 func (c *RuleBuilderCommand) GetImplicits() Paths {
 	return c.implicits
diff --git a/android/singleton.go b/android/singleton.go
index 913bf6a..0754b0c 100644
--- a/android/singleton.go
+++ b/android/singleton.go
@@ -64,6 +64,7 @@
 
 	VisitAllModulesBlueprint(visit func(blueprint.Module))
 	VisitAllModules(visit func(Module))
+	VisitAllModuleProxies(visit func(proxy ModuleProxy))
 	VisitAllModulesIf(pred func(Module) bool, visit func(Module))
 
 	VisitDirectDeps(module Module, visit func(Module))
@@ -77,8 +78,10 @@
 
 	VisitAllModuleVariants(module Module, visit func(Module))
 
+	VisitAllModuleVariantProxies(module Module, visit func(proxy ModuleProxy))
+
 	PrimaryModule(module Module) Module
-	FinalModule(module Module) Module
+	IsFinalModule(module Module) bool
 
 	AddNinjaFileDeps(deps ...string)
 
@@ -193,7 +196,7 @@
 }
 
 // visitAdaptor wraps a visit function that takes an android.Module parameter into
-// a function that takes an blueprint.Module parameter and only calls the visit function if the
+// a function that takes a blueprint.Module parameter and only calls the visit function if the
 // blueprint.Module is an android.Module.
 func visitAdaptor(visit func(Module)) func(blueprint.Module) {
 	return func(module blueprint.Module) {
@@ -203,6 +206,16 @@
 	}
 }
 
+// visitProxyAdaptor wraps a visit function that takes an android.ModuleProxy parameter into
+// a function that takes a blueprint.ModuleProxy parameter.
+func visitProxyAdaptor(visit func(proxy ModuleProxy)) func(proxy blueprint.ModuleProxy) {
+	return func(module blueprint.ModuleProxy) {
+		visit(ModuleProxy{
+			module: module,
+		})
+	}
+}
+
 // predAdaptor wraps a pred function that takes an android.Module parameter
 // into a function that takes an blueprint.Module parameter and only calls the visit function if the
 // blueprint.Module is an android.Module, otherwise returns false.
@@ -224,6 +237,10 @@
 	s.SingletonContext.VisitAllModules(visitAdaptor(visit))
 }
 
+func (s *singletonContextAdaptor) VisitAllModuleProxies(visit func(proxy ModuleProxy)) {
+	s.SingletonContext.VisitAllModuleProxies(visitProxyAdaptor(visit))
+}
+
 func (s *singletonContextAdaptor) VisitAllModulesIf(pred func(Module) bool, visit func(Module)) {
 	s.SingletonContext.VisitAllModulesIf(predAdaptor(pred), visitAdaptor(visit))
 }
@@ -248,12 +265,16 @@
 	s.SingletonContext.VisitAllModuleVariants(module, visitAdaptor(visit))
 }
 
+func (s *singletonContextAdaptor) VisitAllModuleVariantProxies(module Module, visit func(proxy ModuleProxy)) {
+	s.SingletonContext.VisitAllModuleVariantProxies(module, visitProxyAdaptor(visit))
+}
+
 func (s *singletonContextAdaptor) PrimaryModule(module Module) Module {
 	return s.SingletonContext.PrimaryModule(module).(Module)
 }
 
-func (s *singletonContextAdaptor) FinalModule(module Module) Module {
-	return s.SingletonContext.FinalModule(module).(Module)
+func (s *singletonContextAdaptor) IsFinalModule(module Module) bool {
+	return s.SingletonContext.IsFinalModule(module)
 }
 
 func (s *singletonContextAdaptor) ModuleVariantsFromName(referer Module, name string) []Module {
diff --git a/android/testing.go b/android/testing.go
index 23aadda..f243e81 100644
--- a/android/testing.go
+++ b/android/testing.go
@@ -1132,11 +1132,6 @@
 	config.katiEnabled = true
 }
 
-func SetTrimmedApexEnabledForTests(config Config) {
-	config.productVariables.TrimmedApex = new(bool)
-	*config.productVariables.TrimmedApex = true
-}
-
 func AndroidMkEntriesForTest(t *testing.T, ctx *TestContext, mod blueprint.Module) []AndroidMkEntries {
 	t.Helper()
 	var p AndroidMkEntriesProvider
diff --git a/android/util.go b/android/util.go
index 2d269b7..3fc4608 100644
--- a/android/util.go
+++ b/android/util.go
@@ -660,3 +660,13 @@
 	v, loaded := m.Map.LoadOrStore(key, value)
 	return v.(V), loaded
 }
+
+// AppendIfNotZero append the given value to the slice if it is not the zero value
+// for its type.
+func AppendIfNotZero[T comparable](slice []T, value T) []T {
+	var zeroValue T // Get the zero value of the type T
+	if value != zeroValue {
+		return append(slice, value)
+	}
+	return slice
+}
diff --git a/android/variable.go b/android/variable.go
index 6693d91..2d43c6d 100644
--- a/android/variable.go
+++ b/android/variable.go
@@ -338,12 +338,19 @@
 	HWASanIncludePaths []string `json:",omitempty"`
 	HWASanExcludePaths []string `json:",omitempty"`
 
-	VendorPath           *string `json:",omitempty"`
-	BuildingVendorImage  *bool   `json:",omitempty"`
-	OdmPath              *string `json:",omitempty"`
-	ProductPath          *string `json:",omitempty"`
-	BuildingProductImage *bool   `json:",omitempty"`
-	SystemExtPath        *string `json:",omitempty"`
+	VendorPath            *string `json:",omitempty"`
+	VendorDlkmPath        *string `json:",omitempty"`
+	BuildingVendorImage   *bool   `json:",omitempty"`
+	OdmPath               *string `json:",omitempty"`
+	BuildingOdmImage      *bool   `json:",omitempty"`
+	OdmDlkmPath           *string `json:",omitempty"`
+	ProductPath           *string `json:",omitempty"`
+	BuildingProductImage  *bool   `json:",omitempty"`
+	SystemExtPath         *string `json:",omitempty"`
+	SystemDlkmPath        *string `json:",omitempty"`
+	OemPath               *string `json:",omitempty"`
+	UserdataPath          *string `json:",omitempty"`
+	BuildingUserdataImage *bool   `json:",omitempty"`
 
 	ClangTidy  *bool   `json:",omitempty"`
 	TidyChecks *string `json:",omitempty"`
@@ -401,7 +408,6 @@
 
 	Ndk_abis *bool `json:",omitempty"`
 
-	TrimmedApex                  *bool `json:",omitempty"`
 	ForceApexSymlinkOptimization *bool `json:",omitempty"`
 	CompressedApex               *bool `json:",omitempty"`
 	Aml_abis                     *bool `json:",omitempty"`
@@ -535,6 +541,13 @@
 	ExtraAllowedDepsTxt *string `json:",omitempty"`
 
 	AdbKeys *string `json:",omitempty"`
+
+	DeviceMatrixFile       []string `json:",omitempty"`
+	ProductManifestFiles   []string `json:",omitempty"`
+	SystemManifestFile     []string `json:",omitempty"`
+	SystemExtManifestFiles []string `json:",omitempty"`
+	DeviceManifestFiles    []string `json:",omitempty"`
+	OdmManifestFiles       []string `json:",omitempty"`
 }
 
 type PartitionQualifiedVariablesType struct {
@@ -565,6 +578,14 @@
 	BoardAvbRollbackIndexLocation string `json:",omitempty"`
 }
 
+type ChainedAvbPartitionProps struct {
+	Partitions            []string `json:",omitempty"`
+	Key                   string   `json:",omitempty"`
+	Algorithm             string   `json:",omitempty"`
+	RollbackIndex         string   `json:",omitempty"`
+	RollbackIndexLocation string   `json:",omitempty"`
+}
+
 type PartitionVariables struct {
 	ProductDirectory            string `json:",omitempty"`
 	PartitionQualifiedVariables map[string]PartitionQualifiedVariablesType
@@ -585,16 +606,47 @@
 	BoardExt4ShareDupBlocks        string `json:",omitempty"`
 	BoardFlashLogicalBlockSize     string `json:",omitempty"`
 	BoardFlashEraseBlockSize       string `json:",omitempty"`
-	BoardUsesRecoveryAsBoot        bool   `json:",omitempty"`
 	ProductUseDynamicPartitionSize bool   `json:",omitempty"`
 	CopyImagesForTargetFilesZip    bool   `json:",omitempty"`
 
-	BoardAvbEnable bool `json:",omitempty"`
+	// Boot image stuff
+	ProductBuildBootImage           bool   `json:",omitempty"`
+	ProductBuildInitBootImage       bool   `json:",omitempty"`
+	BoardUsesRecoveryAsBoot         bool   `json:",omitempty"`
+	BoardPrebuiltBootimage          string `json:",omitempty"`
+	BoardPrebuiltInitBootimage      string `json:",omitempty"`
+	BoardBootimagePartitionSize     string `json:",omitempty"`
+	BoardInitBootimagePartitionSize string `json:",omitempty"`
+	BoardBootHeaderVersion          string `json:",omitempty"`
 
-	ProductPackages      []string `json:",omitempty"`
-	ProductPackagesDebug []string `json:",omitempty"`
+	// Avb (android verified boot) stuff
+	BoardAvbEnable          bool                                `json:",omitempty"`
+	BoardAvbAlgorithm       string                              `json:",omitempty"`
+	BoardAvbKeyPath         string                              `json:",omitempty"`
+	BoardAvbRollbackIndex   string                              `json:",omitempty"`
+	BuildingVbmetaImage     bool                                `json:",omitempty"`
+	ChainedVbmetaPartitions map[string]ChainedAvbPartitionProps `json:",omitempty"`
 
-	ProductCopyFiles map[string]string `json:",omitempty"`
+	ProductPackages         []string `json:",omitempty"`
+	ProductPackagesDebug    []string `json:",omitempty"`
+	VendorLinkerConfigSrcs  []string `json:",omitempty"`
+	ProductLinkerConfigSrcs []string `json:",omitempty"`
+
+	BoardInfoFiles      []string `json:",omitempty"`
+	BootLoaderBoardName string   `json:",omitempty"`
+
+	ProductCopyFiles []string `json:",omitempty"`
+
+	BuildingSystemDlkmImage   bool     `json:",omitempty"`
+	SystemKernelModules       []string `json:",omitempty"`
+	SystemKernelBlocklistFile string   `json:",omitempty"`
+	SystemKernelLoadModules   []string `json:",omitempty"`
+	BuildingVendorDlkmImage   bool     `json:",omitempty"`
+	VendorKernelModules       []string `json:",omitempty"`
+	VendorKernelBlocklistFile string   `json:",omitempty"`
+	BuildingOdmDlkmImage      bool     `json:",omitempty"`
+	OdmKernelModules          []string `json:",omitempty"`
+	OdmKernelBlocklistFile    string   `json:",omitempty"`
 }
 
 func boolPtr(v bool) *bool {
@@ -645,7 +697,6 @@
 		Malloc_zero_contents:         boolPtr(true),
 		Malloc_pattern_fill_contents: boolPtr(false),
 		Safestack:                    boolPtr(false),
-		TrimmedApex:                  boolPtr(false),
 		Build_from_text_stub:         boolPtr(false),
 
 		BootJars:     ConfiguredJarList{apexes: []string{}, jars: []string{}},
diff --git a/android/vintf_data.go b/android/vintf_data.go
new file mode 100644
index 0000000..7823397
--- /dev/null
+++ b/android/vintf_data.go
@@ -0,0 +1,171 @@
+// Copyright 2024 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 (
+	"fmt"
+	"strings"
+
+	"github.com/google/blueprint/proptools"
+)
+
+const (
+	deviceCmType          = "device_cm"
+	systemManifestType    = "system_manifest"
+	productManifestType   = "product_manifest"
+	systemExtManifestType = "system_ext_manifest"
+	vendorManifestType    = "vendor_manifest"
+	odmManifestType       = "odm_manifest"
+
+	defaultDcm               = "system/libhidl/vintfdata/device_compatibility_matrix.default.xml"
+	defaultSystemManifest    = "system/libhidl/vintfdata/manifest.xml"
+	defaultSystemExtManifest = "system/libhidl/vintfdata/system_ext_manifest.default.xml"
+)
+
+type vintfDataProperties struct {
+	// Optional name for the installed file. If unspecified it will be manifest.xml by default.
+	Filename *string
+
+	// Type of the vintf data type, the allowed type are device_compatibility_matrix, system_manifest,
+	// product_manifest, and system_ext_manifest.
+	Type *string
+}
+
+type vintfDataRule struct {
+	ModuleBase
+
+	properties vintfDataProperties
+
+	installDirPath InstallPath
+	outputFilePath OutputPath
+	noAction       bool
+}
+
+func init() {
+	registerVintfDataComponents(InitRegistrationContext)
+}
+
+func registerVintfDataComponents(ctx RegistrationContext) {
+	ctx.RegisterModuleType("vintf_data", vintfDataFactory)
+}
+
+// vintf_fragment module processes vintf fragment file and installs under etc/vintf/manifest.
+func vintfDataFactory() Module {
+	m := &vintfDataRule{}
+	m.AddProperties(
+		&m.properties,
+	)
+	InitAndroidArchModule(m, DeviceSupported, MultilibFirst)
+
+	return m
+}
+
+func (m *vintfDataRule) GenerateAndroidBuildActions(ctx ModuleContext) {
+	builder := NewRuleBuilder(pctx, ctx)
+	gensrc := PathForModuleOut(ctx, "manifest.xml")
+	assembleVintfEnvs := []string{}
+	inputPaths := make(Paths, 0)
+
+	switch proptools.String(m.properties.Type) {
+	case deviceCmType:
+		assembleVintfEnvs = append(assembleVintfEnvs, fmt.Sprintf("BOARD_SYSTEMSDK_VERSIONS=\"%s\"", strings.Join(ctx.DeviceConfig().SystemSdkVersions(), " ")))
+
+		deviceMatrixs := PathsForSource(ctx, ctx.Config().DeviceMatrixFile())
+		if len(deviceMatrixs) > 0 {
+			inputPaths = append(inputPaths, deviceMatrixs...)
+		} else {
+			inputPaths = append(inputPaths, PathForSource(ctx, defaultDcm))
+		}
+	case systemManifestType:
+		assembleVintfEnvs = append(assembleVintfEnvs, fmt.Sprintf("PLATFORM_SYSTEMSDK_VERSIONS=\"%s\"", strings.Join(ctx.DeviceConfig().PlatformSystemSdkVersions(), " ")))
+
+		inputPaths = append(inputPaths, PathForSource(ctx, defaultSystemManifest))
+		systemManifestFiles := PathsForSource(ctx, ctx.Config().SystemManifestFile())
+		if len(systemManifestFiles) > 0 {
+			inputPaths = append(inputPaths, systemManifestFiles...)
+		}
+	case productManifestType:
+		productManifestFiles := PathsForSource(ctx, ctx.Config().ProductManifestFiles())
+		// Only need to generate the manifest if PRODUCT_MANIFEST_FILES not defined.
+		if len(productManifestFiles) == 0 {
+			m.noAction = true
+			return
+		}
+
+		inputPaths = append(inputPaths, productManifestFiles...)
+	case systemExtManifestType:
+		assembleVintfEnvs = append(assembleVintfEnvs, fmt.Sprintf("PROVIDED_VNDK_VERSIONS=\"%s\"", strings.Join(ctx.DeviceConfig().ExtraVndkVersions(), " ")))
+
+		inputPaths = append(inputPaths, PathForSource(ctx, defaultSystemExtManifest))
+		systemExtManifestFiles := PathsForSource(ctx, ctx.Config().SystemExtManifestFiles())
+		if len(systemExtManifestFiles) > 0 {
+			inputPaths = append(inputPaths, systemExtManifestFiles...)
+		}
+	case vendorManifestType:
+		assembleVintfEnvs = append(assembleVintfEnvs, fmt.Sprintf("BOARD_SEPOLICY_VERS=\"%s\"", ctx.DeviceConfig().BoardSepolicyVers()))
+		assembleVintfEnvs = append(assembleVintfEnvs, fmt.Sprintf("PRODUCT_ENFORCE_VINTF_MANIFEST=%t", *ctx.Config().productVariables.Enforce_vintf_manifest))
+		deviceManifestFiles := PathsForSource(ctx, ctx.Config().DeviceManifestFiles())
+		// Only need to generate the manifest if DEVICE_MANIFEST_FILE is defined.
+		if len(deviceManifestFiles) == 0 {
+			m.noAction = true
+			return
+		}
+
+		inputPaths = append(inputPaths, deviceManifestFiles...)
+	case odmManifestType:
+		assembleVintfEnvs = append(assembleVintfEnvs, "VINTF_IGNORE_TARGET_FCM_VERSION=true")
+		odmManifestFiles := PathsForSource(ctx, ctx.Config().OdmManifestFiles())
+		// Only need to generate the manifest if ODM_MANIFEST_FILES is defined.
+		if len(odmManifestFiles) == 0 {
+			m.noAction = true
+			return
+		}
+
+		inputPaths = append(inputPaths, odmManifestFiles...)
+	default:
+		panic(fmt.Errorf("For %s: The attribute 'type' value only allowed device_cm, system_manifest, product_manifest, system_ext_manifest!", ctx.Module().Name()))
+	}
+
+	// Process vintf fragment source file with assemble_vintf tool
+	builder.Command().
+		Flags(assembleVintfEnvs).
+		BuiltTool("assemble_vintf").
+		FlagWithArg("-i ", strings.Join(inputPaths.Strings(), ":")).
+		FlagWithOutput("-o ", gensrc)
+
+	builder.Build("assemble_vintf", "Process vintf data "+gensrc.String())
+
+	m.installDirPath = PathForModuleInstall(ctx, "etc", "vintf")
+	m.outputFilePath = gensrc.OutputPath
+
+	installFileName := "manifest.xml"
+	if filename := proptools.String(m.properties.Filename); filename != "" {
+		installFileName = filename
+	}
+
+	ctx.InstallFile(m.installDirPath, installFileName, gensrc)
+}
+
+// Make this module visible to AndroidMK so it can be referenced from modules defined from Android.mk files
+func (m *vintfDataRule) AndroidMkEntries() []AndroidMkEntries {
+	if m.noAction {
+		return []AndroidMkEntries{}
+	}
+
+	return []AndroidMkEntries{{
+		Class:      "ETC",
+		OutputFile: OptionalPathForPath(m.outputFilePath),
+	}}
+}
diff --git a/apex/Android.bp b/apex/Android.bp
index 0e2f564..870ca7e 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -7,6 +7,7 @@
     pkgPath: "android/soong/apex",
     deps: [
         "blueprint",
+        "blueprint-bpmodify",
         "soong",
         "soong-aconfig",
         "soong-aconfig-codegen",
diff --git a/apex/apex.go b/apex/apex.go
index 80af9c5..04b5a07 100644
--- a/apex/apex.go
+++ b/apex/apex.go
@@ -20,10 +20,12 @@
 	"fmt"
 	"path/filepath"
 	"regexp"
+	"slices"
 	"sort"
 	"strings"
 
 	"github.com/google/blueprint"
+	"github.com/google/blueprint/depset"
 	"github.com/google/blueprint/proptools"
 
 	"android/soong/android"
@@ -61,14 +63,11 @@
 func RegisterPostDepsMutators(ctx android.RegisterMutatorsContext) {
 	ctx.TopDown("apex_info", apexInfoMutator)
 	ctx.BottomUp("apex_unique", apexUniqueVariationsMutator)
-	ctx.BottomUp("apex_test_for_deps", apexTestForDepsMutator)
-	ctx.BottomUp("apex_test_for", apexTestForMutator)
 	// Run mark_platform_availability before the apexMutator as the apexMutator needs to know whether
 	// it should create a platform variant.
 	ctx.BottomUp("mark_platform_availability", markPlatformAvailability)
 	ctx.Transition("apex", &apexTransitionMutator{})
 	ctx.BottomUp("apex_directly_in_any", apexDirectlyInAnyMutator).MutatesDependencies()
-	ctx.BottomUp("apex_dcla_deps", apexDCLADepsMutator)
 }
 
 type apexBundleProperties struct {
@@ -106,11 +105,10 @@
 	Rros []string
 
 	// List of bootclasspath fragments that are embedded inside this APEX bundle.
-	Bootclasspath_fragments []string
+	Bootclasspath_fragments proptools.Configurable[[]string]
 
 	// List of systemserverclasspath fragments that are embedded inside this APEX bundle.
-	Systemserverclasspath_fragments        proptools.Configurable[[]string]
-	ResolvedSystemserverclasspathFragments []string `blueprint:"mutated"`
+	Systemserverclasspath_fragments proptools.Configurable[[]string]
 
 	// List of java libraries that are embedded inside this APEX bundle.
 	Java_libs []string
@@ -464,6 +462,12 @@
 	// GenerateAndroidBuildActions.
 	filesInfo []apexFile
 
+	// List of files that were excluded by the unwanted_transitive_deps property.
+	unwantedTransitiveFilesInfo []apexFile
+
+	// List of files that were excluded due to conflicts with other variants of the same module.
+	duplicateTransitiveFilesInfo []apexFile
+
 	// List of other module names that should be installed when this APEX gets installed (LOCAL_REQUIRED_MODULES).
 	makeModulesToInstall []string
 
@@ -726,7 +730,6 @@
 	androidAppTag  = &dependencyTag{name: "androidApp", payload: true}
 	bpfTag         = &dependencyTag{name: "bpf", payload: true}
 	certificateTag = &dependencyTag{name: "certificate"}
-	dclaTag        = &dependencyTag{name: "dcla"}
 	executableTag  = &dependencyTag{name: "executable", payload: true}
 	fsTag          = &dependencyTag{name: "filesystem", payload: true}
 	bcpfTag        = &dependencyTag{name: "bootclasspathFragment", payload: true, sourceOnly: true, memberType: java.BootclasspathFragmentSdkMemberType}
@@ -739,7 +742,6 @@
 	prebuiltTag     = &dependencyTag{name: "prebuilt", payload: true}
 	rroTag          = &dependencyTag{name: "rro", payload: true}
 	sharedLibTag    = &dependencyTag{name: "sharedLib", payload: true}
-	testForTag      = &dependencyTag{name: "test for"}
 	testTag         = &dependencyTag{name: "test", payload: true}
 	shBinaryTag     = &dependencyTag{name: "shBinary", payload: true}
 )
@@ -888,13 +890,11 @@
 		}
 	}
 
-	a.properties.ResolvedSystemserverclasspathFragments = a.properties.Systemserverclasspath_fragments.GetOrDefault(ctx, nil)
-
 	// Common-arch dependencies come next
 	commonVariation := ctx.Config().AndroidCommonTarget.Variations()
 	ctx.AddFarVariationDependencies(commonVariation, rroTag, a.properties.Rros...)
-	ctx.AddFarVariationDependencies(commonVariation, bcpfTag, a.properties.Bootclasspath_fragments...)
-	ctx.AddFarVariationDependencies(commonVariation, sscpfTag, a.properties.ResolvedSystemserverclasspathFragments...)
+	ctx.AddFarVariationDependencies(commonVariation, bcpfTag, a.properties.Bootclasspath_fragments.GetOrDefault(ctx, nil)...)
+	ctx.AddFarVariationDependencies(commonVariation, sscpfTag, a.properties.Systemserverclasspath_fragments.GetOrDefault(ctx, nil)...)
 	ctx.AddFarVariationDependencies(commonVariation, javaLibTag, a.properties.Java_libs...)
 	ctx.AddFarVariationDependencies(commonVariation, fsTag, a.properties.Filesystems...)
 	ctx.AddFarVariationDependencies(commonVariation, compatConfigTag, a.properties.Compat_configs...)
@@ -944,33 +944,6 @@
 	}
 }
 
-func apexDCLADepsMutator(mctx android.BottomUpMutatorContext) {
-	if !mctx.Config().ApexTrimEnabled() {
-		return
-	}
-	if a, ok := mctx.Module().(*apexBundle); ok && a.overridableProperties.Trim_against != nil {
-		commonVariation := mctx.Config().AndroidCommonTarget.Variations()
-		mctx.AddFarVariationDependencies(commonVariation, dclaTag, String(a.overridableProperties.Trim_against))
-	} else if o, ok := mctx.Module().(*OverrideApex); ok {
-		for _, p := range o.GetProperties() {
-			properties, ok := p.(*overridableProperties)
-			if !ok {
-				continue
-			}
-			if properties.Trim_against != nil {
-				commonVariation := mctx.Config().AndroidCommonTarget.Variations()
-				mctx.AddFarVariationDependencies(commonVariation, dclaTag, String(properties.Trim_against))
-			}
-		}
-	}
-}
-
-type DCLAInfo struct {
-	ProvidedLibs []string
-}
-
-var DCLAInfoProvider = blueprint.NewMutatorProvider[DCLAInfo]("apex_info")
-
 var _ ApexInfoMutator = (*apexBundle)(nil)
 
 func (a *apexBundle) ApexVariationName() string {
@@ -1079,12 +1052,6 @@
 		child.(android.ApexModule).BuildForApex(apexInfo) // leave a mark!
 		return true
 	})
-
-	if a.dynamic_common_lib_apex() {
-		android.SetProvider(mctx, DCLAInfoProvider, DCLAInfo{
-			ProvidedLibs: a.properties.Native_shared_libs.GetOrDefault(mctx, nil),
-		})
-	}
 }
 
 type ApexInfoMutator interface {
@@ -1177,40 +1144,6 @@
 	}
 }
 
-// apexTestForDepsMutator checks if this module is a test for an apex. If so, add a dependency on
-// the apex in order to retrieve its contents later.
-// TODO(jiyong): move this to android/apex.go?
-func apexTestForDepsMutator(mctx android.BottomUpMutatorContext) {
-	if !mctx.Module().Enabled(mctx) {
-		return
-	}
-	if am, ok := mctx.Module().(android.ApexModule); ok {
-		if testFor := am.TestFor(); len(testFor) > 0 {
-			mctx.AddFarVariationDependencies([]blueprint.Variation{
-				{Mutator: "os", Variation: am.Target().OsVariation()},
-				{"arch", "common"},
-			}, testForTag, testFor...)
-		}
-	}
-}
-
-// TODO(jiyong): move this to android/apex.go?
-func apexTestForMutator(mctx android.BottomUpMutatorContext) {
-	if !mctx.Module().Enabled(mctx) {
-		return
-	}
-	if _, ok := mctx.Module().(android.ApexModule); ok {
-		var contents []*android.ApexContents
-		for _, testFor := range mctx.GetDirectDepsWithTag(testForTag) {
-			abInfo, _ := android.OtherModuleProvider(mctx, testFor, android.ApexBundleInfoProvider)
-			contents = append(contents, abInfo.Contents)
-		}
-		android.SetProvider(mctx, android.ApexTestForInfoProvider, android.ApexTestForInfo{
-			ApexContents: contents,
-		})
-	}
-}
-
 // markPlatformAvailability marks whether or not a module can be available to platform. A module
 // cannot be available to platform if 1) it is explicitly marked as not available (i.e.
 // "//apex_available:platform" is absent) or 2) it depends on another module that isn't (or can't
@@ -1434,19 +1367,6 @@
 	return proptools.BoolDefault(a.properties.Dynamic_common_lib_apex, false)
 }
 
-// See the list of libs to trim
-func (a *apexBundle) libs_to_trim(ctx android.ModuleContext) []string {
-	dclaModules := ctx.GetDirectDepsWithTag(dclaTag)
-	if len(dclaModules) > 1 {
-		panic(fmt.Errorf("expected exactly at most one dcla dependency, got %d", len(dclaModules)))
-	}
-	if len(dclaModules) > 0 {
-		DCLAInfo, _ := android.OtherModuleProvider(ctx, dclaModules[0], DCLAInfoProvider)
-		return DCLAInfo.ProvidedLibs
-	}
-	return []string{}
-}
-
 // These functions are interfacing with cc/sanitizer.go. The entire APEX (along with all of its
 // members) can be sanitized, either forcibly, or by the global configuration. For some of the
 // sanitizers, extra dependencies can be forcibly added as well.
@@ -1877,6 +1797,14 @@
 
 	// visitor skips these from this list of module names
 	unwantedTransitiveDeps []string
+
+	// unwantedTransitiveFilesInfo contains files that would have been in the apex
+	// except that they were listed in unwantedTransitiveDeps.
+	unwantedTransitiveFilesInfo []apexFile
+
+	// duplicateTransitiveFilesInfo contains files that would ahve been in the apex
+	// except that another variant of the same module was already in the apex.
+	duplicateTransitiveFilesInfo []apexFile
 }
 
 func (vctx *visitorContext) normalizeFileInfo(mctx android.ModuleContext) {
@@ -1887,6 +1815,7 @@
 		// Needs additional verification for the resulting APEX to ensure that skipped artifacts don't make problems.
 		// For example, DT_NEEDED modules should be found within the APEX unless they are marked in `requiredNativeLibs`.
 		if f.transitiveDep && f.module != nil && android.InList(mctx.OtherModuleName(f.module), vctx.unwantedTransitiveDeps) {
+			vctx.unwantedTransitiveFilesInfo = append(vctx.unwantedTransitiveFilesInfo, f)
 			continue
 		}
 		dest := filepath.Join(f.installDir, f.builtFile.Base())
@@ -1897,6 +1826,8 @@
 				mctx.ModuleErrorf("apex file %v is provided by two different files %v and %v",
 					dest, e.builtFile, f.builtFile)
 				return
+			} else {
+				vctx.duplicateTransitiveFilesInfo = append(vctx.duplicateTransitiveFilesInfo, f)
 			}
 			// If a module is directly included and also transitively depended on
 			// consider it as directly included.
@@ -1911,6 +1842,7 @@
 	for _, v := range encountered {
 		vctx.filesInfo = append(vctx.filesInfo, v)
 	}
+
 	sort.Slice(vctx.filesInfo, func(i, j int) bool {
 		// Sort by destination path so as to ensure consistent ordering even if the source of the files
 		// changes.
@@ -2341,6 +2273,8 @@
 	// 3) some fields in apexBundle struct are configured
 	a.installDir = android.PathForModuleInstall(ctx, "apex")
 	a.filesInfo = vctx.filesInfo
+	a.unwantedTransitiveFilesInfo = vctx.unwantedTransitiveFilesInfo
+	a.duplicateTransitiveFilesInfo = vctx.duplicateTransitiveFilesInfo
 
 	a.setPayloadFsType(ctx)
 	a.setSystemLibLink(ctx)
@@ -2367,6 +2301,8 @@
 
 	a.setOutputFiles(ctx)
 	a.enforcePartitionTagOnApexSystemServerJar(ctx)
+
+	a.verifyNativeImplementationLibs(ctx)
 }
 
 // Set prebuiltInfoProvider. This will be used by `apex_prebuiltinfo_singleton` to print out a metadata file
@@ -2837,8 +2773,8 @@
 // Collect information for opening IDE project files in java/jdeps.go.
 func (a *apexBundle) IDEInfo(ctx android.BaseModuleContext, dpInfo *android.IdeInfo) {
 	dpInfo.Deps = append(dpInfo.Deps, a.properties.Java_libs...)
-	dpInfo.Deps = append(dpInfo.Deps, a.properties.Bootclasspath_fragments...)
-	dpInfo.Deps = append(dpInfo.Deps, a.properties.ResolvedSystemserverclasspathFragments...)
+	dpInfo.Deps = append(dpInfo.Deps, a.properties.Bootclasspath_fragments.GetOrDefault(ctx, nil)...)
+	dpInfo.Deps = append(dpInfo.Deps, a.properties.Systemserverclasspath_fragments.GetOrDefault(ctx, nil)...)
 }
 
 func init() {
@@ -2917,6 +2853,104 @@
 	}
 }
 
-func (a *apexBundle) IsTestApex() bool {
-	return a.testApex
+// verifyNativeImplementationLibs compares the list of transitive implementation libraries used to link native
+// libraries in the apex against the list of implementation libraries in the apex, ensuring that none of the
+// libraries in the apex have references to private APIs from outside the apex.
+func (a *apexBundle) verifyNativeImplementationLibs(ctx android.ModuleContext) {
+	var directImplementationLibs android.Paths
+	var transitiveImplementationLibs []depset.DepSet[android.Path]
+
+	if a.properties.IsCoverageVariant {
+		return
+	}
+
+	if a.testApex {
+		return
+	}
+
+	if a.UsePlatformApis() {
+		return
+	}
+
+	checkApexTag := func(tag blueprint.DependencyTag) bool {
+		switch tag {
+		case sharedLibTag, jniLibTag, executableTag, androidAppTag:
+			return true
+		default:
+			return false
+		}
+	}
+
+	checkTransitiveTag := func(tag blueprint.DependencyTag) bool {
+		switch {
+		case cc.IsSharedDepTag(tag), java.IsJniDepTag(tag), rust.IsRlibDepTag(tag), rust.IsDylibDepTag(tag), checkApexTag(tag):
+			return true
+		default:
+			return false
+		}
+	}
+
+	var appEmbeddedJNILibs android.Paths
+	ctx.VisitDirectDeps(func(dep android.Module) {
+		tag := ctx.OtherModuleDependencyTag(dep)
+		if !checkApexTag(tag) {
+			return
+		}
+		if tag == sharedLibTag || tag == jniLibTag {
+			outputFile := android.OutputFileForModule(ctx, dep, "")
+			directImplementationLibs = append(directImplementationLibs, outputFile)
+		}
+		if info, ok := android.OtherModuleProvider(ctx, dep, cc.ImplementationDepInfoProvider); ok {
+			transitiveImplementationLibs = append(transitiveImplementationLibs, info.ImplementationDeps)
+		}
+		if info, ok := android.OtherModuleProvider(ctx, dep, java.AppInfoProvider); ok {
+			appEmbeddedJNILibs = append(appEmbeddedJNILibs, info.EmbeddedJNILibs...)
+		}
+	})
+
+	depSet := depset.New(depset.PREORDER, directImplementationLibs, transitiveImplementationLibs)
+	allImplementationLibs := depSet.ToList()
+
+	allFileInfos := slices.Concat(a.filesInfo, a.unwantedTransitiveFilesInfo, a.duplicateTransitiveFilesInfo)
+
+	for _, lib := range allImplementationLibs {
+		inApex := slices.ContainsFunc(allFileInfos, func(fi apexFile) bool {
+			return fi.builtFile == lib
+		})
+		inApkInApex := slices.Contains(appEmbeddedJNILibs, lib)
+
+		if !inApex && !inApkInApex {
+			ctx.ModuleErrorf("library in apex transitively linked against implementation library %q not in apex", lib)
+			var depPath []android.Module
+			ctx.WalkDeps(func(child, parent android.Module) bool {
+				if depPath != nil {
+					return false
+				}
+
+				tag := ctx.OtherModuleDependencyTag(child)
+
+				if parent == ctx.Module() {
+					if !checkApexTag(tag) {
+						return false
+					}
+				}
+
+				if checkTransitiveTag(tag) {
+					if android.OutputFileForModule(ctx, child, "") == lib {
+						depPath = ctx.GetWalkPath()
+					}
+					return true
+				}
+
+				return false
+			})
+			if depPath != nil {
+				ctx.ModuleErrorf("dependency path:")
+				for _, m := range depPath {
+					ctx.ModuleErrorf("   %s", ctx.OtherModuleName(m))
+				}
+				return
+			}
+		}
+	}
 }
diff --git a/apex/apex_test.go b/apex/apex_test.go
index 988c1ce..17cea5e 100644
--- a/apex/apex_test.go
+++ b/apex/apex_test.go
@@ -20,6 +20,7 @@
 	"path/filepath"
 	"reflect"
 	"regexp"
+	"slices"
 	"sort"
 	"strconv"
 	"strings"
@@ -28,6 +29,7 @@
 	"android/soong/aconfig/codegen"
 
 	"github.com/google/blueprint"
+	"github.com/google/blueprint/bpmodify"
 	"github.com/google/blueprint/proptools"
 
 	"android/soong/android"
@@ -225,6 +227,10 @@
 	"system/sepolicy/apex/myapex-file_contexts": nil,
 })
 
+var prepareForTestWithOtherapex = android.FixtureMergeMockFs(android.MockFS{
+	"system/sepolicy/apex/otherapex-file_contexts": nil,
+})
+
 // ensure that 'result' equals 'expected'
 func ensureEquals(t *testing.T, result string, expected string) {
 	t.Helper()
@@ -8707,196 +8713,6 @@
 	}
 }
 
-func TestTestFor(t *testing.T) {
-	t.Parallel()
-	ctx := testApex(t, `
-		apex {
-			name: "myapex",
-			key: "myapex.key",
-			native_shared_libs: ["mylib", "myprivlib"],
-			updatable: false,
-		}
-
-		apex_key {
-			name: "myapex.key",
-			public_key: "testkey.avbpubkey",
-			private_key: "testkey.pem",
-		}
-
-		cc_library {
-			name: "mylib",
-			srcs: ["mylib.cpp"],
-			system_shared_libs: [],
-			stl: "none",
-			stubs: {
-				versions: ["1"],
-			},
-			apex_available: ["myapex"],
-		}
-
-		cc_library {
-			name: "myprivlib",
-			srcs: ["mylib.cpp"],
-			system_shared_libs: [],
-			stl: "none",
-			apex_available: ["myapex"],
-		}
-
-
-		cc_test {
-			name: "mytest",
-			gtest: false,
-			srcs: ["mylib.cpp"],
-			system_shared_libs: [],
-			stl: "none",
-			shared_libs: ["mylib", "myprivlib", "mytestlib"],
-			test_for: ["myapex"]
-		}
-
-		cc_library {
-			name: "mytestlib",
-			srcs: ["mylib.cpp"],
-			system_shared_libs: [],
-			shared_libs: ["mylib", "myprivlib"],
-			stl: "none",
-			test_for: ["myapex"],
-		}
-
-		cc_benchmark {
-			name: "mybench",
-			srcs: ["mylib.cpp"],
-			system_shared_libs: [],
-			shared_libs: ["mylib", "myprivlib"],
-			stl: "none",
-			test_for: ["myapex"],
-		}
-	`)
-
-	ensureLinkedLibIs := func(mod, variant, linkedLib, expectedVariant string) {
-		ldFlags := strings.Split(ctx.ModuleForTests(mod, variant).Rule("ld").Args["libFlags"], " ")
-		mylibLdFlags := android.FilterListPred(ldFlags, func(s string) bool { return strings.HasPrefix(s, linkedLib) })
-		android.AssertArrayString(t, "unexpected "+linkedLib+" link library for "+mod, []string{linkedLib + expectedVariant}, mylibLdFlags)
-	}
-
-	// These modules are tests for the apex, therefore are linked to the
-	// actual implementation of mylib instead of its stub.
-	ensureLinkedLibIs("mytest", "android_arm64_armv8-a", "out/soong/.intermediates/mylib/", "android_arm64_armv8-a_shared/mylib.so")
-	ensureLinkedLibIs("mytestlib", "android_arm64_armv8-a_shared", "out/soong/.intermediates/mylib/", "android_arm64_armv8-a_shared/mylib.so")
-	ensureLinkedLibIs("mybench", "android_arm64_armv8-a", "out/soong/.intermediates/mylib/", "android_arm64_armv8-a_shared/mylib.so")
-}
-
-func TestIndirectTestFor(t *testing.T) {
-	t.Parallel()
-	ctx := testApex(t, `
-		apex {
-			name: "myapex",
-			key: "myapex.key",
-			native_shared_libs: ["mylib", "myprivlib"],
-			updatable: false,
-		}
-
-		apex_key {
-			name: "myapex.key",
-			public_key: "testkey.avbpubkey",
-			private_key: "testkey.pem",
-		}
-
-		cc_library {
-			name: "mylib",
-			srcs: ["mylib.cpp"],
-			system_shared_libs: [],
-			stl: "none",
-			stubs: {
-				versions: ["1"],
-			},
-			apex_available: ["myapex"],
-		}
-
-		cc_library {
-			name: "myprivlib",
-			srcs: ["mylib.cpp"],
-			system_shared_libs: [],
-			stl: "none",
-			shared_libs: ["mylib"],
-			apex_available: ["myapex"],
-		}
-
-		cc_library {
-			name: "mytestlib",
-			srcs: ["mylib.cpp"],
-			system_shared_libs: [],
-			shared_libs: ["myprivlib"],
-			stl: "none",
-			test_for: ["myapex"],
-		}
-	`)
-
-	ensureLinkedLibIs := func(mod, variant, linkedLib, expectedVariant string) {
-		ldFlags := strings.Split(ctx.ModuleForTests(mod, variant).Rule("ld").Args["libFlags"], " ")
-		mylibLdFlags := android.FilterListPred(ldFlags, func(s string) bool { return strings.HasPrefix(s, linkedLib) })
-		android.AssertArrayString(t, "unexpected "+linkedLib+" link library for "+mod, []string{linkedLib + expectedVariant}, mylibLdFlags)
-	}
-
-	// The platform variant of mytestlib links to the platform variant of the
-	// internal myprivlib.
-	ensureLinkedLibIs("mytestlib", "android_arm64_armv8-a_shared", "out/soong/.intermediates/myprivlib/", "android_arm64_armv8-a_shared/myprivlib.so")
-
-	// The platform variant of myprivlib links to the platform variant of mylib
-	// and bypasses its stubs.
-	ensureLinkedLibIs("myprivlib", "android_arm64_armv8-a_shared", "out/soong/.intermediates/mylib/", "android_arm64_armv8-a_shared/mylib.so")
-}
-
-func TestTestForForLibInOtherApex(t *testing.T) {
-	t.Parallel()
-	// This case is only allowed for known overlapping APEXes, i.e. the ART APEXes.
-	_ = testApex(t, `
-		apex {
-			name: "com.android.art",
-			key: "myapex.key",
-			native_shared_libs: ["libnativebridge"],
-			updatable: false,
-		}
-
-		apex {
-			name: "com.android.art.debug",
-			key: "myapex.key",
-			native_shared_libs: ["libnativebridge", "libnativebrdige_test"],
-			updatable: false,
-		}
-
-		apex_key {
-			name: "myapex.key",
-			public_key: "testkey.avbpubkey",
-			private_key: "testkey.pem",
-		}
-
-		cc_library {
-			name: "libnativebridge",
-			srcs: ["libnativebridge.cpp"],
-			system_shared_libs: [],
-			stl: "none",
-			stubs: {
-				versions: ["1"],
-			},
-			apex_available: ["com.android.art", "com.android.art.debug"],
-		}
-
-		cc_library {
-			name: "libnativebrdige_test",
-			srcs: ["mylib.cpp"],
-			system_shared_libs: [],
-			shared_libs: ["libnativebridge"],
-			stl: "none",
-			apex_available: ["com.android.art.debug"],
-			test_for: ["com.android.art"],
-		}
-	`,
-		android.MockFS{
-			"system/sepolicy/apex/com.android.art-file_contexts":       nil,
-			"system/sepolicy/apex/com.android.art.debug-file_contexts": nil,
-		}.AddToFixture())
-}
-
 // TODO(jungjw): Move this to proptools
 func intPtr(i int) *int {
 	return &i
@@ -10237,61 +10053,6 @@
 		RunTestWithBp(t, bp)
 }
 
-func TestTrimmedApex(t *testing.T) {
-	t.Parallel()
-	bp := `
-		apex {
-			name: "myapex",
-			key: "myapex.key",
-			native_shared_libs: ["libfoo","libbaz"],
-			min_sdk_version: "29",
-			trim_against: "mydcla",
-    }
-		apex {
-			name: "mydcla",
-			key: "myapex.key",
-			native_shared_libs: ["libfoo","libbar"],
-			min_sdk_version: "29",
-			file_contexts: ":myapex-file_contexts",
-			dynamic_common_lib_apex: true,
-		}
-		apex_key {
-			name: "myapex.key",
-		}
-		cc_library {
-			name: "libfoo",
-			shared_libs: ["libc"],
-			apex_available: ["myapex","mydcla"],
-			min_sdk_version: "29",
-		}
-		cc_library {
-			name: "libbar",
-			shared_libs: ["libc"],
-			apex_available: ["myapex","mydcla"],
-			min_sdk_version: "29",
-		}
-		cc_library {
-			name: "libbaz",
-			shared_libs: ["libc"],
-			apex_available: ["myapex","mydcla"],
-			min_sdk_version: "29",
-		}
-		`
-	ctx := testApex(t, bp)
-	module := ctx.ModuleForTests("myapex", "android_common_myapex")
-	apexRule := module.MaybeRule("apexRule")
-	if apexRule.Rule == nil {
-		t.Errorf("Expecting regular apex rule but a non regular apex rule found")
-	}
-
-	ctx = testApex(t, bp, android.FixtureModifyConfig(android.SetTrimmedApexEnabledForTests))
-	trimmedApexRule := ctx.ModuleForTests("myapex", "android_common_myapex").Rule("TrimmedApexRule")
-	libs_to_trim := trimmedApexRule.Args["libs_to_trim"]
-	android.AssertStringDoesContain(t, "missing lib to trim", libs_to_trim, "libfoo")
-	android.AssertStringDoesContain(t, "missing lib to trim", libs_to_trim, "libbar")
-	android.AssertStringDoesNotContain(t, "unexpected libs in the libs to trim", libs_to_trim, "libbaz")
-}
-
 func TestCannedFsConfig(t *testing.T) {
 	t.Parallel()
 	ctx := testApex(t, `
@@ -10426,7 +10187,10 @@
 			deps: [
 				"libfoo",
 			],
-			linker_config_src: "linker.config.json",
+			linker_config: {
+				gen_linker_config: true,
+				linker_config_srcs: ["linker.config.json"],
+			},
 		}
 
 		cc_library {
@@ -12114,3 +11878,398 @@
 	fileList := android.ContentFromFileRuleForTests(t, result, partition.Output("fileList"))
 	android.AssertDeepEquals(t, "filesystem with apex", "apex/myapex.apex\n", fileList)
 }
+
+func TestApexVerifyNativeImplementationLibs(t *testing.T) {
+	t.Parallel()
+
+	extractDepenencyPathFromErrors := func(errs []error) []string {
+		i := slices.IndexFunc(errs, func(err error) bool {
+			return strings.Contains(err.Error(), "dependency path:")
+		})
+		if i < 0 {
+			return nil
+		}
+		var dependencyPath []string
+		for _, err := range errs[i+1:] {
+			s := err.Error()
+			lastSpace := strings.LastIndexByte(s, ' ')
+			if lastSpace >= 0 {
+				dependencyPath = append(dependencyPath, s[lastSpace+1:])
+			}
+		}
+		return dependencyPath
+	}
+
+	checkErrors := func(wantDependencyPath []string) func(t *testing.T, result *android.TestResult) {
+		return func(t *testing.T, result *android.TestResult) {
+			t.Helper()
+			if len(result.Errs) == 0 {
+				t.Fatalf("expected errors")
+			}
+			t.Log("found errors:")
+			for _, err := range result.Errs {
+				t.Log(err)
+			}
+			if g, w := result.Errs[0].Error(), "library in apex transitively linked against implementation library"; !strings.Contains(g, w) {
+				t.Fatalf("expected error %q, got %q", w, g)
+			}
+			dependencyPath := extractDepenencyPathFromErrors(result.Errs)
+			if g, w := dependencyPath, wantDependencyPath; !slices.Equal(g, w) {
+				t.Errorf("expected dependency path %q, got %q", w, g)
+			}
+		}
+	}
+
+	addToSharedLibs := func(module, lib string) func(bp *bpmodify.Blueprint) {
+		return func(bp *bpmodify.Blueprint) {
+			m := bp.ModulesByName(module)
+			props, err := m.GetOrCreateProperty(bpmodify.List, "shared_libs")
+			if err != nil {
+				panic(err)
+			}
+			props.AddStringToList(lib)
+		}
+	}
+
+	bpTemplate := `
+	apex {
+		name: "myapex",
+		key: "myapex.key",
+		native_shared_libs: ["mylib"],
+		rust_dyn_libs: ["libmyrust"],
+		binaries: ["mybin", "myrustbin"],
+		jni_libs: ["libjni"],
+		apps: ["myapp"],
+		updatable: false,
+	}
+
+	apex {
+		name: "otherapex",
+		key: "myapex.key",
+		native_shared_libs: ["libotherapex"],
+		updatable: false,
+	}
+
+	apex_key {
+		name: "myapex.key",
+		public_key: "testkey.avbpubkey",
+		private_key: "testkey.pem",
+	}
+
+	cc_library {
+		name: "mylib",
+		srcs: ["foo.cpp"],
+		apex_available: ["myapex"],
+	}
+
+	cc_binary {
+		name: "mybin",
+		srcs: ["foo.cpp"],
+		apex_available: ["myapex"],
+	}
+
+	rust_library {
+		name: "libmyrust",
+		crate_name: "myrust",
+		srcs: ["src/lib.rs"],
+		rustlibs: ["libmyrust_transitive_dylib"],
+		rlibs: ["libmyrust_transitive_rlib"],
+		apex_available: ["myapex"],
+	}
+
+	rust_library{
+		name: "libmyrust_transitive_dylib",
+		crate_name: "myrust_transitive_dylib",
+		srcs: ["src/lib.rs"],
+		apex_available: ["myapex"],
+	}
+
+	rust_library {
+		name: "libmyrust_transitive_rlib",
+		crate_name: "myrust_transitive_rlib",
+		srcs: ["src/lib.rs"],
+		apex_available: ["myapex"],
+	}
+
+	rust_binary {
+		name: "myrustbin",
+		srcs: ["src/main.rs"],
+		apex_available: ["myapex"],
+	}
+
+	cc_library {
+		name: "libbar",
+		sdk_version: "current",
+		srcs: ["bar.cpp"],
+		apex_available: ["myapex"],
+		stl: "none",
+	}
+
+	android_app {
+		name: "myapp",
+		jni_libs: ["libembeddedjni"],
+		use_embedded_native_libs: true,
+		sdk_version: "current",
+		apex_available: ["myapex"],
+	}
+
+	cc_library {
+		name: "libembeddedjni",
+		sdk_version: "current",
+		srcs: ["bar.cpp"],
+		apex_available: ["myapex"],
+		stl: "none",
+	}
+
+	cc_library {
+		name: "libjni",
+		sdk_version: "current",
+		srcs: ["bar.cpp"],
+		apex_available: ["myapex"],
+		stl: "none",
+	}
+
+	cc_library {
+		name: "libotherapex",
+		sdk_version: "current",
+		srcs: ["otherapex.cpp"],
+		apex_available: ["otherapex"],
+		stubs: {
+			symbol_file: "libotherapex.map.txt",
+			versions: ["1", "2", "3"],
+		},
+		stl: "none",
+	}
+
+	cc_library {
+		name: "libplatform",
+		sdk_version: "current",
+		srcs: ["libplatform.cpp"],
+		stubs: {
+			symbol_file: "libplatform.map.txt",
+			versions: ["1", "2", "3"],
+		},
+		stl: "none",
+		system_shared_libs: [],
+	}
+	`
+
+	testCases := []struct {
+		name           string
+		bpModifier     func(bp *bpmodify.Blueprint)
+		dependencyPath []string
+	}{
+		{
+			name:           "library dependency in other apex",
+			bpModifier:     addToSharedLibs("mylib", "libotherapex#impl"),
+			dependencyPath: []string{"myapex", "mylib", "libotherapex"},
+		},
+		{
+			name: "transitive library dependency in other apex",
+			bpModifier: func(bp *bpmodify.Blueprint) {
+				addToSharedLibs("mylib", "libbar")(bp)
+				addToSharedLibs("libbar", "libotherapex#impl")(bp)
+			},
+			dependencyPath: []string{"myapex", "mylib", "libbar", "libotherapex"},
+		},
+		{
+			name:           "library dependency in platform",
+			bpModifier:     addToSharedLibs("mylib", "libplatform#impl"),
+			dependencyPath: []string{"myapex", "mylib", "libplatform"},
+		},
+		{
+			name:           "jni library dependency in other apex",
+			bpModifier:     addToSharedLibs("libjni", "libotherapex#impl"),
+			dependencyPath: []string{"myapex", "libjni", "libotherapex"},
+		},
+		{
+			name: "transitive jni library dependency in other apex",
+			bpModifier: func(bp *bpmodify.Blueprint) {
+				addToSharedLibs("libjni", "libbar")(bp)
+				addToSharedLibs("libbar", "libotherapex#impl")(bp)
+			},
+			dependencyPath: []string{"myapex", "libjni", "libbar", "libotherapex"},
+		},
+		{
+			name:           "jni library dependency in platform",
+			bpModifier:     addToSharedLibs("libjni", "libplatform#impl"),
+			dependencyPath: []string{"myapex", "libjni", "libplatform"},
+		},
+		{
+			name: "transitive jni library dependency in platform",
+			bpModifier: func(bp *bpmodify.Blueprint) {
+				addToSharedLibs("libjni", "libbar")(bp)
+				addToSharedLibs("libbar", "libplatform#impl")(bp)
+			},
+			dependencyPath: []string{"myapex", "libjni", "libbar", "libplatform"},
+		},
+		// TODO: embedded JNI in apps should be checked too, but Soong currently just packages the transitive
+		//  JNI libraries even if they came from another apex.
+		//{
+		//	name:           "app jni library dependency in other apex",
+		//	bpModifier:     addToSharedLibs("libembeddedjni", "libotherapex#impl"),
+		//	dependencyPath: []string{"myapex", "myapp", "libembeddedjni", "libotherapex"},
+		//},
+		//{
+		//	name: "transitive app jni library dependency in other apex",
+		//	bpModifier: func(bp *bpmodify.Blueprint) {
+		//		addToSharedLibs("libembeddedjni", "libbar")(bp)
+		//		addToSharedLibs("libbar", "libotherapex#impl")(bp)
+		//	},
+		//	dependencyPath: []string{"myapex", "myapp", "libembeddedjni", "libbar", "libotherapex"},
+		//},
+		//{
+		//	name:           "app jni library dependency in platform",
+		//	bpModifier:     addToSharedLibs("libembeddedjni", "libplatform#impl"),
+		//	dependencyPath: []string{"myapex", "myapp", "libembeddedjni", "libplatform"},
+		//},
+		//{
+		//	name: "transitive app jni library dependency in platform",
+		//	bpModifier: func(bp *bpmodify.Blueprint) {
+		//		addToSharedLibs("libembeddedjni", "libbar")(bp)
+		//		addToSharedLibs("libbar", "libplatform#impl")(bp)
+		//	},
+		//	dependencyPath: []string{"myapex", "myapp", "libembeddedjni", "libbar", "libplatform"},
+		//},
+		{
+			name:           "binary dependency in other apex",
+			bpModifier:     addToSharedLibs("mybin", "libotherapex#impl"),
+			dependencyPath: []string{"myapex", "mybin", "libotherapex"},
+		},
+		{
+			name: "transitive binary dependency in other apex",
+			bpModifier: func(bp *bpmodify.Blueprint) {
+				addToSharedLibs("mybin", "libbar")(bp)
+				addToSharedLibs("libbar", "libotherapex#impl")(bp)
+			},
+			dependencyPath: []string{"myapex", "mybin", "libbar", "libotherapex"},
+		},
+		{
+			name:           "binary dependency in platform",
+			bpModifier:     addToSharedLibs("mybin", "libplatform#impl"),
+			dependencyPath: []string{"myapex", "mybin", "libplatform"},
+		},
+		{
+			name: "transitive binary dependency in platform",
+			bpModifier: func(bp *bpmodify.Blueprint) {
+				addToSharedLibs("mybin", "libbar")(bp)
+				addToSharedLibs("libbar", "libplatform#impl")(bp)
+			},
+			dependencyPath: []string{"myapex", "mybin", "libbar", "libplatform"},
+		},
+
+		{
+			name:           "rust library dependency in other apex",
+			bpModifier:     addToSharedLibs("libmyrust", "libotherapex#impl"),
+			dependencyPath: []string{"myapex", "libmyrust", "libotherapex"},
+		},
+		{
+			name: "transitive rust library dependency in other apex",
+			bpModifier: func(bp *bpmodify.Blueprint) {
+				addToSharedLibs("libmyrust", "libbar")(bp)
+				addToSharedLibs("libbar", "libotherapex#impl")(bp)
+			},
+			dependencyPath: []string{"myapex", "libmyrust", "libbar", "libotherapex"},
+		},
+		{
+			name:           "rust library dependency in platform",
+			bpModifier:     addToSharedLibs("libmyrust", "libplatform#impl"),
+			dependencyPath: []string{"myapex", "libmyrust", "libplatform"},
+		},
+		{
+			name: "transitive rust library dependency in platform",
+			bpModifier: func(bp *bpmodify.Blueprint) {
+				addToSharedLibs("libmyrust", "libbar")(bp)
+				addToSharedLibs("libbar", "libplatform#impl")(bp)
+			},
+			dependencyPath: []string{"myapex", "libmyrust", "libbar", "libplatform"},
+		},
+		{
+			name: "transitive rust library dylib dependency in other apex",
+			bpModifier: func(bp *bpmodify.Blueprint) {
+				addToSharedLibs("libmyrust_transitive_dylib", "libotherapex#impl")(bp)
+			},
+			dependencyPath: []string{"myapex", "libmyrust", "libmyrust_transitive_dylib", "libotherapex"},
+		},
+		{
+			name: "transitive rust library dylib dependency in platform",
+			bpModifier: func(bp *bpmodify.Blueprint) {
+				addToSharedLibs("libmyrust_transitive_dylib", "libplatform#impl")(bp)
+			},
+			dependencyPath: []string{"myapex", "libmyrust", "libmyrust_transitive_dylib", "libplatform"},
+		},
+		{
+			name: "transitive rust library rlib dependency in other apex",
+			bpModifier: func(bp *bpmodify.Blueprint) {
+				addToSharedLibs("libmyrust_transitive_rlib", "libotherapex#impl")(bp)
+			},
+			dependencyPath: []string{"myapex", "libmyrust", "libmyrust_transitive_rlib", "libotherapex"},
+		},
+		{
+			name: "transitive rust library rlib dependency in platform",
+			bpModifier: func(bp *bpmodify.Blueprint) {
+				addToSharedLibs("libmyrust_transitive_rlib", "libplatform#impl")(bp)
+			},
+			dependencyPath: []string{"myapex", "libmyrust", "libmyrust_transitive_rlib", "libplatform"},
+		},
+		{
+			name:           "rust binary dependency in other apex",
+			bpModifier:     addToSharedLibs("myrustbin", "libotherapex#impl"),
+			dependencyPath: []string{"myapex", "myrustbin", "libotherapex"},
+		},
+		{
+			name: "transitive rust binary dependency in other apex",
+			bpModifier: func(bp *bpmodify.Blueprint) {
+				addToSharedLibs("myrustbin", "libbar")(bp)
+				addToSharedLibs("libbar", "libotherapex#impl")(bp)
+			},
+			dependencyPath: []string{"myapex", "myrustbin", "libbar", "libotherapex"},
+		},
+		{
+			name:           "rust binary dependency in platform",
+			bpModifier:     addToSharedLibs("myrustbin", "libplatform#impl"),
+			dependencyPath: []string{"myapex", "myrustbin", "libplatform"},
+		},
+		{
+			name: "transitive rust binary dependency in platform",
+			bpModifier: func(bp *bpmodify.Blueprint) {
+				addToSharedLibs("myrustbin", "libbar")(bp)
+				addToSharedLibs("libbar", "libplatform#impl")(bp)
+			},
+			dependencyPath: []string{"myapex", "myrustbin", "libbar", "libplatform"},
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			t.Parallel()
+			bp, err := bpmodify.NewBlueprint("", []byte(bpTemplate))
+			if err != nil {
+				t.Fatal(err)
+			}
+			if testCase.bpModifier != nil {
+				func() {
+					defer func() {
+						if r := recover(); r != nil {
+							t.Fatalf("panic in bpModifier: %v", r)
+						}
+					}()
+					testCase.bpModifier(bp)
+				}()
+			}
+			android.GroupFixturePreparers(
+				android.PrepareForTestWithAndroidBuildComponents,
+				cc.PrepareForTestWithCcBuildComponents,
+				java.PrepareForTestWithDexpreopt,
+				rust.PrepareForTestWithRustDefaultModules,
+				PrepareForTestWithApexBuildComponents,
+				prepareForTestWithMyapex,
+				prepareForTestWithOtherapex,
+				android.FixtureModifyProductVariables(func(variables android.FixtureProductVariables) {
+					variables.BuildId = proptools.StringPtr("TEST.BUILD_ID")
+				}),
+			).ExtendWithErrorHandler(android.FixtureCustomErrorHandler(checkErrors(testCase.dependencyPath))).
+				RunTestWithBp(t, bp.String())
+		})
+	}
+}
diff --git a/apex/builder.go b/apex/builder.go
index 20b4dbe..305d509 100644
--- a/apex/builder.go
+++ b/apex/builder.go
@@ -43,7 +43,6 @@
 	pctx.Import("android/soong/java")
 	pctx.HostBinToolVariable("apexer", "apexer")
 	pctx.HostBinToolVariable("apexer_with_DCLA_preprocessing", "apexer_with_DCLA_preprocessing")
-	pctx.HostBinToolVariable("apexer_with_trim_preprocessing", "apexer_with_trim_preprocessing")
 
 	// ART minimal builds (using the master-art manifest) do not have the "frameworks/base"
 	// projects, and hence cannot build 'aapt2'. Use the SDK prebuilt instead.
@@ -173,34 +172,6 @@
 	}, "tool_path", "image_dir", "copy_commands", "file_contexts", "canned_fs_config", "key",
 		"opt_flags", "manifest", "is_DCLA")
 
-	TrimmedApexRule = pctx.StaticRule("TrimmedApexRule", blueprint.RuleParams{
-		Command: `rm -rf ${image_dir} && mkdir -p ${image_dir} && ` +
-			`(. ${out}.copy_commands) && ` +
-			`APEXER_TOOL_PATH=${tool_path} ` +
-			`${apexer_with_trim_preprocessing} ` +
-			`--apexer ${apexer} ` +
-			`--canned_fs_config ${canned_fs_config} ` +
-			`--manifest ${manifest} ` +
-			`--libs_to_trim ${libs_to_trim} ` +
-			`${image_dir} ` +
-			`${out} ` +
-			`-- ` +
-			`--include_build_info ` +
-			`--force ` +
-			`--payload_type image ` +
-			`--key ${key} ` +
-			`--file_contexts ${file_contexts} ` +
-			`${opt_flags} `,
-		CommandDeps: []string{"${apexer_with_trim_preprocessing}", "${apexer}", "${avbtool}", "${e2fsdroid}",
-			"${merge_zips}", "${mke2fs}", "${resize2fs}", "${sefcontext_compile}", "${make_f2fs}",
-			"${sload_f2fs}", "${make_erofs}", "${soong_zip}", "${zipalign}", "${aapt2}",
-			"prebuilts/sdk/current/public/android.jar"},
-		Rspfile:        "${out}.copy_commands",
-		RspfileContent: "${copy_commands}",
-		Description:    "APEX ${image_dir} => ${out}",
-	}, "tool_path", "image_dir", "copy_commands", "file_contexts", "canned_fs_config", "key",
-		"opt_flags", "manifest", "libs_to_trim")
-
 	apexProtoConvertRule = pctx.AndroidStaticRule("apexProtoConvertRule",
 		blueprint.RuleParams{
 			Command:     `${aapt2} convert --output-format proto $in -o $out`,
@@ -831,24 +802,6 @@
 				"opt_flags":        strings.Join(optFlags, " "),
 			},
 		})
-	} else if ctx.Config().ApexTrimEnabled() && len(a.libs_to_trim(ctx)) > 0 {
-		ctx.Build(pctx, android.BuildParams{
-			Rule:        TrimmedApexRule,
-			Implicits:   implicitInputs,
-			Output:      unsignedOutputFile,
-			Description: "apex",
-			Args: map[string]string{
-				"tool_path":        outHostBinDir + ":" + prebuiltSdkToolsBinDir,
-				"image_dir":        imageDir.String(),
-				"copy_commands":    strings.Join(copyCommands, " && "),
-				"manifest":         a.manifestPbOut.String(),
-				"file_contexts":    fileContexts.String(),
-				"canned_fs_config": cannedFsConfig.String(),
-				"key":              a.privateKeyFile.String(),
-				"opt_flags":        strings.Join(optFlags, " "),
-				"libs_to_trim":     strings.Join(a.libs_to_trim(ctx), ","),
-			},
-		})
 	} else {
 		ctx.Build(pctx, android.BuildParams{
 			Rule:        apexRule,
diff --git a/apex/classpath_element_test.go b/apex/classpath_element_test.go
index f8e8899..f367174 100644
--- a/apex/classpath_element_test.go
+++ b/apex/classpath_element_test.go
@@ -45,10 +45,7 @@
 		prepareForTestWithPlatformBootclasspath,
 		prepareForTestWithArtApex,
 		prepareForTestWithMyapex,
-		// For otherapex.
-		android.FixtureMergeMockFs(android.MockFS{
-			"system/sepolicy/apex/otherapex-file_contexts": nil,
-		}),
+		prepareForTestWithOtherapex,
 		java.PrepareForTestWithJavaSdkLibraryFiles,
 		java.FixtureWithLastReleaseApis("foo", "othersdklibrary"),
 		java.FixtureConfigureApexBootJars("myapex:bar"),
diff --git a/cc/Android.bp b/cc/Android.bp
index a7b6d81..3b29ae8 100644
--- a/cc/Android.bp
+++ b/cc/Android.bp
@@ -122,3 +122,31 @@
     // Used by plugins
     visibility: ["//visibility:public"],
 }
+
+phony {
+    name: "llndk_libs",
+    required: [
+        "libEGL",
+        "libGLESv1_CM",
+        "libGLESv2",
+        "libGLESv3",
+        "libRS",
+        "libandroid_net",
+        "libapexsupport",
+        "libbinder_ndk",
+        "libc",
+        "libcgrouprc",
+        "libclang_rt.asan",
+        "libdl",
+        "libft2",
+        "liblog",
+        "libm",
+        "libmediandk",
+        "libnativewindow",
+        "libselinux",
+        "libsync",
+        "libvendorsupport",
+        "libvndksupport",
+        "libvulkan",
+    ],
+}
diff --git a/cc/builder.go b/cc/builder.go
index cd535c1..2948ca3 100644
--- a/cc/builder.go
+++ b/cc/builder.go
@@ -821,7 +821,7 @@
 		return nil
 	}
 
-	output := android.PathForModuleOut(ctx, "generated_rust_staticlib", "lib"+ctx.ModuleName()+"_rust_staticlib.a")
+	output := android.PathForModuleOut(ctx, "generated_rust_staticlib", "librustlibs.a")
 	stemFile := output.ReplaceExtension(ctx, "rs")
 	crateNames := []string{}
 
diff --git a/cc/cc.go b/cc/cc.go
index 4cda633..08a93cb9 100644
--- a/cc/cc.go
+++ b/cc/cc.go
@@ -19,8 +19,10 @@
 // is handled in builder.go
 
 import (
+	"errors"
 	"fmt"
 	"io"
+	"slices"
 	"strconv"
 	"strings"
 
@@ -35,6 +37,22 @@
 	"android/soong/genrule"
 )
 
+type CcMakeVarsInfo struct {
+	WarningsAllowed string
+	UsingWnoError   string
+	MissingProfile  string
+}
+
+var CcMakeVarsInfoProvider = blueprint.NewProvider[*CcMakeVarsInfo]()
+
+type CcObjectInfo struct {
+	objFiles   android.Paths
+	tidyFiles  android.Paths
+	kytheFiles android.Paths
+}
+
+var CcObjectInfoProvider = blueprint.NewProvider[CcObjectInfo]()
+
 func init() {
 	RegisterCCBuildComponents(android.InitRegistrationContext)
 
@@ -213,6 +231,9 @@
 	// LLNDK headers for the ABI checker to check LLNDK implementation library.
 	LlndkIncludeDirs       android.Paths
 	LlndkSystemIncludeDirs android.Paths
+
+	directImplementationDeps     android.Paths
+	transitiveImplementationDeps []depset.DepSet[android.Path]
 }
 
 // LocalOrGlobalFlags contains flags that need to have values set globally by the build system or locally by the module
@@ -389,11 +410,6 @@
 	// variant to have a ".sdk" suffix.
 	SdkAndPlatformVariantVisibleToMake bool `blueprint:"mutated"`
 
-	// List of APEXes that this module has private access to for testing purpose. The module
-	// can depend on libraries that are not exported by the APEXes and use private symbols
-	// from the exported libraries.
-	Test_for []string `android:"arch_variant"`
-
 	Target struct {
 		Platform struct {
 			// List of modules required by the core variant.
@@ -531,6 +547,7 @@
 	getSharedFlags() *SharedFlags
 	notInPlatform() bool
 	optimizeForSize() bool
+	getOrCreateMakeVarsInfo() *CcMakeVarsInfo
 }
 
 type SharedFlags struct {
@@ -639,10 +656,6 @@
 	installInRoot() bool
 }
 
-type xref interface {
-	XrefCcFiles() android.Paths
-}
-
 type overridable interface {
 	overriddenModules() []string
 }
@@ -845,9 +858,10 @@
 	sourceProperties android.SourceProperties
 
 	// initialize before calling Init
-	hod        android.HostOrDeviceSupported
-	multilib   android.Multilib
-	testModule bool
+	hod         android.HostOrDeviceSupported
+	multilib    android.Multilib
+	testModule  bool
+	incremental bool
 
 	// Allowable SdkMemberTypes of this module type.
 	sdkMemberTypes []android.SdkMemberType
@@ -890,12 +904,6 @@
 	staticAnalogue *StaticLibraryInfo
 
 	makeLinkType string
-	// Kythe (source file indexer) paths for this compilation module
-	kytheFiles android.Paths
-	// Object .o file output paths for this compilation module
-	objFiles android.Paths
-	// Tidy .tidy file output paths for this compilation module
-	tidyFiles android.Paths
 
 	// For apex variants, this is set as apex.min_sdk_version
 	apexSdkVersion android.ApiLevel
@@ -913,8 +921,16 @@
 	hasSysprop      bool
 	hasWinMsg       bool
 	hasYacc         bool
+
+	makeVarsInfo *CcMakeVarsInfo
 }
 
+func (c *Module) IncrementalSupported() bool {
+	return c.incremental
+}
+
+var _ blueprint.Incremental = (*Module)(nil)
+
 func (c *Module) AddJSONData(d *map[string]interface{}) {
 	c.AndroidModuleBase().AddJSONData(d)
 	(*d)["Cc"] = map[string]interface{}{
@@ -944,7 +960,6 @@
 		"IsLlndk":                c.IsLlndk(),
 		"IsVendorPublicLibrary":  c.IsVendorPublicLibrary(),
 		"ApexSdkVersion":         c.apexSdkVersion,
-		"TestFor":                c.TestFor(),
 		"AidlSrcs":               c.hasAidl,
 		"LexSrcs":                c.hasLex,
 		"ProtoSrcs":              c.hasProto,
@@ -1455,10 +1470,6 @@
 	return isBionic(name)
 }
 
-func (c *Module) XrefCcFiles() android.Paths {
-	return c.kytheFiles
-}
-
 func (c *Module) isCfiAssemblySupportEnabled() bool {
 	return c.sanitize != nil &&
 		Bool(c.sanitize.Properties.Sanitize.Config.Cfi_assembly_support)
@@ -1549,12 +1560,11 @@
 	}
 
 	if ctx.ctx.Device() {
-		config := ctx.ctx.Config()
-		if ctx.inVendor() {
-			// If building for vendor with final API, then use the latest _stable_ API as "current".
-			if config.VendorApiLevelFrozen() && (ver == "" || ver == "current") {
-				ver = config.PlatformSdkVersion().String()
-			}
+		// When building for vendor/product, use the latest _stable_ API as "current".
+		// This is passed to clang/aidl compilers so that compiled/generated code works
+		// with the system.
+		if (ctx.inVendor() || ctx.inProduct()) && (ver == "" || ver == "current") {
+			ver = ctx.ctx.Config().PlatformSdkVersion().String()
 		}
 	}
 
@@ -1700,6 +1710,13 @@
 	return ctx.mod.NotInPlatform()
 }
 
+func (ctx *moduleContextImpl) getOrCreateMakeVarsInfo() *CcMakeVarsInfo {
+	if ctx.mod.makeVarsInfo == nil {
+		ctx.mod.makeVarsInfo = &CcMakeVarsInfo{}
+	}
+	return ctx.mod.makeVarsInfo
+}
+
 func newBaseModule(hod android.HostOrDeviceSupported, multilib android.Multilib) *Module {
 	return &Module{
 		hod:      hod,
@@ -2023,9 +2040,6 @@
 		if ctx.Failed() {
 			return
 		}
-		c.kytheFiles = objs.kytheFiles
-		c.objFiles = objs.objFiles
-		c.tidyFiles = objs.tidyFiles
 	}
 
 	if c.linker != nil {
@@ -2036,6 +2050,10 @@
 		c.outputFile = android.OptionalPathForPath(outputFile)
 
 		c.maybeUnhideFromMake()
+
+		android.SetProvider(ctx, ImplementationDepInfoProvider, &ImplementationDepInfo{
+			ImplementationDeps: depset.New(depset.PREORDER, deps.directImplementationDeps, deps.transitiveImplementationDeps),
+		})
 	}
 
 	android.SetProvider(ctx, blueprint.SrcsFileProviderKey, blueprint.SrcsFileProviderData{SrcPaths: deps.GeneratedSources.Strings()})
@@ -2090,7 +2108,28 @@
 		c.hasYacc = b.hasSrcExt(ctx, ".y") || b.hasSrcExt(ctx, ".yy")
 	}
 
+	ccObjectInfo := CcObjectInfo{
+		kytheFiles: objs.kytheFiles,
+	}
+	if !ctx.Config().KatiEnabled() || !android.ShouldSkipAndroidMkProcessing(ctx, c) {
+		ccObjectInfo.objFiles = objs.objFiles
+		ccObjectInfo.tidyFiles = objs.tidyFiles
+	}
+	if len(ccObjectInfo.kytheFiles)+len(ccObjectInfo.objFiles)+len(ccObjectInfo.tidyFiles) > 0 {
+		android.SetProvider(ctx, CcObjectInfoProvider, ccObjectInfo)
+	}
+
 	c.setOutputFiles(ctx)
+
+	if c.makeVarsInfo != nil {
+		android.SetProvider(ctx, CcMakeVarsInfoProvider, c.makeVarsInfo)
+	}
+}
+
+func setOutputFilesIfNotEmpty(ctx ModuleContext, files android.Paths, tag string) {
+	if len(files) > 0 {
+		ctx.SetOutputFiles(files, tag)
+	}
 }
 
 func (c *Module) setOutputFiles(ctx ModuleContext) {
@@ -2166,6 +2205,7 @@
 		// modules can be hidden from make as some are needed for resolving make side
 		// dependencies.
 		c.HideFromMake()
+		c.SkipInstall()
 	} else if !installable(c, apexInfo) {
 		c.SkipInstall()
 	}
@@ -2254,6 +2294,10 @@
 	deps.RuntimeLibs = android.LastUniqueStrings(deps.RuntimeLibs)
 	deps.LlndkHeaderLibs = android.LastUniqueStrings(deps.LlndkHeaderLibs)
 
+	if err := checkConflictingExplicitVersions(deps.SharedLibs); err != nil {
+		ctx.PropertyErrorf("shared_libs", "%s", err.Error())
+	}
+
 	for _, lib := range deps.ReexportSharedLibHeaders {
 		if !inList(lib, deps.SharedLibs) {
 			ctx.PropertyErrorf("export_shared_lib_headers", "Shared library not in shared_libs: '%s'", lib)
@@ -2281,6 +2325,26 @@
 	return deps
 }
 
+func checkConflictingExplicitVersions(libs []string) error {
+	withoutVersion := func(s string) string {
+		name, _ := StubsLibNameAndVersion(s)
+		return name
+	}
+	var errs []error
+	for i, lib := range libs {
+		libName := withoutVersion(lib)
+		libsToCompare := libs[i+1:]
+		j := slices.IndexFunc(libsToCompare, func(s string) bool {
+			return withoutVersion(s) == libName
+		})
+		if j >= 0 {
+			errs = append(errs, fmt.Errorf("duplicate shared libraries with different explicit versions: %q and %q",
+				lib, libsToCompare[j]))
+		}
+	}
+	return errors.Join(errs...)
+}
+
 func (c *Module) beginMutator(actx android.BottomUpMutatorContext) {
 	ctx := &baseModuleContext{
 		BaseModuleContext: actx,
@@ -2342,6 +2406,9 @@
 
 	if version != "" && canBeOrLinkAgainstVersionVariants(mod) {
 		// Version is explicitly specified. i.e. libFoo#30
+		if version == "impl" {
+			version = ""
+		}
 		variations = append(variations, blueprint.Variation{Mutator: "version", Variation: version})
 		if tag, ok := depTag.(libraryDependencyTag); ok {
 			tag.explicitlyVersioned = true
@@ -3040,6 +3107,13 @@
 				linkFile = android.OptionalPathForPath(sharedLibraryInfo.SharedLibrary)
 				depFile = sharedLibraryInfo.TableOfContents
 
+				if !sharedLibraryInfo.IsStubs {
+					depPaths.directImplementationDeps = append(depPaths.directImplementationDeps, android.OutputFileForModule(ctx, dep, ""))
+					if info, ok := android.OtherModuleProvider(ctx, dep, ImplementationDepInfoProvider); ok {
+						depPaths.transitiveImplementationDeps = append(depPaths.transitiveImplementationDeps, info.ImplementationDeps)
+					}
+				}
+
 				ptr = &depPaths.SharedLibs
 				switch libDepTag.Order {
 				case earlyLibraryDependency:
@@ -3280,10 +3354,6 @@
 
 func ShouldUseStubForApex(ctx android.ModuleContext, dep android.Module) bool {
 	depName := ctx.OtherModuleName(dep)
-	thisModule, ok := ctx.Module().(android.ApexModule)
-	if !ok {
-		panic(fmt.Errorf("Not an APEX module: %q", ctx.ModuleName()))
-	}
 
 	inVendorOrProduct := false
 	bootstrap := false
@@ -3313,34 +3383,6 @@
 		isNotInPlatform := dep.(android.ApexModule).NotInPlatform()
 
 		useStubs = isNotInPlatform && !bootstrap
-
-		if useStubs {
-			// Another exception: if this module is a test for an APEX, then
-			// it is linked with the non-stub variant of a module in the APEX
-			// as if this is part of the APEX.
-			testFor, _ := android.ModuleProvider(ctx, android.ApexTestForInfoProvider)
-			for _, apexContents := range testFor.ApexContents {
-				if apexContents.DirectlyInApex(depName) {
-					useStubs = false
-					break
-				}
-			}
-		}
-		if useStubs {
-			// Yet another exception: If this module and the dependency are
-			// available to the same APEXes then skip stubs between their
-			// platform variants. This complements the test_for case above,
-			// which avoids the stubs on a direct APEX library dependency, by
-			// avoiding stubs for indirect test dependencies as well.
-			//
-			// TODO(b/183882457): This doesn't work if the two libraries have
-			// only partially overlapping apex_available. For that test_for
-			// modules would need to be split into APEX variants and resolved
-			// separately for each APEX they have access to.
-			if android.AvailableToSameApexes(thisModule, dep.(android.ApexModule)) {
-				useStubs = false
-			}
-		}
 	} else {
 		// If building for APEX, use stubs when the parent is in any APEX that
 		// the child is not in.
@@ -3641,10 +3683,6 @@
 	}
 }
 
-func (c *Module) TestFor() []string {
-	return c.Properties.Test_for
-}
-
 func (c *Module) EverInstallable() bool {
 	return c.installer != nil &&
 		// Check to see whether the module is actually ever installable.
@@ -3928,9 +3966,10 @@
 
 func (ks *kytheExtractAllSingleton) GenerateBuildActions(ctx android.SingletonContext) {
 	var xrefTargets android.Paths
-	ctx.VisitAllModules(func(module android.Module) {
-		if ccModule, ok := module.(xref); ok {
-			xrefTargets = append(xrefTargets, ccModule.XrefCcFiles()...)
+	ctx.VisitAllModuleProxies(func(module android.ModuleProxy) {
+		files := android.OtherModuleProviderOrDefault(ctx, module, CcObjectInfoProvider).kytheFiles
+		if len(files) > 0 {
+			xrefTargets = append(xrefTargets, files...)
 		}
 	})
 	// TODO(asmundak): Perhaps emit a rule to output a warning if there were no xrefTargets
diff --git a/cc/cc_test.go b/cc/cc_test.go
index e906706..144b90b 100644
--- a/cc/cc_test.go
+++ b/cc/cc_test.go
@@ -1008,7 +1008,7 @@
 	android.AssertArrayString(t, "variants for llndk stubs", expected, actual)
 
 	params := result.ModuleForTests("libllndk", "android_vendor_arm_armv7-a-neon_shared").Description("generate stub")
-	android.AssertSame(t, "use Vendor API level for default stubs", "202404", params.Args["apiLevel"])
+	android.AssertSame(t, "use Vendor API level for default stubs", "999999", params.Args["apiLevel"])
 
 	checkExportedIncludeDirs := func(module, variant string, expectedSystemDirs []string, expectedDirs ...string) {
 		t.Helper()
@@ -1505,111 +1505,6 @@
 	}
 }
 
-func TestStubsForLibraryInMultipleApexes(t *testing.T) {
-	t.Parallel()
-	ctx := testCc(t, `
-		cc_library_shared {
-			name: "libFoo",
-			srcs: ["foo.c"],
-			stubs: {
-				symbol_file: "foo.map.txt",
-				versions: ["current"],
-			},
-			apex_available: ["bar", "a1"],
-		}
-
-		cc_library_shared {
-			name: "libBar",
-			srcs: ["bar.c"],
-			shared_libs: ["libFoo"],
-			apex_available: ["a1"],
-		}
-
-		cc_library_shared {
-			name: "libA1",
-			srcs: ["a1.c"],
-			shared_libs: ["libFoo"],
-			apex_available: ["a1"],
-		}
-
-		cc_library_shared {
-			name: "libBarA1",
-			srcs: ["bara1.c"],
-			shared_libs: ["libFoo"],
-			apex_available: ["bar", "a1"],
-		}
-
-		cc_library_shared {
-			name: "libAnyApex",
-			srcs: ["anyApex.c"],
-			shared_libs: ["libFoo"],
-			apex_available: ["//apex_available:anyapex"],
-		}
-
-		cc_library_shared {
-			name: "libBaz",
-			srcs: ["baz.c"],
-			shared_libs: ["libFoo"],
-			apex_available: ["baz"],
-		}
-
-		cc_library_shared {
-			name: "libQux",
-			srcs: ["qux.c"],
-			shared_libs: ["libFoo"],
-			apex_available: ["qux", "bar"],
-		}`)
-
-	variants := ctx.ModuleVariantsForTests("libFoo")
-	expectedVariants := []string{
-		"android_arm64_armv8-a_shared",
-		"android_arm64_armv8-a_shared_current",
-		"android_arm_armv7-a-neon_shared",
-		"android_arm_armv7-a-neon_shared_current",
-	}
-	variantsMismatch := false
-	if len(variants) != len(expectedVariants) {
-		variantsMismatch = true
-	} else {
-		for _, v := range expectedVariants {
-			if !inList(v, variants) {
-				variantsMismatch = false
-			}
-		}
-	}
-	if variantsMismatch {
-		t.Errorf("variants of libFoo expected:\n")
-		for _, v := range expectedVariants {
-			t.Errorf("%q\n", v)
-		}
-		t.Errorf(", but got:\n")
-		for _, v := range variants {
-			t.Errorf("%q\n", v)
-		}
-	}
-
-	linkAgainstFoo := []string{"libBarA1"}
-	linkAgainstFooStubs := []string{"libBar", "libA1", "libBaz", "libQux", "libAnyApex"}
-
-	libFooPath := "libFoo/android_arm64_armv8-a_shared/libFoo.so"
-	for _, lib := range linkAgainstFoo {
-		libLinkRule := ctx.ModuleForTests(lib, "android_arm64_armv8-a_shared").Rule("ld")
-		libFlags := libLinkRule.Args["libFlags"]
-		if !strings.Contains(libFlags, libFooPath) {
-			t.Errorf("%q: %q is not found in %q", lib, libFooPath, libFlags)
-		}
-	}
-
-	libFooStubPath := "libFoo/android_arm64_armv8-a_shared_current/libFoo.so"
-	for _, lib := range linkAgainstFooStubs {
-		libLinkRule := ctx.ModuleForTests(lib, "android_arm64_armv8-a_shared").Rule("ld")
-		libFlags := libLinkRule.Args["libFlags"]
-		if !strings.Contains(libFlags, libFooStubPath) {
-			t.Errorf("%q: %q is not found in %q", lib, libFooStubPath, libFlags)
-		}
-	}
-}
-
 func TestVersioningMacro(t *testing.T) {
 	t.Parallel()
 	for _, tc := range []struct{ moduleName, expected string }{
@@ -3258,7 +3153,7 @@
 	testDepWithVariant("product")
 }
 
-func TestVendorSdkVersion(t *testing.T) {
+func TestVendorOrProductVariantUsesPlatformSdkVersionAsDefault(t *testing.T) {
 	t.Parallel()
 
 	bp := `
@@ -3266,31 +3161,29 @@
 			name: "libfoo",
 			srcs: ["libfoo.cc"],
 			vendor_available: true,
+			product_available: true,
 		}
 
 		cc_library {
 			name: "libbar",
 			srcs: ["libbar.cc"],
 			vendor_available: true,
+			product_available: true,
 			min_sdk_version: "29",
 		}
 	`
 
 	ctx := prepareForCcTest.RunTestWithBp(t, bp)
-	testSdkVersionFlag := func(module, version string) {
-		flags := ctx.ModuleForTests(module, "android_vendor_arm64_armv8-a_static").Rule("cc").Args["cFlags"]
-		android.AssertStringDoesContain(t, "min sdk version", flags, "-target aarch64-linux-android"+version)
+	testSdkVersionFlag := func(module, variant, version string) {
+		flags := ctx.ModuleForTests(module, "android_"+variant+"_arm64_armv8-a_static").Rule("cc").Args["cFlags"]
+		android.AssertStringDoesContain(t, "target SDK version", flags, "-target aarch64-linux-android"+version)
 	}
 
-	testSdkVersionFlag("libfoo", "10000")
-	testSdkVersionFlag("libbar", "29")
-
-	ctx = android.GroupFixturePreparers(
-		prepareForCcTest,
-		android.PrepareForTestWithBuildFlag("RELEASE_BOARD_API_LEVEL_FROZEN", "true"),
-	).RunTestWithBp(t, bp)
-	testSdkVersionFlag("libfoo", "30")
-	testSdkVersionFlag("libbar", "29")
+	testSdkVersionFlag("libfoo", "vendor", "30")
+	testSdkVersionFlag("libfoo", "product", "30")
+	// target SDK version can be set explicitly with min_sdk_version
+	testSdkVersionFlag("libbar", "vendor", "29")
+	testSdkVersionFlag("libbar", "product", "29")
 }
 
 func TestClangVerify(t *testing.T) {
@@ -3321,3 +3214,20 @@
 		t.Errorf("expected %q in cflags, got %q", "-Xclang -verify", cFlags_cv)
 	}
 }
+
+func TestCheckConflictingExplicitVersions(t *testing.T) {
+	PrepareForIntegrationTestWithCc.
+		ExtendWithErrorHandler(android.FixtureExpectsOneErrorPattern(
+			`shared_libs: duplicate shared libraries with different explicit versions: "libbar" and "libbar#impl"`,
+		)).
+		RunTestWithBp(t, `
+			cc_library {
+				name: "libfoo",
+				shared_libs: ["libbar", "libbar#impl"],
+			}
+
+			cc_library {
+				name: "libbar",
+			}
+		`)
+}
diff --git a/cc/compdb.go b/cc/compdb.go
index b33f490..4132e09 100644
--- a/cc/compdb.go
+++ b/cc/compdb.go
@@ -146,6 +146,8 @@
 		isAsm = false
 		isCpp = true
 		clangPath = cxxPath
+	case ".o":
+		return nil
 	default:
 		log.Print("Unknown file extension " + src.Ext() + " on file " + src.String())
 		isAsm = true
@@ -185,6 +187,10 @@
 	}
 	for _, src := range srcs {
 		if _, ok := builds[src.String()]; !ok {
+			args := getArguments(src, ctx, ccModule, ccPath, cxxPath)
+			if args == nil {
+				continue
+			}
 			builds[src.String()] = compDbEntry{
 				Directory: android.AbsSrcDirForExistingUseCases(),
 				Arguments: getArguments(src, ctx, ccModule, ccPath, cxxPath),
diff --git a/cc/compiler.go b/cc/compiler.go
index f06287c..91f107c 100644
--- a/cc/compiler.go
+++ b/cc/compiler.go
@@ -78,11 +78,11 @@
 	// If possible, don't use this.  If adding paths from the current directory use
 	// local_include_dirs, if adding paths from other modules use export_include_dirs in
 	// that module.
-	Include_dirs []string `android:"arch_variant,variant_prepend"`
+	Include_dirs proptools.Configurable[[]string] `android:"arch_variant,variant_prepend"`
 
 	// list of directories relative to the Blueprints file that will
 	// be added to the include path using -I
-	Local_include_dirs []string `android:"arch_variant,variant_prepend"`
+	Local_include_dirs proptools.Configurable[[]string] `android:"arch_variant,variant_prepend"`
 
 	// Add the directory containing the Android.bp file to the list of include
 	// directories. Defaults to true.
@@ -411,13 +411,13 @@
 	}
 
 	// Include dir cflags
-	localIncludeDirs := android.PathsForModuleSrc(ctx, compiler.Properties.Local_include_dirs)
+	localIncludeDirs := android.PathsForModuleSrc(ctx, compiler.Properties.Local_include_dirs.GetOrDefault(ctx, nil))
 	if len(localIncludeDirs) > 0 {
 		f := includeDirsToFlags(localIncludeDirs)
 		flags.Local.CommonFlags = append(flags.Local.CommonFlags, f)
 		flags.Local.YasmFlags = append(flags.Local.YasmFlags, f)
 	}
-	rootIncludeDirs := android.PathsForSource(ctx, compiler.Properties.Include_dirs)
+	rootIncludeDirs := android.PathsForSource(ctx, compiler.Properties.Include_dirs.GetOrDefault(ctx, nil))
 	if len(rootIncludeDirs) > 0 {
 		f := includeDirsToFlags(rootIncludeDirs)
 		flags.Local.CommonFlags = append(flags.Local.CommonFlags, f)
@@ -685,10 +685,10 @@
 	if len(srcs) > 0 {
 		module := ctx.ModuleDir() + "/Android.bp:" + ctx.ModuleName()
 		if inList("-Wno-error", flags.Local.CFlags) || inList("-Wno-error", flags.Local.CppFlags) {
-			addToModuleList(ctx, modulesUsingWnoErrorKey, module)
+			ctx.getOrCreateMakeVarsInfo().UsingWnoError = module
 		} else if !inList("-Werror", flags.Local.CFlags) && !inList("-Werror", flags.Local.CppFlags) {
 			if warningsAreAllowed(ctx.ModuleDir()) {
-				addToModuleList(ctx, modulesWarningsAllowedKey, module)
+				ctx.getOrCreateMakeVarsInfo().WarningsAllowed = module
 			} else {
 				flags.Local.CFlags = append([]string{"-Werror"}, flags.Local.CFlags...)
 			}
@@ -807,7 +807,7 @@
 type RustBindgenClangProperties struct {
 	// list of directories relative to the Blueprints file that will
 	// be added to the include path using -I
-	Local_include_dirs []string `android:"arch_variant,variant_prepend"`
+	Local_include_dirs proptools.Configurable[[]string] `android:"arch_variant,variant_prepend"`
 
 	// list of static libraries that provide headers for this binding.
 	Static_libs proptools.Configurable[[]string] `android:"arch_variant,variant_prepend"`
diff --git a/cc/config/global.go b/cc/config/global.go
index d63f86d..36690d6 100644
--- a/cc/config/global.go
+++ b/cc/config/global.go
@@ -72,6 +72,9 @@
 
 		// Warnings disabled by default.
 
+		// We should encourage use of C23 features even when the whole project
+		// isn't C23-ready.
+		"-Wno-c23-extensions",
 		// Designated initializer syntax is recommended by the Google C++ style
 		// and is OK to use even if not formally supported by the chosen C++
 		// version.
diff --git a/cc/library.go b/cc/library.go
index 1f21614..4ce506e 100644
--- a/cc/library.go
+++ b/cc/library.go
@@ -566,16 +566,10 @@
 
 func (library *libraryDecorator) compile(ctx ModuleContext, flags Flags, deps PathDeps) Objects {
 	if ctx.IsLlndk() {
-		vendorApiLevel := ctx.Config().VendorApiLevel()
-		if vendorApiLevel == "" {
-			// TODO(b/321892570): Some tests relying on old fixtures which
-			// doesn't set vendorApiLevel. Needs to fix them.
-			vendorApiLevel = ctx.Config().PlatformSdkVersion().String()
-		}
-		// This is the vendor variant of an LLNDK library, build the LLNDK stubs.
+		futureVendorApiLevel := android.ApiLevelOrPanic(ctx, "999999")
 		nativeAbiResult := parseNativeAbiDefinition(ctx,
 			String(library.Properties.Llndk.Symbol_file),
-			android.ApiLevelOrPanic(ctx, vendorApiLevel), "--llndk")
+			futureVendorApiLevel, "--llndk")
 		objs := compileStubLibrary(ctx, flags, nativeAbiResult.stubSrc)
 		if !Bool(library.Properties.Llndk.Unversioned) {
 			library.versionScriptPath = android.OptionalPathForPath(
@@ -1194,6 +1188,7 @@
 		SharedLibrary:                        unstrippedOutputFile,
 		TransitiveStaticLibrariesForOrdering: transitiveStaticLibrariesForOrdering,
 		Target:                               ctx.Target(),
+		IsStubs:                              library.buildStubs(),
 	})
 
 	addStubDependencyProviders(ctx)
diff --git a/cc/linkable.go b/cc/linkable.go
index cd33e28..ef204eb 100644
--- a/cc/linkable.go
+++ b/cc/linkable.go
@@ -317,7 +317,9 @@
 	SharedLibrary android.Path
 	Target        android.Target
 
-	TableOfContents android.OptionalPath
+	TableOfContents    android.OptionalPath
+	IsStubs            bool
+	ImplementationDeps depset.DepSet[string]
 
 	// should be obtained from static analogue
 	TransitiveStaticLibrariesForOrdering depset.DepSet[android.Path]
@@ -386,3 +388,9 @@
 }
 
 var FlagExporterInfoProvider = blueprint.NewProvider[FlagExporterInfo]()
+
+var ImplementationDepInfoProvider = blueprint.NewProvider[*ImplementationDepInfo]()
+
+type ImplementationDepInfo struct {
+	ImplementationDeps depset.DepSet[android.Path]
+}
diff --git a/cc/makevars.go b/cc/makevars.go
index f82e0e9..4cb98e7 100644
--- a/cc/makevars.go
+++ b/cc/makevars.go
@@ -25,10 +25,7 @@
 )
 
 var (
-	modulesWarningsAllowedKey    = android.NewOnceKey("ModulesWarningsAllowed")
-	modulesUsingWnoErrorKey      = android.NewOnceKey("ModulesUsingWnoError")
-	modulesMissingProfileFileKey = android.NewOnceKey("ModulesMissingProfileFile")
-	sanitizerVariables           = map[string]string{
+	sanitizerVariables = map[string]string{
 		"ADDRESS_SANITIZER_RUNTIME_LIBRARY":   config.AddressSanitizerRuntimeLibrary(),
 		"HWADDRESS_SANITIZER_RUNTIME_LIBRARY": config.HWAddressSanitizerRuntimeLibrary(),
 		"HWADDRESS_SANITIZER_STATIC_LIBRARY":  config.HWAddressSanitizerStaticLibrary(),
@@ -50,15 +47,9 @@
 	}).(*sync.Map)
 }
 
-func makeStringOfKeys(ctx android.MakeVarsContext, key android.OnceKey) string {
-	set := getNamedMapForConfig(ctx.Config(), key)
-	keys := []string{}
-	set.Range(func(key interface{}, value interface{}) bool {
-		keys = append(keys, key.(string))
-		return true
-	})
-	sort.Strings(keys)
-	return strings.Join(keys, " ")
+func makeVarsString(items []string) string {
+	items = android.SortedUniqueStrings(items)
+	return strings.Join(items, " ")
 }
 
 func makeStringOfWarningAllowedProjects() string {
@@ -108,28 +99,33 @@
 	ctx.Strict("GLOBAL_CLANG_EXTERNAL_CFLAGS_NO_OVERRIDE", "${config.NoOverrideExternalGlobalCflags}")
 
 	// Filter vendor_public_library that are exported to make
-	exportedVendorPublicLibraries := []string{}
+	var exportedVendorPublicLibraries []string
+	var warningsAllowed []string
+	var usingWnoErrors []string
+	var missingProfiles []string
 	ctx.VisitAllModules(func(module android.Module) {
+		if v, ok := android.OtherModuleProvider(ctx, module, CcMakeVarsInfoProvider); ok {
+			warningsAllowed = android.AppendIfNotZero(warningsAllowed, v.WarningsAllowed)
+			usingWnoErrors = android.AppendIfNotZero(usingWnoErrors, v.UsingWnoError)
+			missingProfiles = android.AppendIfNotZero(missingProfiles, v.MissingProfile)
+		}
 		if ccModule, ok := module.(*Module); ok {
 			baseName := ccModule.BaseModuleName()
 			if ccModule.IsVendorPublicLibrary() && module.ExportedToMake() {
-				if !inList(baseName, exportedVendorPublicLibraries) {
-					exportedVendorPublicLibraries = append(exportedVendorPublicLibraries, baseName)
-				}
+				exportedVendorPublicLibraries = append(exportedVendorPublicLibraries, baseName)
 			}
 		}
 	})
-	sort.Strings(exportedVendorPublicLibraries)
-	ctx.Strict("VENDOR_PUBLIC_LIBRARIES", strings.Join(exportedVendorPublicLibraries, " "))
+	ctx.Strict("VENDOR_PUBLIC_LIBRARIES", makeVarsString(exportedVendorPublicLibraries))
 
 	lsdumpPaths := *lsdumpPaths(ctx.Config())
 	sort.Strings(lsdumpPaths)
 	ctx.Strict("LSDUMP_PATHS", strings.Join(lsdumpPaths, " "))
 
 	ctx.Strict("ANDROID_WARNING_ALLOWED_PROJECTS", makeStringOfWarningAllowedProjects())
-	ctx.Strict("SOONG_MODULES_WARNINGS_ALLOWED", makeStringOfKeys(ctx, modulesWarningsAllowedKey))
-	ctx.Strict("SOONG_MODULES_USING_WNO_ERROR", makeStringOfKeys(ctx, modulesUsingWnoErrorKey))
-	ctx.Strict("SOONG_MODULES_MISSING_PGO_PROFILE_FILE", makeStringOfKeys(ctx, modulesMissingProfileFileKey))
+	ctx.Strict("SOONG_MODULES_WARNINGS_ALLOWED", makeVarsString(warningsAllowed))
+	ctx.Strict("SOONG_MODULES_USING_WNO_ERROR", makeVarsString(usingWnoErrors))
+	ctx.Strict("SOONG_MODULES_MISSING_PGO_PROFILE_FILE", makeVarsString(missingProfiles))
 
 	ctx.Strict("CLANG_COVERAGE_CONFIG_CFLAGS", strings.Join(clangCoverageCFlags, " "))
 	ctx.Strict("CLANG_COVERAGE_CONFIG_COMMFLAGS", strings.Join(clangCoverageCommonFlags, " "))
diff --git a/cc/ndk_library.go b/cc/ndk_library.go
index 01551ab..2411614 100644
--- a/cc/ndk_library.go
+++ b/cc/ndk_library.go
@@ -144,11 +144,9 @@
 }
 
 func ndkLibraryVersions(ctx android.BaseModuleContext, from android.ApiLevel) []string {
-	var versions []android.ApiLevel
 	versionStrs := []string{}
-	for _, version := range ctx.Config().AllSupportedApiLevels() {
+	for _, version := range ctx.Config().FinalApiLevels() {
 		if version.GreaterThanOrEqualTo(from) {
-			versions = append(versions, version)
 			versionStrs = append(versionStrs, version.String())
 		}
 	}
@@ -330,6 +328,12 @@
 	return android.ExistentPathForSource(ctx, subpath)
 }
 
+func (this *stubDecorator) builtAbiDumpLocation(ctx ModuleContext, apiLevel android.ApiLevel) android.OutputPath {
+	return getNdkAbiDumpInstallBase(ctx).Join(ctx,
+		apiLevel.String(), ctx.Arch().ArchType.String(),
+		this.libraryName(ctx), "abi.stg")
+}
+
 // Feature flag.
 func (this *stubDecorator) canDumpAbi(ctx ModuleContext) bool {
 	if runtime.GOOS == "darwin" {
@@ -345,25 +349,24 @@
 		return false
 	}
 
-	if this.apiLevel.IsCurrent() {
-		// "current" (AKA 10000) is not tracked.
-		return false
-	}
-
 	// http://b/156513478
 	return ctx.Config().ReleaseNdkAbiMonitored()
 }
 
 // Feature flag to disable diffing against prebuilts.
-func canDiffAbi(config android.Config) bool {
+func (this *stubDecorator) canDiffAbi(config android.Config) bool {
+	if this.apiLevel.IsCurrent() {
+		// Diffs are performed from this to next, and there's nothing after
+		// current.
+		return false
+	}
+
 	return config.ReleaseNdkAbiMonitored()
 }
 
 func (this *stubDecorator) dumpAbi(ctx ModuleContext, symbolList android.Path) {
 	implementationLibrary := this.findImplementationLibrary(ctx)
-	this.abiDumpPath = getNdkAbiDumpInstallBase(ctx).Join(ctx,
-		this.apiLevel.String(), ctx.Arch().ArchType.String(),
-		this.libraryName(ctx), "abi.stg")
+	this.abiDumpPath = this.builtAbiDumpLocation(ctx, this.apiLevel)
 	this.hasAbiDump = true
 	headersList := getNdkABIHeadersFile(ctx)
 	ctx.Build(pctx, android.BuildParams{
@@ -383,7 +386,7 @@
 }
 
 func findNextApiLevel(ctx ModuleContext, apiLevel android.ApiLevel) *android.ApiLevel {
-	apiLevels := append(ctx.Config().AllSupportedApiLevels(),
+	apiLevels := append(ctx.Config().FinalApiLevels(),
 		android.FutureApiLevel)
 	for _, api := range apiLevels {
 		if api.GreaterThan(apiLevel) {
@@ -436,38 +439,49 @@
 				"non-current API level %s", this.apiLevel))
 		}
 
-		// "current" ABI is not tracked.
-		if !nextApiLevel.IsCurrent() {
-			nextAbiDiffPath := android.PathForModuleOut(ctx,
-				"abidiff_next.timestamp")
-			nextAbiDump := this.findPrebuiltAbiDump(ctx, *nextApiLevel)
+		// Preview ABI levels are not recorded in prebuilts. ABI compatibility
+		// for preview APIs is still monitored via "current" so we have early
+		// warning rather than learning about an ABI break during finalization,
+		// but is only checked against the "current" API dumps in the out
+		// directory.
+		nextAbiDiffPath := android.PathForModuleOut(ctx,
+			"abidiff_next.timestamp")
+
+		var nextAbiDump android.OptionalPath
+		if nextApiLevel.IsCurrent() {
+			nextAbiDump = android.OptionalPathForPath(
+				this.builtAbiDumpLocation(ctx, *nextApiLevel),
+			)
+		} else {
+			nextAbiDump = this.findPrebuiltAbiDump(ctx, *nextApiLevel)
+		}
+
+		if !nextAbiDump.Valid() {
 			missingNextPrebuiltError := fmt.Sprintf(
 				missingPrebuiltErrorTemplate, this.libraryName(ctx),
 				nextAbiDump.InvalidReason())
-			if !nextAbiDump.Valid() {
-				ctx.Build(pctx, android.BuildParams{
-					Rule:   android.ErrorRule,
-					Output: nextAbiDiffPath,
-					Args: map[string]string{
-						"error": missingNextPrebuiltError,
-					},
-				})
-			} else {
-				ctx.Build(pctx, android.BuildParams{
-					Rule: stgdiff,
-					Description: fmt.Sprintf(
-						"Comparing ABI to the next API level %s %s",
-						prebuiltAbiDump, nextAbiDump),
-					Output: nextAbiDiffPath,
-					Inputs: android.Paths{
-						prebuiltAbiDump.Path(), nextAbiDump.Path()},
-					Args: map[string]string{
-						"args": "--format=small --ignore=interface_addition",
-					},
-				})
-			}
-			this.abiDiffPaths = append(this.abiDiffPaths, nextAbiDiffPath)
+			ctx.Build(pctx, android.BuildParams{
+				Rule:   android.ErrorRule,
+				Output: nextAbiDiffPath,
+				Args: map[string]string{
+					"error": missingNextPrebuiltError,
+				},
+			})
+		} else {
+			ctx.Build(pctx, android.BuildParams{
+				Rule: stgdiff,
+				Description: fmt.Sprintf(
+					"Comparing ABI to the next API level %s %s",
+					prebuiltAbiDump, nextAbiDump),
+				Output: nextAbiDiffPath,
+				Inputs: android.Paths{
+					prebuiltAbiDump.Path(), nextAbiDump.Path()},
+				Args: map[string]string{
+					"args": "--format=small --ignore=interface_addition",
+				},
+			})
 		}
+		this.abiDiffPaths = append(this.abiDiffPaths, nextAbiDiffPath)
 	}
 }
 
@@ -492,7 +506,7 @@
 	c.versionScriptPath = nativeAbiResult.versionScript
 	if c.canDumpAbi(ctx) {
 		c.dumpAbi(ctx, nativeAbiResult.symbolList)
-		if canDiffAbi(ctx.Config()) {
+		if c.canDiffAbi(ctx.Config()) {
 			c.diffAbi(ctx)
 		}
 	}
diff --git a/cc/orderfile.go b/cc/orderfile.go
index 38b8905..6e08da7 100644
--- a/cc/orderfile.go
+++ b/cc/orderfile.go
@@ -54,10 +54,6 @@
 	})
 }
 
-func recordMissingOrderfile(ctx BaseModuleContext, missing string) {
-	getNamedMapForConfig(ctx.Config(), modulesMissingProfileFileKey).Store(missing, true)
-}
-
 type OrderfileProperties struct {
 	Orderfile struct {
 		Instrumentation *bool
@@ -117,7 +113,7 @@
 
 	// Record that this module's order file is absent
 	missing := *props.Orderfile.Order_file_path + ":" + ctx.ModuleDir() + "/Android.bp:" + ctx.ModuleName()
-	recordMissingOrderfile(ctx, missing)
+	ctx.getOrCreateMakeVarsInfo().MissingProfile = missing
 
 	return android.OptionalPath{}
 }
diff --git a/cc/prebuilt.go b/cc/prebuilt.go
index 7c87297..ba4c662 100644
--- a/cc/prebuilt.go
+++ b/cc/prebuilt.go
@@ -221,6 +221,7 @@
 				Target:        ctx.Target(),
 
 				TableOfContents: p.tocFile,
+				IsStubs:         p.buildStubs(),
 			})
 
 			return outputFile
@@ -232,6 +233,7 @@
 		android.SetProvider(ctx, SharedLibraryInfoProvider, SharedLibraryInfo{
 			SharedLibrary: latestStub,
 			Target:        ctx.Target(),
+			IsStubs:       true,
 		})
 
 		return latestStub
diff --git a/cc/tidy.go b/cc/tidy.go
index ec1e8a2..5cbf8f0 100644
--- a/cc/tidy.go
+++ b/cc/tidy.go
@@ -219,15 +219,11 @@
 	subsetTidyFileGroups := make(map[string]android.Paths) // subset group name => tidy file Paths
 
 	// (1) Collect all obj/tidy files into OS-specific groups.
-	ctx.VisitAllModuleVariants(module, func(variant android.Module) {
-		if ctx.Config().KatiEnabled() && android.ShouldSkipAndroidMkProcessing(ctx, variant) {
-			return
-		}
-		if m, ok := variant.(*Module); ok {
-			osName := variant.Target().Os.Name
-			addToOSGroup(osName, m.objFiles, allObjFileGroups, subsetObjFileGroups)
-			addToOSGroup(osName, m.tidyFiles, allTidyFileGroups, subsetTidyFileGroups)
-		}
+	ctx.VisitAllModuleVariantProxies(module, func(variant android.ModuleProxy) {
+		osName := android.OtherModuleProviderOrDefault(ctx, variant, android.CommonPropertiesProviderKey).CompileTarget.Os.Name
+		info := android.OtherModuleProviderOrDefault(ctx, variant, CcObjectInfoProvider)
+		addToOSGroup(osName, info.objFiles, allObjFileGroups, subsetObjFileGroups)
+		addToOSGroup(osName, info.tidyFiles, allTidyFileGroups, subsetTidyFileGroups)
 	})
 
 	// (2) Add an all-OS group, with "" or "subset" name, to include all os-specific phony targets.
@@ -258,7 +254,7 @@
 
 	// Collect tidy/obj targets from the 'final' modules.
 	ctx.VisitAllModules(func(module android.Module) {
-		if module == ctx.FinalModule(module) {
+		if ctx.IsFinalModule(module) {
 			collectTidyObjModuleTargets(ctx, module, tidyModulesInDirGroup, objModulesInDirGroup)
 		}
 	})
diff --git a/cc/vndk_prebuilt.go b/cc/vndk_prebuilt.go
index e7dff40..4a2adf0 100644
--- a/cc/vndk_prebuilt.go
+++ b/cc/vndk_prebuilt.go
@@ -166,6 +166,7 @@
 			Target:        ctx.Target(),
 
 			TableOfContents: p.tocFile,
+			IsStubs:         false,
 		})
 
 		p.libraryDecorator.flagExporter.setProvider(ctx)
diff --git a/cmd/find_input_delta/find_input_delta/Android.bp b/cmd/find_input_delta/find_input_delta/Android.bp
new file mode 100644
index 0000000..93a7708
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta/Android.bp
@@ -0,0 +1,18 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+blueprint_go_binary {
+    name: "find_input_delta",
+    deps: [
+        "golang-protobuf-encoding-prototext",
+        "golang-protobuf-reflect-protoreflect",
+        "golang-protobuf-runtime-protoimpl",
+        "soong-cmd-find_input_delta-lib",
+        "soong-cmd-find_input_delta-proto",
+        "soong-cmd-find_input_delta-proto_internal",
+    ],
+    srcs: [
+        "main.go",
+    ],
+}
diff --git a/cmd/find_input_delta/find_input_delta/main.go b/cmd/find_input_delta/find_input_delta/main.go
new file mode 100644
index 0000000..6b657ea
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta/main.go
@@ -0,0 +1,88 @@
+// Copyright 2024 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 main
+
+import (
+	"flag"
+	"os"
+	"strings"
+
+	fid_lib "android/soong/cmd/find_input_delta/find_input_delta_lib"
+)
+
+func main() {
+	var top string
+	var prior_state_file string
+	var new_state_file string
+	var target string
+	var inputs_file string
+	var template string
+	var inputs []string
+	var inspect bool
+	var err error
+
+	flag.StringVar(&top, "top", ".", "path to top of workspace")
+	flag.StringVar(&prior_state_file, "prior_state", "", "prior internal state file")
+	flag.StringVar(&new_state_file, "new_state", "", "new internal state file")
+	flag.StringVar(&target, "target", "", "name of ninja output file for build action")
+	flag.StringVar(&inputs_file, "inputs_file", "", "file containing list of input files")
+	flag.StringVar(&template, "template", fid_lib.DefaultTemplate, "output template for FileList")
+	flag.BoolVar(&inspect, "inspect", false, "whether to inspect file contents")
+
+	flag.Parse()
+
+	if target == "" {
+		panic("must specify --target")
+	}
+	if prior_state_file == "" {
+		prior_state_file = target + ".pc_state"
+	}
+	if new_state_file == "" {
+		new_state_file = prior_state_file + ".new"
+	}
+
+	if err = os.Chdir(top); err != nil {
+		panic(err)
+	}
+
+	inputs = flag.Args()
+	if inputs_file != "" {
+		data, err := os.ReadFile(inputs_file)
+		if err != nil {
+			panic(err)
+		}
+		inputs = append(inputs, strings.Split(string(data), "\n")...)
+	}
+
+	// Read the prior state
+	prior_state, err := fid_lib.LoadState(prior_state_file, fid_lib.OsFs)
+	if err != nil {
+		panic(err)
+	}
+	// Create the new state
+	new_state, err := fid_lib.CreateState(inputs, inspect, fid_lib.OsFs)
+	if err != nil {
+		panic(err)
+	}
+	if err = fid_lib.WriteState(new_state, new_state_file); err != nil {
+		panic(err)
+	}
+
+	file_list := *fid_lib.CompareInternalState(prior_state, new_state, target)
+
+	if err = file_list.Format(os.Stdout, template); err != nil {
+		panic(err)
+	}
+}
diff --git a/cmd/find_input_delta/find_input_delta_lib/Android.bp b/cmd/find_input_delta/find_input_delta_lib/Android.bp
new file mode 100644
index 0000000..95bdba8
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_lib/Android.bp
@@ -0,0 +1,35 @@
+// Copyright 2024 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+bootstrap_go_package {
+    name: "soong-cmd-find_input_delta-lib",
+    pkgPath: "android/soong/cmd/find_input_delta/find_input_delta_lib",
+    deps: [
+        "golang-protobuf-encoding-prototext",
+        "golang-protobuf-reflect-protoreflect",
+        "golang-protobuf-runtime-protoimpl",
+        "soong-cmd-find_input_delta-proto",
+        "soong-cmd-find_input_delta-proto_internal",
+        "blueprint-pathtools",
+    ],
+    srcs: [
+        "fs.go",
+        "file_list.go",
+        "internal_state.go",
+    ],
+}
diff --git a/cmd/find_input_delta/find_input_delta_lib/file_list.go b/cmd/find_input_delta/find_input_delta_lib/file_list.go
new file mode 100644
index 0000000..23337ad
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_lib/file_list.go
@@ -0,0 +1,78 @@
+// Copyright 2024 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 find_input_delta_lib
+
+import (
+	"io"
+	"text/template"
+
+	fid_exp "android/soong/cmd/find_input_delta/find_input_delta_proto"
+	"google.golang.org/protobuf/proto"
+)
+
+var DefaultTemplate = `
+	{{- define "contents"}}
+		{{- range .Deletions}}-{{.}} {{end}}
+		{{- range .Additions}}+{{.}} {{end}}
+		{{- range .Changes}}+{{- .Name}} {{end}}
+		{{- range .Changes}}
+		  {{- if or .Additions .Deletions .Changes}}--file {{.Name}} {{template "contents" .}}--endfile {{end}}
+		{{- end}}
+	{{- end}}
+	{{- template "contents" .}}`
+
+type FileList struct {
+	// The name of the parent for the list of file differences.
+	// For the outermost FileList, this is the name of the ninja target.
+	// Under `Changes`, it is the name of the changed file.
+	Name string
+
+	// The added files
+	Additions []string
+
+	// The deleted files
+	Deletions []string
+
+	// The modified files
+	Changes []FileList
+}
+
+func (fl FileList) Marshal() (*fid_exp.FileList, error) {
+	ret := &fid_exp.FileList{
+		Name: proto.String(fl.Name),
+	}
+	if len(fl.Additions) > 0 {
+		ret.Additions = fl.Additions
+	}
+	for _, ch := range fl.Changes {
+		change, err := ch.Marshal()
+		if err != nil {
+			return nil, err
+		}
+		ret.Changes = append(ret.Changes, change)
+	}
+	if len(fl.Deletions) > 0 {
+		ret.Deletions = fl.Deletions
+	}
+	return ret, nil
+}
+
+func (fl FileList) Format(wr io.Writer, format string) error {
+	tmpl, err := template.New("filelist").Parse(format)
+	if err != nil {
+		return err
+	}
+	return tmpl.Execute(wr, fl)
+}
diff --git a/cmd/find_input_delta/find_input_delta_lib/file_list_test.go b/cmd/find_input_delta/find_input_delta_lib/file_list_test.go
new file mode 100644
index 0000000..2459f1e
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_lib/file_list_test.go
@@ -0,0 +1,131 @@
+// Copyright 2024 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 find_input_delta_lib
+
+import (
+	"bytes"
+	"slices"
+	"testing"
+
+	// For Assert*.
+	"android/soong/android"
+)
+
+func (fl *FileList) Equal(other *FileList) bool {
+	if fl.Name != other.Name {
+		return false
+	}
+	if !slices.Equal(fl.Additions, other.Additions) {
+		return false
+	}
+	if !slices.Equal(fl.Deletions, other.Deletions) {
+		return false
+	}
+	if len(fl.Changes) != len(other.Changes) {
+		return false
+	}
+	for idx, ch := range fl.Changes {
+		if !ch.Equal(&other.Changes[idx]) {
+			return false
+		}
+	}
+	return true
+}
+
+func TestFormat(t *testing.T) {
+	testCases := []struct {
+		Name     string
+		Template string
+		Input    FileList
+		Expected string
+		Err      error
+	}{
+		{
+			Name:     "no contents",
+			Template: DefaultTemplate,
+			Input: FileList{
+				Name:      "target",
+				Additions: []string{"add1", "add2"},
+				Deletions: []string{"del1", "del2"},
+				Changes: []FileList{
+					FileList{Name: "mod1"},
+					FileList{Name: "mod2"},
+				},
+			},
+			Expected: "-del1 -del2 +add1 +add2 +mod1 +mod2 ",
+			Err:      nil,
+		},
+		{
+			Name:     "adds",
+			Template: DefaultTemplate,
+			Input: FileList{
+				Name:      "target",
+				Additions: []string{"add1", "add2"},
+			},
+			Expected: "+add1 +add2 ",
+			Err:      nil,
+		},
+		{
+			Name:     "deletes",
+			Template: DefaultTemplate,
+			Input: FileList{
+				Name:      "target",
+				Deletions: []string{"del1", "del2"},
+			},
+			Expected: "-del1 -del2 ",
+			Err:      nil,
+		},
+		{
+			Name:     "changes",
+			Template: DefaultTemplate,
+			Input: FileList{
+				Name: "target",
+				Changes: []FileList{
+					FileList{Name: "mod1"},
+					FileList{Name: "mod2"},
+				},
+			},
+			Expected: "+mod1 +mod2 ",
+			Err:      nil,
+		},
+		{
+			Name:     "with contents",
+			Template: DefaultTemplate,
+			Input: FileList{
+				Name:      "target",
+				Additions: []string{"add1", "add2"},
+				Deletions: []string{"del1", "del2"},
+				Changes: []FileList{
+					FileList{
+						Name: "mod1",
+					},
+					FileList{
+						Name:      "mod2",
+						Additions: []string{"a1"},
+						Deletions: []string{"d1"},
+					},
+				},
+			},
+			Expected: "-del1 -del2 +add1 +add2 +mod1 +mod2 --file mod2 -d1 +a1 --endfile ",
+			Err:      nil,
+		},
+	}
+	for _, tc := range testCases {
+		buf := bytes.NewBuffer([]byte{})
+		err := tc.Input.Format(buf, tc.Template)
+		android.AssertSame(t, tc.Name, tc.Err, err)
+		android.AssertSame(t, tc.Name, tc.Expected, buf.String())
+	}
+}
diff --git a/cmd/find_input_delta/find_input_delta_lib/fs.go b/cmd/find_input_delta/find_input_delta_lib/fs.go
new file mode 100644
index 0000000..4a83ed7
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_lib/fs.go
@@ -0,0 +1,46 @@
+// Copyright 2024 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 find_input_delta_lib
+
+import (
+	"io"
+	"io/fs"
+	"os"
+)
+
+// OsFs provides a minimal implementation so that we can use testing/fstest for
+// unit tests.
+var OsFs fileSystem = osFS{}
+
+type fileSystem interface {
+	Open(path string) (fs.File, error)
+	Stat(path string) (os.FileInfo, error)
+	ReadFile(path string) ([]byte, error)
+}
+
+type file interface {
+	io.Closer
+	io.Reader
+	io.ReaderAt
+	io.Seeker
+	Stat() (os.FileInfo, error)
+}
+
+// osFS implements fileSystem using the local disk.
+type osFS struct{}
+
+func (osFS) Open(path string) (fs.File, error)     { return os.Open(path) }
+func (osFS) Stat(path string) (os.FileInfo, error) { return os.Stat(path) }
+func (osFS) ReadFile(path string) ([]byte, error)  { return os.ReadFile(path) }
diff --git a/cmd/find_input_delta/find_input_delta_lib/internal_state.go b/cmd/find_input_delta/find_input_delta_lib/internal_state.go
new file mode 100644
index 0000000..b2ff8c7
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_lib/internal_state.go
@@ -0,0 +1,122 @@
+// Copyright 2024 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 find_input_delta_lib
+
+import (
+	"errors"
+	"fmt"
+	"io/fs"
+	"slices"
+
+	fid_proto "android/soong/cmd/find_input_delta/find_input_delta_proto_internal"
+	"github.com/google/blueprint/pathtools"
+	"google.golang.org/protobuf/proto"
+)
+
+// Load the internal state from a file.
+// If the file does not exist, an empty state is returned.
+func LoadState(filename string, fsys fs.ReadFileFS) (*fid_proto.PartialCompileInputs, error) {
+	var message = &fid_proto.PartialCompileInputs{}
+	data, err := fsys.ReadFile(filename)
+	if err != nil && !errors.Is(err, fs.ErrNotExist) {
+		return message, err
+	}
+	proto.Unmarshal(data, message)
+	return message, nil
+}
+
+type StatReadFileFS interface {
+	fs.StatFS
+	fs.ReadFileFS
+}
+
+// Create the internal state by examining the inputs.
+func CreateState(inputs []string, inspect_contents bool, fsys StatReadFileFS) (*fid_proto.PartialCompileInputs, error) {
+	ret := &fid_proto.PartialCompileInputs{}
+	slices.Sort(inputs)
+	for _, input := range inputs {
+		stat, err := fs.Stat(fsys, input)
+		if err != nil {
+			return ret, err
+		}
+		pci := &fid_proto.PartialCompileInput{
+			Name:      proto.String(input),
+			MtimeNsec: proto.Int64(stat.ModTime().UnixNano()),
+			// If we ever have an easy hash, assign it here.
+		}
+		if inspect_contents {
+			contents, err := InspectFileContents(input)
+			if err != nil {
+				return ret, err
+			}
+			if contents != nil {
+				pci.Contents = contents
+			}
+		}
+		ret.InputFiles = append(ret.InputFiles, pci)
+	}
+	return ret, nil
+}
+
+// Inspect the file and extract the state of the elements in the archive.
+// If this is not an archive of some sort, nil is returned.
+func InspectFileContents(name string) ([]*fid_proto.PartialCompileInput, error) {
+	// TODO: Actually inspect the contents.
+	fmt.Printf("inspecting contents for %s\n", name)
+	return nil, nil
+}
+
+func WriteState(s *fid_proto.PartialCompileInputs, path string) error {
+	data, err := proto.Marshal(s)
+	if err != nil {
+		return err
+	}
+	return pathtools.WriteFileIfChanged(path, data, 0644)
+}
+
+func CompareInternalState(prior, other *fid_proto.PartialCompileInputs, target string) *FileList {
+	return CompareInputFiles(prior.GetInputFiles(), other.GetInputFiles(), target)
+}
+
+func CompareInputFiles(prior, other []*fid_proto.PartialCompileInput, name string) *FileList {
+	fl := &FileList{
+		Name: name,
+	}
+	PriorMap := make(map[string]*fid_proto.PartialCompileInput, len(prior))
+	// We know that the lists are properly sorted, so we can simply compare them.
+	for _, v := range prior {
+		PriorMap[v.GetName()] = v
+	}
+	otherMap := make(map[string]*fid_proto.PartialCompileInput, len(other))
+	for _, v := range other {
+		name = v.GetName()
+		otherMap[name] = v
+		if _, ok := PriorMap[name]; !ok {
+			// Added file
+			fl.Additions = append(fl.Additions, name)
+		} else if !proto.Equal(PriorMap[name], v) {
+			// Changed file
+			fl.Changes = append(fl.Changes, *CompareInputFiles(PriorMap[name].GetContents(), v.GetContents(), name))
+		}
+	}
+	for _, v := range prior {
+		name := v.GetName()
+		if _, ok := otherMap[name]; !ok {
+			// Deleted file
+			fl.Deletions = append(fl.Deletions, name)
+		}
+	}
+	return fl
+}
diff --git a/cmd/find_input_delta/find_input_delta_lib/internal_state_test.go b/cmd/find_input_delta/find_input_delta_lib/internal_state_test.go
new file mode 100644
index 0000000..20b8efa
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_lib/internal_state_test.go
@@ -0,0 +1,232 @@
+// Copyright 2024 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 find_input_delta_lib
+
+import (
+	"errors"
+	"io/fs"
+	"testing"
+	"testing/fstest"
+	"time"
+
+	// For Assert*.
+	"android/soong/android"
+
+	fid_proto "android/soong/cmd/find_input_delta/find_input_delta_proto_internal"
+	"google.golang.org/protobuf/proto"
+)
+
+// Various state files
+
+func marshalProto(t *testing.T, message proto.Message) []byte {
+	data, err := proto.Marshal(message)
+	if err != nil {
+		t.Errorf("%v", err)
+	}
+	return data
+}
+
+func protoFile(name string, mtime_nsec int64, hash string, contents []*fid_proto.PartialCompileInput) (pci *fid_proto.PartialCompileInput) {
+	pci = &fid_proto.PartialCompileInput{
+		Name: proto.String(name),
+	}
+	if mtime_nsec != 0 {
+		pci.MtimeNsec = proto.Int64(mtime_nsec)
+	}
+	if len(hash) > 0 {
+		pci.Hash = proto.String(hash)
+	}
+	if contents != nil {
+		pci.Contents = contents
+	}
+	return
+}
+
+func TestLoadState(t *testing.T) {
+	testCases := []struct {
+		Name     string
+		Filename string
+		Mapfs    fs.ReadFileFS
+		Expected *fid_proto.PartialCompileInputs
+		Err      error
+	}{
+		{
+			Name:     "missing file",
+			Filename: "missing",
+			Mapfs:    fstest.MapFS{},
+			Expected: &fid_proto.PartialCompileInputs{},
+			Err:      nil,
+		},
+		{
+			Name:     "bad file",
+			Filename: ".",
+			Mapfs:    OsFs,
+			Expected: &fid_proto.PartialCompileInputs{},
+			Err:      errors.New("read failed"),
+		},
+		{
+			Name:     "file with mtime",
+			Filename: "state.old",
+			Mapfs: fstest.MapFS{
+				"state.old": &fstest.MapFile{
+					Data: marshalProto(t, &fid_proto.PartialCompileInputs{
+						InputFiles: []*fid_proto.PartialCompileInput{
+							protoFile("input1", 100, "", nil),
+						},
+					}),
+				},
+			},
+			Expected: &fid_proto.PartialCompileInputs{
+				InputFiles: []*fid_proto.PartialCompileInput{
+					protoFile("input1", 100, "", nil),
+				},
+			},
+			Err: nil,
+		},
+		{
+			Name:     "file with mtime and hash",
+			Filename: "state.old",
+			Mapfs: fstest.MapFS{
+				"state.old": &fstest.MapFile{
+					Data: marshalProto(t, &fid_proto.PartialCompileInputs{
+						InputFiles: []*fid_proto.PartialCompileInput{
+							protoFile("input1", 100, "crc:crc_value", nil),
+						},
+					}),
+				},
+			},
+			Expected: &fid_proto.PartialCompileInputs{
+				InputFiles: []*fid_proto.PartialCompileInput{
+					protoFile("input1", 100, "crc:crc_value", nil),
+				},
+			},
+			Err: nil,
+		},
+	}
+	for _, tc := range testCases {
+		actual, err := LoadState(tc.Filename, tc.Mapfs)
+		if tc.Err == nil {
+			android.AssertSame(t, tc.Name, tc.Err, err)
+		} else if err == nil {
+			t.Errorf("%s: expected error, did not get one", tc.Name)
+		}
+		if !proto.Equal(tc.Expected, actual) {
+			t.Errorf("%s: expected %v, actual %v", tc.Name, tc.Expected, actual)
+		}
+	}
+}
+
+func TestCreateState(t *testing.T) {
+	testCases := []struct {
+		Name     string
+		Inputs   []string
+		Inspect  bool
+		Mapfs    StatReadFileFS
+		Expected *fid_proto.PartialCompileInputs
+		Err      error
+	}{
+		{
+			Name:     "no inputs",
+			Inputs:   []string{},
+			Mapfs:    fstest.MapFS{},
+			Expected: &fid_proto.PartialCompileInputs{},
+			Err:      nil,
+		},
+		{
+			Name:   "files found",
+			Inputs: []string{"baz", "foo", "bar"},
+			Mapfs: fstest.MapFS{
+				"foo": &fstest.MapFile{ModTime: time.Unix(0, 100).UTC()},
+				"baz": &fstest.MapFile{ModTime: time.Unix(0, 300).UTC()},
+				"bar": &fstest.MapFile{ModTime: time.Unix(0, 200).UTC()},
+			},
+			Expected: &fid_proto.PartialCompileInputs{
+				InputFiles: []*fid_proto.PartialCompileInput{
+					// Files are always sorted.
+					protoFile("bar", 200, "", nil),
+					protoFile("baz", 300, "", nil),
+					protoFile("foo", 100, "", nil),
+				},
+			},
+			Err: nil,
+		},
+	}
+	for _, tc := range testCases {
+		actual, err := CreateState(tc.Inputs, tc.Inspect, tc.Mapfs)
+		if tc.Err == nil {
+			android.AssertSame(t, tc.Name, tc.Err, err)
+		} else if err == nil {
+			t.Errorf("%s: expected error, did not get one", tc.Name)
+		}
+		if !proto.Equal(tc.Expected, actual) {
+			t.Errorf("%s: expected %v, actual %v", tc.Name, tc.Expected, actual)
+		}
+	}
+}
+
+func TestCompareInternalState(t *testing.T) {
+	testCases := []struct {
+		Name     string
+		Target   string
+		Prior    *fid_proto.PartialCompileInputs
+		New      *fid_proto.PartialCompileInputs
+		Expected *FileList
+	}{
+		{
+			Name:   "prior is empty",
+			Target: "foo",
+			Prior:  &fid_proto.PartialCompileInputs{},
+			New: &fid_proto.PartialCompileInputs{
+				InputFiles: []*fid_proto.PartialCompileInput{
+					protoFile("file1", 100, "", nil),
+				},
+			},
+			Expected: &FileList{
+				Name:      "foo",
+				Additions: []string{"file1"},
+			},
+		},
+		{
+			Name:   "one of each",
+			Target: "foo",
+			Prior: &fid_proto.PartialCompileInputs{
+				InputFiles: []*fid_proto.PartialCompileInput{
+					protoFile("file0", 100, "", nil),
+					protoFile("file1", 100, "", nil),
+					protoFile("file2", 200, "", nil),
+				},
+			},
+			New: &fid_proto.PartialCompileInputs{
+				InputFiles: []*fid_proto.PartialCompileInput{
+					protoFile("file0", 100, "", nil),
+					protoFile("file1", 200, "", nil),
+					protoFile("file3", 300, "", nil),
+				},
+			},
+			Expected: &FileList{
+				Name:      "foo",
+				Additions: []string{"file3"},
+				Changes:   []FileList{FileList{Name: "file1"}},
+				Deletions: []string{"file2"},
+			},
+		},
+	}
+	for _, tc := range testCases {
+		actual := CompareInternalState(tc.Prior, tc.New, tc.Target)
+		if !tc.Expected.Equal(actual) {
+			t.Errorf("%s: expected %q, actual %q", tc.Name, tc.Expected, actual)
+		}
+	}
+}
diff --git a/cmd/find_input_delta/find_input_delta_proto/Android.bp b/cmd/find_input_delta/find_input_delta_proto/Android.bp
new file mode 100644
index 0000000..1a05b9e
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_proto/Android.bp
@@ -0,0 +1,29 @@
+// Copyright 2024 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+bootstrap_go_package {
+    name: "soong-cmd-find_input_delta-proto",
+    pkgPath: "android/soong/cmd/find_input_delta/find_input_delta_proto",
+    deps: [
+        "golang-protobuf-reflect-protoreflect",
+        "golang-protobuf-runtime-protoimpl",
+    ],
+    srcs: [
+        "file_list.pb.go",
+    ],
+}
diff --git a/cmd/find_input_delta/find_input_delta_proto/file_list.pb.go b/cmd/find_input_delta/find_input_delta_proto/file_list.pb.go
new file mode 100644
index 0000000..648ef22
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_proto/file_list.pb.go
@@ -0,0 +1,198 @@
+//
+// Copyright (C) 2024 The Android Open-Source Project
+//
+// 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.
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.33.0
+// 	protoc        v3.21.12
+// source: file_list.proto
+
+package proto
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type FileList struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The name of the file.
+	// In the outermost message, this is the name of the Ninja target.
+	// When used in `changes`, this is the name of the changed file.
+	Name *string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
+	// The added files.
+	Additions []string `protobuf:"bytes,2,rep,name=additions" json:"additions,omitempty"`
+	// The deleted files.
+	Deletions []string `protobuf:"bytes,3,rep,name=deletions" json:"deletions,omitempty"`
+	// The changed files.
+	Changes []*FileList `protobuf:"bytes,4,rep,name=changes" json:"changes,omitempty"`
+}
+
+func (x *FileList) Reset() {
+	*x = FileList{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_file_list_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *FileList) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FileList) ProtoMessage() {}
+
+func (x *FileList) ProtoReflect() protoreflect.Message {
+	mi := &file_file_list_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use FileList.ProtoReflect.Descriptor instead.
+func (*FileList) Descriptor() ([]byte, []int) {
+	return file_file_list_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *FileList) GetName() string {
+	if x != nil && x.Name != nil {
+		return *x.Name
+	}
+	return ""
+}
+
+func (x *FileList) GetAdditions() []string {
+	if x != nil {
+		return x.Additions
+	}
+	return nil
+}
+
+func (x *FileList) GetDeletions() []string {
+	if x != nil {
+		return x.Deletions
+	}
+	return nil
+}
+
+func (x *FileList) GetChanges() []*FileList {
+	if x != nil {
+		return x.Changes
+	}
+	return nil
+}
+
+var File_file_list_proto protoreflect.FileDescriptor
+
+var file_file_list_proto_rawDesc = []byte{
+	0x0a, 0x0f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x12, 0x1e, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2e, 0x66, 0x69, 0x6e, 0x64, 0x5f,
+	0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x5f, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x22, 0x9e, 0x01, 0x0a, 0x08, 0x46, 0x69, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x12,
+	0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
+	0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18,
+	0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73,
+	0x12, 0x1c, 0x0a, 0x09, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20,
+	0x03, 0x28, 0x09, 0x52, 0x09, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x42,
+	0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32,
+	0x28, 0x2e, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2e, 0x66, 0x69, 0x6e, 0x64, 0x5f, 0x69,
+	0x6e, 0x70, 0x75, 0x74, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x2e, 0x46, 0x69, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x67,
+	0x65, 0x73, 0x42, 0x26, 0x5a, 0x24, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2f, 0x73, 0x6f,
+	0x6f, 0x6e, 0x67, 0x2f, 0x66, 0x69, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x64,
+	0x65, 0x6c, 0x74, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+}
+
+var (
+	file_file_list_proto_rawDescOnce sync.Once
+	file_file_list_proto_rawDescData = file_file_list_proto_rawDesc
+)
+
+func file_file_list_proto_rawDescGZIP() []byte {
+	file_file_list_proto_rawDescOnce.Do(func() {
+		file_file_list_proto_rawDescData = protoimpl.X.CompressGZIP(file_file_list_proto_rawDescData)
+	})
+	return file_file_list_proto_rawDescData
+}
+
+var file_file_list_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_file_list_proto_goTypes = []interface{}{
+	(*FileList)(nil), // 0: android.find_input_delta_proto.FileList
+}
+var file_file_list_proto_depIdxs = []int32{
+	0, // 0: android.find_input_delta_proto.FileList.changes:type_name -> android.find_input_delta_proto.FileList
+	1, // [1:1] is the sub-list for method output_type
+	1, // [1:1] is the sub-list for method input_type
+	1, // [1:1] is the sub-list for extension type_name
+	1, // [1:1] is the sub-list for extension extendee
+	0, // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_file_list_proto_init() }
+func file_file_list_proto_init() {
+	if File_file_list_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_file_list_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*FileList); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_file_list_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   1,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_file_list_proto_goTypes,
+		DependencyIndexes: file_file_list_proto_depIdxs,
+		MessageInfos:      file_file_list_proto_msgTypes,
+	}.Build()
+	File_file_list_proto = out.File
+	file_file_list_proto_rawDesc = nil
+	file_file_list_proto_goTypes = nil
+	file_file_list_proto_depIdxs = nil
+}
diff --git a/cmd/find_input_delta/find_input_delta_proto/file_list.proto b/cmd/find_input_delta/find_input_delta_proto/file_list.proto
new file mode 100644
index 0000000..d7faca9
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_proto/file_list.proto
@@ -0,0 +1,34 @@
+//
+// Copyright (C) 2024 The Android Open-Source Project
+//
+// 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.
+
+syntax = "proto2";
+package android.find_input_delta_proto;
+option go_package = "android/soong/find_input_delta/proto";
+
+message FileList {
+  // The name of the file.
+  // In the outermost message, this is the name of the Ninja target.
+  // When used in `changes`, this is the name of the changed file.
+  optional string name = 1;
+
+  // The added files.
+  repeated string additions = 2;
+
+  // The deleted files.
+  repeated string deletions = 3;
+
+  // The changed files.
+  repeated FileList changes = 4;
+}
diff --git a/cmd/find_input_delta/find_input_delta_proto/regen.sh b/cmd/find_input_delta/find_input_delta_proto/regen.sh
new file mode 100644
index 0000000..d773659
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_proto/regen.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+aprotoc --go_out=paths=source_relative:. file_list.proto
diff --git a/cmd/find_input_delta/find_input_delta_proto_internal/Android.bp b/cmd/find_input_delta/find_input_delta_proto_internal/Android.bp
new file mode 100644
index 0000000..00ba9ff
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_proto_internal/Android.bp
@@ -0,0 +1,29 @@
+// Copyright 2024 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+bootstrap_go_package {
+    name: "soong-cmd-find_input_delta-proto_internal",
+    pkgPath: "android/soong/cmd/find_input_delta/find_input_delta_proto_internal",
+    deps: [
+        "golang-protobuf-reflect-protoreflect",
+        "golang-protobuf-runtime-protoimpl",
+    ],
+    srcs: [
+        "internal_state.pb.go",
+    ],
+}
diff --git a/cmd/find_input_delta/find_input_delta_proto_internal/internal_state.pb.go b/cmd/find_input_delta/find_input_delta_proto_internal/internal_state.pb.go
new file mode 100644
index 0000000..2229a32
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_proto_internal/internal_state.pb.go
@@ -0,0 +1,268 @@
+//
+// Copyright (C) 2024 The Android Open-Source Project
+//
+// 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.
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.33.0
+// 	protoc        v3.21.12
+// source: internal_state.proto
+
+package proto
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// The state of all inputs.
+type PartialCompileInputs struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The status of each file.
+	InputFiles []*PartialCompileInput `protobuf:"bytes,1,rep,name=input_files,json=inputFiles" json:"input_files,omitempty"`
+}
+
+func (x *PartialCompileInputs) Reset() {
+	*x = PartialCompileInputs{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_internal_state_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *PartialCompileInputs) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PartialCompileInputs) ProtoMessage() {}
+
+func (x *PartialCompileInputs) ProtoReflect() protoreflect.Message {
+	mi := &file_internal_state_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use PartialCompileInputs.ProtoReflect.Descriptor instead.
+func (*PartialCompileInputs) Descriptor() ([]byte, []int) {
+	return file_internal_state_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *PartialCompileInputs) GetInputFiles() []*PartialCompileInput {
+	if x != nil {
+		return x.InputFiles
+	}
+	return nil
+}
+
+// The state of one input.
+type PartialCompileInput struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The name of the file.
+	Name *string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
+	// The timestamp of the file in (Unix) nanoseconds.
+	MtimeNsec *int64 `protobuf:"varint,2,opt,name=mtime_nsec,json=mtimeNsec" json:"mtime_nsec,omitempty"`
+	// The hash of the file, in the form ‘{HASHNAME}:{VALUE}’
+	Hash *string `protobuf:"bytes,3,opt,name=hash" json:"hash,omitempty"`
+	// Contents of the file, if the file was inspected (such as jar files, etc).
+	Contents []*PartialCompileInput `protobuf:"bytes,4,rep,name=contents" json:"contents,omitempty"`
+}
+
+func (x *PartialCompileInput) Reset() {
+	*x = PartialCompileInput{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_internal_state_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *PartialCompileInput) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PartialCompileInput) ProtoMessage() {}
+
+func (x *PartialCompileInput) ProtoReflect() protoreflect.Message {
+	mi := &file_internal_state_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use PartialCompileInput.ProtoReflect.Descriptor instead.
+func (*PartialCompileInput) Descriptor() ([]byte, []int) {
+	return file_internal_state_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *PartialCompileInput) GetName() string {
+	if x != nil && x.Name != nil {
+		return *x.Name
+	}
+	return ""
+}
+
+func (x *PartialCompileInput) GetMtimeNsec() int64 {
+	if x != nil && x.MtimeNsec != nil {
+		return *x.MtimeNsec
+	}
+	return 0
+}
+
+func (x *PartialCompileInput) GetHash() string {
+	if x != nil && x.Hash != nil {
+		return *x.Hash
+	}
+	return ""
+}
+
+func (x *PartialCompileInput) GetContents() []*PartialCompileInput {
+	if x != nil {
+		return x.Contents
+	}
+	return nil
+}
+
+var File_internal_state_proto protoreflect.FileDescriptor
+
+var file_internal_state_proto_rawDesc = []byte{
+	0x0a, 0x14, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65,
+	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1e, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2e,
+	0x66, 0x69, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61,
+	0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6c, 0x0a, 0x14, 0x50, 0x61, 0x72, 0x74, 0x69, 0x61,
+	0x6c, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x73, 0x12, 0x54,
+	0x0a, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20,
+	0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2e, 0x66, 0x69,
+	0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x5f, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x43, 0x6f, 0x6d, 0x70,
+	0x69, 0x6c, 0x65, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x52, 0x0a, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x46,
+	0x69, 0x6c, 0x65, 0x73, 0x22, 0xad, 0x01, 0x0a, 0x13, 0x50, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c,
+	0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x12, 0x0a, 0x04,
+	0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
+	0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6e, 0x73, 0x65, 0x63, 0x18, 0x02,
+	0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x4e, 0x73, 0x65, 0x63, 0x12,
+	0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68,
+	0x61, 0x73, 0x68, 0x12, 0x4f, 0x0a, 0x08, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x18,
+	0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2e,
+	0x66, 0x69, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61,
+	0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x43, 0x6f,
+	0x6d, 0x70, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x52, 0x08, 0x63, 0x6f, 0x6e, 0x74,
+	0x65, 0x6e, 0x74, 0x73, 0x42, 0x26, 0x5a, 0x24, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2f,
+	0x73, 0x6f, 0x6f, 0x6e, 0x67, 0x2f, 0x66, 0x69, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74,
+	0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+}
+
+var (
+	file_internal_state_proto_rawDescOnce sync.Once
+	file_internal_state_proto_rawDescData = file_internal_state_proto_rawDesc
+)
+
+func file_internal_state_proto_rawDescGZIP() []byte {
+	file_internal_state_proto_rawDescOnce.Do(func() {
+		file_internal_state_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_state_proto_rawDescData)
+	})
+	return file_internal_state_proto_rawDescData
+}
+
+var file_internal_state_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_internal_state_proto_goTypes = []interface{}{
+	(*PartialCompileInputs)(nil), // 0: android.find_input_delta_proto.PartialCompileInputs
+	(*PartialCompileInput)(nil),  // 1: android.find_input_delta_proto.PartialCompileInput
+}
+var file_internal_state_proto_depIdxs = []int32{
+	1, // 0: android.find_input_delta_proto.PartialCompileInputs.input_files:type_name -> android.find_input_delta_proto.PartialCompileInput
+	1, // 1: android.find_input_delta_proto.PartialCompileInput.contents:type_name -> android.find_input_delta_proto.PartialCompileInput
+	2, // [2:2] is the sub-list for method output_type
+	2, // [2:2] is the sub-list for method input_type
+	2, // [2:2] is the sub-list for extension type_name
+	2, // [2:2] is the sub-list for extension extendee
+	0, // [0:2] is the sub-list for field type_name
+}
+
+func init() { file_internal_state_proto_init() }
+func file_internal_state_proto_init() {
+	if File_internal_state_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_internal_state_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*PartialCompileInputs); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_internal_state_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*PartialCompileInput); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_internal_state_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   2,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_internal_state_proto_goTypes,
+		DependencyIndexes: file_internal_state_proto_depIdxs,
+		MessageInfos:      file_internal_state_proto_msgTypes,
+	}.Build()
+	File_internal_state_proto = out.File
+	file_internal_state_proto_rawDesc = nil
+	file_internal_state_proto_goTypes = nil
+	file_internal_state_proto_depIdxs = nil
+}
diff --git a/cmd/find_input_delta/find_input_delta_proto_internal/internal_state.proto b/cmd/find_input_delta/find_input_delta_proto_internal/internal_state.proto
new file mode 100644
index 0000000..113fc64
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_proto_internal/internal_state.proto
@@ -0,0 +1,39 @@
+//
+// Copyright (C) 2024 The Android Open-Source Project
+//
+// 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.
+
+syntax = "proto2";
+package android.find_input_delta_proto;
+option go_package = "android/soong/find_input_delta/proto";
+
+// The state of all inputs.
+message PartialCompileInputs {
+  // The status of each file.
+  repeated PartialCompileInput input_files = 1;
+}
+
+// The state of one input.
+message PartialCompileInput {
+  // The name of the file.
+  optional string name = 1;
+
+  // The timestamp of the file in (Unix) nanoseconds.
+  optional int64 mtime_nsec = 2;
+
+  // The hash of the file, in the form ‘{HASHNAME}:{VALUE}’
+  optional string hash = 3;
+
+  // Contents of the file, if the file was inspected (such as jar files, etc).
+  repeated PartialCompileInput contents = 4;
+}
diff --git a/cmd/find_input_delta/find_input_delta_proto_internal/regen.sh b/cmd/find_input_delta/find_input_delta_proto_internal/regen.sh
new file mode 100644
index 0000000..cbaf7d0
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_proto_internal/regen.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+aprotoc --go_out=paths=source_relative:.  internal_state.proto
diff --git a/cmd/symbols_map/symbols_map_proto/symbols_map.proto b/cmd/symbols_map/symbols_map_proto/symbols_map.proto
index 693fe3e..a76d171 100644
--- a/cmd/symbols_map/symbols_map_proto/symbols_map.proto
+++ b/cmd/symbols_map/symbols_map_proto/symbols_map.proto
@@ -37,6 +37,21 @@
 
   // type is the type of the mapping, either ELF or R8.
   optional Type type = 3;
+
+  // LocationType is the place where to look for the file with the given
+  // identifier.
+  Enum LocationType {
+    // ZIP denotes the file with the given identifier is in the distribuited
+    // symbols.zip or proguard_dict.zip files, or the local disc.
+    ZIP = 0;
+    // AB denotes the file with the given identifier is in the AB artifacts but
+    // not in a symbols.zip or proguard_dict.zip.
+    AB = 1;
+  }
+
+  // location_type is the Location Type that dictates where to search for the
+  // file with the given identifier. Defaults to ZIP if not present.
+  optional LocationType location_type = 4;
 }
 
 message Mappings {
diff --git a/compliance/notice.go b/compliance/notice.go
index 4fc83ab..edd1b34 100644
--- a/compliance/notice.go
+++ b/compliance/notice.go
@@ -18,6 +18,7 @@
 	"path/filepath"
 
 	"android/soong/android"
+
 	"github.com/google/blueprint"
 )
 
@@ -62,8 +63,7 @@
 
 	props noticeXmlProperties
 
-	outputFile  android.OutputPath
-	installPath android.InstallPath
+	outputFile android.OutputPath
 }
 
 type noticeXmlProperties struct {
@@ -86,10 +86,8 @@
 
 	nx.outputFile = output.OutputPath
 
-	if android.Bool(ctx.Config().ProductVariables().UseSoongSystemImage) {
-		nx.installPath = android.PathForModuleInPartitionInstall(ctx, nx.props.Partition_name, "etc")
-		ctx.InstallFile(nx.installPath, "NOTICE.xml.gz", nx.outputFile)
-	}
+	installPath := android.PathForModuleInPartitionInstall(ctx, nx.props.Partition_name, "etc")
+	ctx.PackageFile(installPath, "NOTICE.xml.gz", nx.outputFile)
 }
 
 func (nx *NoticeXmlModule) AndroidMkEntries() []android.AndroidMkEntries {
diff --git a/docs/selects.md b/docs/selects.md
new file mode 100644
index 0000000..4f436ac
--- /dev/null
+++ b/docs/selects.md
@@ -0,0 +1,170 @@
+# Select statements
+
+## Introduction
+
+Soong currently has the arch, target, product_variables, and soong config variable properties that all support changing the values of soong properties based on some condition. These are confusing for users, and particularly the soong config variable properties require a lot of boilerplate that is annoying to write.
+
+In addition, these properties are all implemented using reflection on property structs, and can combine in ways that the original authors did not expect. For example, soong config variables can be combined with arch/target by saying that they operate on "arch.arm.enabled" instead of just "enabled". But product variables do not have the same abilities.
+
+The goal here is to combine all these different configuration mechanisms into one, to reduce complexity and boilerplate both in Android.bp files and in soong code.
+
+## Usage
+
+The soong select statements take their name and inspiration from [bazel select statements](https://bazel.build/docs/configurable-attributes).
+
+### Syntax
+
+#### Basic
+
+The basic syntax for select statements looks like:
+
+```
+my_module_type {
+  name: "my_module",
+  some_string_property: select(arch(), {
+    "arm": "foo",
+    "x86": "bar",
+    default: "baz",
+  }),
+}
+```
+
+That is, `select(` followed by a variable function, then a map of values of the variable to values to set the property to.
+
+Arguments can be passed to the "functions" that look up axes:
+
+```
+select(soong_config_variable("my_namespace", "my_variable"), {
+  "value1": "foo",
+  default: "bar",
+})
+```
+
+
+The list of functions that can currently be selected on:
+ - `arch()`
+ - `os()`
+ - `soong_config_variable(namespace, variable)`
+ - `release_flag(flag)`
+
+The functions are [defined here](https://cs.android.com/android/platform/superproject/main/+/main:build/soong/android/module.go;l=2144;drc=3f01580c04bfe37c920e247015cce93cff2451c0), and it should be easy to add more.
+
+#### Multivariable
+
+To handle multivariable selects, multiple axes can be specified within parenthesis, to look like tuple destructuring. All of the variables being selected must match the corresponding value from the branch in order for the branch to be chosen.
+
+```
+select((arch(), os()), {
+  ("arm", "linux"): "foo",
+  (default, "windows"): "bar",
+  (default, default): "baz",
+})
+```
+
+#### Unset
+
+You can have unset branches of selects using the "unset" keyword, which will act as if the property was not assigned to. This is only really useful if you’re using defaults modules.
+
+```
+cc_binary {
+  name: "my_binary",
+  enabled: select(os(), {
+    "darwin": false,
+    default: unset,
+  }),
+}
+```
+
+#### Appending
+
+You can append select statements to both scalar values and other select statements:
+
+```
+my_module_type {
+  name: "my_module",
+  // string_property will be set to something like penguin-four, apple-two, etc.
+  string_property: select(os(), {
+    "linux_glibc": "penguin",
+    "darwin": "apple",
+    default: "unknown",
+  }) + "-" + select(soong_config_variable("ANDROID", "favorite_vehicle"), {
+    "car": "four",
+    "tricycle": "three",
+    "bike": "two",
+     default: "unknown",
+  })
+}
+```
+
+
+You can also append a select with a value with another select that may not have a value, because some of its branches are "unset". If an unset branch was selected it will not append anything to the other select.
+
+#### Binding the selected value to a Blueprint variable and the "any" keyword
+
+In case you want to allow a selected value to have an unbounded number of possible values, you can bind its value to a blueprint variable and use it within the expression for that select branch.
+
+```
+my_module_type {
+  name: "my_module",
+  my_string_property: select(soong_config_variable("my_namespace", "my_variable"), {
+    "some_value": "baz",
+    any @ my_var: "foo" + my_var,
+    default: "bar",
+  }),
+}
+```
+
+The syntax is `any @ my_variable_name: <expression using my_variable_name>`. `any` is currently the only pattern that can be bound to a variable, but we may add more in the future. `any` is equivalent to `default` except it will not match undefined select conditions.
+
+#### Errors
+
+If a select statement does not have a "default" branch, and none of the other branches match the variable being selected on, it’s a compile-time error. This may be useful for enforcing a variable is 1 of only a few values.
+
+```
+# in product config:
+$(call soong_config_set,ANDROID,my_variable,foo)
+
+// in an Android.bp:
+my_module_type {
+  name: "my_module",
+  // Will error out with: soong_config_variable("ANDROID", "my_variable") had value "foo", which was not handled by the select
+  enabled: select(soong_config_variable("ANDROID", "my_variable"), {
+    "bar": true,
+    "baz": false,
+  }),
+}
+```
+
+### Changes to property structs to support selects
+
+Currently, the way configurable properties work is that there is secretly another property struct that has the `target`, `arch`, etc. properties, and then when the arch mutator (or other relevant mutator) runs, it copies the values of these properties onto the regular property structs. There’s nothing stopping you from accessing your properties from a mutator that runs before the one that updates the properties based on configurable values. This is a potential source of bugs, and we want to make sure that select statements don’t have the same pitfall. For that reason, you have to read property’s values through a getter which can do this check. This requires changing the code on a property-by-property basis to support selects.
+
+To make a property support selects, it must be of type [proptools.Configurable[T]](https://cs.android.com/android/platform/superproject/main/+/main:build/blueprint/proptools/configurable.go;l=341;drc=a52b058cccd2caa778d0f97077adcd4ef7ffb68a). T is the old type of the property. Currently we support bool, string, and []string. Configurable has a `Get(evaluator)` method to get the value of the property. The evaluator can be a ModuleContext, or if you’re in a situation where you only have a very limited context and a module, (such as in a singleton) you can use [ModuleBase.ConfigurableEvaluator](https://cs.android.com/android/platform/superproject/main/+/main:build/soong/android/module.go;l=2133;drc=e19f741052cce097da940d9083d3f29e668de5cb).
+
+`proptools.Configurable[T]` will handle unset properties for you, so you don’t need to make it a pointer type. However, there is a not-widely-known feature of property structs, where normally, properties are appended when squashing defaults. But if the property was a pointer property, later defaults replace earlier values instead of appending. With selects, to maintain this behavior, add the `android:"replace_instead_of_append"` struct tag. The "append" behavior for boolean values is to boolean OR them together, which is rarely what you want, so most boolean properties are pointers today.
+
+Old:
+```
+type commonProperties struct {
+  Enabled *bool `android:"arch_variant"`
+}
+
+func (m *ModuleBase) Enabled() *bool {
+  return m.commonProperties.Enabled
+}
+```
+
+New:
+```
+type commonProperties struct {
+  Enabled proptools.Configurable[bool] `android:"arch_variant,replace_instead_of_append"`
+}
+
+func (m *ModuleBase) Enabled(ctx ConfigAndErrorContext) *bool {
+  return m.commonProperties.Enabled.Get(m.ConfigurableEvaluator(ctx))
+}
+```
+
+The `android:"arch_variant"` tag is kept to support the old `target:` and `arch:` properties with this property, but if all their usages in bp files were replaced by selects, then that tag could be removed.
+
+The enabled property underwent this migration in https://r.android.com/3066188
diff --git a/etc/adb_keys.go b/etc/adb_keys.go
index a2df41c..73bc347 100644
--- a/etc/adb_keys.go
+++ b/etc/adb_keys.go
@@ -36,6 +36,11 @@
 
 func (m *AdbKeysModule) GenerateAndroidBuildActions(ctx android.ModuleContext) {
 	productVariables := ctx.Config().ProductVariables()
+
+	if !m.ProductSpecific() {
+		ctx.ModuleErrorf("adb_keys module type must set product_specific to true")
+	}
+
 	if !(android.Bool(productVariables.Debuggable) && len(android.String(productVariables.AdbKeys)) > 0) {
 		m.SkipInstall()
 		return
@@ -48,7 +53,7 @@
 		Output: m.outputPath,
 		Input:  input.Path(),
 	})
-	m.installPath = android.PathForModuleInPartitionInstall(ctx, ctx.DeviceConfig().ProductPath(), "etc/security")
+	m.installPath = android.PathForModuleInstall(ctx, "etc/security")
 	ctx.InstallFile(m.installPath, "adb_keys", m.outputPath)
 }
 
diff --git a/etc/prebuilt_etc.go b/etc/prebuilt_etc.go
index 743bfd4..47b391c 100644
--- a/etc/prebuilt_etc.go
+++ b/etc/prebuilt_etc.go
@@ -59,16 +59,28 @@
 	ctx.RegisterModuleType("prebuilt_usr_keylayout", PrebuiltUserKeyLayoutFactory)
 	ctx.RegisterModuleType("prebuilt_usr_keychars", PrebuiltUserKeyCharsFactory)
 	ctx.RegisterModuleType("prebuilt_usr_idc", PrebuiltUserIdcFactory)
+	ctx.RegisterModuleType("prebuilt_usr_srec", PrebuiltUserSrecFactory)
 	ctx.RegisterModuleType("prebuilt_font", PrebuiltFontFactory)
 	ctx.RegisterModuleType("prebuilt_overlay", PrebuiltOverlayFactory)
 	ctx.RegisterModuleType("prebuilt_firmware", PrebuiltFirmwareFactory)
 	ctx.RegisterModuleType("prebuilt_dsp", PrebuiltDSPFactory)
 	ctx.RegisterModuleType("prebuilt_rfsa", PrebuiltRFSAFactory)
 	ctx.RegisterModuleType("prebuilt_renderscript_bitcode", PrebuiltRenderScriptBitcodeFactory)
-	ctx.RegisterModuleType("prebuilt_media_audio", PrebuiltMediaAudioFactory)
+	ctx.RegisterModuleType("prebuilt_media", PrebuiltMediaFactory)
 	ctx.RegisterModuleType("prebuilt_voicepack", PrebuiltVoicepackFactory)
 	ctx.RegisterModuleType("prebuilt_bin", PrebuiltBinaryFactory)
 	ctx.RegisterModuleType("prebuilt_wallpaper", PrebuiltWallpaperFactory)
+	ctx.RegisterModuleType("prebuilt_priv_app", PrebuiltPrivAppFactory)
+	ctx.RegisterModuleType("prebuilt_rfs", PrebuiltRfsFactory)
+	ctx.RegisterModuleType("prebuilt_framework", PrebuiltFrameworkFactory)
+	ctx.RegisterModuleType("prebuilt_res", PrebuiltResFactory)
+	ctx.RegisterModuleType("prebuilt_wlc_upt", PrebuiltWlcUptFactory)
+	ctx.RegisterModuleType("prebuilt_odm", PrebuiltOdmFactory)
+	ctx.RegisterModuleType("prebuilt_vendor_dlkm", PrebuiltVendorDlkmFactory)
+	ctx.RegisterModuleType("prebuilt_bt_firmware", PrebuiltBtFirmwareFactory)
+	ctx.RegisterModuleType("prebuilt_tvservice", PrebuiltTvServiceFactory)
+	ctx.RegisterModuleType("prebuilt_optee", PrebuiltOpteeFactory)
+	ctx.RegisterModuleType("prebuilt_tvconfig", PrebuiltTvConfigFactory)
 
 	ctx.RegisterModuleType("prebuilt_defaults", defaultsFactory)
 
@@ -125,6 +137,9 @@
 
 	// Install symlinks to the installed file.
 	Symlinks []string `android:"arch_variant"`
+
+	// Install to partition oem when set to true.
+	Oem_specific *bool `android:"arch_variant"`
 }
 
 type prebuiltSubdirProperties struct {
@@ -362,6 +377,10 @@
 		ctx.PropertyErrorf("sub_dir", "relative_install_path is set. Cannot set sub_dir")
 	}
 	baseInstallDirPath := android.PathForModuleInstall(ctx, p.installBaseDir(ctx), p.SubDir())
+	// TODO(b/377304441)
+	if android.Bool(p.properties.Oem_specific) {
+		baseInstallDirPath = android.PathForModuleInPartitionInstall(ctx, ctx.DeviceConfig().OemPath(), p.installBaseDir(ctx), p.SubDir())
+	}
 
 	filename := proptools.String(p.properties.Filename)
 	filenameFromSrc := proptools.Bool(p.properties.Filename_from_src)
@@ -720,6 +739,17 @@
 	return module
 }
 
+// prebuilt_usr_srec is for a prebuilt artifact that is installed in
+// <partition>/usr/srec/<sub_dir> directory.
+func PrebuiltUserSrecFactory() android.Module {
+	module := &PrebuiltEtc{}
+	InitPrebuiltEtcModule(module, "usr/srec")
+	// This module is device-only
+	android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibFirst)
+	android.InitDefaultableModule(module)
+	return module
+}
+
 // prebuilt_font installs a font in <partition>/fonts directory.
 func PrebuiltFontFactory() android.Module {
 	module := &PrebuiltEtc{}
@@ -793,10 +823,10 @@
 	return module
 }
 
-// prebuilt_media_audio installs audio files in <partition>/media/audio directory.
-func PrebuiltMediaAudioFactory() android.Module {
+// prebuilt_media installs media files in <partition>/media directory.
+func PrebuiltMediaFactory() android.Module {
 	module := &PrebuiltEtc{}
-	InitPrebuiltEtcModule(module, "media/audio")
+	InitPrebuiltEtcModule(module, "media")
 	// This module is device-only
 	android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibCommon)
 	android.InitDefaultableModule(module)
@@ -832,3 +862,113 @@
 	android.InitDefaultableModule(module)
 	return module
 }
+
+// prebuilt_priv_app installs files in <partition>/priv-app directory.
+func PrebuiltPrivAppFactory() android.Module {
+	module := &PrebuiltEtc{}
+	InitPrebuiltEtcModule(module, "priv-app")
+	// This module is device-only
+	android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibCommon)
+	android.InitDefaultableModule(module)
+	return module
+}
+
+// prebuilt_rfs installs files in <partition>/rfs directory.
+func PrebuiltRfsFactory() android.Module {
+	module := &PrebuiltEtc{}
+	InitPrebuiltEtcModule(module, "rfs")
+	// This module is device-only
+	android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibCommon)
+	android.InitDefaultableModule(module)
+	return module
+}
+
+// prebuilt_framework installs files in <partition>/framework directory.
+func PrebuiltFrameworkFactory() android.Module {
+	module := &PrebuiltEtc{}
+	InitPrebuiltEtcModule(module, "framework")
+	// This module is device-only
+	android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibCommon)
+	android.InitDefaultableModule(module)
+	return module
+}
+
+// prebuilt_res installs files in <partition>/res directory.
+func PrebuiltResFactory() android.Module {
+	module := &PrebuiltEtc{}
+	InitPrebuiltEtcModule(module, "res")
+	// This module is device-only
+	android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibCommon)
+	android.InitDefaultableModule(module)
+	return module
+}
+
+// prebuilt_wlc_upt installs files in <partition>/wlc_upt directory.
+func PrebuiltWlcUptFactory() android.Module {
+	module := &PrebuiltEtc{}
+	InitPrebuiltEtcModule(module, "wlc_upt")
+	// This module is device-only
+	android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibCommon)
+	android.InitDefaultableModule(module)
+	return module
+}
+
+// prebuilt_odm installs files in <partition>/odm directory.
+func PrebuiltOdmFactory() android.Module {
+	module := &PrebuiltEtc{}
+	InitPrebuiltEtcModule(module, "odm")
+	// This module is device-only
+	android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibCommon)
+	android.InitDefaultableModule(module)
+	return module
+}
+
+// prebuilt_vendor_dlkm installs files in <partition>/vendor_dlkm directory.
+func PrebuiltVendorDlkmFactory() android.Module {
+	module := &PrebuiltEtc{}
+	InitPrebuiltEtcModule(module, "vendor_dlkm")
+	// This module is device-only
+	android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibCommon)
+	android.InitDefaultableModule(module)
+	return module
+}
+
+// prebuilt_bt_firmware installs files in <partition>/bt_firmware directory.
+func PrebuiltBtFirmwareFactory() android.Module {
+	module := &PrebuiltEtc{}
+	InitPrebuiltEtcModule(module, "bt_firmware")
+	// This module is device-only
+	android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibCommon)
+	android.InitDefaultableModule(module)
+	return module
+}
+
+// prebuilt_tvservice installs files in <partition>/tvservice directory.
+func PrebuiltTvServiceFactory() android.Module {
+	module := &PrebuiltEtc{}
+	InitPrebuiltEtcModule(module, "tvservice")
+	// This module is device-only
+	android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibCommon)
+	android.InitDefaultableModule(module)
+	return module
+}
+
+// prebuilt_optee installs files in <partition>/optee directory.
+func PrebuiltOpteeFactory() android.Module {
+	module := &PrebuiltEtc{}
+	InitPrebuiltEtcModule(module, "optee")
+	// This module is device-only
+	android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibCommon)
+	android.InitDefaultableModule(module)
+	return module
+}
+
+// prebuilt_tvconfig installs files in <partition>/tvconfig directory.
+func PrebuiltTvConfigFactory() android.Module {
+	module := &PrebuiltEtc{}
+	InitPrebuiltEtcModule(module, "tvconfig")
+	// This module is device-only
+	android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibCommon)
+	android.InitDefaultableModule(module)
+	return module
+}
diff --git a/etc/prebuilt_etc_test.go b/etc/prebuilt_etc_test.go
index 4a0312f..0fd04d8 100644
--- a/etc/prebuilt_etc_test.go
+++ b/etc/prebuilt_etc_test.go
@@ -591,7 +591,7 @@
 
 func TestPrebuiltMediaAutoDirPath(t *testing.T) {
 	result := prepareForPrebuiltEtcTest.RunTestWithBp(t, `
-		prebuilt_media_audio {
+		prebuilt_media {
 			name: "foo",
 			src: "Alarm_Beep_01.ogg",
 			product_specific: true,
@@ -600,6 +600,6 @@
 	`)
 
 	p := result.Module("foo", "android_common").(*PrebuiltEtc)
-	expected := "out/soong/target/product/test_device/product/media/audio/alarms"
+	expected := "out/soong/target/product/test_device/product/media/alarms"
 	android.AssertPathRelativeToTopEquals(t, "install dir", expected, p.installDirPaths[0])
 }
diff --git a/filesystem/android_device.go b/filesystem/android_device.go
index 68e6053..ab1b96e 100644
--- a/filesystem/android_device.go
+++ b/filesystem/android_device.go
@@ -32,6 +32,12 @@
 	Product_partition_name *string
 	// Name of the Vendor partition filesystem module
 	Vendor_partition_name *string
+	// Name of the Odm partition filesystem module
+	Odm_partition_name *string
+	// The vbmeta partition and its "chained" partitions
+	Vbmeta_partitions []string
+	// Name of the Userdata partition filesystem module
+	Userdata_partition_name *string
 }
 
 type androidDevice struct {
@@ -44,7 +50,6 @@
 	module := &androidDevice{}
 	module.AddProperties(&module.partitionProps)
 	android.InitAndroidMultiTargetsArchModule(module, android.DeviceSupported, android.MultilibCommon)
-
 	return module
 }
 
@@ -66,6 +71,11 @@
 	addDependencyIfDefined(a.partitionProps.System_ext_partition_name)
 	addDependencyIfDefined(a.partitionProps.Product_partition_name)
 	addDependencyIfDefined(a.partitionProps.Vendor_partition_name)
+	addDependencyIfDefined(a.partitionProps.Odm_partition_name)
+	addDependencyIfDefined(a.partitionProps.Userdata_partition_name)
+	for _, vbmetaPartition := range a.partitionProps.Vbmeta_partitions {
+		ctx.AddDependency(ctx.Module(), filesystemDepTag, vbmetaPartition)
+	}
 }
 
 func (a *androidDevice) GenerateAndroidBuildActions(ctx android.ModuleContext) {
diff --git a/filesystem/filesystem.go b/filesystem/filesystem.go
index 97421c8..78e24e2 100644
--- a/filesystem/filesystem.go
+++ b/filesystem/filesystem.go
@@ -25,6 +25,7 @@
 
 	"android/soong/android"
 	"android/soong/cc"
+	"android/soong/linkerconfig"
 
 	"github.com/google/blueprint"
 	"github.com/google/blueprint/proptools"
@@ -51,12 +52,6 @@
 
 	properties FilesystemProperties
 
-	// Function that builds extra files under the root directory and returns the files
-	buildExtraFiles func(ctx android.ModuleContext, root android.OutputPath) android.OutputPaths
-
-	// Function that filters PackagingSpec in PackagingBase.GatherPackagingSpecs()
-	filterPackagingSpec func(spec android.PackagingSpec) bool
-
 	output     android.OutputPath
 	installDir android.InstallPath
 
@@ -64,8 +59,18 @@
 
 	// Keeps the entries installed from this filesystem
 	entries []string
+
+	filesystemBuilder filesystemBuilder
 }
 
+type filesystemBuilder interface {
+	BuildLinkerConfigFile(ctx android.ModuleContext, builder *android.RuleBuilder, rebasedDir android.OutputPath)
+	// Function that filters PackagingSpec in PackagingBase.GatherPackagingSpecs()
+	FilterPackagingSpec(spec android.PackagingSpec) bool
+}
+
+var _ filesystemBuilder = (*filesystem)(nil)
+
 type SymlinkDefinition struct {
 	Target *string
 	Name   *string
@@ -146,6 +151,10 @@
 
 	Erofs ErofsProperties
 
+	F2fs F2fsProperties
+
+	Linker_config LinkerConfigProperties
+
 	// Determines if the module is auto-generated from Soong or not. If the module is
 	// auto-generated, its deps are exempted from visibility enforcement.
 	Is_auto_generated *bool
@@ -163,6 +172,21 @@
 	Sparse *bool
 }
 
+// Additional properties required to generate f2fs FS partitions.
+type F2fsProperties struct {
+	Sparse *bool
+}
+
+type LinkerConfigProperties struct {
+
+	// Build a linker.config.pb file
+	Gen_linker_config *bool
+
+	// List of files (in .json format) that will be converted to a linker config file (in .pb format).
+	// The linker config file be installed in the filesystem at /etc/linker.config.pb
+	Linker_config_srcs []string `android:"path"`
+}
+
 // android_filesystem packages a set of modules and their transitive dependencies into a filesystem
 // image. The filesystem images are expected to be mounted in the target device, which means the
 // modules in the filesystem image are built for the target device (i.e. Android, not Linux host).
@@ -170,7 +194,7 @@
 // partitions like system.img. For example, cc_library modules are placed under ./lib[64] directory.
 func FilesystemFactory() android.Module {
 	module := &filesystem{}
-	module.filterPackagingSpec = module.filterInstallablePackagingSpec
+	module.filesystemBuilder = module
 	initFilesystemModule(module, module)
 	return module
 }
@@ -179,6 +203,7 @@
 	module.AddProperties(&filesystemModule.properties)
 	android.InitPackageModule(filesystemModule)
 	filesystemModule.PackagingBase.DepsCollectFirstTargetOnly = true
+	filesystemModule.PackagingBase.AllowHighPriorityDeps = true
 	android.InitAndroidMultiTargetsArchModule(module, android.DeviceSupported, android.MultilibCommon)
 	android.InitDefaultableModule(module)
 }
@@ -213,6 +238,7 @@
 const (
 	ext4Type fsType = iota
 	erofsType
+	f2fsType
 	compressedCpioType
 	cpioType // uncompressed
 	unknown
@@ -235,6 +261,8 @@
 		return ext4Type
 	case "erofs":
 		return erofsType
+	case "f2fs":
+		return f2fsType
 	case "compressed_cpio":
 		return compressedCpioType
 	case "cpio":
@@ -261,13 +289,17 @@
 	return proptools.StringDefault(f.properties.Partition_name, f.Name())
 }
 
-func (f *filesystem) filterInstallablePackagingSpec(ps android.PackagingSpec) bool {
+func (f *filesystem) FilterPackagingSpec(ps android.PackagingSpec) bool {
 	// Filesystem module respects the installation semantic. A PackagingSpec from a module with
 	// IsSkipInstall() is skipped.
-	if proptools.Bool(f.properties.Is_auto_generated) { // TODO (spandandas): Remove this.
-		return !ps.SkipInstall() && (ps.Partition() == f.PartitionType())
+	if ps.SkipInstall() {
+		return false
 	}
-	return !ps.SkipInstall()
+	if proptools.Bool(f.properties.Is_auto_generated) { // TODO (spandandas): Remove this.
+		pt := f.PartitionType()
+		return pt == "ramdisk" || ps.Partition() == pt
+	}
+	return true
 }
 
 var pctx = android.NewPackageContext("android/soong/filesystem")
@@ -275,7 +307,7 @@
 func (f *filesystem) GenerateAndroidBuildActions(ctx android.ModuleContext) {
 	validatePartitionType(ctx, f)
 	switch f.fsType(ctx) {
-	case ext4Type, erofsType:
+	case ext4Type, erofsType, f2fsType:
 		f.output = f.buildImageUsingBuildImage(ctx)
 	case compressedCpioType:
 		f.output = f.buildCpioImage(ctx, true)
@@ -363,25 +395,6 @@
 		builder.Command().Text("ln -sf").Text(proptools.ShellEscape(target)).Text(dst.String())
 		f.appendToEntry(ctx, dst)
 	}
-
-	// create extra files if there's any
-	if f.buildExtraFiles != nil {
-		rootForExtraFiles := android.PathForModuleGen(ctx, "root-extra").OutputPath
-		extraFiles := f.buildExtraFiles(ctx, rootForExtraFiles)
-		for _, extraFile := range extraFiles {
-			rel, err := filepath.Rel(rootForExtraFiles.String(), extraFile.String())
-			if err != nil || strings.HasPrefix(rel, "..") {
-				ctx.ModuleErrorf("can't make %q relative to %q", extraFile, rootForExtraFiles)
-			}
-			f.appendToEntry(ctx, rootDir.Join(ctx, rel))
-		}
-		if len(extraFiles) > 0 {
-			builder.Command().BuiltTool("merge_directories").
-				Implicits(extraFiles.Paths()).
-				Text(rootDir.String()).
-				Text(rootForExtraFiles.String())
-		}
-	}
 }
 
 func (f *filesystem) copyPackagingSpecs(ctx android.ModuleContext, builder *android.RuleBuilder, specs map[string]android.PackagingSpec, rootDir, rebasedDir android.WritablePath) []string {
@@ -428,6 +441,7 @@
 	f.buildFsverityMetadataFiles(ctx, builder, specs, rootDir, rebasedDir)
 	f.buildEventLogtagsFile(ctx, builder, rebasedDir)
 	f.buildAconfigFlagsFiles(ctx, builder, specs, rebasedDir)
+	f.filesystemBuilder.BuildLinkerConfigFile(ctx, builder, rebasedDir)
 	f.copyFilesToProductOut(ctx, builder, rebasedDir)
 
 	// run host_init_verifier
@@ -490,6 +504,8 @@
 			return "ext4"
 		case erofsType:
 			return "erofs"
+		case f2fsType:
+			return "f2fs"
 		}
 		panic(fmt.Errorf("unsupported fs type %v", t))
 	}
@@ -539,8 +555,11 @@
 		addStr("uuid", uuid)
 		addStr("hash_seed", uuid)
 	}
-	// Add erofs properties
-	if f.fsType(ctx) == erofsType {
+
+	fst := f.fsType(ctx)
+	switch fst {
+	case erofsType:
+		// Add erofs properties
 		if compressor := f.properties.Erofs.Compressor; compressor != nil {
 			addStr("erofs_default_compressor", proptools.String(compressor))
 		}
@@ -551,17 +570,39 @@
 			// https://source.corp.google.com/h/googleplex-android/platform/build/+/88b1c67239ca545b11580237242774b411f2fed9:core/Makefile;l=2292;bpv=1;bpt=0;drc=ea8f34bc1d6e63656b4ec32f2391e9d54b3ebb6b
 			addStr("erofs_sparse_flag", "-s")
 		}
-	} else if f.properties.Erofs.Compressor != nil || f.properties.Erofs.Compress_hints != nil || f.properties.Erofs.Sparse != nil {
-		// Raise an exception if the propfile contains erofs properties, but the fstype is not erofs
-		fs := fsTypeStr(f.fsType(ctx))
-		ctx.PropertyErrorf("erofs", "erofs is non-empty, but FS type is %s\n. Please delete erofs properties if this partition should use %s\n", fs, fs)
+	case f2fsType:
+		if proptools.BoolDefault(f.properties.F2fs.Sparse, true) {
+			// https://source.corp.google.com/h/googleplex-android/platform/build/+/88b1c67239ca545b11580237242774b411f2fed9:core/Makefile;l=2294;drc=ea8f34bc1d6e63656b4ec32f2391e9d54b3ebb6b;bpv=1;bpt=0
+			addStr("f2fs_sparse_flag", "-S")
+		}
 	}
+	f.checkFsTypePropertyError(ctx, fst, fsTypeStr(fst))
 
 	propFile = android.PathForModuleOut(ctx, "prop").OutputPath
 	android.WriteFileRuleVerbatim(ctx, propFile, propFileString.String())
 	return propFile, deps
 }
 
+// This method checks if there is any property set for the fstype(s) other than
+// the current fstype.
+func (f *filesystem) checkFsTypePropertyError(ctx android.ModuleContext, t fsType, fs string) {
+	raiseError := func(otherFsType, currentFsType string) {
+		errMsg := fmt.Sprintf("%s is non-empty, but FS type is %s\n. Please delete %s properties if this partition should use %s\n", otherFsType, currentFsType, otherFsType, currentFsType)
+		ctx.PropertyErrorf(otherFsType, errMsg)
+	}
+
+	if t != erofsType {
+		if f.properties.Erofs.Compressor != nil || f.properties.Erofs.Compress_hints != nil || f.properties.Erofs.Sparse != nil {
+			raiseError("erofs", fs)
+		}
+	}
+	if t != f2fsType {
+		if f.properties.F2fs.Sparse != nil {
+			raiseError("f2fs", fs)
+		}
+	}
+}
+
 func (f *filesystem) buildCpioImage(ctx android.ModuleContext, compressed bool) android.OutputPath {
 	if proptools.Bool(f.properties.Use_avb) {
 		ctx.PropertyErrorf("use_avb", "signing compresed cpio image using avbtool is not supported."+
@@ -591,6 +632,7 @@
 	f.buildFsverityMetadataFiles(ctx, builder, specs, rootDir, rebasedDir)
 	f.buildEventLogtagsFile(ctx, builder, rebasedDir)
 	f.buildAconfigFlagsFiles(ctx, builder, specs, rebasedDir)
+	f.filesystemBuilder.BuildLinkerConfigFile(ctx, builder, rebasedDir)
 	f.copyFilesToProductOut(ctx, builder, rebasedDir)
 
 	output := android.PathForModuleOut(ctx, f.installFileName()).OutputPath
@@ -626,6 +668,7 @@
 	"vendor_dlkm",
 	"odm_dlkm",
 	"system_dlkm",
+	"ramdisk",
 }
 
 func (f *filesystem) addMakeBuiltFiles(ctx android.ModuleContext, builder *android.RuleBuilder, rootDir android.Path) {
@@ -682,6 +725,18 @@
 	f.appendToEntry(ctx, eventLogtagsPath)
 }
 
+func (f *filesystem) BuildLinkerConfigFile(ctx android.ModuleContext, builder *android.RuleBuilder, rebasedDir android.OutputPath) {
+	if !proptools.Bool(f.properties.Linker_config.Gen_linker_config) {
+		return
+	}
+
+	provideModules, _ := f.getLibsForLinkerConfig(ctx)
+	output := rebasedDir.Join(ctx, "etc", "linker.config.pb")
+	linkerconfig.BuildLinkerConfig(ctx, builder, android.PathsForModuleSrc(ctx, f.properties.Linker_config.Linker_config_srcs), provideModules, nil, output)
+
+	f.appendToEntry(ctx, output)
+}
+
 type partition interface {
 	PartitionType() string
 }
@@ -737,7 +792,7 @@
 // Note that "apex" module installs its contents to "apex"(fake partition) as well
 // for symbol lookup by imitating "activated" paths.
 func (f *filesystem) gatherFilteredPackagingSpecs(ctx android.ModuleContext) map[string]android.PackagingSpec {
-	specs := f.PackagingBase.GatherPackagingSpecsWithFilter(ctx, f.filterPackagingSpec)
+	specs := f.PackagingBase.GatherPackagingSpecsWithFilter(ctx, f.filesystemBuilder.FilterPackagingSpec)
 	return specs
 }
 
@@ -790,3 +845,48 @@
 func (f *filesystemDefaults) GenerateAndroidBuildActions(ctx android.ModuleContext) {
 	validatePartitionType(ctx, f)
 }
+
+// getLibsForLinkerConfig returns
+// 1. A list of libraries installed in this filesystem
+// 2. A list of dep libraries _not_ installed in this filesystem
+//
+// `linkerconfig.BuildLinkerConfig` will convert these two to a linker.config.pb for the filesystem
+// (1) will be added to --provideLibs if they are C libraries with a stable interface (has stubs)
+// (2) will be added to --requireLibs if they are C libraries with a stable interface (has stubs)
+func (f *filesystem) getLibsForLinkerConfig(ctx android.ModuleContext) ([]android.Module, []android.Module) {
+	// we need "Module"s for packaging items
+	modulesInPackageByModule := make(map[android.Module]bool)
+	modulesInPackageByName := make(map[string]bool)
+
+	deps := f.gatherFilteredPackagingSpecs(ctx)
+	ctx.WalkDeps(func(child, parent android.Module) bool {
+		for _, ps := range android.OtherModuleProviderOrDefault(
+			ctx, child, android.InstallFilesProvider).PackagingSpecs {
+			if _, ok := deps[ps.RelPathInPackage()]; ok {
+				modulesInPackageByModule[child] = true
+				modulesInPackageByName[child.Name()] = true
+				return true
+			}
+		}
+		return true
+	})
+
+	provideModules := make([]android.Module, 0, len(modulesInPackageByModule))
+	for mod := range modulesInPackageByModule {
+		provideModules = append(provideModules, mod)
+	}
+
+	var requireModules []android.Module
+	ctx.WalkDeps(func(child, parent android.Module) bool {
+		_, parentInPackage := modulesInPackageByModule[parent]
+		_, childInPackageName := modulesInPackageByName[child.Name()]
+
+		// When parent is in the package, and child (or its variant) is not, this can be from an interface.
+		if parentInPackage && !childInPackageName {
+			requireModules = append(requireModules, child)
+		}
+		return true
+	})
+
+	return provideModules, requireModules
+}
diff --git a/filesystem/filesystem_test.go b/filesystem/filesystem_test.go
index 7300061..f325d96 100644
--- a/filesystem/filesystem_test.go
+++ b/filesystem/filesystem_test.go
@@ -156,11 +156,15 @@
 	result := fixture.RunTestWithBp(t, `
 		android_system_image {
 			name: "myfilesystem",
+			base_dir: "system",
 			deps: [
 				"libfoo",
 				"libbar",
 			],
-			linker_config_src: "linker.config.json",
+			linker_config: {
+				gen_linker_config: true,
+				linker_config_srcs: ["linker.config.json"],
+			},
 		}
 
 		cc_library {
@@ -176,7 +180,7 @@
 	`)
 
 	module := result.ModuleForTests("myfilesystem", "android_common")
-	output := module.Output("system/etc/linker.config.pb")
+	output := module.Output("out/soong/.intermediates/myfilesystem/android_common/root/system/etc/linker.config.pb")
 
 	android.AssertStringDoesContain(t, "linker.config.pb should have libfoo",
 		output.RuleParams.Command, "libfoo.so")
@@ -223,7 +227,10 @@
 					deps: ["foo"],
 				},
 			},
-			linker_config_src: "linker.config.json",
+			linker_config: {
+				gen_linker_config: true,
+				linker_config_srcs: ["linker.config.json"],
+			},
 		}
 		component {
 			name: "foo",
@@ -318,7 +325,10 @@
 			deps: [
 				"libfoo",
 			],
-			linker_config_src: "linker.config.json",
+			linker_config: {
+				gen_linker_config: true,
+				linker_config_srcs: ["linker.config.json"],
+			},
 		}
 
 		cc_library {
@@ -585,6 +595,35 @@
 	android.AssertStringDoesContain(t, "erofs fs type sparse", buildImageConfig, "erofs_sparse_flag=-s")
 }
 
+func TestF2fsPartition(t *testing.T) {
+	result := fixture.RunTestWithBp(t, `
+		android_filesystem {
+			name: "f2fs_partition",
+			type: "f2fs",
+		}
+	`)
+
+	partition := result.ModuleForTests("f2fs_partition", "android_common")
+	buildImageConfig := android.ContentFromFileRuleForTests(t, result.TestContext, partition.Output("prop"))
+	android.AssertStringDoesContain(t, "f2fs fs type", buildImageConfig, "fs_type=f2fs")
+	android.AssertStringDoesContain(t, "f2fs fs type sparse", buildImageConfig, "f2fs_sparse_flag=-S")
+}
+
+func TestFsTypesPropertyError(t *testing.T) {
+	fixture.ExtendWithErrorHandler(android.FixtureExpectsOneErrorPattern(
+		"erofs: erofs is non-empty, but FS type is f2fs\n. Please delete erofs properties if this partition should use f2fs\n")).
+		RunTestWithBp(t, `
+		android_filesystem {
+			name: "f2fs_partition",
+			type: "f2fs",
+			erofs: {
+				compressor: "lz4hc,9",
+				compress_hints: "compress_hints.txt",
+			},
+		}
+	`)
+}
+
 // If a system_ext/ module depends on system/ module, the dependency should *not*
 // be installed in system_ext/
 func TestDoNotPackageCrossPartitionDependencies(t *testing.T) {
@@ -664,3 +703,54 @@
 	fileList := android.ContentFromFileRuleForTests(t, result.TestContext, partition.Output("fileList"))
 	android.AssertDeepEquals(t, "Shared library dep of overridden binary should not be installed", fileList, "bin/binfoo1\nlib64/libc++.so\nlib64/libc.so\nlib64/libdl.so\nlib64/libfoo2.so\nlib64/libm.so\n")
 }
+
+func TestInstallLinkerConfigFile(t *testing.T) {
+	result := fixture.RunTestWithBp(t, `
+android_filesystem {
+    name: "myfilesystem",
+    deps: ["libfoo_has_no_stubs", "libfoo_has_stubs"],
+    linker_config: {
+        gen_linker_config: true,
+        linker_config_srcs: ["linker.config.json"],
+    },
+    partition_type: "vendor",
+}
+cc_library {
+    name: "libfoo_has_no_stubs",
+    vendor: true,
+}
+cc_library {
+    name: "libfoo_has_stubs",
+    stubs: {symbol_file: "libfoo.map.txt"},
+    vendor: true,
+}
+	`)
+
+	linkerConfigCmd := result.ModuleForTests("myfilesystem", "android_common").Rule("build_filesystem_image").RuleParams.Command
+	android.AssertStringDoesContain(t, "Could not find linker.config.json file in cmd", linkerConfigCmd, "conv_linker_config proto --force -s linker.config.json")
+	android.AssertStringDoesContain(t, "Could not find stub in `provideLibs`", linkerConfigCmd, "--key provideLibs --value libfoo_has_stubs.so")
+}
+
+// override_android_* modules implicitly override their base module.
+// If both of these are listed in `deps`, the base module should not be installed.
+func TestOverrideModulesInDeps(t *testing.T) {
+	result := fixture.RunTestWithBp(t, `
+		android_filesystem {
+			name: "myfilesystem",
+			deps: ["myapp", "myoverrideapp"],
+		}
+
+		android_app {
+			name: "myapp",
+			platform_apis: true,
+		}
+		override_android_app {
+			name: "myoverrideapp",
+			base: "myapp",
+		}
+	`)
+
+	partition := result.ModuleForTests("myfilesystem", "android_common")
+	fileList := android.ContentFromFileRuleForTests(t, result.TestContext, partition.Output("fileList"))
+	android.AssertStringEquals(t, "filesystem with override app", "app/myoverrideapp/myoverrideapp.apk\n", fileList)
+}
diff --git a/filesystem/system_image.go b/filesystem/system_image.go
index 7dbf986..672458c 100644
--- a/filesystem/system_image.go
+++ b/filesystem/system_image.go
@@ -17,27 +17,22 @@
 import (
 	"android/soong/android"
 	"android/soong/linkerconfig"
+
+	"github.com/google/blueprint/proptools"
 )
 
 type systemImage struct {
 	filesystem
-
-	properties systemImageProperties
 }
 
-type systemImageProperties struct {
-	// Path to the input linker config json file.
-	Linker_config_src *string `android:"path"`
-}
+var _ filesystemBuilder = (*systemImage)(nil)
 
 // android_system_image is a specialization of android_filesystem for the 'system' partition.
 // Currently, the only difference is the inclusion of linker.config.pb file which specifies
 // the provided and the required libraries to and from APEXes.
 func SystemImageFactory() android.Module {
 	module := &systemImage{}
-	module.AddProperties(&module.properties)
-	module.filesystem.buildExtraFiles = module.buildExtraFiles
-	module.filesystem.filterPackagingSpec = module.filterPackagingSpec
+	module.filesystemBuilder = module
 	initFilesystemModule(module, &module.filesystem)
 	return module
 }
@@ -46,63 +41,22 @@
 	return s.filesystem.properties
 }
 
-func (s *systemImage) buildExtraFiles(ctx android.ModuleContext, root android.OutputPath) android.OutputPaths {
-	if s.filesystem.properties.Partition_type != nil {
-		ctx.PropertyErrorf("partition_type", "partition_type must be unset on an android_system_image module. It is assumed to be 'system'.")
-	}
-	lc := s.buildLinkerConfigFile(ctx, root)
-	// Add more files if needed
-	return []android.OutputPath{lc}
-}
-
-func (s *systemImage) buildLinkerConfigFile(ctx android.ModuleContext, root android.OutputPath) android.OutputPath {
-	input := android.PathForModuleSrc(ctx, android.String(s.properties.Linker_config_src))
-	output := root.Join(ctx, "system", "etc", "linker.config.pb")
-
-	// we need "Module"s for packaging items
-	modulesInPackageByModule := make(map[android.Module]bool)
-	modulesInPackageByName := make(map[string]bool)
-
-	deps := s.gatherFilteredPackagingSpecs(ctx)
-	ctx.WalkDeps(func(child, parent android.Module) bool {
-		for _, ps := range android.OtherModuleProviderOrDefault(
-			ctx, child, android.InstallFilesProvider).PackagingSpecs {
-			if _, ok := deps[ps.RelPathInPackage()]; ok {
-				modulesInPackageByModule[child] = true
-				modulesInPackageByName[child.Name()] = true
-				return true
-			}
-		}
-		return true
-	})
-
-	provideModules := make([]android.Module, 0, len(modulesInPackageByModule))
-	for mod := range modulesInPackageByModule {
-		provideModules = append(provideModules, mod)
+func (s *systemImage) BuildLinkerConfigFile(ctx android.ModuleContext, builder *android.RuleBuilder, rebasedDir android.OutputPath) {
+	if !proptools.Bool(s.filesystem.properties.Linker_config.Gen_linker_config) {
+		return
 	}
 
-	var requireModules []android.Module
-	ctx.WalkDeps(func(child, parent android.Module) bool {
-		_, parentInPackage := modulesInPackageByModule[parent]
-		_, childInPackageName := modulesInPackageByName[child.Name()]
+	provideModules, requireModules := s.getLibsForLinkerConfig(ctx)
+	output := rebasedDir.Join(ctx, "etc", "linker.config.pb")
+	linkerconfig.BuildLinkerConfig(ctx, builder, android.PathsForModuleSrc(ctx, s.filesystem.properties.Linker_config.Linker_config_srcs), provideModules, requireModules, output)
 
-		// When parent is in the package, and child (or its variant) is not, this can be from an interface.
-		if parentInPackage && !childInPackageName {
-			requireModules = append(requireModules, child)
-		}
-		return true
-	})
-
-	builder := android.NewRuleBuilder(pctx, ctx)
-	linkerconfig.BuildLinkerConfig(ctx, builder, input, provideModules, requireModules, output)
-	builder.Build("conv_linker_config", "Generate linker config protobuf "+output.String())
-	return output
+	s.appendToEntry(ctx, output)
 }
 
 // Filter the result of GatherPackagingSpecs to discard items targeting outside "system" / "root"
 // partition.  Note that "apex" module installs its contents to "apex"(fake partition) as well
 // for symbol lookup by imitating "activated" paths.
-func (s *systemImage) filterPackagingSpec(ps android.PackagingSpec) bool {
+func (s *systemImage) FilterPackagingSpec(ps android.PackagingSpec) bool {
 	return !ps.SkipInstall() &&
 		(ps.Partition() == "system" || ps.Partition() == "root")
 }
diff --git a/filesystem/vbmeta.go b/filesystem/vbmeta.go
index 0bae479..ebb3ff9 100644
--- a/filesystem/vbmeta.go
+++ b/filesystem/vbmeta.go
@@ -25,19 +25,30 @@
 )
 
 func init() {
-	android.RegisterModuleType("vbmeta", vbmetaFactory)
+	android.RegisterModuleType("vbmeta", VbmetaFactory)
+	pctx.HostBinToolVariable("avbtool", "avbtool")
 }
 
+var (
+	extractPublicKeyRule = pctx.AndroidStaticRule("avb_extract_public_key",
+		blueprint.RuleParams{
+			Command: `${avbtool} extract_public_key --key $in --output $out`,
+			CommandDeps: []string{
+				"${avbtool}",
+			},
+		})
+)
+
 type vbmeta struct {
 	android.ModuleBase
 
-	properties vbmetaProperties
+	properties VbmetaProperties
 
 	output     android.OutputPath
 	installDir android.InstallPath
 }
 
-type vbmetaProperties struct {
+type VbmetaProperties struct {
 	// Name of the partition stored in vbmeta desc. Defaults to the name of this module.
 	Partition_name *string
 
@@ -50,9 +61,8 @@
 	// Algorithm that avbtool will use to sign this vbmeta image. Default is SHA256_RSA4096.
 	Algorithm *string
 
-	// File whose content will provide the rollback index. If unspecified, the rollback index
-	// is from PLATFORM_SECURITY_PATCH
-	Rollback_index_file *string `android:"path"`
+	// The rollback index. If unspecified, the rollback index is from PLATFORM_SECURITY_PATCH
+	Rollback_index *int64
 
 	// Rollback index location of this vbmeta image. Must be 0, 1, 2, etc. Default is 0.
 	Rollback_index_location *int64
@@ -61,8 +71,15 @@
 	// have to be signed (use_avb: true).
 	Partitions proptools.Configurable[[]string]
 
-	// List of chained partitions that this vbmeta deletages the verification.
-	Chained_partitions []chainedPartitionProperties
+	// Metadata about the chained partitions that this vbmeta delegates the verification.
+	// This is an alternative to chained_partitions, using chained_partitions instead is simpler
+	// in most cases. However, this property allows building this vbmeta partition without
+	// its chained partitions existing in this build.
+	Chained_partition_metadata []ChainedPartitionProperties
+
+	// List of chained partitions that this vbmeta delegates the verification. They are the
+	// names of other vbmeta modules.
+	Chained_partitions []string
 
 	// List of key-value pair of avb properties
 	Avb_properties []avbProperty
@@ -76,7 +93,7 @@
 	Value *string
 }
 
-type chainedPartitionProperties struct {
+type ChainedPartitionProperties struct {
 	// Name of the chained partition
 	Name *string
 
@@ -94,8 +111,22 @@
 	Private_key *string `android:"path"`
 }
 
+type vbmetaPartitionInfo struct {
+	// Name of the partition
+	Name string
+
+	// Rollback index location, non-negative int
+	RollbackIndexLocation int
+
+	// The path to the public key of the private key used to sign this partition. Derived from
+	// the private key.
+	PublicKey android.Path
+}
+
+var vbmetaPartitionProvider = blueprint.NewProvider[vbmetaPartitionInfo]()
+
 // vbmeta is the partition image that has the verification information for other partitions.
-func vbmetaFactory() android.Module {
+func VbmetaFactory() android.Module {
 	module := &vbmeta{}
 	module.AddProperties(&module.properties)
 	android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibCommon)
@@ -104,13 +135,18 @@
 
 type vbmetaDep struct {
 	blueprint.BaseDependencyTag
-	kind string
 }
 
-var vbmetaPartitionDep = vbmetaDep{kind: "partition"}
+type chainedPartitionDep struct {
+	blueprint.BaseDependencyTag
+}
+
+var vbmetaPartitionDep = vbmetaDep{}
+var vbmetaChainedPartitionDep = chainedPartitionDep{}
 
 func (v *vbmeta) DepsMutator(ctx android.BottomUpMutatorContext) {
 	ctx.AddDependency(ctx.Module(), vbmetaPartitionDep, v.properties.Partitions.GetOrDefault(ctx, nil)...)
+	ctx.AddDependency(ctx.Module(), vbmetaChainedPartitionDep, v.properties.Chained_partitions...)
 }
 
 func (v *vbmeta) installFileName() string {
@@ -125,8 +161,6 @@
 const vbmetaMaxSize = 64 * 1024
 
 func (v *vbmeta) GenerateAndroidBuildActions(ctx android.ModuleContext) {
-	extractedPublicKeys := v.extractPublicKeys(ctx)
-
 	v.output = android.PathForModuleOut(ctx, v.installFileName()).OutputPath
 
 	builder := android.NewRuleBuilder(pctx, ctx)
@@ -176,25 +210,66 @@
 		cmd.FlagWithInput("--include_descriptors_from_image ", signedImage)
 	}
 
-	for i, cp := range v.properties.Chained_partitions {
-		name := proptools.String(cp.Name)
+	seenRils := make(map[int]bool)
+	for _, cp := range ctx.GetDirectDepsWithTag(vbmetaChainedPartitionDep) {
+		info, ok := android.OtherModuleProvider(ctx, cp, vbmetaPartitionProvider)
+		if !ok {
+			ctx.PropertyErrorf("chained_partitions", "Expected all modules in chained_partitions to provide vbmetaPartitionProvider, but %s did not", cp.Name())
+			continue
+		}
+		if info.Name == "" {
+			ctx.PropertyErrorf("chained_partitions", "name must be specified")
+			continue
+		}
+
+		ril := info.RollbackIndexLocation
+		if ril < 0 {
+			ctx.PropertyErrorf("chained_partitions", "rollback index location must be 0, 1, 2, ...")
+			continue
+		} else if seenRils[ril] {
+			ctx.PropertyErrorf("chained_partitions", "Multiple chained partitions with the same rollback index location %d", ril)
+			continue
+		}
+		seenRils[ril] = true
+
+		publicKey := info.PublicKey
+		cmd.FlagWithArg("--chain_partition ", fmt.Sprintf("%s:%d:%s", info.Name, ril, publicKey.String()))
+		cmd.Implicit(publicKey)
+	}
+	for _, cpm := range v.properties.Chained_partition_metadata {
+		name := proptools.String(cpm.Name)
 		if name == "" {
 			ctx.PropertyErrorf("chained_partitions", "name must be specified")
 			continue
 		}
 
-		ril := proptools.IntDefault(cp.Rollback_index_location, i+1)
+		ril := proptools.IntDefault(cpm.Rollback_index_location, -1)
 		if ril < 0 {
-			ctx.PropertyErrorf("chained_partitions", "must be 0, 1, 2, ...")
+			ctx.PropertyErrorf("chained_partition_metadata", "rollback index location must be 0, 1, 2, ...")
+			continue
+		} else if seenRils[ril] {
+			ctx.PropertyErrorf("chained_partition_metadata", "Multiple chained partitions with the same rollback index location %d", ril)
+			continue
+		}
+		seenRils[ril] = true
+
+		var publicKey android.Path
+		if cpm.Public_key != nil {
+			publicKey = android.PathForModuleSrc(ctx, *cpm.Public_key)
+		} else if cpm.Private_key != nil {
+			privateKey := android.PathForModuleSrc(ctx, *cpm.Private_key)
+			extractedPublicKey := android.PathForModuleOut(ctx, "chained_metadata", name+".avbpubkey")
+			ctx.Build(pctx, android.BuildParams{
+				Rule:   extractPublicKeyRule,
+				Input:  privateKey,
+				Output: extractedPublicKey,
+			})
+			publicKey = extractedPublicKey
+		} else {
+			ctx.PropertyErrorf("public_key", "Either public_key or private_key must be specified")
 			continue
 		}
 
-		var publicKey android.Path
-		if cp.Public_key != nil {
-			publicKey = android.PathForModuleSrc(ctx, proptools.String(cp.Public_key))
-		} else {
-			publicKey = extractedPublicKeys[name]
-		}
 		cmd.FlagWithArg("--chain_partition ", fmt.Sprintf("%s:%d:%s", name, ril, publicKey.String()))
 		cmd.Implicit(publicKey)
 	}
@@ -212,57 +287,30 @@
 	v.installDir = android.PathForModuleInstall(ctx, "etc")
 	ctx.InstallFile(v.installDir, v.installFileName(), v.output)
 
+	extractedPublicKey := android.PathForModuleOut(ctx, v.partitionName()+".avbpubkey")
+	ctx.Build(pctx, android.BuildParams{
+		Rule:   extractPublicKeyRule,
+		Input:  key,
+		Output: extractedPublicKey,
+	})
+
+	android.SetProvider(ctx, vbmetaPartitionProvider, vbmetaPartitionInfo{
+		Name:                  v.partitionName(),
+		RollbackIndexLocation: ril,
+		PublicKey:             extractedPublicKey,
+	})
+
 	ctx.SetOutputFiles([]android.Path{v.output}, "")
 }
 
 // Returns the embedded shell command that prints the rollback index
 func (v *vbmeta) rollbackIndexCommand(ctx android.ModuleContext) string {
-	var cmd string
-	if v.properties.Rollback_index_file != nil {
-		f := android.PathForModuleSrc(ctx, proptools.String(v.properties.Rollback_index_file))
-		cmd = "cat " + f.String()
+	if v.properties.Rollback_index != nil {
+		return fmt.Sprintf("%d", *v.properties.Rollback_index)
 	} else {
-		cmd = "date -d 'TZ=\"GMT\" " + ctx.Config().PlatformSecurityPatch() + "' +%s"
+		// Take the first line and remove the newline char
+		return "$(date -d 'TZ=\"GMT\" " + ctx.Config().PlatformSecurityPatch() + "' +%s | head -1 | tr -d '\n'" + ")"
 	}
-	// Take the first line and remove the newline char
-	return "$(" + cmd + " | head -1 | tr -d '\n'" + ")"
-}
-
-// Extract public keys from chained_partitions.private_key. The keys are indexed with the partition
-// name.
-func (v *vbmeta) extractPublicKeys(ctx android.ModuleContext) map[string]android.OutputPath {
-	result := make(map[string]android.OutputPath)
-
-	builder := android.NewRuleBuilder(pctx, ctx)
-	for _, cp := range v.properties.Chained_partitions {
-		if cp.Private_key == nil {
-			continue
-		}
-
-		name := proptools.String(cp.Name)
-		if name == "" {
-			ctx.PropertyErrorf("chained_partitions", "name must be specified")
-			continue
-		}
-
-		if _, ok := result[name]; ok {
-			ctx.PropertyErrorf("chained_partitions", "name %q is duplicated", name)
-			continue
-		}
-
-		privateKeyFile := android.PathForModuleSrc(ctx, proptools.String(cp.Private_key))
-		publicKeyFile := android.PathForModuleOut(ctx, name+".avbpubkey").OutputPath
-
-		builder.Command().
-			BuiltTool("avbtool").
-			Text("extract_public_key").
-			FlagWithInput("--key ", privateKeyFile).
-			FlagWithOutput("--output ", publicKeyFile)
-
-		result[name] = publicKeyFile
-	}
-	builder.Build("vbmeta_extract_public_key", fmt.Sprintf("Extract public keys for %s", ctx.ModuleName()))
-	return result
 }
 
 var _ android.AndroidMkProviderInfoProducer = (*vbmeta)(nil)
diff --git a/fsgen/Android.bp b/fsgen/Android.bp
index 9fa9557..8cd7518 100644
--- a/fsgen/Android.bp
+++ b/fsgen/Android.bp
@@ -10,14 +10,19 @@
         "soong",
         "soong-android",
         "soong-filesystem",
+        "soong-kernel",
     ],
     srcs: [
         "filesystem_creator.go",
+        "fsgen_mutators.go",
+        "prebuilt_etc_modules_gen.go",
+        "vbmeta_partitions.go",
     ],
     testSrcs: [
         "filesystem_creator_test.go",
     ],
     pluginFor: ["soong_build"],
+    visibility: ["//visibility:public"],
 }
 
 soong_filesystem_creator {
diff --git a/fsgen/filesystem_creator.go b/fsgen/filesystem_creator.go
index 766176d..0a65c6c 100644
--- a/fsgen/filesystem_creator.go
+++ b/fsgen/filesystem_creator.go
@@ -17,13 +17,13 @@
 import (
 	"crypto/sha256"
 	"fmt"
-	"slices"
+	"path/filepath"
 	"strconv"
 	"strings"
-	"sync"
 
 	"android/soong/android"
 	"android/soong/filesystem"
+	"android/soong/kernel"
 
 	"github.com/google/blueprint"
 	"github.com/google/blueprint/parser"
@@ -41,315 +41,12 @@
 	ctx.PreDepsMutators(RegisterCollectFileSystemDepsMutators)
 }
 
-func RegisterCollectFileSystemDepsMutators(ctx android.RegisterMutatorsContext) {
-	ctx.BottomUp("fs_collect_deps", collectDepsMutator).MutatesGlobalState()
-	ctx.BottomUp("fs_set_deps", setDepsMutator)
-}
-
-var fsGenStateOnceKey = android.NewOnceKey("FsGenState")
-var fsGenRemoveOverridesOnceKey = android.NewOnceKey("FsGenRemoveOverrides")
-
-// Map of partition module name to its partition that may be generated by Soong.
-// Note that it is not guaranteed that all modules returned by this function are successfully
-// created.
-func getAllSoongGeneratedPartitionNames(config android.Config, partitions []string) map[string]string {
-	ret := map[string]string{}
-	for _, partition := range partitions {
-		ret[generatedModuleNameForPartition(config, partition)] = partition
-	}
-	return ret
-}
-
-type depCandidateProps struct {
-	Namespace string
-	Multilib  string
-	Arch      []android.ArchType
-}
-
-// Map of module name to depCandidateProps
-type multilibDeps *map[string]*depCandidateProps
-
-// Information necessary to generate the filesystem modules, including details about their
-// dependencies
-type FsGenState struct {
-	// List of modules in `PRODUCT_PACKAGES` and `PRODUCT_PACKAGES_DEBUG`
-	depCandidates []string
-	// Map of names of partition to the information of modules to be added as deps
-	fsDeps map[string]multilibDeps
-	// List of name of partitions to be generated by the filesystem_creator module
-	soongGeneratedPartitions []string
-	// Mutex to protect the fsDeps
-	fsDepsMutex sync.Mutex
-	// Map of _all_ soong module names to their corresponding installation properties
-	moduleToInstallationProps map[string]installationProperties
-}
-
-type installationProperties struct {
-	Required  []string
-	Overrides []string
-}
-
-func newMultilibDeps() multilibDeps {
-	return &map[string]*depCandidateProps{}
-}
-
-func defaultDepCandidateProps(config android.Config) *depCandidateProps {
-	return &depCandidateProps{
-		Namespace: ".",
-		Arch:      []android.ArchType{config.BuildArch},
-	}
-}
-
-func createFsGenState(ctx android.LoadHookContext) *FsGenState {
-	return ctx.Config().Once(fsGenStateOnceKey, func() interface{} {
-		partitionVars := ctx.Config().ProductVariables().PartitionVarsForSoongMigrationOnlyDoNotUse
-		candidates := android.FirstUniqueStrings(android.Concat(partitionVars.ProductPackages, partitionVars.ProductPackagesDebug))
-
-		generatedPartitions := []string{"system"}
-		if ctx.DeviceConfig().SystemExtPath() == "system_ext" {
-			generatedPartitions = append(generatedPartitions, "system_ext")
-		}
-		if ctx.DeviceConfig().BuildingVendorImage() && ctx.DeviceConfig().VendorPath() == "vendor" {
-			generatedPartitions = append(generatedPartitions, "vendor")
-		}
-		if ctx.DeviceConfig().BuildingProductImage() && ctx.DeviceConfig().ProductPath() == "product" {
-			generatedPartitions = append(generatedPartitions, "product")
-		}
-
-		return &FsGenState{
-			depCandidates: candidates,
-			fsDeps: map[string]multilibDeps{
-				// These additional deps are added according to the cuttlefish system image bp.
-				"system": &map[string]*depCandidateProps{
-					"com.android.apex.cts.shim.v1_prebuilt":     defaultDepCandidateProps(ctx.Config()),
-					"dex_bootjars":                              defaultDepCandidateProps(ctx.Config()),
-					"framework_compatibility_matrix.device.xml": defaultDepCandidateProps(ctx.Config()),
-					"idc_data":                     defaultDepCandidateProps(ctx.Config()),
-					"init.environ.rc-soong":        defaultDepCandidateProps(ctx.Config()),
-					"keychars_data":                defaultDepCandidateProps(ctx.Config()),
-					"keylayout_data":               defaultDepCandidateProps(ctx.Config()),
-					"libclang_rt.asan":             defaultDepCandidateProps(ctx.Config()),
-					"libcompiler_rt":               defaultDepCandidateProps(ctx.Config()),
-					"libdmabufheap":                defaultDepCandidateProps(ctx.Config()),
-					"libgsi":                       defaultDepCandidateProps(ctx.Config()),
-					"llndk.libraries.txt":          defaultDepCandidateProps(ctx.Config()),
-					"logpersist.start":             defaultDepCandidateProps(ctx.Config()),
-					"preloaded-classes":            defaultDepCandidateProps(ctx.Config()),
-					"public.libraries.android.txt": defaultDepCandidateProps(ctx.Config()),
-					"update_engine_sideload":       defaultDepCandidateProps(ctx.Config()),
-				},
-				"vendor": &map[string]*depCandidateProps{
-					"fs_config_files_vendor":                               defaultDepCandidateProps(ctx.Config()),
-					"fs_config_dirs_vendor":                                defaultDepCandidateProps(ctx.Config()),
-					generatedModuleName(ctx.Config(), "vendor-build.prop"): defaultDepCandidateProps(ctx.Config()),
-				},
-				"odm":     newMultilibDeps(),
-				"product": newMultilibDeps(),
-				"system_ext": &map[string]*depCandidateProps{
-					// VNDK apexes are automatically included.
-					// This hardcoded list will need to be updated if `PRODUCT_EXTRA_VNDK_VERSIONS` is updated.
-					// https://cs.android.com/android/_/android/platform/build/+/adba533072b00c53ac0f198c550a3cbd7a00e4cd:core/main.mk;l=984;bpv=1;bpt=0;drc=174db7b179592cf07cbfd2adb0119486fda911e7
-					"com.android.vndk.v30": defaultDepCandidateProps(ctx.Config()),
-					"com.android.vndk.v31": defaultDepCandidateProps(ctx.Config()),
-					"com.android.vndk.v32": defaultDepCandidateProps(ctx.Config()),
-					"com.android.vndk.v33": defaultDepCandidateProps(ctx.Config()),
-					"com.android.vndk.v34": defaultDepCandidateProps(ctx.Config()),
-				},
-			},
-			soongGeneratedPartitions:  generatedPartitions,
-			fsDepsMutex:               sync.Mutex{},
-			moduleToInstallationProps: map[string]installationProperties{},
-		}
-	}).(*FsGenState)
-}
-
-func checkDepModuleInMultipleNamespaces(mctx android.BottomUpMutatorContext, foundDeps map[string]*depCandidateProps, module string, partitionName string) {
-	otherNamespace := mctx.Namespace().Path
-	if val, found := foundDeps[module]; found && otherNamespace != "." && !android.InList(val.Namespace, []string{".", otherNamespace}) {
-		mctx.ModuleErrorf("found in multiple namespaces(%s and %s) when including in %s partition", val.Namespace, otherNamespace, partitionName)
-	}
-}
-
-func appendDepIfAppropriate(mctx android.BottomUpMutatorContext, deps *map[string]*depCandidateProps, installPartition string) {
-	checkDepModuleInMultipleNamespaces(mctx, *deps, mctx.Module().Name(), installPartition)
-	if _, ok := (*deps)[mctx.Module().Name()]; ok {
-		// Prefer the namespace-specific module over the platform module
-		if mctx.Namespace().Path != "." {
-			(*deps)[mctx.Module().Name()].Namespace = mctx.Namespace().Path
-		}
-		(*deps)[mctx.Module().Name()].Arch = append((*deps)[mctx.Module().Name()].Arch, mctx.Module().Target().Arch.ArchType)
-	} else {
-		multilib, _ := mctx.Module().DecodeMultilib(mctx)
-		(*deps)[mctx.Module().Name()] = &depCandidateProps{
-			Namespace: mctx.Namespace().Path,
-			Multilib:  multilib,
-			Arch:      []android.ArchType{mctx.Module().Target().Arch.ArchType},
-		}
-	}
-}
-
-func collectDepsMutator(mctx android.BottomUpMutatorContext) {
-	fsGenState := mctx.Config().Get(fsGenStateOnceKey).(*FsGenState)
-
-	m := mctx.Module()
-	if m.Target().Os.Class == android.Device && slices.Contains(fsGenState.depCandidates, m.Name()) {
-		installPartition := m.PartitionTag(mctx.DeviceConfig())
-		fsGenState.fsDepsMutex.Lock()
-		// Only add the module as dependency when:
-		// - its enabled
-		// - its namespace is included in PRODUCT_SOONG_NAMESPACES
-		if m.Enabled(mctx) && m.ExportedToMake() {
-			appendDepIfAppropriate(mctx, fsGenState.fsDeps[installPartition], installPartition)
-		}
-		fsGenState.fsDepsMutex.Unlock()
-	}
-	// store the map of module to (required,overrides) even if the module is not in PRODUCT_PACKAGES.
-	// the module might be installed transitively.
-	if m.Target().Os.Class == android.Device && m.Enabled(mctx) && m.ExportedToMake() {
-		fsGenState.fsDepsMutex.Lock()
-		fsGenState.moduleToInstallationProps[m.Name()] = installationProperties{
-			Required:  m.RequiredModuleNames(mctx),
-			Overrides: m.Overrides(),
-		}
-		fsGenState.fsDepsMutex.Unlock()
-	}
-}
-
-type depsStruct struct {
-	Deps []string
-}
-
-type multilibDepsStruct struct {
-	Common   depsStruct
-	Lib32    depsStruct
-	Lib64    depsStruct
-	Both     depsStruct
-	Prefer32 depsStruct
-}
-
-type packagingPropsStruct struct {
-	Deps     []string
-	Multilib multilibDepsStruct
-}
-
-func fullyQualifiedModuleName(moduleName, namespace string) string {
-	if namespace == "." {
-		return moduleName
-	}
-	return fmt.Sprintf("//%s:%s", namespace, moduleName)
-}
-
-func getBitness(archTypes []android.ArchType) (ret []string) {
-	for _, archType := range archTypes {
-		if archType.Multilib == "" {
-			ret = append(ret, android.COMMON_VARIANT)
-		} else {
-			ret = append(ret, archType.Bitness())
-		}
-	}
-	return ret
-}
-
-func setDepsMutator(mctx android.BottomUpMutatorContext) {
-	removeOverriddenDeps(mctx)
-	fsGenState := mctx.Config().Get(fsGenStateOnceKey).(*FsGenState)
-	fsDeps := fsGenState.fsDeps
-	soongGeneratedPartitionMap := getAllSoongGeneratedPartitionNames(mctx.Config(), fsGenState.soongGeneratedPartitions)
-	m := mctx.Module()
-	if partition, ok := soongGeneratedPartitionMap[m.Name()]; ok {
-		depsStruct := generateDepStruct(*fsDeps[partition])
-		if err := proptools.AppendMatchingProperties(m.GetProperties(), depsStruct, nil); err != nil {
-			mctx.ModuleErrorf(err.Error())
-		}
-	}
-}
-
-// removeOverriddenDeps collects PRODUCT_PACKAGES and (transitive) required deps.
-// it then removes any modules which appear in `overrides` of the above list.
-func removeOverriddenDeps(mctx android.BottomUpMutatorContext) {
-	mctx.Config().Once(fsGenRemoveOverridesOnceKey, func() interface{} {
-		fsGenState := mctx.Config().Get(fsGenStateOnceKey).(*FsGenState)
-		fsDeps := fsGenState.fsDeps
-		overridden := map[string]bool{}
-		allDeps := []string{}
-
-		// Step 1: Initialization: Append PRODUCT_PACKAGES to the queue
-		for _, fsDep := range fsDeps {
-			for depName, _ := range *fsDep {
-				allDeps = append(allDeps, depName)
-			}
-		}
-
-		// Step 2: Process the queue, and add required modules to the queue.
-		i := 0
-		for {
-			if i == len(allDeps) {
-				break
-			}
-			depName := allDeps[i]
-			for _, overrides := range fsGenState.moduleToInstallationProps[depName].Overrides {
-				overridden[overrides] = true
-			}
-			// add required dep to the queue.
-			allDeps = append(allDeps, fsGenState.moduleToInstallationProps[depName].Required...)
-			i += 1
-		}
-
-		// Step 3: Delete all the overridden modules.
-		for overridden, _ := range overridden {
-			for partition, _ := range fsDeps {
-				delete(*fsDeps[partition], overridden)
-			}
-		}
-		return nil
-	})
-}
-
-func generateDepStruct(deps map[string]*depCandidateProps) *packagingPropsStruct {
-	depsStruct := packagingPropsStruct{}
-	for depName, depProps := range deps {
-		bitness := getBitness(depProps.Arch)
-		fullyQualifiedDepName := fullyQualifiedModuleName(depName, depProps.Namespace)
-		if android.InList("32", bitness) && android.InList("64", bitness) {
-			// If both 32 and 64 bit variants are enabled for this module
-			switch depProps.Multilib {
-			case string(android.MultilibBoth):
-				depsStruct.Multilib.Both.Deps = append(depsStruct.Multilib.Both.Deps, fullyQualifiedDepName)
-			case string(android.MultilibCommon), string(android.MultilibFirst):
-				depsStruct.Deps = append(depsStruct.Deps, fullyQualifiedDepName)
-			case "32":
-				depsStruct.Multilib.Lib32.Deps = append(depsStruct.Multilib.Lib32.Deps, fullyQualifiedDepName)
-			case "64", "darwin_universal":
-				depsStruct.Multilib.Lib64.Deps = append(depsStruct.Multilib.Lib64.Deps, fullyQualifiedDepName)
-			case "prefer32", "first_prefer32":
-				depsStruct.Multilib.Prefer32.Deps = append(depsStruct.Multilib.Prefer32.Deps, fullyQualifiedDepName)
-			default:
-				depsStruct.Multilib.Both.Deps = append(depsStruct.Multilib.Both.Deps, fullyQualifiedDepName)
-			}
-		} else if android.InList("64", bitness) {
-			// If only 64 bit variant is enabled
-			depsStruct.Multilib.Lib64.Deps = append(depsStruct.Multilib.Lib64.Deps, fullyQualifiedDepName)
-		} else if android.InList("32", bitness) {
-			// If only 32 bit variant is enabled
-			depsStruct.Multilib.Lib32.Deps = append(depsStruct.Multilib.Lib32.Deps, fullyQualifiedDepName)
-		} else {
-			// If only common variant is enabled
-			depsStruct.Multilib.Common.Deps = append(depsStruct.Multilib.Common.Deps, fullyQualifiedDepName)
-		}
-	}
-	depsStruct.Deps = android.SortedUniqueStrings(depsStruct.Deps)
-	depsStruct.Multilib.Lib32.Deps = android.SortedUniqueStrings(depsStruct.Multilib.Lib32.Deps)
-	depsStruct.Multilib.Lib64.Deps = android.SortedUniqueStrings(depsStruct.Multilib.Lib64.Deps)
-	depsStruct.Multilib.Prefer32.Deps = android.SortedUniqueStrings(depsStruct.Multilib.Prefer32.Deps)
-	depsStruct.Multilib.Both.Deps = android.SortedUniqueStrings(depsStruct.Multilib.Both.Deps)
-	depsStruct.Multilib.Common.Deps = android.SortedUniqueStrings(depsStruct.Multilib.Common.Deps)
-
-	return &depsStruct
-}
-
 type filesystemCreatorProps struct {
 	Generated_partition_types   []string `blueprint:"mutated"`
 	Unsupported_partition_types []string `blueprint:"mutated"`
+
+	Vbmeta_module_names    []string `blueprint:"mutated"`
+	Vbmeta_partition_names []string `blueprint:"mutated"`
 }
 
 type filesystemCreator struct {
@@ -364,7 +61,8 @@
 	android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibCommon)
 	module.AddProperties(&module.properties)
 	android.AddLoadHook(module, func(ctx android.LoadHookContext) {
-		createFsGenState(ctx)
+		generatedPrebuiltEtcModuleNames := createPrebuiltEtcModules(ctx)
+		createFsGenState(ctx, generatedPrebuiltEtcModuleNames)
 		module.createInternalModules(ctx)
 	})
 
@@ -372,16 +70,24 @@
 }
 
 func (f *filesystemCreator) createInternalModules(ctx android.LoadHookContext) {
-	soongGeneratedPartitions := &ctx.Config().Get(fsGenStateOnceKey).(*FsGenState).soongGeneratedPartitions
-	for _, partitionType := range *soongGeneratedPartitions {
+	soongGeneratedPartitions := generatedPartitions(ctx)
+	finalSoongGeneratedPartitions := make([]string, 0, len(soongGeneratedPartitions))
+	for _, partitionType := range soongGeneratedPartitions {
 		if f.createPartition(ctx, partitionType) {
 			f.properties.Generated_partition_types = append(f.properties.Generated_partition_types, partitionType)
+			finalSoongGeneratedPartitions = append(finalSoongGeneratedPartitions, partitionType)
 		} else {
 			f.properties.Unsupported_partition_types = append(f.properties.Unsupported_partition_types, partitionType)
-			_, *soongGeneratedPartitions = android.RemoveFromList(partitionType, *soongGeneratedPartitions)
 		}
 	}
-	f.createDeviceModule(ctx)
+
+	for _, x := range createVbmetaPartitions(ctx, finalSoongGeneratedPartitions) {
+		f.properties.Vbmeta_module_names = append(f.properties.Vbmeta_module_names, x.moduleName)
+		f.properties.Vbmeta_partition_names = append(f.properties.Vbmeta_partition_names, x.partitionName)
+	}
+
+	ctx.Config().Get(fsGenStateOnceKey).(*FsGenState).soongGeneratedPartitions = finalSoongGeneratedPartitions
+	f.createDeviceModule(ctx, finalSoongGeneratedPartitions, f.properties.Vbmeta_module_names)
 }
 
 func generatedModuleName(cfg android.Config, suffix string) string {
@@ -396,7 +102,11 @@
 	return generatedModuleName(cfg, fmt.Sprintf("%s_image", partitionType))
 }
 
-func (f *filesystemCreator) createDeviceModule(ctx android.LoadHookContext) {
+func (f *filesystemCreator) createDeviceModule(
+	ctx android.LoadHookContext,
+	generatedPartitionTypes []string,
+	vbmetaPartitions []string,
+) {
 	baseProps := &struct {
 		Name *string
 	}{
@@ -405,18 +115,25 @@
 
 	// Currently, only the system and system_ext partition module is created.
 	partitionProps := &filesystem.PartitionNameProperties{}
-	if android.InList("system", f.properties.Generated_partition_types) {
+	if android.InList("system", generatedPartitionTypes) {
 		partitionProps.System_partition_name = proptools.StringPtr(generatedModuleNameForPartition(ctx.Config(), "system"))
 	}
-	if android.InList("system_ext", f.properties.Generated_partition_types) {
+	if android.InList("system_ext", generatedPartitionTypes) {
 		partitionProps.System_ext_partition_name = proptools.StringPtr(generatedModuleNameForPartition(ctx.Config(), "system_ext"))
 	}
-	if android.InList("vendor", f.properties.Generated_partition_types) {
+	if android.InList("vendor", generatedPartitionTypes) {
 		partitionProps.Vendor_partition_name = proptools.StringPtr(generatedModuleNameForPartition(ctx.Config(), "vendor"))
 	}
-	if android.InList("product", f.properties.Generated_partition_types) {
+	if android.InList("product", generatedPartitionTypes) {
 		partitionProps.Product_partition_name = proptools.StringPtr(generatedModuleNameForPartition(ctx.Config(), "product"))
 	}
+	if android.InList("odm", generatedPartitionTypes) {
+		partitionProps.Odm_partition_name = proptools.StringPtr(generatedModuleNameForPartition(ctx.Config(), "odm"))
+	}
+	if android.InList("userdata", f.properties.Generated_partition_types) {
+		partitionProps.Userdata_partition_name = proptools.StringPtr(generatedModuleNameForPartition(ctx.Config(), "userdata"))
+	}
+	partitionProps.Vbmeta_partitions = vbmetaPartitions
 
 	ctx.CreateModule(filesystem.AndroidDeviceFactory, baseProps, partitionProps)
 }
@@ -438,6 +155,26 @@
 			"framework/oat/*/*", // framework/oat/{arch}
 		}
 		fsProps.Fsverity.Libs = []string{":framework-res{.export-package.apk}"}
+		// TODO(b/377734331): only generate the symlinks if the relevant partitions exist
+		fsProps.Symlinks = []filesystem.SymlinkDefinition{
+			filesystem.SymlinkDefinition{
+				Target: proptools.StringPtr("/product"),
+				Name:   proptools.StringPtr("system/product"),
+			},
+			filesystem.SymlinkDefinition{
+				Target: proptools.StringPtr("/system_ext"),
+				Name:   proptools.StringPtr("system/system_ext"),
+			},
+			filesystem.SymlinkDefinition{
+				Target: proptools.StringPtr("/vendor"),
+				Name:   proptools.StringPtr("system/vendor"),
+			},
+			filesystem.SymlinkDefinition{
+				Target: proptools.StringPtr("/system_dlkm/lib/modules"),
+				Name:   proptools.StringPtr("system/lib/modules"),
+			},
+		}
+		fsProps.Base_dir = proptools.StringPtr("system")
 	case "system_ext":
 		fsProps.Fsverity.Inputs = []string{
 			"framework/*",
@@ -460,9 +197,28 @@
 			},
 		}
 		fsProps.Base_dir = proptools.StringPtr("vendor")
+	case "odm":
+		fsProps.Symlinks = []filesystem.SymlinkDefinition{
+			filesystem.SymlinkDefinition{
+				Target: proptools.StringPtr("/odm_dlkm/lib/modules"),
+				Name:   proptools.StringPtr("odm/lib/modules"),
+			},
+		}
+		fsProps.Base_dir = proptools.StringPtr("odm")
+	case "userdata":
+		fsProps.Base_dir = proptools.StringPtr("data")
+
 	}
 }
 
+var (
+	dlkmPartitions = []string{
+		"system_dlkm",
+		"vendor_dlkm",
+		"odm_dlkm",
+	}
+)
+
 // Creates a soong module to build the given partition. Returns false if we can't support building
 // it.
 func (f *filesystemCreator) createPartition(ctx android.LoadHookContext, partitionType string) bool {
@@ -473,6 +229,15 @@
 		return false
 	}
 
+	if partitionType == "vendor" || partitionType == "product" {
+		fsProps.Linker_config.Gen_linker_config = proptools.BoolPtr(true)
+		fsProps.Linker_config.Linker_config_srcs = f.createLinkerConfigSourceFilegroups(ctx, partitionType)
+	}
+
+	if android.InList(partitionType, dlkmPartitions) {
+		f.createPrebuiltKernelModules(ctx, partitionType)
+	}
+
 	var module android.Module
 	if partitionType == "system" {
 		module = ctx.CreateModule(filesystem.SystemImageFactory, baseProps, fsProps)
@@ -483,36 +248,172 @@
 	}
 	module.HideFromMake()
 	if partitionType == "vendor" {
-		// Create a build prop for vendor
-		vendorBuildProps := &struct {
-			Name           *string
-			Vendor         *bool
-			Stem           *string
-			Product_config *string
-		}{
-			Name:           proptools.StringPtr(generatedModuleName(ctx.Config(), "vendor-build.prop")),
-			Vendor:         proptools.BoolPtr(true),
-			Stem:           proptools.StringPtr("build.prop"),
-			Product_config: proptools.StringPtr(":product_config"),
-		}
-		vendorBuildProp := ctx.CreateModule(
-			android.BuildPropFactory,
-			vendorBuildProps,
-		)
-		vendorBuildProp.HideFromMake()
+		f.createVendorBuildProp(ctx)
 	}
 	return true
 }
 
+// createPrebuiltKernelModules creates `prebuilt_kernel_modules`. These modules will be added to deps of the
+// autogenerated *_dlkm filsystem modules. Each _dlkm partition should have a single prebuilt_kernel_modules dependency.
+// This ensures that the depmod artifacts (modules.* installed in /lib/modules/) are generated with a complete view.
+func (f *filesystemCreator) createPrebuiltKernelModules(ctx android.LoadHookContext, partitionType string) {
+	fsGenState := ctx.Config().Get(fsGenStateOnceKey).(*FsGenState)
+	name := generatedModuleName(ctx.Config(), fmt.Sprintf("%s-kernel-modules", partitionType))
+	props := &struct {
+		Name                 *string
+		Srcs                 []string
+		System_deps          []string
+		System_dlkm_specific *bool
+		Vendor_dlkm_specific *bool
+		Odm_dlkm_specific    *bool
+		Load_by_default      *bool
+		Blocklist_file       *string
+	}{
+		Name: proptools.StringPtr(name),
+	}
+	switch partitionType {
+	case "system_dlkm":
+		props.Srcs = ctx.Config().ProductVariables().PartitionVarsForSoongMigrationOnlyDoNotUse.SystemKernelModules
+		props.System_dlkm_specific = proptools.BoolPtr(true)
+		if len(ctx.Config().ProductVariables().PartitionVarsForSoongMigrationOnlyDoNotUse.SystemKernelLoadModules) == 0 {
+			// Create empty modules.load file for system
+			// https://source.corp.google.com/h/googleplex-android/platform/build/+/ef55daac9954896161b26db4f3ef1781b5a5694c:core/Makefile;l=695-700;drc=549fe2a5162548bd8b47867d35f907eb22332023;bpv=1;bpt=0
+			props.Load_by_default = proptools.BoolPtr(false)
+		}
+		if blocklistFile := ctx.Config().ProductVariables().PartitionVarsForSoongMigrationOnlyDoNotUse.SystemKernelBlocklistFile; blocklistFile != "" {
+			props.Blocklist_file = proptools.StringPtr(blocklistFile)
+		}
+	case "vendor_dlkm":
+		props.Srcs = ctx.Config().ProductVariables().PartitionVarsForSoongMigrationOnlyDoNotUse.VendorKernelModules
+		if len(ctx.Config().ProductVariables().PartitionVarsForSoongMigrationOnlyDoNotUse.SystemKernelModules) > 0 {
+			props.System_deps = []string{":" + generatedModuleName(ctx.Config(), "system_dlkm-kernel-modules") + "{.modules}"}
+		}
+		props.Vendor_dlkm_specific = proptools.BoolPtr(true)
+		if blocklistFile := ctx.Config().ProductVariables().PartitionVarsForSoongMigrationOnlyDoNotUse.VendorKernelBlocklistFile; blocklistFile != "" {
+			props.Blocklist_file = proptools.StringPtr(blocklistFile)
+		}
+	case "odm_dlkm":
+		props.Srcs = ctx.Config().ProductVariables().PartitionVarsForSoongMigrationOnlyDoNotUse.OdmKernelModules
+		props.Odm_dlkm_specific = proptools.BoolPtr(true)
+		if blocklistFile := ctx.Config().ProductVariables().PartitionVarsForSoongMigrationOnlyDoNotUse.OdmKernelBlocklistFile; blocklistFile != "" {
+			props.Blocklist_file = proptools.StringPtr(blocklistFile)
+		}
+	default:
+		ctx.ModuleErrorf("DLKM is not supported for %s\n", partitionType)
+	}
+
+	if len(props.Srcs) == 0 {
+		return // do not generate `prebuilt_kernel_modules` if there are no sources
+	}
+
+	kernelModule := ctx.CreateModuleInDirectory(
+		kernel.PrebuiltKernelModulesFactory,
+		".", // create in root directory for now
+		props,
+	)
+	kernelModule.HideFromMake()
+	// Add to deps
+	(*fsGenState.fsDeps[partitionType])[name] = defaultDepCandidateProps(ctx.Config())
+}
+
+// Create a build_prop and android_info module. This will be used to create /vendor/build.prop
+func (f *filesystemCreator) createVendorBuildProp(ctx android.LoadHookContext) {
+	// Create a android_info for vendor
+	// The board info files might be in a directory outside the root soong namespace, so create
+	// the module in "."
+	partitionVars := ctx.Config().ProductVariables().PartitionVarsForSoongMigrationOnlyDoNotUse
+	androidInfoProps := &struct {
+		Name                  *string
+		Board_info_files      []string
+		Bootloader_board_name *string
+	}{
+		Name:             proptools.StringPtr(generatedModuleName(ctx.Config(), "android-info.prop")),
+		Board_info_files: partitionVars.BoardInfoFiles,
+	}
+	if len(androidInfoProps.Board_info_files) == 0 {
+		androidInfoProps.Bootloader_board_name = proptools.StringPtr(partitionVars.BootLoaderBoardName)
+	}
+	androidInfoProp := ctx.CreateModuleInDirectory(
+		android.AndroidInfoFactory,
+		".",
+		androidInfoProps,
+	)
+	androidInfoProp.HideFromMake()
+	// Create a build prop for vendor
+	vendorBuildProps := &struct {
+		Name           *string
+		Vendor         *bool
+		Stem           *string
+		Product_config *string
+		Android_info   *string
+	}{
+		Name:           proptools.StringPtr(generatedModuleName(ctx.Config(), "vendor-build.prop")),
+		Vendor:         proptools.BoolPtr(true),
+		Stem:           proptools.StringPtr("build.prop"),
+		Product_config: proptools.StringPtr(":product_config"),
+		Android_info:   proptools.StringPtr(":" + androidInfoProp.Name()),
+	}
+	vendorBuildProp := ctx.CreateModule(
+		android.BuildPropFactory,
+		vendorBuildProps,
+	)
+	vendorBuildProp.HideFromMake()
+}
+
+// createLinkerConfigSourceFilegroups creates filegroup modules to generate linker.config.pb for the following partitions
+// 1. vendor: Using PRODUCT_VENDOR_LINKER_CONFIG_FRAGMENTS (space separated file list)
+// 1. product: Using PRODUCT_PRODUCT_LINKER_CONFIG_FRAGMENTS (space separated file list)
+// It creates a filegroup for each file in the fragment list
+// The filegroup modules are then added to `linker_config_srcs` of the autogenerated vendor `android_filesystem`.
+func (f *filesystemCreator) createLinkerConfigSourceFilegroups(ctx android.LoadHookContext, partitionType string) []string {
+	ret := []string{}
+	partitionVars := ctx.Config().ProductVariables().PartitionVarsForSoongMigrationOnlyDoNotUse
+	var linkerConfigSrcs []string
+	if partitionType == "vendor" {
+		linkerConfigSrcs = android.FirstUniqueStrings(partitionVars.VendorLinkerConfigSrcs)
+	} else if partitionType == "product" {
+		linkerConfigSrcs = android.FirstUniqueStrings(partitionVars.ProductLinkerConfigSrcs)
+	} else {
+		ctx.ModuleErrorf("linker.config.pb is only supported for vendor and product partitions. For system partition, use `android_system_image`")
+	}
+
+	if len(linkerConfigSrcs) > 0 {
+		// Create a filegroup, and add `:<filegroup_name>` to ret.
+		for index, linkerConfigSrc := range linkerConfigSrcs {
+			dir := filepath.Dir(linkerConfigSrc)
+			base := filepath.Base(linkerConfigSrc)
+			fgName := generatedModuleName(ctx.Config(), fmt.Sprintf("%s-linker-config-src%s", partitionType, strconv.Itoa(index)))
+			srcs := []string{base}
+			fgProps := &struct {
+				Name *string
+				Srcs proptools.Configurable[[]string]
+			}{
+				Name: proptools.StringPtr(fgName),
+				Srcs: proptools.NewSimpleConfigurable(srcs),
+			}
+			ctx.CreateModuleInDirectory(
+				android.FileGroupFactory,
+				dir,
+				fgProps,
+			)
+			ret = append(ret, ":"+fgName)
+		}
+	}
+	return ret
+}
+
 type filesystemBaseProperty struct {
 	Name             *string
 	Compile_multilib *string
+	Visibility       []string
 }
 
 func generateBaseProps(namePtr *string) *filesystemBaseProperty {
 	return &filesystemBaseProperty{
 		Name:             namePtr,
 		Compile_multilib: proptools.StringPtr("both"),
+		// The vbmeta modules are currently in the root directory and depend on the partitions
+		Visibility: []string{"//.", "//build/soong:__subpackages__"},
 	}
 }
 
@@ -520,13 +421,20 @@
 	fsProps := &filesystem.FilesystemProperties{}
 
 	partitionVars := ctx.Config().ProductVariables().PartitionVarsForSoongMigrationOnlyDoNotUse
-	specificPartitionVars := partitionVars.PartitionQualifiedVariables[partitionType]
-
-	// BOARD_SYSTEMIMAGE_FILE_SYSTEM_TYPE
-	fsType := specificPartitionVars.BoardFileSystemType
+	var specificPartitionVars android.PartitionQualifiedVariablesType
+	var boardAvbEnable bool
+	var fsType string
+	if strings.Contains(partitionType, "ramdisk") {
+		fsType = "compressed_cpio"
+	} else {
+		specificPartitionVars = partitionVars.PartitionQualifiedVariables[partitionType]
+		boardAvbEnable = partitionVars.BoardAvbEnable
+		fsType = specificPartitionVars.BoardFileSystemType
+	}
 	if fsType == "" {
 		fsType = "ext4" //default
 	}
+
 	fsProps.Type = proptools.StringPtr(fsType)
 	if filesystem.GetFsTypeFromString(ctx, *fsProps.Type).IsUnknown() {
 		// Currently the android_filesystem module type only supports a handful of FS types like ext4, erofs
@@ -538,7 +446,7 @@
 	fsProps.Unchecked_module = proptools.BoolPtr(true)
 
 	// BOARD_AVB_ENABLE
-	fsProps.Use_avb = proptools.BoolPtr(partitionVars.BoardAvbEnable)
+	fsProps.Use_avb = proptools.BoolPtr(boardAvbEnable)
 	// BOARD_AVB_KEY_PATH
 	fsProps.Avb_private_key = proptools.StringPtr(specificPartitionVars.BoardAvbKeyPath)
 	// BOARD_AVB_ALGORITHM
@@ -579,18 +487,13 @@
 		ctx.ModuleErrorf("Expected module %s to provide FileysystemInfo", partitionModuleName)
 	}
 	makeFileList := android.PathForArbitraryOutput(ctx, fmt.Sprintf("target/product/%s/obj/PACKAGING/%s_intermediates/file_list.txt", ctx.Config().DeviceName(), partitionType))
-	// For now, don't allowlist anything. The test will fail, but that's fine in the current
-	// early stages where we're just figuring out what we need
-	emptyAllowlistFile := android.PathForModuleOut(ctx, fmt.Sprintf("allowlist_%s.txt", partitionModuleName))
-	android.WriteFileRule(ctx, emptyAllowlistFile, "")
 	diffTestResultFile := android.PathForModuleOut(ctx, fmt.Sprintf("diff_test_%s.txt", partitionModuleName))
 
 	builder := android.NewRuleBuilder(pctx, ctx)
 	builder.Command().BuiltTool("file_list_diff").
 		Input(makeFileList).
 		Input(filesystemInfo.FileListFile).
-		Text(partitionModuleName).
-		FlagWithInput("--allowlists ", emptyAllowlistFile)
+		Text(partitionModuleName)
 	builder.Command().Text("touch").Output(diffTestResultFile)
 	builder.Build(partitionModuleName+" diff test", partitionModuleName+" diff test")
 	return diffTestResultFile
@@ -608,16 +511,42 @@
 	return file
 }
 
+func createVbmetaDiff(ctx android.ModuleContext, vbmetaModuleName string, vbmetaPartitionName string) android.Path {
+	vbmetaModule := ctx.GetDirectDepWithTag(vbmetaModuleName, generatedVbmetaPartitionDepTag)
+	outputFilesProvider, ok := android.OtherModuleProvider(ctx, vbmetaModule, android.OutputFilesProvider)
+	if !ok {
+		ctx.ModuleErrorf("Expected module %s to provide OutputFiles", vbmetaModule)
+	}
+	if len(outputFilesProvider.DefaultOutputFiles) != 1 {
+		ctx.ModuleErrorf("Expected 1 output file from module %s", vbmetaModule)
+	}
+	soongVbMetaFile := outputFilesProvider.DefaultOutputFiles[0]
+	makeVbmetaFile := android.PathForArbitraryOutput(ctx, fmt.Sprintf("target/product/%s/%s.img", ctx.Config().DeviceName(), vbmetaPartitionName))
+
+	diffTestResultFile := android.PathForModuleOut(ctx, fmt.Sprintf("diff_test_%s.txt", vbmetaModuleName))
+	builder := android.NewRuleBuilder(pctx, ctx)
+	builder.Command().Text("diff").
+		Input(soongVbMetaFile).
+		Input(makeVbmetaFile)
+	builder.Command().Text("touch").Output(diffTestResultFile)
+	builder.Build(vbmetaModuleName+" diff test", vbmetaModuleName+" diff test")
+	return diffTestResultFile
+}
+
 type systemImageDepTagType struct {
 	blueprint.BaseDependencyTag
 }
 
 var generatedFilesystemDepTag systemImageDepTagType
+var generatedVbmetaPartitionDepTag systemImageDepTagType
 
 func (f *filesystemCreator) DepsMutator(ctx android.BottomUpMutatorContext) {
 	for _, partitionType := range f.properties.Generated_partition_types {
 		ctx.AddDependency(ctx.Module(), generatedFilesystemDepTag, generatedModuleNameForPartition(ctx.Config(), partitionType))
 	}
+	for _, vbmetaModule := range f.properties.Vbmeta_module_names {
+		ctx.AddDependency(ctx.Module(), generatedVbmetaPartitionDepTag, vbmetaModule)
+	}
 }
 
 func (f *filesystemCreator) GenerateAndroidBuildActions(ctx android.ModuleContext) {
@@ -647,6 +576,11 @@
 		diffTestFiles = append(diffTestFiles, diffTestFile)
 		ctx.Phony(fmt.Sprintf("soong_generated_%s_filesystem_test", partitionType), diffTestFile)
 	}
+	for i, vbmetaModule := range f.properties.Vbmeta_module_names {
+		diffTestFile := createVbmetaDiff(ctx, vbmetaModule, f.properties.Vbmeta_partition_names[i])
+		diffTestFiles = append(diffTestFiles, diffTestFile)
+		ctx.Phony(fmt.Sprintf("soong_generated_%s_filesystem_test", f.properties.Vbmeta_partition_names[i]), diffTestFile)
+	}
 	ctx.Phony("soong_generated_filesystem_tests", diffTestFiles...)
 }
 
@@ -655,9 +589,6 @@
 	if !fsTypeSupported {
 		return ""
 	}
-	if partitionType == "vendor" {
-		return "" // TODO: Handle struct props
-	}
 
 	baseProps := generateBaseProps(proptools.StringPtr(generatedModuleNameForPartition(ctx.Config(), partitionType)))
 	deps := ctx.Config().Get(fsGenStateOnceKey).(*FsGenState).fsDeps[partitionType]
@@ -665,7 +596,8 @@
 
 	result, err := proptools.RepackProperties([]interface{}{baseProps, fsProps, depProps})
 	if err != nil {
-		ctx.ModuleErrorf(err.Error())
+		ctx.ModuleErrorf("%s", err.Error())
+		return ""
 	}
 
 	moduleType := "android_filesystem"
diff --git a/fsgen/fsgen_mutators.go b/fsgen/fsgen_mutators.go
new file mode 100644
index 0000000..e9fd513
--- /dev/null
+++ b/fsgen/fsgen_mutators.go
@@ -0,0 +1,382 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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 fsgen
+
+import (
+	"fmt"
+	"slices"
+	"strings"
+	"sync"
+
+	"android/soong/android"
+
+	"github.com/google/blueprint/proptools"
+)
+
+func RegisterCollectFileSystemDepsMutators(ctx android.RegisterMutatorsContext) {
+	ctx.BottomUp("fs_collect_deps", collectDepsMutator).MutatesGlobalState()
+	ctx.BottomUp("fs_set_deps", setDepsMutator)
+}
+
+var fsGenStateOnceKey = android.NewOnceKey("FsGenState")
+var fsGenRemoveOverridesOnceKey = android.NewOnceKey("FsGenRemoveOverrides")
+
+// Map of partition module name to its partition that may be generated by Soong.
+// Note that it is not guaranteed that all modules returned by this function are successfully
+// created.
+func getAllSoongGeneratedPartitionNames(config android.Config, partitions []string) map[string]string {
+	ret := map[string]string{}
+	for _, partition := range partitions {
+		ret[generatedModuleNameForPartition(config, partition)] = partition
+	}
+	return ret
+}
+
+type depCandidateProps struct {
+	Namespace string
+	Multilib  string
+	Arch      []android.ArchType
+}
+
+// Map of module name to depCandidateProps
+type multilibDeps map[string]*depCandidateProps
+
+// Information necessary to generate the filesystem modules, including details about their
+// dependencies
+type FsGenState struct {
+	// List of modules in `PRODUCT_PACKAGES` and `PRODUCT_PACKAGES_DEBUG`
+	depCandidates []string
+	// Map of names of partition to the information of modules to be added as deps
+	fsDeps map[string]*multilibDeps
+	// List of name of partitions to be generated by the filesystem_creator module
+	soongGeneratedPartitions []string
+	// Mutex to protect the fsDeps
+	fsDepsMutex sync.Mutex
+	// Map of _all_ soong module names to their corresponding installation properties
+	moduleToInstallationProps map[string]installationProperties
+}
+
+type installationProperties struct {
+	Required  []string
+	Overrides []string
+}
+
+func defaultDepCandidateProps(config android.Config) *depCandidateProps {
+	return &depCandidateProps{
+		Namespace: ".",
+		Arch:      []android.ArchType{config.BuildArch},
+	}
+}
+
+func generatedPartitions(ctx android.LoadHookContext) []string {
+	generatedPartitions := []string{"system", "ramdisk"}
+	if ctx.DeviceConfig().SystemExtPath() == "system_ext" {
+		generatedPartitions = append(generatedPartitions, "system_ext")
+	}
+	if ctx.DeviceConfig().BuildingVendorImage() && ctx.DeviceConfig().VendorPath() == "vendor" {
+		generatedPartitions = append(generatedPartitions, "vendor")
+	}
+	if ctx.DeviceConfig().BuildingProductImage() && ctx.DeviceConfig().ProductPath() == "product" {
+		generatedPartitions = append(generatedPartitions, "product")
+	}
+	if ctx.DeviceConfig().BuildingOdmImage() && ctx.DeviceConfig().OdmPath() == "odm" {
+		generatedPartitions = append(generatedPartitions, "odm")
+	}
+	if ctx.DeviceConfig().BuildingUserdataImage() && ctx.DeviceConfig().UserdataPath() == "data" {
+		generatedPartitions = append(generatedPartitions, "userdata")
+	}
+	if ctx.Config().ProductVariables().PartitionVarsForSoongMigrationOnlyDoNotUse.BuildingSystemDlkmImage {
+		generatedPartitions = append(generatedPartitions, "system_dlkm")
+	}
+	if ctx.Config().ProductVariables().PartitionVarsForSoongMigrationOnlyDoNotUse.BuildingVendorDlkmImage {
+		generatedPartitions = append(generatedPartitions, "vendor_dlkm")
+	}
+	if ctx.Config().ProductVariables().PartitionVarsForSoongMigrationOnlyDoNotUse.BuildingOdmDlkmImage {
+		generatedPartitions = append(generatedPartitions, "odm_dlkm")
+	}
+
+	return generatedPartitions
+}
+
+func createFsGenState(ctx android.LoadHookContext, generatedPrebuiltEtcModuleNames []string) *FsGenState {
+	return ctx.Config().Once(fsGenStateOnceKey, func() interface{} {
+		partitionVars := ctx.Config().ProductVariables().PartitionVarsForSoongMigrationOnlyDoNotUse
+		candidates := android.FirstUniqueStrings(android.Concat(partitionVars.ProductPackages, partitionVars.ProductPackagesDebug))
+		candidates = android.Concat(candidates, generatedPrebuiltEtcModuleNames)
+
+		return &FsGenState{
+			depCandidates: candidates,
+			fsDeps: map[string]*multilibDeps{
+				// These additional deps are added according to the cuttlefish system image bp.
+				"system": {
+					"com.android.apex.cts.shim.v1_prebuilt":     defaultDepCandidateProps(ctx.Config()),
+					"dex_bootjars":                              defaultDepCandidateProps(ctx.Config()),
+					"framework_compatibility_matrix.device.xml": defaultDepCandidateProps(ctx.Config()),
+					"init.environ.rc-soong":                     defaultDepCandidateProps(ctx.Config()),
+					"libcompiler_rt":                            defaultDepCandidateProps(ctx.Config()),
+					"libdmabufheap":                             defaultDepCandidateProps(ctx.Config()),
+					"libgsi":                                    defaultDepCandidateProps(ctx.Config()),
+					"llndk.libraries.txt":                       defaultDepCandidateProps(ctx.Config()),
+					"logpersist.start":                          defaultDepCandidateProps(ctx.Config()),
+					"update_engine_sideload":                    defaultDepCandidateProps(ctx.Config()),
+				},
+				"vendor": {
+					"fs_config_files_vendor":                               defaultDepCandidateProps(ctx.Config()),
+					"fs_config_dirs_vendor":                                defaultDepCandidateProps(ctx.Config()),
+					generatedModuleName(ctx.Config(), "vendor-build.prop"): defaultDepCandidateProps(ctx.Config()),
+				},
+				"odm": {
+					// fs_config_* files are automatically installed for all products with odm partitions.
+					// https://cs.android.com/android/_/android/platform/build/+/e4849e87ab660b59a6501b3928693db065ee873b:tools/fs_config/Android.mk;l=34;drc=8d6481b92c4b4e9b9f31a61545b6862090fcc14b;bpv=1;bpt=0
+					"fs_config_files_odm": defaultDepCandidateProps(ctx.Config()),
+					"fs_config_dirs_odm":  defaultDepCandidateProps(ctx.Config()),
+				},
+				"product": {},
+				"system_ext": {
+					// VNDK apexes are automatically included.
+					// This hardcoded list will need to be updated if `PRODUCT_EXTRA_VNDK_VERSIONS` is updated.
+					// https://cs.android.com/android/_/android/platform/build/+/adba533072b00c53ac0f198c550a3cbd7a00e4cd:core/main.mk;l=984;bpv=1;bpt=0;drc=174db7b179592cf07cbfd2adb0119486fda911e7
+					"com.android.vndk.v30": defaultDepCandidateProps(ctx.Config()),
+					"com.android.vndk.v31": defaultDepCandidateProps(ctx.Config()),
+					"com.android.vndk.v32": defaultDepCandidateProps(ctx.Config()),
+					"com.android.vndk.v33": defaultDepCandidateProps(ctx.Config()),
+					"com.android.vndk.v34": defaultDepCandidateProps(ctx.Config()),
+				},
+				"userdata": {},
+				"system_dlkm": {
+					// these are phony required deps of the phony fs_config_dirs_nonsystem
+					"fs_config_dirs_system_dlkm":  defaultDepCandidateProps(ctx.Config()),
+					"fs_config_files_system_dlkm": defaultDepCandidateProps(ctx.Config()),
+					// build props are automatically added to `ALL_DEFAULT_INSTALLED_MODULES`
+					"system_dlkm-build.prop": defaultDepCandidateProps(ctx.Config()),
+				},
+				"vendor_dlkm": {
+					"fs_config_dirs_vendor_dlkm":  defaultDepCandidateProps(ctx.Config()),
+					"fs_config_files_vendor_dlkm": defaultDepCandidateProps(ctx.Config()),
+					"vendor_dlkm-build.prop":      defaultDepCandidateProps(ctx.Config()),
+				},
+				"odm_dlkm": {
+					"fs_config_dirs_odm_dlkm":  defaultDepCandidateProps(ctx.Config()),
+					"fs_config_files_odm_dlkm": defaultDepCandidateProps(ctx.Config()),
+					"odm_dlkm-build.prop":      defaultDepCandidateProps(ctx.Config()),
+				},
+				"ramdisk": {},
+			},
+			fsDepsMutex:               sync.Mutex{},
+			moduleToInstallationProps: map[string]installationProperties{},
+		}
+	}).(*FsGenState)
+}
+
+func checkDepModuleInMultipleNamespaces(mctx android.BottomUpMutatorContext, foundDeps multilibDeps, module string, partitionName string) {
+	otherNamespace := mctx.Namespace().Path
+	if val, found := foundDeps[module]; found && otherNamespace != "." && !android.InList(val.Namespace, []string{".", otherNamespace}) {
+		mctx.ModuleErrorf("found in multiple namespaces(%s and %s) when including in %s partition", val.Namespace, otherNamespace, partitionName)
+	}
+}
+
+func appendDepIfAppropriate(mctx android.BottomUpMutatorContext, deps *multilibDeps, installPartition string) {
+	moduleName := mctx.ModuleName()
+	checkDepModuleInMultipleNamespaces(mctx, *deps, moduleName, installPartition)
+	if _, ok := (*deps)[moduleName]; ok {
+		// Prefer the namespace-specific module over the platform module
+		if mctx.Namespace().Path != "." {
+			(*deps)[moduleName].Namespace = mctx.Namespace().Path
+		}
+		(*deps)[moduleName].Arch = append((*deps)[moduleName].Arch, mctx.Module().Target().Arch.ArchType)
+	} else {
+		multilib, _ := mctx.Module().DecodeMultilib(mctx)
+		(*deps)[moduleName] = &depCandidateProps{
+			Namespace: mctx.Namespace().Path,
+			Multilib:  multilib,
+			Arch:      []android.ArchType{mctx.Module().Target().Arch.ArchType},
+		}
+	}
+}
+
+func collectDepsMutator(mctx android.BottomUpMutatorContext) {
+	m := mctx.Module()
+	if m.Target().Os.Class != android.Device {
+		return
+	}
+	fsGenState := mctx.Config().Get(fsGenStateOnceKey).(*FsGenState)
+
+	fsGenState.fsDepsMutex.Lock()
+	defer fsGenState.fsDepsMutex.Unlock()
+
+	if slices.Contains(fsGenState.depCandidates, mctx.ModuleName()) {
+		installPartition := m.PartitionTag(mctx.DeviceConfig())
+		// Only add the module as dependency when:
+		// - its enabled
+		// - its namespace is included in PRODUCT_SOONG_NAMESPACES
+		if m.Enabled(mctx) && m.ExportedToMake() {
+			appendDepIfAppropriate(mctx, fsGenState.fsDeps[installPartition], installPartition)
+		}
+	}
+	// store the map of module to (required,overrides) even if the module is not in PRODUCT_PACKAGES.
+	// the module might be installed transitively.
+	if m.Enabled(mctx) && m.ExportedToMake() {
+		fsGenState.moduleToInstallationProps[m.Name()] = installationProperties{
+			Required:  m.RequiredModuleNames(mctx),
+			Overrides: m.Overrides(),
+		}
+	}
+}
+
+type depsStruct struct {
+	Deps []string
+}
+
+type multilibDepsStruct struct {
+	Common   depsStruct
+	Lib32    depsStruct
+	Lib64    depsStruct
+	Both     depsStruct
+	Prefer32 depsStruct
+}
+
+type packagingPropsStruct struct {
+	High_priority_deps []string
+	Deps               []string
+	Multilib           multilibDepsStruct
+}
+
+func fullyQualifiedModuleName(moduleName, namespace string) string {
+	if namespace == "." {
+		return moduleName
+	}
+	return fmt.Sprintf("//%s:%s", namespace, moduleName)
+}
+
+func getBitness(archTypes []android.ArchType) (ret []string) {
+	for _, archType := range archTypes {
+		if archType.Multilib == "" {
+			ret = append(ret, android.COMMON_VARIANT)
+		} else {
+			ret = append(ret, archType.Bitness())
+		}
+	}
+	return ret
+}
+
+func setDepsMutator(mctx android.BottomUpMutatorContext) {
+	removeOverriddenDeps(mctx)
+	fsGenState := mctx.Config().Get(fsGenStateOnceKey).(*FsGenState)
+	fsDeps := fsGenState.fsDeps
+	soongGeneratedPartitionMap := getAllSoongGeneratedPartitionNames(mctx.Config(), fsGenState.soongGeneratedPartitions)
+	m := mctx.Module()
+	if partition, ok := soongGeneratedPartitionMap[m.Name()]; ok {
+		depsStruct := generateDepStruct(*fsDeps[partition])
+		if err := proptools.AppendMatchingProperties(m.GetProperties(), depsStruct, nil); err != nil {
+			mctx.ModuleErrorf(err.Error())
+		}
+	}
+}
+
+// removeOverriddenDeps collects PRODUCT_PACKAGES and (transitive) required deps.
+// it then removes any modules which appear in `overrides` of the above list.
+func removeOverriddenDeps(mctx android.BottomUpMutatorContext) {
+	mctx.Config().Once(fsGenRemoveOverridesOnceKey, func() interface{} {
+		fsGenState := mctx.Config().Get(fsGenStateOnceKey).(*FsGenState)
+		fsDeps := fsGenState.fsDeps
+		overridden := map[string]bool{}
+		allDeps := []string{}
+
+		// Step 1: Initialization: Append PRODUCT_PACKAGES to the queue
+		for _, fsDep := range fsDeps {
+			for depName, _ := range *fsDep {
+				allDeps = append(allDeps, depName)
+			}
+		}
+
+		// Step 2: Process the queue, and add required modules to the queue.
+		i := 0
+		for {
+			if i == len(allDeps) {
+				break
+			}
+			depName := allDeps[i]
+			for _, overrides := range fsGenState.moduleToInstallationProps[depName].Overrides {
+				overridden[overrides] = true
+			}
+			// add required dep to the queue.
+			allDeps = append(allDeps, fsGenState.moduleToInstallationProps[depName].Required...)
+			i += 1
+		}
+
+		// Step 3: Delete all the overridden modules.
+		for overridden, _ := range overridden {
+			for partition, _ := range fsDeps {
+				delete(*fsDeps[partition], overridden)
+			}
+		}
+		return nil
+	})
+}
+
+var HighPriorityDeps = []string{}
+
+func isHighPriorityDep(depName string) bool {
+	for _, highPriorityDeps := range HighPriorityDeps {
+		if strings.HasPrefix(depName, highPriorityDeps) {
+			return true
+		}
+	}
+	return false
+}
+
+func generateDepStruct(deps map[string]*depCandidateProps) *packagingPropsStruct {
+	depsStruct := packagingPropsStruct{}
+	for depName, depProps := range deps {
+		bitness := getBitness(depProps.Arch)
+		fullyQualifiedDepName := fullyQualifiedModuleName(depName, depProps.Namespace)
+		if isHighPriorityDep(depName) {
+			depsStruct.High_priority_deps = append(depsStruct.High_priority_deps, fullyQualifiedDepName)
+		} else if android.InList("32", bitness) && android.InList("64", bitness) {
+			// If both 32 and 64 bit variants are enabled for this module
+			switch depProps.Multilib {
+			case string(android.MultilibBoth):
+				depsStruct.Multilib.Both.Deps = append(depsStruct.Multilib.Both.Deps, fullyQualifiedDepName)
+			case string(android.MultilibCommon), string(android.MultilibFirst):
+				depsStruct.Deps = append(depsStruct.Deps, fullyQualifiedDepName)
+			case "32":
+				depsStruct.Multilib.Lib32.Deps = append(depsStruct.Multilib.Lib32.Deps, fullyQualifiedDepName)
+			case "64", "darwin_universal":
+				depsStruct.Multilib.Lib64.Deps = append(depsStruct.Multilib.Lib64.Deps, fullyQualifiedDepName)
+			case "prefer32", "first_prefer32":
+				depsStruct.Multilib.Prefer32.Deps = append(depsStruct.Multilib.Prefer32.Deps, fullyQualifiedDepName)
+			default:
+				depsStruct.Multilib.Both.Deps = append(depsStruct.Multilib.Both.Deps, fullyQualifiedDepName)
+			}
+		} else if android.InList("64", bitness) {
+			// If only 64 bit variant is enabled
+			depsStruct.Multilib.Lib64.Deps = append(depsStruct.Multilib.Lib64.Deps, fullyQualifiedDepName)
+		} else if android.InList("32", bitness) {
+			// If only 32 bit variant is enabled
+			depsStruct.Multilib.Lib32.Deps = append(depsStruct.Multilib.Lib32.Deps, fullyQualifiedDepName)
+		} else {
+			// If only common variant is enabled
+			depsStruct.Multilib.Common.Deps = append(depsStruct.Multilib.Common.Deps, fullyQualifiedDepName)
+		}
+	}
+	depsStruct.Deps = android.SortedUniqueStrings(depsStruct.Deps)
+	depsStruct.Multilib.Lib32.Deps = android.SortedUniqueStrings(depsStruct.Multilib.Lib32.Deps)
+	depsStruct.Multilib.Lib64.Deps = android.SortedUniqueStrings(depsStruct.Multilib.Lib64.Deps)
+	depsStruct.Multilib.Prefer32.Deps = android.SortedUniqueStrings(depsStruct.Multilib.Prefer32.Deps)
+	depsStruct.Multilib.Both.Deps = android.SortedUniqueStrings(depsStruct.Multilib.Both.Deps)
+	depsStruct.Multilib.Common.Deps = android.SortedUniqueStrings(depsStruct.Multilib.Common.Deps)
+
+	return &depsStruct
+}
diff --git a/fsgen/prebuilt_etc_modules_gen.go b/fsgen/prebuilt_etc_modules_gen.go
new file mode 100644
index 0000000..983dcfb
--- /dev/null
+++ b/fsgen/prebuilt_etc_modules_gen.go
@@ -0,0 +1,346 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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 fsgen
+
+import (
+	"android/soong/android"
+	"android/soong/etc"
+	"fmt"
+	"path/filepath"
+	"strings"
+
+	"github.com/google/blueprint/proptools"
+)
+
+type srcBaseFileInstallBaseFileTuple struct {
+	srcBaseFile     string
+	installBaseFile string
+}
+
+// prebuilt src files grouped by the install partitions.
+// Each groups are a mapping of the relative install path to the name of the files
+type prebuiltSrcGroupByInstallPartition struct {
+	system     map[string][]srcBaseFileInstallBaseFileTuple
+	system_ext map[string][]srcBaseFileInstallBaseFileTuple
+	product    map[string][]srcBaseFileInstallBaseFileTuple
+	vendor     map[string][]srcBaseFileInstallBaseFileTuple
+}
+
+func newPrebuiltSrcGroupByInstallPartition() *prebuiltSrcGroupByInstallPartition {
+	return &prebuiltSrcGroupByInstallPartition{
+		system:     map[string][]srcBaseFileInstallBaseFileTuple{},
+		system_ext: map[string][]srcBaseFileInstallBaseFileTuple{},
+		product:    map[string][]srcBaseFileInstallBaseFileTuple{},
+		vendor:     map[string][]srcBaseFileInstallBaseFileTuple{},
+	}
+}
+
+func isSubdirectory(parent, child string) bool {
+	rel, err := filepath.Rel(parent, child)
+	if err != nil {
+		return false
+	}
+	return !strings.HasPrefix(rel, "..")
+}
+
+func appendIfCorrectInstallPartition(partitionToInstallPathList []partitionToInstallPath, destPath, srcPath string, srcGroup *prebuiltSrcGroupByInstallPartition) {
+	for _, part := range partitionToInstallPathList {
+		partition := part.name
+		installPath := part.installPath
+
+		if isSubdirectory(installPath, destPath) {
+			relativeInstallPath, _ := filepath.Rel(installPath, destPath)
+			relativeInstallDir := filepath.Dir(relativeInstallPath)
+			var srcMap map[string][]srcBaseFileInstallBaseFileTuple
+			switch partition {
+			case "system":
+				srcMap = srcGroup.system
+			case "system_ext":
+				srcMap = srcGroup.system_ext
+			case "product":
+				srcMap = srcGroup.product
+			case "vendor":
+				srcMap = srcGroup.vendor
+			}
+			if srcMap != nil {
+				srcMap[relativeInstallDir] = append(srcMap[relativeInstallDir], srcBaseFileInstallBaseFileTuple{
+					srcBaseFile:     filepath.Base(srcPath),
+					installBaseFile: filepath.Base(destPath),
+				})
+			}
+			return
+		}
+	}
+}
+
+// Create a map of source files to the list of destination files from PRODUCT_COPY_FILES entries.
+// Note that the value of the map is a list of string, given that a single source file can be
+// copied to multiple files.
+// This function also checks the existence of the source files, and validates that there is no
+// multiple source files copying to the same dest file.
+func uniqueExistingProductCopyFileMap(ctx android.LoadHookContext) map[string][]string {
+	seen := make(map[string]bool)
+	filtered := make(map[string][]string)
+
+	for _, copyFilePair := range ctx.Config().ProductVariables().PartitionVarsForSoongMigrationOnlyDoNotUse.ProductCopyFiles {
+		srcDestList := strings.Split(copyFilePair, ":")
+		if len(srcDestList) < 2 {
+			ctx.ModuleErrorf("PRODUCT_COPY_FILES must follow the format \"src:dest\", got: %s", copyFilePair)
+		}
+		src, dest := srcDestList[0], srcDestList[1]
+		if _, ok := seen[dest]; !ok {
+			if optionalPath := android.ExistentPathForSource(ctx, src); optionalPath.Valid() {
+				seen[dest] = true
+				filtered[src] = append(filtered[src], dest)
+			}
+		}
+	}
+
+	return filtered
+}
+
+type partitionToInstallPath struct {
+	name        string
+	installPath string
+}
+
+func processProductCopyFiles(ctx android.LoadHookContext) map[string]*prebuiltSrcGroupByInstallPartition {
+	// Filter out duplicate dest entries and non existing src entries
+	productCopyFileMap := uniqueExistingProductCopyFileMap(ctx)
+
+	// System is intentionally added at the last to consider the scenarios where
+	// non-system partitions are installed as part of the system partition
+	partitionToInstallPathList := []partitionToInstallPath{
+		{name: "vendor", installPath: ctx.DeviceConfig().VendorPath()},
+		{name: "product", installPath: ctx.DeviceConfig().ProductPath()},
+		{name: "system_ext", installPath: ctx.DeviceConfig().SystemExtPath()},
+		{name: "system", installPath: "system"},
+	}
+
+	groupedSources := map[string]*prebuiltSrcGroupByInstallPartition{}
+	for _, src := range android.SortedKeys(productCopyFileMap) {
+		destFiles := productCopyFileMap[src]
+		srcFileDir := filepath.Dir(src)
+		if _, ok := groupedSources[srcFileDir]; !ok {
+			groupedSources[srcFileDir] = newPrebuiltSrcGroupByInstallPartition()
+		}
+		for _, dest := range destFiles {
+			appendIfCorrectInstallPartition(partitionToInstallPathList, dest, filepath.Base(src), groupedSources[srcFileDir])
+		}
+	}
+
+	return groupedSources
+}
+
+type prebuiltModuleProperties struct {
+	Name *string
+
+	Soc_specific        *bool
+	Product_specific    *bool
+	System_ext_specific *bool
+
+	Srcs []string
+	Dsts []string
+
+	No_full_install *bool
+
+	NamespaceExportedToMake bool
+
+	Visibility []string
+}
+
+// Split relative_install_path to a separate struct, because it is not supported for every
+// modules listed in [etcInstallPathToFactoryMap]
+type prebuiltSubdirProperties struct {
+	// If the base file name of the src and dst all match, dsts property does not need to be
+	// set, and only relative_install_path can be set.
+	Relative_install_path *string
+}
+
+var (
+	etcInstallPathToFactoryList = map[string]android.ModuleFactory{
+		"":                etc.PrebuiltRootFactory,
+		"avb":             etc.PrebuiltAvbFactory,
+		"bin":             etc.PrebuiltBinaryFactory,
+		"bt_firmware":     etc.PrebuiltBtFirmwareFactory,
+		"cacerts":         etc.PrebuiltEtcCaCertsFactory,
+		"dsp":             etc.PrebuiltDSPFactory,
+		"etc":             etc.PrebuiltEtcFactory,
+		"etc/dsp":         etc.PrebuiltDSPFactory,
+		"etc/firmware":    etc.PrebuiltFirmwareFactory,
+		"firmware":        etc.PrebuiltFirmwareFactory,
+		"fonts":           etc.PrebuiltFontFactory,
+		"framework":       etc.PrebuiltFrameworkFactory,
+		"lib":             etc.PrebuiltRenderScriptBitcodeFactory,
+		"lib64":           etc.PrebuiltRenderScriptBitcodeFactory,
+		"lib/rfsa":        etc.PrebuiltRFSAFactory,
+		"media":           etc.PrebuiltMediaFactory,
+		"odm":             etc.PrebuiltOdmFactory,
+		"optee":           etc.PrebuiltOpteeFactory,
+		"overlay":         etc.PrebuiltOverlayFactory,
+		"priv-app":        etc.PrebuiltPrivAppFactory,
+		"res":             etc.PrebuiltResFactory,
+		"rfs":             etc.PrebuiltRfsFactory,
+		"tts":             etc.PrebuiltVoicepackFactory,
+		"tvconfig":        etc.PrebuiltTvConfigFactory,
+		"tvservice":       etc.PrebuiltTvServiceFactory,
+		"usr/share":       etc.PrebuiltUserShareFactory,
+		"usr/hyphen-data": etc.PrebuiltUserHyphenDataFactory,
+		"usr/keylayout":   etc.PrebuiltUserKeyLayoutFactory,
+		"usr/keychars":    etc.PrebuiltUserKeyCharsFactory,
+		"usr/srec":        etc.PrebuiltUserSrecFactory,
+		"usr/idc":         etc.PrebuiltUserIdcFactory,
+		"vendor_dlkm":     etc.PrebuiltVendorDlkmFactory,
+		"wallpaper":       etc.PrebuiltWallpaperFactory,
+		"wlc_upt":         etc.PrebuiltWlcUptFactory,
+	}
+)
+
+func generatedPrebuiltEtcModuleName(partition, srcDir, destDir string, count int) string {
+	// generated module name follows the pattern:
+	// <install partition>-<src file path>-<relative install path from partition root>-<number>
+	// Note that all path separators are replaced with "_" in the name
+	moduleName := partition
+	if !android.InList(srcDir, []string{"", "."}) {
+		moduleName += fmt.Sprintf("-%s", strings.ReplaceAll(srcDir, string(filepath.Separator), "_"))
+	}
+	if !android.InList(destDir, []string{"", "."}) {
+		moduleName += fmt.Sprintf("-%s", strings.ReplaceAll(destDir, string(filepath.Separator), "_"))
+	}
+	moduleName += fmt.Sprintf("-%d", count)
+
+	return moduleName
+}
+
+func groupDestFilesBySrc(destFiles []srcBaseFileInstallBaseFileTuple) (ret map[string][]srcBaseFileInstallBaseFileTuple, maxLen int) {
+	ret = map[string][]srcBaseFileInstallBaseFileTuple{}
+	maxLen = 0
+	for _, tuple := range destFiles {
+		if _, ok := ret[tuple.srcBaseFile]; !ok {
+			ret[tuple.srcBaseFile] = []srcBaseFileInstallBaseFileTuple{}
+		}
+		ret[tuple.srcBaseFile] = append(ret[tuple.srcBaseFile], tuple)
+		maxLen = max(maxLen, len(ret[tuple.srcBaseFile]))
+	}
+	return ret, maxLen
+}
+
+func prebuiltEtcModuleProps(moduleName, partition string) prebuiltModuleProperties {
+	moduleProps := prebuiltModuleProperties{}
+	moduleProps.Name = proptools.StringPtr(moduleName)
+
+	// Set partition specific properties
+	switch partition {
+	case "system_ext":
+		moduleProps.System_ext_specific = proptools.BoolPtr(true)
+	case "product":
+		moduleProps.Product_specific = proptools.BoolPtr(true)
+	case "vendor":
+		moduleProps.Soc_specific = proptools.BoolPtr(true)
+	}
+
+	moduleProps.No_full_install = proptools.BoolPtr(true)
+	moduleProps.NamespaceExportedToMake = true
+	moduleProps.Visibility = []string{"//visibility:public"}
+
+	return moduleProps
+}
+
+func createPrebuiltEtcModulesInDirectory(ctx android.LoadHookContext, partition, srcDir, destDir string, destFiles []srcBaseFileInstallBaseFileTuple) (moduleNames []string) {
+	groupedDestFiles, maxLen := groupDestFilesBySrc(destFiles)
+
+	// Find out the most appropriate module type to generate
+	var etcInstallPathKey string
+	for _, etcInstallPath := range android.SortedKeys(etcInstallPathToFactoryList) {
+		// Do not break when found but iterate until the end to find a module with more
+		// specific install path
+		if strings.HasPrefix(destDir, etcInstallPath) {
+			etcInstallPathKey = etcInstallPath
+		}
+	}
+	relDestDirFromInstallDirBase, _ := filepath.Rel(etcInstallPathKey, destDir)
+
+	for fileIndex := range maxLen {
+		srcTuple := []srcBaseFileInstallBaseFileTuple{}
+		for _, groupedDestFile := range groupedDestFiles {
+			if len(groupedDestFile) > fileIndex {
+				srcTuple = append(srcTuple, groupedDestFile[fileIndex])
+			}
+		}
+
+		moduleName := generatedPrebuiltEtcModuleName(partition, srcDir, destDir, fileIndex)
+		moduleProps := prebuiltEtcModuleProps(moduleName, partition)
+		modulePropsPtr := &moduleProps
+		propsList := []interface{}{modulePropsPtr}
+
+		allCopyFileNamesUnchanged := true
+		var srcBaseFiles, installBaseFiles []string
+		for _, tuple := range srcTuple {
+			if tuple.srcBaseFile != tuple.installBaseFile {
+				allCopyFileNamesUnchanged = false
+			}
+			srcBaseFiles = append(srcBaseFiles, tuple.srcBaseFile)
+			installBaseFiles = append(installBaseFiles, tuple.installBaseFile)
+		}
+
+		// Set appropriate srcs, dsts, and releative_install_path based on
+		// the source and install file names
+		if allCopyFileNamesUnchanged {
+			modulePropsPtr.Srcs = srcBaseFiles
+
+			// Specify relative_install_path if it is not installed in the root directory of the
+			// partition
+			if !android.InList(relDestDirFromInstallDirBase, []string{"", "."}) {
+				propsList = append(propsList, &prebuiltSubdirProperties{
+					Relative_install_path: proptools.StringPtr(relDestDirFromInstallDirBase),
+				})
+			}
+		} else {
+			modulePropsPtr.Srcs = srcBaseFiles
+			dsts := []string{}
+			for _, installBaseFile := range installBaseFiles {
+				dsts = append(dsts, filepath.Join(relDestDirFromInstallDirBase, installBaseFile))
+			}
+			modulePropsPtr.Dsts = dsts
+		}
+
+		ctx.CreateModuleInDirectory(etcInstallPathToFactoryList[etcInstallPathKey], srcDir, propsList...)
+		moduleNames = append(moduleNames, moduleName)
+	}
+
+	return moduleNames
+}
+
+func createPrebuiltEtcModulesForPartition(ctx android.LoadHookContext, partition, srcDir string, destDirFilesMap map[string][]srcBaseFileInstallBaseFileTuple) (ret []string) {
+	for _, destDir := range android.SortedKeys(destDirFilesMap) {
+		ret = append(ret, createPrebuiltEtcModulesInDirectory(ctx, partition, srcDir, destDir, destDirFilesMap[destDir])...)
+	}
+	return ret
+}
+
+// Creates prebuilt_* modules based on the install paths and returns the list of generated
+// module names
+func createPrebuiltEtcModules(ctx android.LoadHookContext) (ret []string) {
+	groupedSources := processProductCopyFiles(ctx)
+	for _, srcDir := range android.SortedKeys(groupedSources) {
+		groupedSource := groupedSources[srcDir]
+		ret = append(ret, createPrebuiltEtcModulesForPartition(ctx, "system", srcDir, groupedSource.system)...)
+		ret = append(ret, createPrebuiltEtcModulesForPartition(ctx, "system_ext", srcDir, groupedSource.system_ext)...)
+		ret = append(ret, createPrebuiltEtcModulesForPartition(ctx, "product", srcDir, groupedSource.product)...)
+		ret = append(ret, createPrebuiltEtcModulesForPartition(ctx, "vendor", srcDir, groupedSource.vendor)...)
+	}
+
+	return ret
+}
diff --git a/fsgen/vbmeta_partitions.go b/fsgen/vbmeta_partitions.go
new file mode 100644
index 0000000..52f8ae5
--- /dev/null
+++ b/fsgen/vbmeta_partitions.go
@@ -0,0 +1,184 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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 fsgen
+
+import (
+	"android/soong/android"
+	"android/soong/filesystem"
+	"slices"
+	"strconv"
+
+	"github.com/google/blueprint/proptools"
+)
+
+type vbmetaModuleInfo struct {
+	// The name of the generated vbmeta module
+	moduleName string
+	// The name of the module that avb understands. This is the name passed to --chain_partition,
+	// and also the basename of the output file. (the output file is called partitionName + ".img")
+	partitionName string
+}
+
+// Creates the vbmeta partition and the chained vbmeta partitions. Returns the list of module names
+// that the function created. May return nil if the product isn't using avb.
+//
+// AVB is Android Verified Boot: https://source.android.com/docs/security/features/verifiedboot
+// It works by signing all the partitions, but then also including an extra metadata paritition
+// called vbmeta that depends on all the other signed partitions. This creates a requirement
+// that you update all those partitions and the vbmeta partition together, so in order to relax
+// that requirement products can set up "chained" vbmeta partitions, where a chained partition
+// like vbmeta_system might contain the avb metadata for just a few products. In cuttlefish
+// vbmeta_system contains metadata about product, system, and system_ext. Using chained partitions,
+// that group of partitions can be updated independently from the other signed partitions.
+func createVbmetaPartitions(ctx android.LoadHookContext, generatedPartitionTypes []string) []vbmetaModuleInfo {
+	partitionVars := ctx.Config().ProductVariables().PartitionVarsForSoongMigrationOnlyDoNotUse
+	// Some products seem to have BuildingVbmetaImage as true even when BoardAvbEnable is false
+	if !partitionVars.BuildingVbmetaImage || !partitionVars.BoardAvbEnable {
+		return nil
+	}
+
+	var result []vbmetaModuleInfo
+
+	var chainedPartitions []string
+	var partitionTypesHandledByChainedPartitions []string
+	for chainedName, props := range partitionVars.ChainedVbmetaPartitions {
+		chainedName = "vbmeta_" + chainedName
+		if len(props.Partitions) == 0 {
+			continue
+		}
+		if len(props.Key) == 0 {
+			ctx.ModuleErrorf("No key found for chained avb partition %q", chainedName)
+			continue
+		}
+		if len(props.Algorithm) == 0 {
+			ctx.ModuleErrorf("No algorithm found for chained avb partition %q", chainedName)
+			continue
+		}
+		if len(props.RollbackIndex) == 0 {
+			ctx.ModuleErrorf("No rollback index found for chained avb partition %q", chainedName)
+			continue
+		}
+		ril, err := strconv.ParseInt(props.RollbackIndexLocation, 10, 32)
+		if err != nil {
+			ctx.ModuleErrorf("Rollback index location must be an int, got %q", props.RollbackIndexLocation)
+			continue
+		}
+		// The default is to use the PlatformSecurityPatch, and a lot of product config files
+		// just set it to the platform security patch, so detect that and don't set the property
+		// in soong.
+		var rollbackIndex *int64
+		if props.RollbackIndex != ctx.Config().PlatformSecurityPatch() {
+			i, err := strconv.ParseInt(props.RollbackIndex, 10, 32)
+			if err != nil {
+				ctx.ModuleErrorf("Rollback index must be an int, got %q", props.RollbackIndex)
+				continue
+			}
+			rollbackIndex = &i
+		}
+
+		var partitionModules []string
+		for _, partition := range props.Partitions {
+			partitionTypesHandledByChainedPartitions = append(partitionTypesHandledByChainedPartitions, partition)
+			if !slices.Contains(generatedPartitionTypes, partition) {
+				// The partition is probably unsupported.
+				continue
+			}
+			partitionModules = append(partitionModules, generatedModuleNameForPartition(ctx.Config(), partition))
+		}
+
+		name := generatedModuleName(ctx.Config(), chainedName)
+		ctx.CreateModuleInDirectory(
+			filesystem.VbmetaFactory,
+			".", // Create in the root directory for now so its easy to get the key
+			&filesystem.VbmetaProperties{
+				Partition_name:          proptools.StringPtr(chainedName),
+				Stem:                    proptools.StringPtr(chainedName + ".img"),
+				Private_key:             proptools.StringPtr(props.Key),
+				Algorithm:               &props.Algorithm,
+				Rollback_index:          rollbackIndex,
+				Rollback_index_location: &ril,
+				Partitions:              proptools.NewSimpleConfigurable(partitionModules),
+			}, &struct {
+				Name *string
+			}{
+				Name: &name,
+			},
+		).HideFromMake()
+
+		chainedPartitions = append(chainedPartitions, name)
+
+		result = append(result, vbmetaModuleInfo{
+			moduleName:    name,
+			partitionName: chainedName,
+		})
+	}
+
+	vbmetaModuleName := generatedModuleName(ctx.Config(), "vbmeta")
+
+	var algorithm *string
+	var ri *int64
+	var key *string
+	if len(partitionVars.BoardAvbKeyPath) == 0 {
+		// Match make's defaults: https://cs.android.com/android/platform/superproject/main/+/main:build/make/core/Makefile;l=4568;drc=5b55f926830963c02ab1d2d91e46442f04ba3af0
+		key = proptools.StringPtr("external/avb/test/data/testkey_rsa4096.pem")
+		algorithm = proptools.StringPtr("SHA256_RSA4096")
+	} else {
+		key = proptools.StringPtr(partitionVars.BoardAvbKeyPath)
+		algorithm = proptools.StringPtr(partitionVars.BoardAvbAlgorithm)
+	}
+	if len(partitionVars.BoardAvbRollbackIndex) > 0 {
+		parsedRi, err := strconv.ParseInt(partitionVars.BoardAvbRollbackIndex, 10, 32)
+		if err != nil {
+			ctx.ModuleErrorf("Rollback index location must be an int, got %q", partitionVars.BoardAvbRollbackIndex)
+		}
+		ri = &parsedRi
+	}
+
+	var partitionModules []string
+	for _, partitionType := range generatedPartitionTypes {
+		if slices.Contains(partitionTypesHandledByChainedPartitions, partitionType) {
+			// Already handled by a chained vbmeta partition
+			continue
+		}
+		if partitionType == "ramdisk" {
+			// ramdisk is never signed with avb information
+			continue
+		}
+		partitionModules = append(partitionModules, generatedModuleNameForPartition(ctx.Config(), partitionType))
+	}
+
+	ctx.CreateModuleInDirectory(
+		filesystem.VbmetaFactory,
+		".", // Create in the root directory for now so its easy to get the key
+		&filesystem.VbmetaProperties{
+			Stem:               proptools.StringPtr("vbmeta.img"),
+			Algorithm:          algorithm,
+			Private_key:        key,
+			Rollback_index:     ri,
+			Chained_partitions: chainedPartitions,
+			Partitions:         proptools.NewSimpleConfigurable(partitionModules),
+		}, &struct {
+			Name *string
+		}{
+			Name: &vbmetaModuleName,
+		},
+	).HideFromMake()
+
+	result = append(result, vbmetaModuleInfo{
+		moduleName:    vbmetaModuleName,
+		partitionName: "vbmeta",
+	})
+	return result
+}
diff --git a/genrule/genrule.go b/genrule/genrule.go
index 1ab1378..1282bfb 100644
--- a/genrule/genrule.go
+++ b/genrule/genrule.go
@@ -231,7 +231,7 @@
 
 	// For nsjail tasks
 	useNsjail bool
-	dirSrcs   android.Paths
+	dirSrcs   android.DirectoryPaths
 }
 
 func (g *Module) GeneratedSourceFiles() android.Paths {
@@ -604,7 +604,8 @@
 
 		if task.useNsjail {
 			for _, input := range task.dirSrcs {
-				cmd.Implicit(input)
+				cmd.ImplicitDirectory(input)
+				// TODO(b/375551969): remove glob
 				if paths, err := ctx.GlobWithDeps(filepath.Join(input.String(), "**/*"), nil); err == nil {
 					rule.NsjailImplicits(android.PathsForSource(ctx, paths))
 				} else {
diff --git a/java/aar.go b/java/aar.go
index 66ca00a..b5cdde3 100644
--- a/java/aar.go
+++ b/java/aar.go
@@ -77,7 +77,7 @@
 	// list of directories relative to the Blueprints file containing
 	// Android resources.  Defaults to ["res"] if a directory called res exists.
 	// Set to [] to disable the default.
-	Resource_dirs []string `android:"path"`
+	Resource_dirs proptools.Configurable[[]string] `android:"path"`
 
 	// list of zip files containing Android resources.
 	Resource_zips []string `android:"path"`
@@ -275,7 +275,7 @@
 		IncludeDirs: false,
 	})
 	assetDirs := android.PathsWithOptionalDefaultForModuleSrc(ctx, a.aaptProperties.Asset_dirs, "assets")
-	resourceDirs := android.PathsWithOptionalDefaultForModuleSrc(ctx, a.aaptProperties.Resource_dirs, "res")
+	resourceDirs := android.PathsWithOptionalDefaultForModuleSrc(ctx, a.aaptProperties.Resource_dirs.GetOrDefault(ctx, nil), "res")
 	resourceZips := android.PathsForModuleSrc(ctx, a.aaptProperties.Resource_zips)
 
 	// Glob directories into lists of paths
diff --git a/java/app.go b/java/app.go
index e01a2ba..0939d17 100644
--- a/java/app.go
+++ b/java/app.go
@@ -67,6 +67,9 @@
 
 	// TestHelperApp is true if the module is a android_test_helper_app
 	TestHelperApp bool
+
+	// EmbeddedJNILibs is the list of paths to JNI libraries that were embedded in the APK.
+	EmbeddedJNILibs android.Paths
 }
 
 var AppInfoProvider = blueprint.NewProvider[*AppInfo]()
@@ -405,9 +408,18 @@
 	a.checkEmbedJnis(ctx)
 	a.generateAndroidBuildActions(ctx)
 	a.generateJavaUsedByApex(ctx)
+
+	var embeddedJniLibs []android.Path
+
+	if a.embeddedJniLibs {
+		for _, jni := range a.jniLibs {
+			embeddedJniLibs = append(embeddedJniLibs, jni.path)
+		}
+	}
 	android.SetProvider(ctx, AppInfoProvider, &AppInfo{
-		Updatable:     Bool(a.appProperties.Updatable),
-		TestHelperApp: false,
+		Updatable:       Bool(a.appProperties.Updatable),
+		TestHelperApp:   false,
+		EmbeddedJNILibs: embeddedJniLibs,
 	})
 }
 
@@ -1070,12 +1082,12 @@
 			app.SdkVersion(ctx).Kind != android.SdkCorePlatform && !app.RequiresStableAPIs(ctx)
 	}
 	jniLib, prebuiltJniPackages := collectJniDeps(ctx, shouldCollectRecursiveNativeDeps,
-		checkNativeSdkVersion, func(dep cc.LinkableInterface) bool {
-			return !dep.IsNdk(ctx.Config()) && !dep.IsStubs()
-		})
+		checkNativeSdkVersion, func(dep cc.LinkableInterface) bool { return !dep.IsNdk(ctx.Config()) && !dep.IsStubs() })
 
 	var certificates []Certificate
 
+	var directImplementationDeps android.Paths
+	var transitiveImplementationDeps []depset.DepSet[android.Path]
 	ctx.VisitDirectDeps(func(module android.Module) {
 		otherName := ctx.OtherModuleName(module)
 		tag := ctx.OtherModuleDependencyTag(module)
@@ -1087,7 +1099,18 @@
 				ctx.ModuleErrorf("certificate dependency %q must be an android_app_certificate module", otherName)
 			}
 		}
+
+		if IsJniDepTag(tag) {
+			directImplementationDeps = append(directImplementationDeps, android.OutputFileForModule(ctx, module, ""))
+			if info, ok := android.OtherModuleProvider(ctx, module, cc.ImplementationDepInfoProvider); ok {
+				transitiveImplementationDeps = append(transitiveImplementationDeps, info.ImplementationDeps)
+			}
+		}
 	})
+	android.SetProvider(ctx, cc.ImplementationDepInfoProvider, &cc.ImplementationDepInfo{
+		ImplementationDeps: depset.New(depset.PREORDER, directImplementationDeps, transitiveImplementationDeps),
+	})
+
 	return jniLib, prebuiltJniPackages, certificates
 }
 
@@ -1338,7 +1361,7 @@
 			Filter_product *string
 			Aaptflags      []string
 			Manifest       *string
-			Resource_dirs  []string
+			Resource_dirs  proptools.Configurable[[]string]
 			Flags_packages []string
 		}{
 			Name:           proptools.StringPtr(rroPackageName),
@@ -1442,8 +1465,9 @@
 	a.data = append(a.data, android.PathsForModuleSrc(ctx, a.testProperties.Device_common_data)...)
 	a.data = append(a.data, android.PathsForModuleSrc(ctx, a.testProperties.Device_first_data)...)
 	a.data = append(a.data, android.PathsForModuleSrc(ctx, a.testProperties.Device_first_prefer32_data)...)
+
 	android.SetProvider(ctx, tradefed.BaseTestProviderKey, tradefed.BaseTestProviderData{
-		InstalledFiles:          a.data,
+		TestcaseRelDataFiles:    testcaseRel(a.data),
 		OutputFile:              a.OutputFile(),
 		TestConfig:              a.testConfig,
 		HostRequiredModuleNames: a.HostRequiredModuleNames(),
@@ -1451,6 +1475,8 @@
 		IsHost:                  false,
 		LocalCertificate:        a.certificate.AndroidMkString(),
 		IsUnitTest:              Bool(a.testProperties.Test_options.Unit_test),
+		MkInclude:               "$(BUILD_SYSTEM)/soong_app_prebuilt.mk",
+		MkAppClass:              "APPS",
 	})
 	android.SetProvider(ctx, android.TestOnlyProviderKey, android.TestModuleInformation{
 		TestOnly:       true,
@@ -1459,6 +1485,14 @@
 
 }
 
+func testcaseRel(paths android.Paths) []string {
+	relPaths := []string{}
+	for _, p := range paths {
+		relPaths = append(relPaths, p.Rel())
+	}
+	return relPaths
+}
+
 func (a *AndroidTest) FixTestConfig(ctx android.ModuleContext, testConfig android.Path) android.Path {
 	if testConfig == nil {
 		return nil
diff --git a/java/bootclasspath_fragment.go b/java/bootclasspath_fragment.go
index 1a33680..c778f04 100644
--- a/java/bootclasspath_fragment.go
+++ b/java/bootclasspath_fragment.go
@@ -111,7 +111,7 @@
 	// property.
 	//
 	// The order of this list matters as it is the order that is used in the bootclasspath.
-	Contents []string
+	Contents proptools.Configurable[[]string] `android:"arch_variant"`
 
 	// The properties for specifying the API stubs provided by this fragment.
 	BootclasspathAPIProperties
@@ -295,8 +295,8 @@
 	return m
 }
 
-func (m *BootclasspathFragmentModule) bootclasspathFragmentPropertyCheck(ctx android.EarlyModuleContext) {
-	contents := m.properties.Contents
+func (m *BootclasspathFragmentModule) bootclasspathFragmentPropertyCheck(ctx android.ModuleContext) {
+	contents := m.properties.Contents.GetOrDefault(ctx, nil)
 	if len(contents) == 0 {
 		ctx.PropertyErrorf("contents", "required property is missing")
 		return
@@ -434,7 +434,7 @@
 	module := ctx.Module()
 	_, isSourceModule := module.(*BootclasspathFragmentModule)
 
-	for _, name := range b.properties.Contents {
+	for _, name := range b.properties.Contents.GetOrDefault(ctx, nil) {
 		// A bootclasspath_fragment must depend only on other source modules, while the
 		// prebuilt_bootclasspath_fragment must only depend on other prebuilt modules.
 		//
@@ -520,7 +520,7 @@
 	// be output to Make but it does not really matter which variant is output. The default/platform
 	// variant is the first (ctx.PrimaryModule()) and is usually hidden from make so this just picks
 	// the last variant (ctx.FinalModule()).
-	if ctx.Module() != ctx.FinalModule() {
+	if !ctx.IsFinalModule(ctx.Module()) {
 		b.HideFromMake()
 	}
 }
@@ -588,7 +588,7 @@
 		return global.ArtApexJars
 	}
 
-	possibleUpdatableModules := gatherPossibleApexModuleNamesAndStems(ctx, b.properties.Contents, bootclasspathFragmentContentDepTag)
+	possibleUpdatableModules := gatherPossibleApexModuleNamesAndStems(ctx, b.properties.Contents.GetOrDefault(ctx, nil), bootclasspathFragmentContentDepTag)
 	jars, unknown := global.ApexBootJars.Filter(possibleUpdatableModules)
 
 	// TODO(satayev): for apex_test we want to include all contents unconditionally to classpaths
@@ -600,7 +600,7 @@
 	} else if android.InList("test_framework-apexd", possibleUpdatableModules) {
 		jars = jars.Append("com.android.apex.test_package", "test_framework-apexd")
 	} else if global.ApexBootJars.Len() != 0 {
-		unknown = android.RemoveListFromList(unknown, b.properties.Coverage.Contents)
+		unknown = android.RemoveListFromList(unknown, b.properties.Coverage.Contents.GetOrDefault(ctx, nil))
 		_, unknown = android.RemoveFromList("core-icu4j", unknown)
 		// This module only exists in car products.
 		// So ignore it even if it is not in PRODUCT_APEX_BOOT_JARS.
@@ -847,7 +847,7 @@
 
 // Collect information for opening IDE project files in java/jdeps.go.
 func (b *BootclasspathFragmentModule) IDEInfo(ctx android.BaseModuleContext, dpInfo *android.IdeInfo) {
-	dpInfo.Deps = append(dpInfo.Deps, b.properties.Contents...)
+	dpInfo.Deps = append(dpInfo.Deps, b.properties.Contents.GetOrDefault(ctx, nil)...)
 }
 
 type bootclasspathFragmentMemberType struct {
@@ -923,7 +923,7 @@
 	module := variant.(*BootclasspathFragmentModule)
 
 	b.Image_name = module.properties.Image_name
-	b.Contents = module.properties.Contents
+	b.Contents = module.properties.Contents.GetOrDefault(ctx.SdkModuleContext(), nil)
 
 	// Get the hidden API information from the module.
 	mctx := ctx.SdkModuleContext()
diff --git a/java/bootclasspath_fragment_test.go b/java/bootclasspath_fragment_test.go
index 60f1a50..3aa1258 100644
--- a/java/bootclasspath_fragment_test.go
+++ b/java/bootclasspath_fragment_test.go
@@ -191,7 +191,8 @@
 
 	checkContents := func(t *testing.T, result *android.TestResult, expected ...string) {
 		module := result.Module("myfragment", "android_common").(*BootclasspathFragmentModule)
-		android.AssertArrayString(t, "contents property", expected, module.properties.Contents)
+		eval := module.ConfigurableEvaluator(android.PanickingConfigAndErrorContext(result.TestContext))
+		android.AssertArrayString(t, "contents property", expected, module.properties.Contents.GetOrDefault(eval, nil))
 	}
 
 	preparer := android.GroupFixturePreparers(
diff --git a/java/core-libraries/Android.bp b/java/core-libraries/Android.bp
index da86540..8c808e4 100644
--- a/java/core-libraries/Android.bp
+++ b/java/core-libraries/Android.bp
@@ -132,10 +132,10 @@
 // prebuilts/sdk/update_prebuilts.py script to update the prebuilts/sdk
 // directory.
 java_library {
-    name: "core-current-stubs-for-system-modules",
+    name: "core-current-stubs-for-system-modules-exportable",
     visibility: ["//development/sdk"],
     static_libs: [
-        "core.current.stubs",
+        "core.current.stubs.exportable",
         // This one is not on device but it's needed when javac compiles code
         // containing lambdas.
         "core-lambda-stubs-for-system-modules",
@@ -155,6 +155,19 @@
     ],
 }
 
+java_library {
+    name: "core-current-stubs-for-system-modules",
+    visibility: ["//development/sdk"],
+    static_libs: [
+        "core.current.stubs",
+        // This one is not on device but it's needed when javac compiles code
+        // containing lambdas.
+        "core-lambda-stubs-for-system-modules",
+    ],
+    sdk_version: "none",
+    system_modules: "none",
+}
+
 // Defaults module to strip out android annotations
 java_defaults {
     name: "system-modules-no-annotations",
diff --git a/java/droidstubs.go b/java/droidstubs.go
index 6bcdf85..cf3e219 100644
--- a/java/droidstubs.go
+++ b/java/droidstubs.go
@@ -997,12 +997,13 @@
 		msg := `$'` + // Enclose with $' ... '
 			`************************************************************\n` +
 			`Your API changes are triggering API Lint warnings or errors.\n` +
-			`To make these errors go away, fix the code according to the\n` +
-			`error and/or warning messages above.\n` +
 			`\n` +
-			`If it is not possible to do so, there are workarounds:\n` +
+			`To make the failures go away:\n` +
 			`\n` +
-			`1. You can suppress the errors with @SuppressLint("<id>")\n` +
+			`1. REQUIRED: Read the messages carefully and address them by` +
+			`   fixing the API if appropriate.\n` +
+			`2. If the failure is a false positive, you can suppress it with:\n` +
+			`        @SuppressLint("<id>")\n` +
 			`   where the <id> is given in brackets in the error message above.\n`
 
 		if baselineFile.Valid() {
@@ -1010,8 +1011,8 @@
 			cmd.FlagWithOutput("--update-baseline:api-lint ", updatedBaselineOutput)
 
 			msg += fmt.Sprintf(``+
-				`2. You can update the baseline by executing the following\n`+
-				`   command:\n`+
+				`3. FOR LSC ONLY: You can update the baseline by executing\n` +
+				`   the following command:\n`+
 				`       (cd $ANDROID_BUILD_TOP && cp \\\n`+
 				`       "%s" \\\n`+
 				`       "%s")\n`+
@@ -1019,7 +1020,7 @@
 				`   repository, you will need approval.\n`, updatedBaselineOutput, baselineFile.Path())
 		} else {
 			msg += fmt.Sprintf(``+
-				`2. You can add a baseline file of existing lint failures\n`+
+				`3. FOR LSC ONLY: You can add a baseline file of existing lint failures\n`+
 				`   to the build rule of %s.\n`, d.Name())
 		}
 		// Note the message ends with a ' (single quote), to close the $' ... ' .
diff --git a/java/java.go b/java/java.go
index c9bda72..078f578 100644
--- a/java/java.go
+++ b/java/java.go
@@ -606,10 +606,8 @@
 	} else if ctx.Device() {
 		return defaultJavaLanguageVersion(ctx, sdkContext.SdkVersion(ctx))
 	} else if ctx.Config().TargetsJava21() {
-		// Temporary experimental flag to be able to try and build with
-		// java version 21 options.  The flag, if used, just sets Java
-		// 21 as the default version, leaving any components that
-		// target an older version intact.
+		// Build flag that controls whether Java 21 is used as the default
+		// target version, or Java 17.
 		return JAVA_VERSION_21
 	} else {
 		return JAVA_VERSION_17
@@ -944,6 +942,7 @@
 		// Even though the source javalib is not used, we need to hide it to prevent duplicate installation rules.
 		// TODO (b/331665856): Implement a principled solution for this.
 		j.HideFromMake()
+		j.SkipInstall()
 	}
 	j.provideHiddenAPIPropertyInfo(ctx)
 
@@ -1558,14 +1557,16 @@
 
 	j.Test.generateAndroidBuildActionsWithConfig(ctx, configs)
 	android.SetProvider(ctx, tradefed.BaseTestProviderKey, tradefed.BaseTestProviderData{
-		InstalledFiles:      j.data,
-		OutputFile:          j.outputFile,
-		TestConfig:          j.testConfig,
-		RequiredModuleNames: j.RequiredModuleNames(ctx),
-		TestSuites:          j.testProperties.Test_suites,
-		IsHost:              true,
-		LocalSdkVersion:     j.sdkVersion.String(),
-		IsUnitTest:          Bool(j.testProperties.Test_options.Unit_test),
+		TestcaseRelDataFiles: testcaseRel(j.data),
+		OutputFile:           j.outputFile,
+		TestConfig:           j.testConfig,
+		RequiredModuleNames:  j.RequiredModuleNames(ctx),
+		TestSuites:           j.testProperties.Test_suites,
+		IsHost:               true,
+		LocalSdkVersion:      j.sdkVersion.String(),
+		IsUnitTest:           Bool(j.testProperties.Test_options.Unit_test),
+		MkInclude:            "$(BUILD_SYSTEM)/soong_java_prebuilt.mk",
+		MkAppClass:           "JAVA_LIBRARIES",
 	})
 }
 
@@ -1609,6 +1610,8 @@
 		j.data = append(j.data, android.OutputFileForModule(ctx, dep, ""))
 	})
 
+	var directImplementationDeps android.Paths
+	var transitiveImplementationDeps []depset.DepSet[android.Path]
 	ctx.VisitDirectDepsWithTag(jniLibTag, func(dep android.Module) {
 		sharedLibInfo, _ := android.OtherModuleProvider(ctx, dep, cc.SharedLibraryInfoProvider)
 		if sharedLibInfo.SharedLibrary != nil {
@@ -1627,11 +1630,20 @@
 				Output: relocatedLib,
 			})
 			j.data = append(j.data, relocatedLib)
+
+			directImplementationDeps = append(directImplementationDeps, android.OutputFileForModule(ctx, dep, ""))
+			if info, ok := android.OtherModuleProvider(ctx, dep, cc.ImplementationDepInfoProvider); ok {
+				transitiveImplementationDeps = append(transitiveImplementationDeps, info.ImplementationDeps)
+			}
 		} else {
 			ctx.PropertyErrorf("jni_libs", "%q of type %q is not supported", dep.Name(), ctx.OtherModuleType(dep))
 		}
 	})
 
+	android.SetProvider(ctx, cc.ImplementationDepInfoProvider, &cc.ImplementationDepInfo{
+		ImplementationDeps: depset.New(depset.PREORDER, directImplementationDeps, transitiveImplementationDeps),
+	})
+
 	j.Library.GenerateAndroidBuildActions(ctx)
 }
 
diff --git a/java/platform_bootclasspath.go b/java/platform_bootclasspath.go
index acfc774..8b0ca97 100644
--- a/java/platform_bootclasspath.go
+++ b/java/platform_bootclasspath.go
@@ -24,7 +24,7 @@
 }
 
 func registerPlatformBootclasspathBuildComponents(ctx android.RegistrationContext) {
-	ctx.RegisterParallelSingletonModuleType("platform_bootclasspath", platformBootclasspathFactory)
+	ctx.RegisterModuleType("platform_bootclasspath", platformBootclasspathFactory)
 }
 
 // The tags used for the dependencies between the platform bootclasspath and any configured boot
@@ -37,7 +37,7 @@
 )
 
 type platformBootclasspathModule struct {
-	android.SingletonModuleBase
+	android.ModuleBase
 	ClasspathFragmentBase
 
 	properties platformBootclasspathProperties
@@ -64,7 +64,7 @@
 	HiddenAPIFlagFileProperties
 }
 
-func platformBootclasspathFactory() android.SingletonModule {
+func platformBootclasspathFactory() android.Module {
 	m := &platformBootclasspathModule{}
 	m.AddProperties(&m.properties)
 	initClasspathFragment(m, BOOTCLASSPATH)
@@ -156,18 +156,6 @@
 	}
 }
 
-// GenerateSingletonBuildActions does nothing and must never do anything.
-//
-// This module only implements android.SingletonModule so that it can implement
-// android.SingletonMakeVarsProvider.
-func (b *platformBootclasspathModule) GenerateSingletonBuildActions(android.SingletonContext) {
-	// Keep empty
-}
-
-func (d *platformBootclasspathModule) MakeVars(ctx android.MakeVarsContext) {
-	d.generateHiddenApiMakeVars(ctx)
-}
-
 func (b *platformBootclasspathModule) GenerateAndroidBuildActions(ctx android.ModuleContext) {
 	// Gather all the dependencies from the art, platform, and apex boot jars.
 	artModules := gatherApexModulePairDepsWithTag(ctx, platformBootclasspathArtBootJarDepTag)
@@ -428,13 +416,3 @@
 
 	rule.Build(desc, desc)
 }
-
-// generateHiddenApiMakeVars generates make variables needed by hidden API related make rules, e.g.
-// veridex and run-appcompat.
-func (b *platformBootclasspathModule) generateHiddenApiMakeVars(ctx android.MakeVarsContext) {
-	if ctx.Config().IsEnvTrue("UNSAFE_DISABLE_HIDDENAPI_FLAGS") {
-		return
-	}
-	// INTERNAL_PLATFORM_HIDDENAPI_FLAGS is used by Make rules in art/ and cts/.
-	ctx.Strict("INTERNAL_PLATFORM_HIDDENAPI_FLAGS", b.hiddenAPIFlagsCSV.String())
-}
diff --git a/java/sdk.go b/java/sdk.go
index 4537f19..bb2aa8d 100644
--- a/java/sdk.go
+++ b/java/sdk.go
@@ -66,10 +66,8 @@
 	} else if sdk.FinalOrFutureInt() <= 33 {
 		return JAVA_VERSION_11
 	} else if ctx.Config().TargetsJava21() {
-		// Temporary experimental flag to be able to try and build with
-		// java version 21 options.  The flag, if used, just sets Java
-		// 21 as the default version, leaving any components that
-		// target an older version intact.
+		// Build flag that controls whether Java 21 is used as the
+		// default target version, or Java 17.
 		return JAVA_VERSION_21
 	} else {
 		return JAVA_VERSION_17
diff --git a/java/sdk_library.go b/java/sdk_library.go
index dfbde0e..f6dfcdd 100644
--- a/java/sdk_library.go
+++ b/java/sdk_library.go
@@ -1403,6 +1403,7 @@
 		// Even though the source javalib is not used, we need to hide it to prevent duplicate installation rules.
 		// TODO (b/331665856): Implement a principled solution for this.
 		module.HideFromMake()
+		module.SkipInstall()
 	}
 
 	module.stem = proptools.StringDefault(module.overridableProperties.Stem, ctx.ModuleName())
diff --git a/java/systemserver_classpath_fragment.go b/java/systemserver_classpath_fragment.go
index aad1060..608a616 100644
--- a/java/systemserver_classpath_fragment.go
+++ b/java/systemserver_classpath_fragment.go
@@ -19,6 +19,7 @@
 	"android/soong/dexpreopt"
 
 	"github.com/google/blueprint"
+	"github.com/google/blueprint/proptools"
 )
 
 func init() {
@@ -98,12 +99,12 @@
 	// List of system_server classpath jars, could be either java_library, or java_sdk_library.
 	//
 	// The order of this list matters as it is the order that is used in the SYSTEMSERVERCLASSPATH.
-	Contents []string
+	Contents proptools.Configurable[[]string] `android:"arch_variant"`
 
 	// List of jars that system_server loads dynamically using separate classloaders.
 	//
 	// The order does not matter.
-	Standalone_contents []string
+	Standalone_contents proptools.Configurable[[]string] `android:"arch_variant"`
 }
 
 func systemServerClasspathFactory() android.Module {
@@ -116,7 +117,7 @@
 }
 
 func (s *SystemServerClasspathModule) GenerateAndroidBuildActions(ctx android.ModuleContext) {
-	if len(s.properties.Contents) == 0 && len(s.properties.Standalone_contents) == 0 {
+	if len(s.properties.Contents.GetOrDefault(ctx, nil)) == 0 && len(s.properties.Standalone_contents.GetOrDefault(ctx, nil)) == 0 {
 		ctx.PropertyErrorf("contents", "Either contents or standalone_contents needs to be non-empty")
 	}
 
@@ -152,7 +153,7 @@
 func (s *SystemServerClasspathModule) configuredJars(ctx android.ModuleContext) android.ConfiguredJarList {
 	global := dexpreopt.GetGlobalConfig(ctx)
 
-	possibleUpdatableModules := gatherPossibleApexModuleNamesAndStems(ctx, s.properties.Contents, systemServerClasspathFragmentContentDepTag)
+	possibleUpdatableModules := gatherPossibleApexModuleNamesAndStems(ctx, s.properties.Contents.GetOrDefault(ctx, nil), systemServerClasspathFragmentContentDepTag)
 	jars, unknown := global.ApexSystemServerJars.Filter(possibleUpdatableModules)
 	// TODO(satayev): remove geotz ssc_fragment, since geotz is not part of SSCP anymore.
 	_, unknown = android.RemoveFromList("geotz", unknown)
@@ -184,7 +185,7 @@
 func (s *SystemServerClasspathModule) standaloneConfiguredJars(ctx android.ModuleContext) android.ConfiguredJarList {
 	global := dexpreopt.GetGlobalConfig(ctx)
 
-	possibleUpdatableModules := gatherPossibleApexModuleNamesAndStems(ctx, s.properties.Standalone_contents, systemServerClasspathFragmentContentDepTag)
+	possibleUpdatableModules := gatherPossibleApexModuleNamesAndStems(ctx, s.properties.Standalone_contents.GetOrDefault(ctx, nil), systemServerClasspathFragmentContentDepTag)
 	jars, _ := global.ApexStandaloneSystemServerJars.Filter(possibleUpdatableModules)
 
 	// TODO(jiakaiz): add a check to ensure that the contents are declared in make.
@@ -245,8 +246,8 @@
 	module := ctx.Module()
 	_, isSourceModule := module.(*SystemServerClasspathModule)
 	var deps []string
-	deps = append(deps, s.properties.Contents...)
-	deps = append(deps, s.properties.Standalone_contents...)
+	deps = append(deps, s.properties.Contents.GetOrDefault(ctx, nil)...)
+	deps = append(deps, s.properties.Standalone_contents.GetOrDefault(ctx, nil)...)
 
 	for _, name := range deps {
 		// A systemserverclasspath_fragment must depend only on other source modules, while the
@@ -260,8 +261,8 @@
 
 // Collect information for opening IDE project files in java/jdeps.go.
 func (s *SystemServerClasspathModule) IDEInfo(ctx android.BaseModuleContext, dpInfo *android.IdeInfo) {
-	dpInfo.Deps = append(dpInfo.Deps, s.properties.Contents...)
-	dpInfo.Deps = append(dpInfo.Deps, s.properties.Standalone_contents...)
+	dpInfo.Deps = append(dpInfo.Deps, s.properties.Contents.GetOrDefault(ctx, nil)...)
+	dpInfo.Deps = append(dpInfo.Deps, s.properties.Standalone_contents.GetOrDefault(ctx, nil)...)
 }
 
 type systemServerClasspathFragmentMemberType struct {
@@ -302,8 +303,8 @@
 func (s *systemServerClasspathFragmentSdkMemberProperties) PopulateFromVariant(ctx android.SdkMemberContext, variant android.Module) {
 	module := variant.(*SystemServerClasspathModule)
 
-	s.Contents = module.properties.Contents
-	s.Standalone_contents = module.properties.Standalone_contents
+	s.Contents = module.properties.Contents.GetOrDefault(ctx.SdkModuleContext(), nil)
+	s.Standalone_contents = module.properties.Standalone_contents.GetOrDefault(ctx.SdkModuleContext(), nil)
 }
 
 func (s *systemServerClasspathFragmentSdkMemberProperties) AddToPropertySet(ctx android.SdkMemberContext, propertySet android.BpPropertySet) {
diff --git a/kernel/prebuilt_kernel_modules.go b/kernel/prebuilt_kernel_modules.go
index e200ee2..13d6482 100644
--- a/kernel/prebuilt_kernel_modules.go
+++ b/kernel/prebuilt_kernel_modules.go
@@ -32,7 +32,7 @@
 }
 
 func registerKernelBuildComponents(ctx android.RegistrationContext) {
-	ctx.RegisterModuleType("prebuilt_kernel_modules", prebuiltKernelModulesFactory)
+	ctx.RegisterModuleType("prebuilt_kernel_modules", PrebuiltKernelModulesFactory)
 }
 
 type prebuiltKernelModules struct {
@@ -47,6 +47,17 @@
 	// List or filegroup of prebuilt kernel module files. Should have .ko suffix.
 	Srcs []string `android:"path,arch_variant"`
 
+	// List of system_dlkm kernel modules that the local kernel modules depend on.
+	// The deps will be assembled into intermediates directory for running depmod
+	// but will not be added to the current module's installed files.
+	System_deps []string `android:"path,arch_variant"`
+
+	// If false, then srcs will not be included in modules.load.
+	// This feature is used by system_dlkm
+	Load_by_default *bool
+
+	Blocklist_file *string `android:"path"`
+
 	// Kernel version that these modules are for. Kernel modules are installed to
 	// /lib/modules/<kernel_version> directory in the corresponding partition. Default is "".
 	Kernel_version *string
@@ -58,7 +69,7 @@
 // prebuilt_kernel_modules installs a set of prebuilt kernel module files to the correct directory.
 // In addition, this module builds modules.load, modules.dep, modules.softdep and modules.alias
 // using depmod and installs them as well.
-func prebuiltKernelModulesFactory() android.Module {
+func PrebuiltKernelModulesFactory() android.Module {
 	module := &prebuiltKernelModules{}
 	module.AddProperties(&module.properties)
 	android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibFirst)
@@ -81,9 +92,11 @@
 	if !pkm.installable() {
 		pkm.SkipInstall()
 	}
-	modules := android.PathsForModuleSrc(ctx, pkm.properties.Srcs)
 
-	depmodOut := runDepmod(ctx, modules)
+	modules := android.PathsForModuleSrc(ctx, pkm.properties.Srcs)
+	systemModules := android.PathsForModuleSrc(ctx, pkm.properties.System_deps)
+
+	depmodOut := pkm.runDepmod(ctx, modules, systemModules)
 	strippedModules := stripDebugSymbols(ctx, modules)
 
 	installDir := android.PathForModuleInstall(ctx, "lib", "modules")
@@ -98,6 +111,23 @@
 	ctx.InstallFile(installDir, "modules.dep", depmodOut.modulesDep)
 	ctx.InstallFile(installDir, "modules.softdep", depmodOut.modulesSoftdep)
 	ctx.InstallFile(installDir, "modules.alias", depmodOut.modulesAlias)
+	pkm.installBlocklistFile(ctx, installDir)
+
+	ctx.SetOutputFiles(modules, ".modules")
+}
+
+func (pkm *prebuiltKernelModules) installBlocklistFile(ctx android.ModuleContext, installDir android.InstallPath) {
+	if pkm.properties.Blocklist_file == nil {
+		return
+	}
+	blocklistOut := android.PathForModuleOut(ctx, "modules.blocklist")
+
+	ctx.Build(pctx, android.BuildParams{
+		Rule:   processBlocklistFile,
+		Input:  android.PathForModuleSrc(ctx, proptools.String(pkm.properties.Blocklist_file)),
+		Output: blocklistOut,
+	})
+	ctx.InstallFile(installDir, "modules.blocklist", blocklistOut)
 }
 
 var (
@@ -137,35 +167,93 @@
 	modulesAlias   android.OutputPath
 }
 
-func runDepmod(ctx android.ModuleContext, modules android.Paths) depmodOutputs {
+var (
+	// system/lib/modules/foo.ko: system/lib/modules/bar.ko
+	// will be converted to
+	// /system/lib/modules/foo.ko: /system/lib/modules/bar.ko
+	addLeadingSlashToPaths = pctx.AndroidStaticRule("add_leading_slash",
+		blueprint.RuleParams{
+			Command: `sed -e 's|\([^: ]*lib/modules/[^: ]*\)|/\1|g' $in > $out`,
+		},
+	)
+	// Remove empty lines. Raise an exception if line is _not_ formatted as `blocklist $name.ko`
+	processBlocklistFile = pctx.AndroidStaticRule("process_blocklist_file",
+		blueprint.RuleParams{
+			Command: `rm -rf $out && awk <$in > $out` +
+				` '/^#/ { print; next }` +
+				` NF == 0 { next }` +
+				` NF != 2 || $$1 != "blocklist"` +
+				` { print "Invalid blocklist line " FNR ": " $$0 >"/dev/stderr";` +
+				` exit_status = 1; next }` +
+				` { $$1 = $$1; print }` +
+				` END { exit exit_status }'`,
+		},
+	)
+)
+
+// This is the path in soong intermediates where the .ko files will be copied.
+// The layout should match the layout on device so that depmod can create meaningful modules.* files.
+func modulesDirForAndroidDlkm(ctx android.ModuleContext, modulesDir android.OutputPath, system bool) android.OutputPath {
+	if ctx.InstallInSystemDlkm() || system {
+		// The first component can be either system or system_dlkm
+		// system works because /system/lib/modules is a symlink to /system_dlkm/lib/modules.
+		// system was chosen to match the contents of the kati built modules.dep
+		return modulesDir.Join(ctx, "system", "lib", "modules")
+	} else if ctx.InstallInVendorDlkm() {
+		return modulesDir.Join(ctx, "vendor", "lib", "modules")
+	} else if ctx.InstallInOdmDlkm() {
+		return modulesDir.Join(ctx, "odm", "lib", "modules")
+	} else {
+		// not an android dlkm module.
+		return modulesDir
+	}
+}
+
+func (pkm *prebuiltKernelModules) runDepmod(ctx android.ModuleContext, modules android.Paths, systemModules android.Paths) depmodOutputs {
 	baseDir := android.PathForModuleOut(ctx, "depmod").OutputPath
 	fakeVer := "0.0" // depmod demands this anyway
 	modulesDir := baseDir.Join(ctx, "lib", "modules", fakeVer)
+	modulesCpDir := modulesDirForAndroidDlkm(ctx, modulesDir, false)
 
 	builder := android.NewRuleBuilder(pctx, ctx)
 
 	// Copy the module files to a temporary dir
-	builder.Command().Text("rm").Flag("-rf").Text(modulesDir.String())
-	builder.Command().Text("mkdir").Flag("-p").Text(modulesDir.String())
+	builder.Command().Text("rm").Flag("-rf").Text(modulesCpDir.String())
+	builder.Command().Text("mkdir").Flag("-p").Text(modulesCpDir.String())
 	for _, m := range modules {
-		builder.Command().Text("cp").Input(m).Text(modulesDir.String())
+		builder.Command().Text("cp").Input(m).Text(modulesCpDir.String())
+	}
+
+	modulesDirForSystemDlkm := modulesDirForAndroidDlkm(ctx, modulesDir, true)
+	if len(systemModules) > 0 {
+		builder.Command().Text("mkdir").Flag("-p").Text(modulesDirForSystemDlkm.String())
+	}
+	for _, m := range systemModules {
+		builder.Command().Text("cp").Input(m).Text(modulesDirForSystemDlkm.String())
 	}
 
 	// Enumerate modules to load
 	modulesLoad := modulesDir.Join(ctx, "modules.load")
-	var basenames []string
-	for _, m := range modules {
-		basenames = append(basenames, filepath.Base(m.String()))
+	// If Load_by_default is set to false explicitly, create an empty modules.load
+	if pkm.properties.Load_by_default != nil && !*pkm.properties.Load_by_default {
+		builder.Command().Text("rm").Flag("-rf").Text(modulesLoad.String())
+		builder.Command().Text("touch").Output(modulesLoad)
+	} else {
+		var basenames []string
+		for _, m := range modules {
+			basenames = append(basenames, filepath.Base(m.String()))
+		}
+		builder.Command().
+			Text("echo").Flag("\"" + strings.Join(basenames, " ") + "\"").
+			Text("|").Text("tr").Flag("\" \"").Flag("\"\\n\"").
+			Text(">").Output(modulesLoad)
 	}
-	builder.Command().
-		Text("echo").Flag("\"" + strings.Join(basenames, " ") + "\"").
-		Text("|").Text("tr").Flag("\" \"").Flag("\"\\n\"").
-		Text(">").Output(modulesLoad)
 
 	// Run depmod to build modules.dep/softdep/alias files
 	modulesDep := modulesDir.Join(ctx, "modules.dep")
 	modulesSoftdep := modulesDir.Join(ctx, "modules.softdep")
 	modulesAlias := modulesDir.Join(ctx, "modules.alias")
+	builder.Command().Text("mkdir").Flag("-p").Text(modulesDir.String())
 	builder.Command().
 		BuiltTool("depmod").
 		FlagWithArg("-b ", baseDir.String()).
@@ -176,5 +264,16 @@
 
 	builder.Build("depmod", fmt.Sprintf("depmod %s", ctx.ModuleName()))
 
-	return depmodOutputs{modulesLoad, modulesDep, modulesSoftdep, modulesAlias}
+	finalModulesDep := modulesDep
+	// Add a leading slash to paths in modules.dep of android dlkm
+	if ctx.InstallInSystemDlkm() || ctx.InstallInVendorDlkm() || ctx.InstallInOdmDlkm() {
+		finalModulesDep := modulesDep.ReplaceExtension(ctx, "intermediates")
+		ctx.Build(pctx, android.BuildParams{
+			Rule:   addLeadingSlashToPaths,
+			Input:  modulesDep,
+			Output: finalModulesDep,
+		})
+	}
+
+	return depmodOutputs{modulesLoad, finalModulesDep, modulesSoftdep, modulesAlias}
 }
diff --git a/linkerconfig/linkerconfig.go b/linkerconfig/linkerconfig.go
index 05b99fd..d422871 100644
--- a/linkerconfig/linkerconfig.go
+++ b/linkerconfig/linkerconfig.go
@@ -77,7 +77,7 @@
 	output := android.PathForModuleOut(ctx, "linker.config.pb").OutputPath
 
 	builder := android.NewRuleBuilder(pctx, ctx)
-	BuildLinkerConfig(ctx, builder, input, nil, nil, output)
+	BuildLinkerConfig(ctx, builder, android.Paths{input}, nil, nil, output)
 	builder.Build("conv_linker_config", "Generate linker config protobuf "+output.String())
 
 	l.outputFilePath = output
@@ -91,16 +91,18 @@
 }
 
 func BuildLinkerConfig(ctx android.ModuleContext, builder *android.RuleBuilder,
-	input android.Path, provideModules []android.Module, requireModules []android.Module, output android.OutputPath) {
+	inputs android.Paths, provideModules []android.Module, requireModules []android.Module, output android.OutputPath) {
 
 	// First, convert the input json to protobuf format
 	interimOutput := android.PathForModuleOut(ctx, "temp.pb")
-	builder.Command().
+	cmd := builder.Command().
 		BuiltTool("conv_linker_config").
 		Flag("proto").
-		Flag("--force").
-		FlagWithInput("-s ", input).
-		FlagWithOutput("-o ", interimOutput)
+		Flag("--force")
+	for _, input := range inputs {
+		cmd.FlagWithInput("-s ", input)
+	}
+	cmd.FlagWithOutput("-o ", interimOutput)
 
 	// Secondly, if there's provideLibs gathered from provideModules, append them
 	var provideLibs []string
diff --git a/rust/bindgen.go b/rust/bindgen.go
index 3944495..d590579 100644
--- a/rust/bindgen.go
+++ b/rust/bindgen.go
@@ -252,7 +252,7 @@
 
 	// Module defined clang flags and include paths
 	cflags = append(cflags, esc(cflagsProp)...)
-	for _, include := range b.ClangProperties.Local_include_dirs {
+	for _, include := range b.ClangProperties.Local_include_dirs.GetOrDefault(ctx, nil) {
 		cflags = append(cflags, "-I"+android.PathForModuleSrc(ctx, include).String())
 		implicits = append(implicits, android.PathForModuleSrc(ctx, include))
 	}
diff --git a/rust/builder.go b/rust/builder.go
index f469f56..a1e17fc 100644
--- a/rust/builder.go
+++ b/rust/builder.go
@@ -170,6 +170,9 @@
 	return transformSrctoCrate(ctx, mainSrc, deps, flags, outputFile, getTransformProperties(ctx, "rlib"))
 }
 
+// TransformRlibstoStaticlib is assumed to be called from the cc module, and
+// thus needs to reconstruct the common set of flags which need to be passed
+// to the rustc compiler.
 func TransformRlibstoStaticlib(ctx android.ModuleContext, mainSrc android.Path, deps []cc.RustRlibDep,
 	outputFile android.WritablePath) android.Path {
 
diff --git a/rust/config/global.go b/rust/config/global.go
index 68a74c2..7b79fca 100644
--- a/rust/config/global.go
+++ b/rust/config/global.go
@@ -15,6 +15,7 @@
 package config
 
 import (
+	"fmt"
 	"strings"
 
 	"android/soong/android"
@@ -93,6 +94,16 @@
 	}
 )
 
+func RustPath(ctx android.PathContext) string {
+	// I can't see any way to flatten the static variable inside Soong, so this
+	// reproduces the init logic.
+	var RustBase string = RustDefaultBase
+	if override := ctx.Config().Getenv("RUST_PREBUILTS_BASE"); override != "" {
+		RustBase = override
+	}
+	return fmt.Sprintf("%s/%s/%s", RustBase, HostPrebuiltTag(ctx.Config()), GetRustVersion(ctx))
+}
+
 func init() {
 	pctx.SourcePathVariable("RustDefaultBase", RustDefaultBase)
 	pctx.VariableConfigMethod("HostPrebuiltTag", HostPrebuiltTag)
diff --git a/rust/library.go b/rust/library.go
index 20cd2af..bd3359b 100644
--- a/rust/library.go
+++ b/rust/library.go
@@ -617,6 +617,9 @@
 			TableOfContents: android.OptionalPathForPath(tocFile),
 			SharedLibrary:   outputFile,
 			Target:          ctx.Target(),
+			// TODO: when rust supports stubs uses the stubs state rather than inferring it from
+			//  apex_exclude.
+			IsStubs: Bool(library.Properties.Apex_exclude),
 		})
 	}
 
diff --git a/rust/project_json.go b/rust/project_json.go
index 6c1e320..6e8cebe 100644
--- a/rust/project_json.go
+++ b/rust/project_json.go
@@ -19,6 +19,7 @@
 	"fmt"
 
 	"android/soong/android"
+	"android/soong/rust/config"
 )
 
 // This singleton collects Rust crate definitions and generates a JSON file
@@ -44,17 +45,19 @@
 }
 
 type rustProjectCrate struct {
-	DisplayName string            `json:"display_name"`
-	RootModule  string            `json:"root_module"`
-	Edition     string            `json:"edition,omitempty"`
-	Deps        []rustProjectDep  `json:"deps"`
-	Cfg         []string          `json:"cfg"`
-	Env         map[string]string `json:"env"`
-	ProcMacro   bool              `json:"is_proc_macro"`
+	DisplayName    string            `json:"display_name"`
+	RootModule     string            `json:"root_module"`
+	Edition        string            `json:"edition,omitempty"`
+	Deps           []rustProjectDep  `json:"deps"`
+	Cfg            []string          `json:"cfg"`
+	Env            map[string]string `json:"env"`
+	ProcMacro      bool              `json:"is_proc_macro"`
+	ProcMacroDylib *string           `json:"proc_macro_dylib_path"`
 }
 
 type rustProjectJson struct {
-	Crates []rustProjectCrate `json:"crates"`
+	Sysroot string             `json:"sysroot"`
+	Crates  []rustProjectCrate `json:"crates"`
 }
 
 // crateInfo is used during the processing to keep track of the known crates.
@@ -135,16 +138,21 @@
 		return 0, false
 	}
 
-	_, procMacro := rModule.compiler.(*procMacroDecorator)
+	var procMacroDylib *string = nil
+	if procDec, procMacro := rModule.compiler.(*procMacroDecorator); procMacro {
+		procMacroDylib = new(string)
+		*procMacroDylib = procDec.baseCompiler.unstrippedOutputFilePath().String()
+	}
 
 	crate := rustProjectCrate{
-		DisplayName: rModule.Name(),
-		RootModule:  rootModule.String(),
-		Edition:     rModule.compiler.edition(),
-		Deps:        make([]rustProjectDep, 0),
-		Cfg:         make([]string, 0),
-		Env:         make(map[string]string),
-		ProcMacro:   procMacro,
+		DisplayName:    rModule.Name(),
+		RootModule:     rootModule.String(),
+		Edition:        rModule.compiler.edition(),
+		Deps:           make([]rustProjectDep, 0),
+		Cfg:            make([]string, 0),
+		Env:            make(map[string]string),
+		ProcMacro:      procMacroDylib != nil,
+		ProcMacroDylib: procMacroDylib,
 	}
 
 	if rModule.compiler.cargoOutDir().Valid() {
@@ -197,6 +205,8 @@
 		return
 	}
 
+	singleton.project.Sysroot = config.RustPath(ctx)
+
 	singleton.knownCrates = make(map[string]crateInfo)
 	ctx.VisitAllModules(func(module android.Module) {
 		singleton.appendCrateAndDependencies(ctx, module)
diff --git a/rust/rust.go b/rust/rust.go
index 6b91ccb..ed38ad7 100644
--- a/rust/rust.go
+++ b/rust/rust.go
@@ -454,6 +454,9 @@
 	// Paths to generated source files
 	SrcDeps          android.Paths
 	srcProviderFiles android.Paths
+
+	directImplementationDeps     android.Paths
+	transitiveImplementationDeps []depset.DepSet[android.Path]
 }
 
 type RustLibraries []RustLibrary
@@ -974,6 +977,7 @@
 			// side dependencies. In particular, proc-macros need to be captured in the
 			// host snapshot.
 			mod.HideFromMake()
+			mod.SkipInstall()
 		} else if !mod.installable(apexInfo) {
 			mod.SkipInstall()
 		}
@@ -990,6 +994,10 @@
 
 		}
 
+		android.SetProvider(ctx, cc.ImplementationDepInfoProvider, &cc.ImplementationDepInfo{
+			ImplementationDeps: depset.New(depset.PREORDER, deps.directImplementationDeps, deps.transitiveImplementationDeps),
+		})
+
 		ctx.Phony("rust", ctx.RustModule().OutputFile().Path())
 	}
 
@@ -1243,6 +1251,11 @@
 				mod.Properties.AndroidMkDylibs = append(mod.Properties.AndroidMkDylibs, makeLibName)
 				mod.Properties.SnapshotDylibs = append(mod.Properties.SnapshotDylibs, cc.BaseLibName(depName))
 
+				depPaths.directImplementationDeps = append(depPaths.directImplementationDeps, android.OutputFileForModule(ctx, dep, ""))
+				if info, ok := android.OtherModuleProvider(ctx, dep, cc.ImplementationDepInfoProvider); ok {
+					depPaths.transitiveImplementationDeps = append(depPaths.transitiveImplementationDeps, info.ImplementationDeps)
+				}
+
 			case depTag == rlibDepTag:
 				rlib, ok := rustDep.compiler.(libraryInterface)
 				if !ok || !rlib.rlib() {
@@ -1258,6 +1271,11 @@
 				depPaths.depIncludePaths = append(depPaths.depIncludePaths, exportedInfo.IncludeDirs...)
 				depPaths.exportedLinkDirs = append(depPaths.exportedLinkDirs, linkPathFromFilePath(rustDep.OutputFile().Path()))
 
+				// rlibs are not installed, so don't add the output file to directImplementationDeps
+				if info, ok := android.OtherModuleProvider(ctx, dep, cc.ImplementationDepInfoProvider); ok {
+					depPaths.transitiveImplementationDeps = append(depPaths.transitiveImplementationDeps, info.ImplementationDeps)
+				}
+
 			case depTag == procMacroDepTag:
 				directProcMacroDeps = append(directProcMacroDeps, rustDep)
 				mod.Properties.AndroidMkProcMacroLibs = append(mod.Properties.AndroidMkProcMacroLibs, makeLibName)
@@ -1390,6 +1408,13 @@
 				// dependency crosses the APEX boundaries).
 				sharedLibraryInfo, exportedInfo := cc.ChooseStubOrImpl(ctx, dep)
 
+				if !sharedLibraryInfo.IsStubs {
+					depPaths.directImplementationDeps = append(depPaths.directImplementationDeps, android.OutputFileForModule(ctx, dep, ""))
+					if info, ok := android.OtherModuleProvider(ctx, dep, cc.ImplementationDepInfoProvider); ok {
+						depPaths.transitiveImplementationDeps = append(depPaths.transitiveImplementationDeps, info.ImplementationDeps)
+					}
+				}
+
 				// Re-get linkObject as ChooseStubOrImpl actually tells us which
 				// object (either from stub or non-stub) to use.
 				linkObject = android.OptionalPathForPath(sharedLibraryInfo.SharedLibrary)
diff --git a/rust/rust_test.go b/rust/rust_test.go
index eeedf3f..767508d 100644
--- a/rust/rust_test.go
+++ b/rust/rust_test.go
@@ -494,7 +494,7 @@
 	}
 
 	// Make sure the static lib is included in the ld command
-	if !strings.Contains(libcc_shared_ld.Args["libFlags"], "generated_rust_staticlib/liblibcc_shared_rust_staticlib.a") {
+	if !strings.Contains(libcc_shared_ld.Args["libFlags"], "generated_rust_staticlib/librustlibs.a") {
 		t.Errorf("missing generated static library in linker step libFlags %#v, libFlags: %#v",
 			"libcc_shared.generated_rust_staticlib.a", libcc_shared_ld.Args["libFlags"])
 	}
@@ -511,7 +511,7 @@
 	}
 
 	// Make sure the static lib is included in the cc command
-	if !strings.Contains(ccbin_ld.Args["libFlags"], "generated_rust_staticlib/libccBin_rust_staticlib.a") {
+	if !strings.Contains(ccbin_ld.Args["libFlags"], "generated_rust_staticlib/librustlibs.a") {
 		t.Errorf("missing generated static library in linker step libFlags, expecting %#v, libFlags: %#v",
 			"ccBin.generated_rust_staticlib.a", ccbin_ld.Args["libFlags"])
 	}
diff --git a/scripts/gen_build_prop.py b/scripts/gen_build_prop.py
index e0686ed..0b7780e 100644
--- a/scripts/gen_build_prop.py
+++ b/scripts/gen_build_prop.py
@@ -608,6 +608,8 @@
         build_product_prop(args)
       case "vendor":
         build_vendor_prop(args)
+      case "system_dlkm" | "vendor_dlkm" | "odm_dlkm":
+        build_prop(args, gen_build_info=False, gen_common_build_props=True, variables=[])
       case _:
         sys.exit(f"not supported partition {args.partition}")
 
diff --git a/sdk/Android.bp b/sdk/Android.bp
index f436320..f42b478 100644
--- a/sdk/Android.bp
+++ b/sdk/Android.bp
@@ -18,7 +18,6 @@
         "bp.go",
         "build_release.go",
         "exports.go",
-        "genrule.go",
         "member_trait.go",
         "member_type.go",
         "sdk.go",
@@ -31,7 +30,6 @@
         "cc_sdk_test.go",
         "compat_config_sdk_test.go",
         "exports_test.go",
-        "genrule_test.go",
         "java_sdk_test.go",
         "license_sdk_test.go",
         "member_trait_test.go",
diff --git a/sdk/genrule.go b/sdk/genrule.go
deleted file mode 100644
index 347ab05..0000000
--- a/sdk/genrule.go
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2023 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 sdk
-
-import (
-	"android/soong/android"
-	"android/soong/genrule"
-)
-
-func init() {
-	registerGenRuleBuildComponents(android.InitRegistrationContext)
-}
-
-func registerGenRuleBuildComponents(ctx android.RegistrationContext) {
-	ctx.RegisterModuleType("sdk_genrule", SdkGenruleFactory)
-}
-
-// sdk_genrule_host is a genrule that can depend on sdk and sdk_snapshot module types
-//
-// What this means is that it's a genrule with only the "common_os" variant.
-// sdk modules have 3 variants: host, android, and common_os. The common_os one depends
-// on the host/device ones and packages their result into a final snapshot zip.
-// Genrules probably want access to this snapshot zip when they depend on an sdk module,
-// which means they want to depend on the common_os variant and not the host/android
-// variants.
-func SdkGenruleFactory() android.Module {
-	module := genrule.NewGenRule()
-
-	android.InitCommonOSAndroidMultiTargetsArchModule(module, android.NeitherHostNorDeviceSupported, android.MultilibCommon)
-	android.InitDefaultableModule(module)
-
-	return module
-}
diff --git a/sdk/genrule_test.go b/sdk/genrule_test.go
deleted file mode 100644
index bf67795..0000000
--- a/sdk/genrule_test.go
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright 2018 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 sdk
-
-import (
-	"testing"
-
-	"android/soong/android"
-	"android/soong/genrule"
-	"android/soong/java"
-)
-
-func TestSdkGenrule(t *testing.T) {
-	// Test that a genrule can depend on an sdk if using common_os_srcs
-	bp := `
-				sdk {
-					name: "my_sdk",
-				}
-				genrule {
-					name: "my_regular_genrule",
-					common_os_srcs: [":my_sdk"],
-					out: ["out"],
-					cmd: "cp $(in) $(out)",
-				}
-			`
-	android.GroupFixturePreparers(
-		// if java components aren't registered, the sdk module doesn't create a snapshot for some reason.
-		java.PrepareForTestWithJavaBuildComponents,
-		genrule.PrepareForTestWithGenRuleBuildComponents,
-		PrepareForTestWithSdkBuildComponents,
-		android.FixtureRegisterWithContext(registerGenRuleBuildComponents),
-	).RunTestWithBp(t, bp)
-}
diff --git a/sh/sh_binary.go b/sh/sh_binary.go
index 9c0db73..320e97f 100644
--- a/sh/sh_binary.go
+++ b/sh/sh_binary.go
@@ -15,6 +15,7 @@
 package sh
 
 import (
+	"fmt"
 	"path/filepath"
 	"strings"
 
@@ -45,6 +46,7 @@
 	ctx.RegisterModuleType("sh_binary_host", ShBinaryHostFactory)
 	ctx.RegisterModuleType("sh_test", ShTestFactory)
 	ctx.RegisterModuleType("sh_test_host", ShTestHostFactory)
+	ctx.RegisterModuleType("sh_defaults", ShDefaultsFactory)
 }
 
 // Test fixture preparer that will register most sh build components.
@@ -163,10 +165,14 @@
 
 	// Test options.
 	Test_options android.CommonTestOptions
+
+	// a list of extra test configuration files that should be installed with the module.
+	Extra_test_configs []string `android:"path,arch_variant"`
 }
 
 type ShBinary struct {
 	android.ModuleBase
+	android.DefaultableModuleBase
 
 	properties shBinaryProperties
 
@@ -184,8 +190,9 @@
 
 	installDir android.InstallPath
 
-	data       []android.DataPath
-	testConfig android.Path
+	data             []android.DataPath
+	testConfig       android.Path
+	extraTestConfigs android.Paths
 
 	dataModules map[string]android.Path
 }
@@ -469,6 +476,7 @@
 		HostTemplate:           "${ShellTestConfigTemplate}",
 	})
 
+	s.extraTestConfigs = android.PathsForModuleSrc(ctx, s.testProperties.Extra_test_configs)
 	s.dataModules = make(map[string]android.Path)
 	ctx.VisitDirectDeps(func(dep android.Module) {
 		depTag := ctx.OtherModuleDependencyTag(dep)
@@ -508,6 +516,27 @@
 
 	installedData := ctx.InstallTestData(s.installDir, s.data)
 	s.installedFile = ctx.InstallExecutable(s.installDir, s.outputFilePath.Base(), s.outputFilePath, installedData...)
+
+	mkEntries := s.AndroidMkEntries()[0]
+	android.SetProvider(ctx, tradefed.BaseTestProviderKey, tradefed.BaseTestProviderData{
+		TestcaseRelDataFiles: addArch(ctx.Arch().ArchType.String(), installedData.Paths()),
+		OutputFile:           s.outputFilePath,
+		TestConfig:           s.testConfig,
+		TestSuites:           s.testProperties.Test_suites,
+		IsHost:               false,
+		IsUnitTest:           Bool(s.testProperties.Test_options.Unit_test),
+		MkInclude:            mkEntries.Include,
+		MkAppClass:           mkEntries.Class,
+		InstallDir:           s.installDir,
+	})
+}
+
+func addArch(archType string, paths android.Paths) []string {
+	archRelPaths := []string{}
+	for _, p := range paths {
+		archRelPaths = append(archRelPaths, fmt.Sprintf("%s/%s", archType, p.Rel()))
+	}
+	return archRelPaths
 }
 
 func (s *ShTest) InstallInData() bool {
@@ -531,6 +560,9 @@
 					entries.AddStrings("LOCAL_TEST_DATA_BINS", s.testProperties.Data_bins...)
 				}
 				entries.SetBoolIfTrue("LOCAL_COMPATIBILITY_PER_TESTCASE_DIRECTORY", Bool(s.testProperties.Per_testcase_directory))
+				if len(s.extraTestConfigs) > 0 {
+					entries.AddStrings("LOCAL_EXTRA_FULL_TEST_CONFIGS", s.extraTestConfigs.Strings()...)
+				}
 
 				s.testProperties.Test_options.SetAndroidMkEntries(entries)
 			},
@@ -548,6 +580,7 @@
 	module := &ShBinary{}
 	initShBinaryModule(module)
 	android.InitAndroidArchModule(module, android.HostAndDeviceSupported, android.MultilibFirst)
+	android.InitDefaultableModule(module)
 	return module
 }
 
@@ -557,6 +590,7 @@
 	module := &ShBinary{}
 	initShBinaryModule(module)
 	android.InitAndroidArchModule(module, android.HostSupported, android.MultilibFirst)
+	android.InitDefaultableModule(module)
 	return module
 }
 
@@ -567,6 +601,7 @@
 	module.AddProperties(&module.testProperties)
 
 	android.InitAndroidArchModule(module, android.HostAndDeviceSupported, android.MultilibFirst)
+	android.InitDefaultableModule(module)
 	return module
 }
 
@@ -581,6 +616,21 @@
 	}
 
 	android.InitAndroidArchModule(module, android.HostSupported, android.MultilibFirst)
+	android.InitDefaultableModule(module)
+	return module
+}
+
+type ShDefaults struct {
+	android.ModuleBase
+	android.DefaultsModuleBase
+}
+
+func ShDefaultsFactory() android.Module {
+	module := &ShDefaults{}
+
+	module.AddProperties(&shBinaryProperties{}, &TestProperties{})
+	android.InitDefaultsModule(module)
+
 	return module
 }
 
diff --git a/sh/sh_binary_test.go b/sh/sh_binary_test.go
index 37450b0..28f997d 100644
--- a/sh/sh_binary_test.go
+++ b/sh/sh_binary_test.go
@@ -176,6 +176,22 @@
 	android.AssertBoolEquals(t, "LOCAL_IS_UNIT_TEST", true, actualData)
 }
 
+func TestShTestExtraTestConfig(t *testing.T) {
+	result, _ := testShBinary(t, `
+		sh_test {
+			name: "foo",
+			src: "test.sh",
+			filename: "test.sh",
+                        extra_test_configs: ["config1.xml", "config2.xml"],
+		}
+	`)
+
+	mod := result.ModuleForTests("foo", "android_arm64_armv8-a").Module().(*ShTest)
+	entries := android.AndroidMkEntriesForTest(t, result, mod)[0]
+	actualData := entries.EntryMap["LOCAL_EXTRA_FULL_TEST_CONFIGS"]
+	android.AssertStringPathsRelativeToTopEquals(t, "extra_configs", result.Config(), []string{"config1.xml", "config2.xml"}, actualData)
+}
+
 func TestShTestHost_dataDeviceModules(t *testing.T) {
 	ctx, config := testShBinary(t, `
 		sh_test_host {
@@ -294,3 +310,92 @@
 	actualData := entries.EntryMap["LOCAL_TEST_DATA"]
 	android.AssertStringPathsRelativeToTopEquals(t, "LOCAL_TEST_DATA", config, expectedData, actualData)
 }
+
+func TestDefaultsForTests(t *testing.T) {
+	ctx, config := testShBinary(t, `
+		sh_defaults {
+			name: "defaults",
+			src: "test.sh",
+			filename: "test.sh",
+			data: [
+				"testdata/data1",
+				"testdata/sub/data2",
+			],
+		}
+		sh_test_host {
+			name: "foo",
+			defaults: ["defaults"],
+			data: [
+				"testdata/more_data",
+			],
+			java_data: [
+				"javalib",
+			],
+		}
+
+		java_library_host {
+			name: "javalib",
+			srcs: [],
+		}
+
+		sh_test {
+			name: "sh-test",
+			defaults: ["defaults"],
+		}
+
+	`)
+	buildOS := ctx.Config().BuildOS.String()
+	mod := ctx.ModuleForTests("foo", buildOS+"_x86_64").Module().(*ShTest)
+	if !mod.Host() {
+		t.Errorf("host bit is not set for a sh_test_host module.")
+	}
+	expectedData := []string{
+		":testdata/data1",
+		":testdata/sub/data2",
+		":testdata/more_data",
+		"out/soong/.intermediates/javalib/" + buildOS + "_common/combined/:javalib.jar",
+	}
+
+	entries := android.AndroidMkEntriesForTest(t, ctx, mod)[0]
+	actualData := entries.EntryMap["LOCAL_TEST_DATA"]
+	android.AssertStringPathsRelativeToTopEquals(t, "LOCAL_TEST_DATA", config, expectedData, actualData)
+
+	// Just the defaults
+	expectedData = []string{
+		":testdata/data1",
+		":testdata/sub/data2",
+	}
+	mod = ctx.ModuleForTests("sh-test", "android_arm64_armv8-a").Module().(*ShTest)
+	entries = android.AndroidMkEntriesForTest(t, ctx, mod)[0]
+	actualData = entries.EntryMap["LOCAL_TEST_DATA"]
+	android.AssertStringPathsRelativeToTopEquals(t, "LOCAL_TEST_DATA", config, expectedData, actualData)
+}
+
+func TestDefaultsForBinaries(t *testing.T) {
+	ctx, _ := testShBinary(t, `
+		sh_defaults {
+			name: "defaults",
+			src: "test.sh",
+			filename: "test.sh",
+		}
+		sh_binary_host {
+			name: "the-host-binary",
+			defaults: ["defaults"],
+		}
+		sh_binary{
+			name: "the-binary",
+			defaults: ["defaults"],
+		}
+	`)
+	buildOS := ctx.Config().BuildOS.String()
+	mod := ctx.ModuleForTests("the-host-binary", buildOS+"_x86_64").Module().(*ShBinary)
+	if !mod.Host() {
+		t.Errorf("host bit is not set for a sh_binary_host module.")
+	}
+
+	expectedFilename := "test.sh"
+	android.AssertStringEquals(t, "Filename", expectedFilename, *mod.properties.Filename)
+
+	mod = ctx.ModuleForTests("the-binary", "android_arm64_armv8-a").Module().(*ShBinary)
+	android.AssertStringEquals(t, "Filename", expectedFilename, *mod.properties.Filename)
+}
diff --git a/tests/lib.sh b/tests/lib.sh
index 4c320d0..0e26de5 100644
--- a/tests/lib.sh
+++ b/tests/lib.sh
@@ -91,7 +91,6 @@
 }
 
 function create_mock_soong {
-  create_mock_bazel
   copy_directory build/blueprint
   copy_directory build/soong
   copy_directory build/make
@@ -143,41 +142,6 @@
   USE_RBE=false TARGET_PRODUCT=aosp_arm TARGET_RELEASE=trunk_staging TARGET_BUILD_VARIANT=userdebug build/soong/soong_ui.bash --make-mode --skip-ninja --skip-config --soong-only --skip-soong-tests "$@"
 }
 
-function create_mock_bazel {
-  copy_directory build/bazel
-  copy_directory build/bazel_common_rules
-
-  # This requires pulling more tools into the mock top to build partitions
-  delete_directory build/bazel/examples/partitions
-
-  symlink_directory packages/modules/common/build
-  symlink_directory prebuilts/bazel
-  symlink_directory prebuilts/clang
-  symlink_directory prebuilts/jdk
-  symlink_directory external/bazel-skylib
-  symlink_directory external/bazelbuild-rules_android
-  symlink_directory external/bazelbuild-rules_go
-  symlink_directory external/bazelbuild-rules_license
-  symlink_directory external/bazelbuild-kotlin-rules
-  symlink_directory external/bazelbuild-rules_cc
-  symlink_directory external/bazelbuild-rules_python
-  symlink_directory external/bazelbuild-rules_java
-  symlink_directory external/bazelbuild-rules_rust
-  symlink_directory external/bazelbuild-rules_testing
-  symlink_directory external/rust/crates/tinyjson
-
-  symlink_file WORKSPACE
-  symlink_file BUILD
-}
-
-function run_bazel {
-  # Remove the ninja_build output marker file to communicate to buildbot that this is not a regular Ninja build, and its
-  # output should not be parsed as such.
-  rm -rf out/ninja_build
-
-  build/bazel/bin/bazel "$@"
-}
-
 function run_ninja {
   build/soong/soong_ui.bash --make-mode --skip-config --soong-only --skip-soong-tests "$@"
 }
diff --git a/tradefed/providers.go b/tradefed/providers.go
index 0abac12..0ae841d 100644
--- a/tradefed/providers.go
+++ b/tradefed/providers.go
@@ -9,8 +9,8 @@
 // Data that test_module_config[_host] modules types will need from
 // their dependencies to write out build rules and AndroidMkEntries.
 type BaseTestProviderData struct {
-	// data files and apps for android_test
-	InstalledFiles android.Paths
+	// data files and apps installed for tests, relative to testcases dir.
+	TestcaseRelDataFiles []string
 	// apk for android_test
 	OutputFile android.Path
 	// Either handwritten or generated TF xml.
@@ -28,6 +28,12 @@
 	LocalCertificate string
 	// Indicates if the base module was a unit test.
 	IsUnitTest bool
+	// The .mk file is used AndroidMkEntries for base (soong_java_prebuilt, etc.)
+	MkInclude string
+	// The AppClass to use for the AndroidMkEntries for the base.
+	MkAppClass string
+	// value for LOCAL_MODULE_PATH.  The directory where the module is installed.
+	InstallDir android.InstallPath
 }
 
 var BaseTestProviderKey = blueprint.NewProvider[BaseTestProviderData]()
diff --git a/tradefed_modules/Android.bp b/tradefed_modules/Android.bp
index 9969ae2..a765a05 100644
--- a/tradefed_modules/Android.bp
+++ b/tradefed_modules/Android.bp
@@ -9,13 +9,16 @@
         "blueprint",
         "soong-android",
         "soong-java",
+        "soong-sh",
         "soong-tradefed",
     ],
     srcs: [
         "test_module_config.go",
+        "test_suite.go",
     ],
     testSrcs: [
         "test_module_config_test.go",
+        "test_suite_test.go",
     ],
     pluginFor: ["soong_build"],
 }
diff --git a/tradefed_modules/test_module_config.go b/tradefed_modules/test_module_config.go
index 7a04c19..988352c 100644
--- a/tradefed_modules/test_module_config.go
+++ b/tradefed_modules/test_module_config.go
@@ -196,7 +196,7 @@
 	module := &testModuleConfigModule{}
 
 	module.AddProperties(&module.tradefedProperties)
-	android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibCommon)
+	android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibFirst)
 	android.InitDefaultableModule(module)
 
 	return module
@@ -216,13 +216,28 @@
 // Implements android.AndroidMkEntriesProvider
 var _ android.AndroidMkEntriesProvider = (*testModuleConfigModule)(nil)
 
+func (m *testModuleConfigModule) nativeExtraEntries(entries *android.AndroidMkEntries) {
+	// TODO(ron) provider for suffix and STEM?
+	entries.SetString("LOCAL_MODULE_SUFFIX", "")
+	// Should the stem and path use the base name or our module name?
+	entries.SetString("LOCAL_MODULE_STEM", m.provider.OutputFile.Rel())
+	entries.SetPath("LOCAL_MODULE_PATH", m.provider.InstallDir)
+}
+
+func (m *testModuleConfigModule) javaExtraEntries(entries *android.AndroidMkEntries) {
+	// The app_prebuilt_internal.mk files try create a copy of the OutputFile as an .apk.
+	// Normally, this copies the "package.apk" from the intermediate directory here.
+	// To prevent the copy of the large apk and to prevent confusion with the real .apk we
+	// link to, we set the STEM here to a bogus name and we set OutputFile to a small file (our manifest).
+	// We do this so we don't have to add more conditionals to base_rules.mk
+	// soong_java_prebult has the same issue for .jars so use this in both module types.
+	entries.SetString("LOCAL_MODULE_STEM", fmt.Sprintf("UNUSED-%s", *m.Base))
+	entries.SetString("LOCAL_MODULE_TAGS", "tests")
+}
+
 func (m *testModuleConfigModule) AndroidMkEntries() []android.AndroidMkEntries {
-	appClass := "APPS"
-	include := "$(BUILD_SYSTEM)/soong_app_prebuilt.mk"
-	if m.isHost {
-		appClass = "JAVA_LIBRARIES"
-		include = "$(BUILD_SYSTEM)/soong_java_prebuilt.mk"
-	}
+	appClass := m.provider.MkAppClass
+	include := m.provider.MkInclude
 	return []android.AndroidMkEntries{{
 		Class:      appClass,
 		OutputFile: android.OptionalPathForPath(m.manifest),
@@ -231,7 +246,6 @@
 		ExtraEntries: []android.AndroidMkExtraEntriesFunc{
 			func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) {
 				entries.SetPath("LOCAL_FULL_TEST_CONFIG", m.testConfig)
-				entries.SetString("LOCAL_MODULE_TAGS", "tests")
 				entries.SetString("LOCAL_TEST_MODULE_CONFIG_BASE", *m.Base)
 				if m.provider.LocalSdkVersion != "" {
 					entries.SetString("LOCAL_SDK_VERSION", m.provider.LocalSdkVersion)
@@ -244,13 +258,11 @@
 				entries.AddCompatibilityTestSuites(m.tradefedProperties.Test_suites...)
 				entries.AddStrings("LOCAL_HOST_REQUIRED_MODULES", m.provider.HostRequiredModuleNames...)
 
-				// The app_prebuilt_internal.mk files try create a copy of the OutputFile as an .apk.
-				// Normally, this copies the "package.apk" from the intermediate directory here.
-				// To prevent the copy of the large apk and to prevent confusion with the real .apk we
-				// link to, we set the STEM here to a bogus name and we set OutputFile to a small file (our manifest).
-				// We do this so we don't have to add more conditionals to base_rules.mk
-				// soong_java_prebult has the same issue for .jars so use this in both module types.
-				entries.SetString("LOCAL_MODULE_STEM", fmt.Sprintf("UNUSED-%s", *m.Base))
+				if m.provider.MkAppClass == "NATIVE_TESTS" {
+					m.nativeExtraEntries(entries)
+				} else {
+					m.javaExtraEntries(entries)
+				}
 
 				// In normal java/app modules, the module writes LOCAL_COMPATIBILITY_SUPPORT_FILES
 				// and then base_rules.mk ends up copying each of those dependencies from .intermediates to the install directory.
@@ -357,16 +369,19 @@
 	// FrameworksServicesTests
 	// └── x86_64
 	//    └── FrameworksServicesTests.apk
-	symlinkName := fmt.Sprintf("%s/%s", ctx.DeviceConfig().DeviceArch(), baseApk.Base())
-	// Only android_test, not java_host_test puts the output in the DeviceArch dir.
-	if m.provider.IsHost || ctx.DeviceConfig().DeviceArch() == "" {
-		// testcases/CtsDevicePolicyManagerTestCases
-		// ├── CtsDevicePolicyManagerTestCases.jar
-		symlinkName = baseApk.Base()
+	if m.provider.MkAppClass != "NATIVE_TESTS" {
+		symlinkName := fmt.Sprintf("%s/%s", ctx.DeviceConfig().DeviceArch(), baseApk.Base())
+		// Only android_test, not java_host_test puts the output in the DeviceArch dir.
+		if m.provider.IsHost || ctx.DeviceConfig().DeviceArch() == "" {
+			// testcases/CtsDevicePolicyManagerTestCases
+			// ├── CtsDevicePolicyManagerTestCases.jar
+			symlinkName = baseApk.Base()
+		}
+
+		target := installedBaseRelativeToHere(symlinkName, *m.tradefedProperties.Base)
+		installedApk := ctx.InstallAbsoluteSymlink(installDir, symlinkName, target)
+		m.supportFiles = append(m.supportFiles, installedApk)
 	}
-	target := installedBaseRelativeToHere(symlinkName, *m.tradefedProperties.Base)
-	installedApk := ctx.InstallAbsoluteSymlink(installDir, symlinkName, target)
-	m.supportFiles = append(m.supportFiles, installedApk)
 
 	// 3) Symlink for all data deps
 	// And like this for data files and required modules
@@ -374,8 +389,7 @@
 	// ├── data
 	// │   └── broken_shortcut.xml
 	// ├── JobTestApp.apk
-	for _, f := range m.provider.InstalledFiles {
-		symlinkName := f.Rel()
+	for _, symlinkName := range m.provider.TestcaseRelDataFiles {
 		target := installedBaseRelativeToHere(symlinkName, *m.tradefedProperties.Base)
 		installedPath := ctx.InstallAbsoluteSymlink(installDir, symlinkName, target)
 		m.supportFiles = append(m.supportFiles, installedPath)
@@ -383,10 +397,31 @@
 
 	// 4) Module.config / AndroidTest.xml
 	m.testConfig = m.fixTestConfig(ctx, m.provider.TestConfig)
+
+	// 5) We provide so we can be listed in test_suites.
+	android.SetProvider(ctx, tradefed.BaseTestProviderKey, tradefed.BaseTestProviderData{
+		TestcaseRelDataFiles:    testcaseRel(m.supportFiles.Paths()),
+		OutputFile:              baseApk,
+		TestConfig:              m.testConfig,
+		HostRequiredModuleNames: m.provider.HostRequiredModuleNames,
+		RequiredModuleNames:     m.provider.RequiredModuleNames,
+		TestSuites:              m.tradefedProperties.Test_suites,
+		IsHost:                  m.provider.IsHost,
+		LocalCertificate:        m.provider.LocalCertificate,
+		IsUnitTest:              m.provider.IsUnitTest,
+	})
 }
 
 var _ android.AndroidMkEntriesProvider = (*testModuleConfigHostModule)(nil)
 
+func testcaseRel(paths android.Paths) []string {
+	relPaths := []string{}
+	for _, p := range paths {
+		relPaths = append(relPaths, p.Rel())
+	}
+	return relPaths
+}
+
 // Given a relative path to a file in the current directory or a subdirectory,
 // return a relative path under our sibling directory named `base`.
 // There should be one "../" for each subdir we descend plus one to backup to "base".
diff --git a/tradefed_modules/test_module_config_test.go b/tradefed_modules/test_module_config_test.go
index cf6c7d1..efd4a04 100644
--- a/tradefed_modules/test_module_config_test.go
+++ b/tradefed_modules/test_module_config_test.go
@@ -16,6 +16,7 @@
 import (
 	"android/soong/android"
 	"android/soong/java"
+	"android/soong/sh"
 	"fmt"
 	"strconv"
 	"strings"
@@ -54,6 +55,8 @@
 
 `
 
+const variant = "android_arm64_armv8-a"
+
 // Ensure we create files needed and set the AndroidMkEntries needed
 func TestModuleConfigAndroidTest(t *testing.T) {
 
@@ -62,7 +65,7 @@
 		android.FixtureRegisterWithContext(RegisterTestModuleConfigBuildComponents),
 	).RunTestWithBp(t, bp)
 
-	derived := ctx.ModuleForTests("derived_test", "android_common")
+	derived := ctx.ModuleForTests("derived_test", variant)
 	// Assert there are rules to create these files.
 	derived.Output("test_module_config.manifest")
 	derived.Output("test_config_fixer/derived_test.config")
@@ -88,7 +91,7 @@
 	// And some new derived entries are there.
 	android.AssertArrayString(t, "", entries.EntryMap["LOCAL_MODULE_TAGS"], []string{"tests"})
 
-	android.AssertStringMatches(t, "", entries.EntryMap["LOCAL_FULL_TEST_CONFIG"][0], "derived_test/android_common/test_config_fixer/derived_test.config")
+	android.AssertStringMatches(t, "", entries.EntryMap["LOCAL_FULL_TEST_CONFIG"][0], fmt.Sprintf("derived_test/%s/test_config_fixer/derived_test.config", variant))
 
 	// Check the footer lines.  Our support files should depend on base's support files.
 	convertedActual := make([]string, 5)
@@ -105,6 +108,80 @@
 	})
 }
 
+func TestModuleConfigShTest(t *testing.T) {
+	ctx := android.GroupFixturePreparers(
+		sh.PrepareForTestWithShBuildComponents,
+		android.PrepareForTestWithAndroidBuildComponents,
+		android.FixtureMergeMockFs(android.MockFS{
+			"test.sh":            nil,
+			"testdata/data1":     nil,
+			"testdata/sub/data2": nil,
+		}),
+		android.FixtureRegisterWithContext(RegisterTestModuleConfigBuildComponents),
+	).RunTestWithBp(t, `
+		sh_test {
+			name: "shell_test",
+			src: "test.sh",
+			filename: "test.sh",
+                        test_suites: ["general-tests"],
+			data: [
+				"testdata/data1",
+				"testdata/sub/data2",
+			],
+		}
+                test_module_config {
+                        name: "conch",
+                        base: "shell_test",
+                        test_suites: ["general-tests"],
+                        options: [{name: "SomeName", value: "OptionValue"}],
+                }
+         `)
+	derived := ctx.ModuleForTests("conch", variant) //
+	conch := derived.Module().(*testModuleConfigModule)
+	android.AssertArrayString(t, "TestcaseRelDataFiles", []string{"arm64/testdata/data1", "arm64/testdata/sub/data2"}, conch.provider.TestcaseRelDataFiles)
+	android.AssertStringEquals(t, "Rel OutputFile", "test.sh", conch.provider.OutputFile.Rel())
+
+	// Assert there are rules to create these files.
+	derived.Output("test_module_config.manifest")
+	derived.Output("test_config_fixer/conch.config")
+
+	// Ensure some basic rules exist.
+	entries := android.AndroidMkEntriesForTest(t, ctx.TestContext, derived.Module())[0]
+
+	// Ensure some entries from base are there, specifically support files for data and helper apps.
+	// Do not use LOCAL_COMPATIBILITY_SUPPORT_FILES, but instead use LOCAL_SOONG_INSTALLED_COMPATIBILITY_SUPPORT_FILES
+	android.AssertStringPathsRelativeToTopEquals(t, "support-files", ctx.Config,
+		[]string{"out/soong/target/product/test_device/testcases/conch/arm64/testdata/data1",
+			"out/soong/target/product/test_device/testcases/conch/arm64/testdata/sub/data2"},
+		entries.EntryMap["LOCAL_SOONG_INSTALLED_COMPATIBILITY_SUPPORT_FILES"])
+	android.AssertArrayString(t, "", entries.EntryMap["LOCAL_COMPATIBILITY_SUPPORT_FILES"], []string{})
+
+	android.AssertStringEquals(t, "app class", "NATIVE_TESTS", entries.Class)
+	android.AssertArrayString(t, "required modules", []string{"shell_test"}, entries.EntryMap["LOCAL_REQUIRED_MODULES"])
+	android.AssertArrayString(t, "host required modules", []string{}, entries.EntryMap["LOCAL_HOST_REQUIRED_MODULES"])
+	android.AssertArrayString(t, "cert", []string{}, entries.EntryMap["LOCAL_CERTIFICATE"])
+
+	// And some new derived entries are there.
+	android.AssertArrayString(t, "tags", []string{}, entries.EntryMap["LOCAL_MODULE_TAGS"])
+
+	android.AssertStringMatches(t, "", entries.EntryMap["LOCAL_FULL_TEST_CONFIG"][0],
+		fmt.Sprintf("conch/%s/test_config_fixer/conch.config", variant))
+
+	// Check the footer lines.  Our support files should depend on base's support files.
+	convertedActual := make([]string, 4)
+	for i, e := range entries.FooterLinesForTests() {
+		// AssertStringPathsRelativeToTop doesn't replace both instances
+		convertedActual[i] = strings.Replace(e, ctx.Config.SoongOutDir(), "", 2)
+	}
+	android.AssertArrayString(t, fmt.Sprintf("%s", ctx.Config.SoongOutDir()), convertedActual, []string{
+		"include $(BUILD_SYSTEM)/soong_cc_rust_prebuilt.mk",
+		"/target/product/test_device/testcases/conch/arm64/testdata/data1: /target/product/test_device/testcases/shell_test/arm64/testdata/data1",
+		"/target/product/test_device/testcases/conch/arm64/testdata/sub/data2: /target/product/test_device/testcases/shell_test/arm64/testdata/sub/data2",
+		"",
+	})
+
+}
+
 // Make sure we call test-config-fixer with the right args.
 func TestModuleConfigOptions(t *testing.T) {
 
@@ -114,7 +191,7 @@
 	).RunTestWithBp(t, bp)
 
 	// Check that we generate a rule to make a new AndroidTest.xml/Module.config file.
-	derived := ctx.ModuleForTests("derived_test", "android_common")
+	derived := ctx.ModuleForTests("derived_test", variant)
 	rule_cmd := derived.Rule("fix_test_config").RuleParams.Command
 	android.AssertStringDoesContain(t, "Bad FixConfig rule inputs", rule_cmd,
 		`--test-runner-options='[{"Name":"exclude-filter","Key":"","Value":"android.test.example.devcodelab.DevCodelabTest#testHelloFail"},{"Name":"include-annotation","Key":"","Value":"android.platform.test.annotations.LargeTest"}]'`)
@@ -211,8 +288,7 @@
 	).ExtendWithErrorHandler(
 		android.FixtureExpectsAtLeastOneErrorMatchingPattern("Test options must be given")).
 		RunTestWithBp(t, badBp)
-
-	ctx.ModuleForTests("derived_test", "android_common")
+	ctx.ModuleForTests("derived_test", variant)
 }
 
 func TestModuleConfigMultipleDerivedTestsWriteDistinctMakeEntries(t *testing.T) {
@@ -250,7 +326,7 @@
 	).RunTestWithBp(t, multiBp)
 
 	{
-		derived := ctx.ModuleForTests("derived_test", "android_common")
+		derived := ctx.ModuleForTests("derived_test", variant)
 		entries := android.AndroidMkEntriesForTest(t, ctx.TestContext, derived.Module())[0]
 		// All these should be the same in both derived tests
 		android.AssertStringPathsRelativeToTopEquals(t, "support-files", ctx.Config,
@@ -260,13 +336,13 @@
 			entries.EntryMap["LOCAL_SOONG_INSTALLED_COMPATIBILITY_SUPPORT_FILES"])
 
 		// Except this one, which points to the updated tradefed xml file.
-		android.AssertStringMatches(t, "", entries.EntryMap["LOCAL_FULL_TEST_CONFIG"][0], "derived_test/android_common/test_config_fixer/derived_test.config")
+		android.AssertStringMatches(t, "", entries.EntryMap["LOCAL_FULL_TEST_CONFIG"][0], fmt.Sprintf("derived_test/%s/test_config_fixer/derived_test.config", variant))
 		// And this one, the module name.
 		android.AssertArrayString(t, "", entries.EntryMap["LOCAL_MODULE"], []string{"derived_test"})
 	}
 
 	{
-		derived := ctx.ModuleForTests("another_derived_test", "android_common")
+		derived := ctx.ModuleForTests("another_derived_test", variant)
 		entries := android.AndroidMkEntriesForTest(t, ctx.TestContext, derived.Module())[0]
 		// All these should be the same in both derived tests
 		android.AssertStringPathsRelativeToTopEquals(t, "support-files", ctx.Config,
@@ -275,7 +351,8 @@
 				"out/soong/target/product/test_device/testcases/another_derived_test/data/testfile"},
 			entries.EntryMap["LOCAL_SOONG_INSTALLED_COMPATIBILITY_SUPPORT_FILES"])
 		// Except this one, which points to the updated tradefed xml file.
-		android.AssertStringMatches(t, "", entries.EntryMap["LOCAL_FULL_TEST_CONFIG"][0], "another_derived_test/android_common/test_config_fixer/another_derived_test.config")
+		android.AssertStringMatches(t, "", entries.EntryMap["LOCAL_FULL_TEST_CONFIG"][0],
+			fmt.Sprintf("another_derived_test/%s/test_config_fixer/another_derived_test.config", variant))
 		// And this one, the module name.
 		android.AssertArrayString(t, "", entries.EntryMap["LOCAL_MODULE"], []string{"another_derived_test"})
 	}
diff --git a/tradefed_modules/test_suite.go b/tradefed_modules/test_suite.go
new file mode 100644
index 0000000..00585f5
--- /dev/null
+++ b/tradefed_modules/test_suite.go
@@ -0,0 +1,173 @@
+// Copyright 2024 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 tradefed_modules
+
+import (
+	"encoding/json"
+	"path"
+	"path/filepath"
+
+	"android/soong/android"
+	"android/soong/tradefed"
+	"github.com/google/blueprint"
+)
+
+const testSuiteModuleType = "test_suite"
+
+type testSuiteTag struct{
+	blueprint.BaseDependencyTag
+}
+
+type testSuiteManifest struct {
+	Name  string `json:"name"`
+	Files []string `json:"files"`
+}
+
+func init() {
+	RegisterTestSuiteBuildComponents(android.InitRegistrationContext)
+}
+
+func RegisterTestSuiteBuildComponents(ctx android.RegistrationContext) {
+	ctx.RegisterModuleType(testSuiteModuleType, TestSuiteFactory)
+}
+
+var PrepareForTestWithTestSuiteBuildComponents = android.GroupFixturePreparers(
+	android.FixtureRegisterWithContext(RegisterTestSuiteBuildComponents),
+)
+
+type testSuiteProperties struct {
+	Description string
+	Tests []string `android:"path,arch_variant"`
+}
+
+type testSuiteModule struct {
+	android.ModuleBase
+	android.DefaultableModuleBase
+	testSuiteProperties
+}
+
+func (t *testSuiteModule) DepsMutator(ctx android.BottomUpMutatorContext) {
+	for _, test := range t.Tests {
+		if ctx.OtherModuleDependencyVariantExists(ctx.Config().BuildOSCommonTarget.Variations(), test) {
+			// Host tests.
+			ctx.AddVariationDependencies(ctx.Config().BuildOSCommonTarget.Variations(), testSuiteTag{}, test)
+		} else {
+			// Target tests.
+			ctx.AddDependency(ctx.Module(), testSuiteTag{}, test)
+		}
+	}
+}
+
+func (t *testSuiteModule) GenerateAndroidBuildActions(ctx android.ModuleContext) {
+	suiteName := ctx.ModuleName()
+	modulesByName := make(map[string]android.Module)
+	ctx.WalkDeps(func(child, parent android.Module) bool {
+		// Recurse into test_suite dependencies.
+		if ctx.OtherModuleType(child) == testSuiteModuleType {
+			ctx.Phony(suiteName, android.PathForPhony(ctx, child.Name()))
+			return true
+		}
+
+		// Only write out top level test suite dependencies here.
+		if _, ok := ctx.OtherModuleDependencyTag(child).(testSuiteTag); !ok {
+			return false
+		}
+
+		if !child.InstallInTestcases() {
+			ctx.ModuleErrorf("test_suite only supports modules installed in testcases. %q is not installed in testcases.", child.Name())
+			return false
+		}
+
+		modulesByName[child.Name()] = child
+		return false
+	})
+
+	var files []string
+	for name, module := range modulesByName {
+		// Get the test provider data from the child.
+		tp, ok := android.OtherModuleProvider(ctx, module, tradefed.BaseTestProviderKey)
+		if !ok {
+			// TODO: Consider printing out a list of all module types.
+			ctx.ModuleErrorf("%q is not a test module.", name)
+			continue
+		}
+
+		files = append(files, packageModuleFiles(ctx, suiteName, module, tp)...)
+		ctx.Phony(suiteName, android.PathForPhony(ctx, name))
+	}
+
+	manifestPath := android.PathForSuiteInstall(ctx, suiteName, suiteName+".json")
+	b, err := json.Marshal(testSuiteManifest{Name: suiteName, Files: files})
+	if err != nil {
+		ctx.ModuleErrorf("Failed to marshal manifest: %v", err)
+		return
+	}
+	android.WriteFileRule(ctx, manifestPath, string(b))
+
+	ctx.Phony(suiteName, manifestPath)
+}
+
+func TestSuiteFactory() android.Module {
+	module := &testSuiteModule{}
+	module.AddProperties(&module.testSuiteProperties)
+
+	android.InitAndroidArchModule(module, android.HostAndDeviceSupported, android.MultilibCommon)
+	android.InitDefaultableModule(module)
+
+	return module
+}
+
+func packageModuleFiles(ctx android.ModuleContext, suiteName string, module android.Module, tp tradefed.BaseTestProviderData) []string {
+
+	hostOrTarget := "target"
+	if tp.IsHost {
+		hostOrTarget = "host"
+	}
+
+	// suiteRoot at out/soong/packaging/<suiteName>.
+	suiteRoot := android.PathForSuiteInstall(ctx, suiteName)
+
+	var installed android.InstallPaths
+	// Install links to installed files from the module.
+	if installFilesInfo, ok := android.OtherModuleProvider(ctx, module, android.InstallFilesProvider); ok {
+		for _, f := range installFilesInfo.InstallFiles {
+			// rel is anything under .../<partition>, normally under .../testcases.
+			rel := android.Rel(ctx, f.PartitionDir(), f.String())
+
+			// Install the file under <suiteRoot>/<host|target>/<partition>.
+			installDir := suiteRoot.Join(ctx, hostOrTarget, f.Partition(), path.Dir(rel))
+			linkTo, err := filepath.Rel(installDir.String(), f.String())
+			if err != nil {
+				ctx.ModuleErrorf("Failed to get relative path from %s to %s: %v", installDir.String(), f.String(), err)
+				continue
+			}
+			installed = append(installed, ctx.InstallAbsoluteSymlink(installDir, path.Base(rel), linkTo))
+		}
+	}
+
+	// Install config file.
+	if tp.TestConfig != nil {
+		moduleRoot := suiteRoot.Join(ctx, hostOrTarget, "testcases", module.Name())
+		installed = append(installed, ctx.InstallFile(moduleRoot, module.Name() + ".config", tp.TestConfig))
+	}
+
+	// Add to phony and manifest, manifestpaths are relative to suiteRoot.
+	var manifestEntries []string
+	for _, f := range installed {
+		manifestEntries = append(manifestEntries, android.Rel(ctx, suiteRoot.String(), f.String()))
+		ctx.Phony(suiteName, f)
+	}
+	return manifestEntries
+}
diff --git a/tradefed_modules/test_suite_test.go b/tradefed_modules/test_suite_test.go
new file mode 100644
index 0000000..3c0a9eb
--- /dev/null
+++ b/tradefed_modules/test_suite_test.go
@@ -0,0 +1,151 @@
+// Copyright 2024 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 tradefed_modules
+
+import (
+	"android/soong/android"
+	"android/soong/java"
+	"encoding/json"
+	"slices"
+	"testing"
+)
+
+func TestTestSuites(t *testing.T) {
+	t.Parallel()
+	ctx := android.GroupFixturePreparers(
+		java.PrepareForTestWithJavaDefaultModules,
+		android.FixtureRegisterWithContext(RegisterTestSuiteBuildComponents),
+	).RunTestWithBp(t, `
+		android_test {
+			name: "TestModule1",
+			sdk_version: "current",
+		}
+
+		android_test {
+			name: "TestModule2",
+			sdk_version: "current",
+		}
+
+		test_suite {
+			name: "my-suite",
+			description: "a test suite",
+			tests: [
+				"TestModule1",
+				"TestModule2",
+			]
+		}
+	`)
+	manifestPath := ctx.ModuleForTests("my-suite", "android_common").Output("out/soong/test_suites/my-suite/my-suite.json")
+	var actual testSuiteManifest
+	if err := json.Unmarshal([]byte(android.ContentFromFileRuleForTests(t, ctx.TestContext, manifestPath)), &actual); err != nil {
+		t.Errorf("failed to unmarshal manifest: %v", err)
+	}
+	slices.Sort(actual.Files)
+
+	expected := testSuiteManifest{
+		Name: "my-suite",
+		Files: []string{
+			"target/testcases/TestModule1/TestModule1.config",
+			"target/testcases/TestModule1/arm64/TestModule1.apk",
+			"target/testcases/TestModule2/TestModule2.config",
+			"target/testcases/TestModule2/arm64/TestModule2.apk",
+		},
+	}
+
+	android.AssertDeepEquals(t, "manifests differ", expected, actual)
+}
+
+func TestTestSuitesWithNested(t *testing.T) {
+	t.Parallel()
+	ctx := android.GroupFixturePreparers(
+		java.PrepareForTestWithJavaDefaultModules,
+		android.FixtureRegisterWithContext(RegisterTestSuiteBuildComponents),
+	).RunTestWithBp(t, `
+		android_test {
+			name: "TestModule1",
+			sdk_version: "current",
+		}
+
+		android_test {
+			name: "TestModule2",
+			sdk_version: "current",
+		}
+
+		android_test {
+			name: "TestModule3",
+			sdk_version: "current",
+		}
+
+		test_suite {
+			name: "my-child-suite",
+			description: "a child test suite",
+			tests: [
+				"TestModule1",
+				"TestModule2",
+			]
+		}
+
+		test_suite {
+			name: "my-all-tests-suite",
+			description: "a parent test suite",
+			tests: [
+				"TestModule1",
+				"TestModule3",
+				"my-child-suite",
+			]
+		}
+	`)
+	manifestPath := ctx.ModuleForTests("my-all-tests-suite", "android_common").Output("out/soong/test_suites/my-all-tests-suite/my-all-tests-suite.json")
+	var actual testSuiteManifest
+	if err := json.Unmarshal([]byte(android.ContentFromFileRuleForTests(t, ctx.TestContext, manifestPath)), &actual); err != nil {
+		t.Errorf("failed to unmarshal manifest: %v", err)
+	}
+	slices.Sort(actual.Files)
+
+	expected := testSuiteManifest{
+		Name: "my-all-tests-suite",
+		Files: []string{
+			"target/testcases/TestModule1/TestModule1.config",
+			"target/testcases/TestModule1/arm64/TestModule1.apk",
+			"target/testcases/TestModule2/TestModule2.config",
+			"target/testcases/TestModule2/arm64/TestModule2.apk",
+			"target/testcases/TestModule3/TestModule3.config",
+			"target/testcases/TestModule3/arm64/TestModule3.apk",
+		},
+	}
+
+	android.AssertDeepEquals(t, "manifests differ", expected, actual)
+}
+
+func TestTestSuitesNotInstalledInTestcases(t *testing.T) {
+	t.Parallel()
+	android.GroupFixturePreparers(
+		java.PrepareForTestWithJavaDefaultModules,
+		android.FixtureRegisterWithContext(RegisterTestSuiteBuildComponents),
+	).ExtendWithErrorHandler(android.FixtureExpectsAllErrorsToMatchAPattern([]string{
+		`"SomeHostTest" is not installed in testcases`,
+	})).RunTestWithBp(t, `
+			java_test_host {
+				name: "SomeHostTest",
+				srcs: ["a.java"],
+			}
+			test_suite {
+				name: "my-suite",
+				description: "a test suite",
+				tests: [
+					"SomeHostTest",
+				]
+			}
+	`)
+}