Merge "Revert "Build vbmeta partitions with soong"" 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/build_prop.go b/android/build_prop.go
index 5547680..8389470 100644
--- a/android/build_prop.go
+++ b/android/build_prop.go
@@ -109,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"
 }
@@ -119,6 +125,9 @@
 	"product",
 	"odm",
 	"vendor",
+	"system_dlkm",
+	"vendor_dlkm",
+	"odm_dlkm",
 }
 
 func (p *buildPropModule) GenerateAndroidBuildActions(ctx ModuleContext) {
diff --git a/android/module.go b/android/module.go
index 72c8b94..6217833 100644
--- a/android/module.go
+++ b/android/module.go
@@ -81,6 +81,9 @@
 	InstallInProduct() bool
 	InstallInVendor() bool
 	InstallInSystemExt() bool
+	InstallInSystemDlkm() bool
+	InstallInVendorDlkm() bool
+	InstallInOdmDlkm() bool
 	InstallForceOS() (*OsType, *ArchType)
 	PartitionTag(DeviceConfig) string
 	HideFromMake()
@@ -386,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"`
 
@@ -1535,6 +1547,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
 }
diff --git a/android/module_context.go b/android/module_context.go
index 1f5e706..41cb0cc 100644
--- a/android/module_context.go
+++ b/android/module_context.go
@@ -196,6 +196,9 @@
 	InstallInOdm() bool
 	InstallInProduct() bool
 	InstallInVendor() bool
+	InstallInSystemDlkm() bool
+	InstallInVendorDlkm() bool
+	InstallInOdmDlkm() bool
 	InstallForceOS() (*OsType, *ArchType)
 
 	RequiredModuleNames(ctx ConfigurableEvaluatorContext) []string
@@ -493,6 +496,18 @@
 	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
diff --git a/android/module_proxy.go b/android/module_proxy.go
index a60a5a8..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")
 }
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/paths.go b/android/paths.go
index a7ee7ac..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()
 }
@@ -2077,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
@@ -2131,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"
 		}
@@ -2334,6 +2359,9 @@
 	inOdm           bool
 	inProduct       bool
 	inVendor        bool
+	inSystemDlkm    bool
+	inVendorDlkm    bool
+	inOdmDlkm       bool
 	forceOS         *OsType
 	forceArch       *ArchType
 }
@@ -2388,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/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/etc/prebuilt_etc.go b/etc/prebuilt_etc.go
index 5d1d5e2..be943a3 100644
--- a/etc/prebuilt_etc.go
+++ b/etc/prebuilt_etc.go
@@ -137,15 +137,6 @@
 	// Install symlinks to the installed file.
 	Symlinks []string `android:"arch_variant"`
 
-	// Install to partition system_dlkm when set to true.
-	System_dlkm_specific *bool `android:"arch_variant"`
-
-	// Install to partition vendor_dlkm when set to true.
-	Vendor_dlkm_specific *bool `android:"arch_variant"`
-
-	// Install to partition odm_dlkm when set to true.
-	Odm_dlkm_specific *bool `android:"arch_variant"`
-
 	// Install to partition oem when set to true.
 	Oem_specific *bool `android:"arch_variant"`
 }
@@ -386,13 +377,7 @@
 	}
 	baseInstallDirPath := android.PathForModuleInstall(ctx, p.installBaseDir(ctx), p.SubDir())
 	// TODO(b/377304441)
-	if android.Bool(p.properties.System_dlkm_specific) {
-		baseInstallDirPath = android.PathForModuleInPartitionInstall(ctx, ctx.DeviceConfig().SystemDlkmPath(), p.installBaseDir(ctx), p.SubDir())
-	} else if android.Bool(p.properties.Vendor_dlkm_specific) {
-		baseInstallDirPath = android.PathForModuleInPartitionInstall(ctx, ctx.DeviceConfig().VendorDlkmPath(), p.installBaseDir(ctx), p.SubDir())
-	} else if android.Bool(p.properties.Odm_dlkm_specific) {
-		baseInstallDirPath = android.PathForModuleInPartitionInstall(ctx, ctx.DeviceConfig().OdmDlkmPath(), p.installBaseDir(ctx), p.SubDir())
-	} else if android.Bool(p.properties.Oem_specific) {
+	if android.Bool(p.properties.Oem_specific) {
 		baseInstallDirPath = android.PathForModuleInPartitionInstall(ctx, ctx.DeviceConfig().OemPath(), p.installBaseDir(ctx), p.SubDir())
 	}
 
diff --git a/fsgen/filesystem_creator.go b/fsgen/filesystem_creator.go
index bdffabf..fe590a9 100644
--- a/fsgen/filesystem_creator.go
+++ b/fsgen/filesystem_creator.go
@@ -136,6 +136,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/*",
@@ -211,38 +231,28 @@
 }
 
 // createPrebuiltKernelModules creates `prebuilt_kernel_modules`. These modules will be added to deps of the
-// autogenerated *_dlkm filsystem modules.
-// The input `kernelModules` is a space separated list of .ko files in the workspace. This will be partitioned per directory
-// and a `prebuilt_kernel_modules` will be created per partition.
-// These autogenerated modules will be subsequently added to the deps of the top level *_dlkm android_filesystem
+// 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.
+
+// The input `kernelModules` is a space separated list of .ko files in the workspace.
 func (f *filesystemCreator) createPrebuiltKernelModules(ctx android.LoadHookContext, partitionType string, kernelModules []string) {
-	// Partition the files per directory
-	dirToFiles := map[string][]string{}
-	for _, kernelModule := range kernelModules {
-		dir := filepath.Dir(kernelModule)
-		base := filepath.Base(kernelModule)
-		dirToFiles[dir] = append(dirToFiles[dir], base)
-	}
-	// Create a prebuilt_kernel_modules module per partition
 	fsGenState := ctx.Config().Get(fsGenStateOnceKey).(*FsGenState)
-	for index, dir := range android.SortedKeys(dirToFiles) {
-		name := generatedModuleName(ctx.Config(), fmt.Sprintf("%s-kernel-modules-%s", partitionType, strconv.Itoa(index)))
-		props := &struct {
-			Name *string
-			Srcs []string
-		}{
-			Name: proptools.StringPtr(name),
-			Srcs: dirToFiles[dir],
-		}
-		kernelModule := ctx.CreateModuleInDirectory(
-			kernel.PrebuiltKernelModulesFactory,
-			dir,
-			props,
-		)
-		kernelModule.HideFromMake()
-		// Add to deps
-		(*fsGenState.fsDeps[partitionType])[name] = defaultDepCandidateProps(ctx.Config())
+	name := generatedModuleName(ctx.Config(), fmt.Sprintf("%s-kernel-modules", partitionType))
+	props := &struct {
+		Name *string
+		Srcs []string
+	}{
+		Name: proptools.StringPtr(name),
+		Srcs: kernelModules,
 	}
+	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
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/tradefed_modules/test_module_config.go b/tradefed_modules/test_module_config.go
index 7a04c19..5c13d64 100644
--- a/tradefed_modules/test_module_config.go
+++ b/tradefed_modules/test_module_config.go
@@ -383,6 +383,19 @@
 
 	// 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{
+		InstalledFiles:          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)
diff --git a/tradefed_modules/test_suite.go b/tradefed_modules/test_suite.go
index 8ab194d..00585f5 100644
--- a/tradefed_modules/test_suite.go
+++ b/tradefed_modules/test_suite.go
@@ -15,15 +15,32 @@
 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("test_suite", TestSuiteFactory)
+	ctx.RegisterModuleType(testSuiteModuleType, TestSuiteFactory)
 }
 
 var PrepareForTestWithTestSuiteBuildComponents = android.GroupFixturePreparers(
@@ -41,16 +58,116 @@
 	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) {
-	// TODO(hwj): Implement this.
+	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.InitAndroidModule(module)
+	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
index f9e9da0..3c0a9eb 100644
--- a/tradefed_modules/test_suite_test.go
+++ b/tradefed_modules/test_suite_test.go
@@ -16,12 +16,14 @@
 import (
 	"android/soong/android"
 	"android/soong/java"
+	"encoding/json"
+	"slices"
 	"testing"
 )
 
 func TestTestSuites(t *testing.T) {
 	t.Parallel()
-	android.GroupFixturePreparers(
+	ctx := android.GroupFixturePreparers(
 		java.PrepareForTestWithJavaDefaultModules,
 		android.FixtureRegisterWithContext(RegisterTestSuiteBuildComponents),
 	).RunTestWithBp(t, `
@@ -44,4 +46,106 @@
 			]
 		}
 	`)
+	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",
+				]
+			}
+	`)
 }