Refactor apexBundle code.

Move dependency walker out of GenerateAndroidBuildActions, reduce IDE
warnings.

Test: treehugger
Change-Id: I520c59772b1da7102d2a2364b2c56789455ea7fb
diff --git a/apex/apex.go b/apex/apex.go
index 7e913e8..5678b06 100644
--- a/apex/apex.go
+++ b/apex/apex.go
@@ -1338,7 +1338,7 @@
 var _ android.DepIsInSameApex = (*apexBundle)(nil)
 
 // Implements android.DepInInSameApex
-func (a *apexBundle) DepIsInSameApex(ctx android.BaseModuleContext, dep android.Module) bool {
+func (a *apexBundle) DepIsInSameApex(_ android.BaseModuleContext, _ android.Module) bool {
 	// direct deps of an APEX bundle are all part of the APEX bundle
 	// TODO(jiyong): shouldn't we look into the payload field of the dependencyTag?
 	return true
@@ -1474,7 +1474,7 @@
 	}
 
 	// Then follow the global setting
-	globalSanitizerNames := []string{}
+	var globalSanitizerNames []string
 	if a.Host() {
 		globalSanitizerNames = config.SanitizeHost()
 	} else {
@@ -1790,6 +1790,382 @@
 	}
 }
 
+type visitorContext struct {
+	// all the files that will be included in this APEX
+	filesInfo []apexFile
+
+	// native lib dependencies
+	provideNativeLibs []string
+	requireNativeLibs []string
+
+	handleSpecialLibs bool
+}
+
+func (vctx *visitorContext) normalizeFileInfo() {
+	encountered := make(map[string]apexFile)
+	for _, f := range vctx.filesInfo {
+		dest := filepath.Join(f.installDir, f.builtFile.Base())
+		if e, ok := encountered[dest]; !ok {
+			encountered[dest] = f
+		} else {
+			// If a module is directly included and also transitively depended on
+			// consider it as directly included.
+			e.transitiveDep = e.transitiveDep && f.transitiveDep
+			encountered[dest] = e
+		}
+	}
+	vctx.filesInfo = vctx.filesInfo[:0]
+	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.
+		return vctx.filesInfo[i].path() < vctx.filesInfo[j].path()
+	})
+}
+
+func (a *apexBundle) depVisitor(vctx *visitorContext, ctx android.ModuleContext, child, parent blueprint.Module) bool {
+	depTag := ctx.OtherModuleDependencyTag(child)
+	if _, ok := depTag.(android.ExcludeFromApexContentsTag); ok {
+		return false
+	}
+	if mod, ok := child.(android.Module); ok && !mod.Enabled() {
+		return false
+	}
+	depName := ctx.OtherModuleName(child)
+	if _, isDirectDep := parent.(*apexBundle); isDirectDep {
+		switch depTag {
+		case sharedLibTag, jniLibTag:
+			isJniLib := depTag == jniLibTag
+			switch ch := child.(type) {
+			case *cc.Module:
+				fi := apexFileForNativeLibrary(ctx, ch, vctx.handleSpecialLibs)
+				fi.isJniLib = isJniLib
+				vctx.filesInfo = append(vctx.filesInfo, fi)
+				// Collect the list of stub-providing libs except:
+				// - VNDK libs are only for vendors
+				// - bootstrap bionic libs are treated as provided by system
+				if ch.HasStubsVariants() && !a.vndkApex && !cc.InstallToBootstrap(ch.BaseModuleName(), ctx.Config()) {
+					vctx.provideNativeLibs = append(vctx.provideNativeLibs, fi.stem())
+				}
+				return true // track transitive dependencies
+			case *rust.Module:
+				fi := apexFileForRustLibrary(ctx, ch)
+				fi.isJniLib = isJniLib
+				vctx.filesInfo = append(vctx.filesInfo, fi)
+				return true // track transitive dependencies
+			default:
+				propertyName := "native_shared_libs"
+				if isJniLib {
+					propertyName = "jni_libs"
+				}
+				ctx.PropertyErrorf(propertyName, "%q is not a cc_library or cc_library_shared module", depName)
+			}
+		case executableTag:
+			switch ch := child.(type) {
+			case *cc.Module:
+				vctx.filesInfo = append(vctx.filesInfo, apexFileForExecutable(ctx, ch))
+				return true // track transitive dependencies
+			case *python.Module:
+				if ch.HostToolPath().Valid() {
+					vctx.filesInfo = append(vctx.filesInfo, apexFileForPyBinary(ctx, ch))
+				}
+			case bootstrap.GoBinaryTool:
+				if a.Host() {
+					vctx.filesInfo = append(vctx.filesInfo, apexFileForGoBinary(ctx, depName, ch))
+				}
+			case *rust.Module:
+				vctx.filesInfo = append(vctx.filesInfo, apexFileForRustExecutable(ctx, ch))
+				return true // track transitive dependencies
+			default:
+				ctx.PropertyErrorf("binaries",
+					"%q is neither cc_binary, rust_binary, (embedded) py_binary, (host) blueprint_go_binary, nor (host) bootstrap_go_binary", depName)
+			}
+		case shBinaryTag:
+			if csh, ok := child.(*sh.ShBinary); ok {
+				vctx.filesInfo = append(vctx.filesInfo, apexFileForShBinary(ctx, csh))
+			} else {
+				ctx.PropertyErrorf("sh_binaries", "%q is not a sh_binary module", depName)
+			}
+		case bcpfTag:
+			bcpfModule, ok := child.(*java.BootclasspathFragmentModule)
+			if !ok {
+				ctx.PropertyErrorf("bootclasspath_fragments", "%q is not a bootclasspath_fragment module", depName)
+				return false
+			}
+
+			vctx.filesInfo = append(vctx.filesInfo, apexBootclasspathFragmentFiles(ctx, child)...)
+			for _, makeModuleName := range bcpfModule.BootImageDeviceInstallMakeModules() {
+				a.requiredDeps = append(a.requiredDeps, makeModuleName)
+			}
+			return true
+		case sscpfTag:
+			if _, ok := child.(*java.SystemServerClasspathModule); !ok {
+				ctx.PropertyErrorf("systemserverclasspath_fragments",
+					"%q is not a systemserverclasspath_fragment module", depName)
+				return false
+			}
+			if af := apexClasspathFragmentProtoFile(ctx, child); af != nil {
+				vctx.filesInfo = append(vctx.filesInfo, *af)
+			}
+			return true
+		case javaLibTag:
+			switch child.(type) {
+			case *java.Library, *java.SdkLibrary, *java.DexImport, *java.SdkLibraryImport, *java.Import:
+				af := apexFileForJavaModule(ctx, child.(javaModule))
+				if !af.ok() {
+					ctx.PropertyErrorf("java_libs", "%q is not configured to be compiled into dex", depName)
+					return false
+				}
+				vctx.filesInfo = append(vctx.filesInfo, af)
+				return true // track transitive dependencies
+			default:
+				ctx.PropertyErrorf("java_libs", "%q of type %q is not supported", depName, ctx.OtherModuleType(child))
+			}
+		case androidAppTag:
+			switch ap := child.(type) {
+			case *java.AndroidApp:
+				vctx.filesInfo = append(vctx.filesInfo, apexFileForAndroidApp(ctx, ap))
+				return true // track transitive dependencies
+			case *java.AndroidAppImport:
+				vctx.filesInfo = append(vctx.filesInfo, apexFileForAndroidApp(ctx, ap))
+			case *java.AndroidTestHelperApp:
+				vctx.filesInfo = append(vctx.filesInfo, apexFileForAndroidApp(ctx, ap))
+			case *java.AndroidAppSet:
+				appDir := "app"
+				if ap.Privileged() {
+					appDir = "priv-app"
+				}
+				// TODO(b/224589412, b/226559955): Ensure that the dirname is
+				// suffixed so that PackageManager correctly invalidates the
+				// existing installed apk in favour of the new APK-in-APEX.
+				// See bugs for more information.
+				appDirName := filepath.Join(appDir, ap.BaseModuleName()+"@"+sanitizedBuildIdForPath(ctx))
+				af := newApexFile(ctx, ap.OutputFile(), ap.BaseModuleName(), appDirName, appSet, ap)
+				af.certificate = java.PresignedCertificate
+				vctx.filesInfo = append(vctx.filesInfo, af)
+			default:
+				ctx.PropertyErrorf("apps", "%q is not an android_app module", depName)
+			}
+		case rroTag:
+			if rro, ok := child.(java.RuntimeResourceOverlayModule); ok {
+				vctx.filesInfo = append(vctx.filesInfo, apexFileForRuntimeResourceOverlay(ctx, rro))
+			} else {
+				ctx.PropertyErrorf("rros", "%q is not an runtime_resource_overlay module", depName)
+			}
+		case bpfTag:
+			if bpfProgram, ok := child.(bpf.BpfModule); ok {
+				filesToCopy, _ := bpfProgram.OutputFiles("")
+				apex_sub_dir := bpfProgram.SubDir()
+				for _, bpfFile := range filesToCopy {
+					vctx.filesInfo = append(vctx.filesInfo, apexFileForBpfProgram(ctx, bpfFile, apex_sub_dir, bpfProgram))
+				}
+			} else {
+				ctx.PropertyErrorf("bpfs", "%q is not a bpf module", depName)
+			}
+		case fsTag:
+			if fs, ok := child.(filesystem.Filesystem); ok {
+				vctx.filesInfo = append(vctx.filesInfo, apexFileForFilesystem(ctx, fs.OutputPath(), fs))
+			} else {
+				ctx.PropertyErrorf("filesystems", "%q is not a filesystem module", depName)
+			}
+		case prebuiltTag:
+			if prebuilt, ok := child.(prebuilt_etc.PrebuiltEtcModule); ok {
+				vctx.filesInfo = append(vctx.filesInfo, apexFileForPrebuiltEtc(ctx, prebuilt, depName))
+			} else {
+				ctx.PropertyErrorf("prebuilts", "%q is not a prebuilt_etc module", depName)
+			}
+		case compatConfigTag:
+			if compatConfig, ok := child.(java.PlatformCompatConfigIntf); ok {
+				vctx.filesInfo = append(vctx.filesInfo, apexFileForCompatConfig(ctx, compatConfig, depName))
+			} else {
+				ctx.PropertyErrorf("compat_configs", "%q is not a platform_compat_config module", depName)
+			}
+		case testTag:
+			if ccTest, ok := child.(*cc.Module); ok {
+				if ccTest.IsTestPerSrcAllTestsVariation() {
+					// Multiple-output test module (where `test_per_src: true`).
+					//
+					// `ccTest` is the "" ("all tests") variation of a `test_per_src` module.
+					// We do not add this variation to `filesInfo`, as it has no output;
+					// however, we do add the other variations of this module as indirect
+					// dependencies (see below).
+				} else {
+					// Single-output test module (where `test_per_src: false`).
+					af := apexFileForExecutable(ctx, ccTest)
+					af.class = nativeTest
+					vctx.filesInfo = append(vctx.filesInfo, af)
+				}
+				return true // track transitive dependencies
+			} else {
+				ctx.PropertyErrorf("tests", "%q is not a cc module", depName)
+			}
+		case keyTag:
+			if key, ok := child.(*apexKey); ok {
+				a.privateKeyFile = key.privateKeyFile
+				a.publicKeyFile = key.publicKeyFile
+			} else {
+				ctx.PropertyErrorf("key", "%q is not an apex_key module", depName)
+			}
+		case certificateTag:
+			if dep, ok := child.(*java.AndroidAppCertificate); ok {
+				a.containerCertificateFile = dep.Certificate.Pem
+				a.containerPrivateKeyFile = dep.Certificate.Key
+			} else {
+				ctx.ModuleErrorf("certificate dependency %q must be an android_app_certificate module", depName)
+			}
+		case android.PrebuiltDepTag:
+			// If the prebuilt is force disabled, remember to delete the prebuilt file
+			// that might have been installed in the previous builds
+			if prebuilt, ok := child.(prebuilt); ok && prebuilt.isForceDisabled() {
+				a.prebuiltFileToDelete = prebuilt.InstallFilename()
+			}
+		}
+		return false
+	}
+
+	if a.vndkApex {
+		return false
+	}
+
+	// indirect dependencies
+	am, ok := child.(android.ApexModule)
+	if !ok {
+		return false
+	}
+	// We cannot use a switch statement on `depTag` here as the checked
+	// tags used below are private (e.g. `cc.sharedDepTag`).
+	if cc.IsSharedDepTag(depTag) || cc.IsRuntimeDepTag(depTag) {
+		if ch, ok := child.(*cc.Module); ok {
+			if ch.UseVndk() && proptools.Bool(a.properties.Use_vndk_as_stable) && ch.IsVndk() {
+				vctx.requireNativeLibs = append(vctx.requireNativeLibs, ":vndk")
+				return false
+			}
+			af := apexFileForNativeLibrary(ctx, ch, vctx.handleSpecialLibs)
+			af.transitiveDep = true
+
+			// Always track transitive dependencies for host.
+			if a.Host() {
+				vctx.filesInfo = append(vctx.filesInfo, af)
+				return true
+			}
+
+			abInfo := ctx.Provider(ApexBundleInfoProvider).(ApexBundleInfo)
+			if !abInfo.Contents.DirectlyInApex(depName) && (ch.IsStubs() || ch.HasStubsVariants()) {
+				// If the dependency is a stubs lib, don't include it in this APEX,
+				// but make sure that the lib is installed on the device.
+				// In case no APEX is having the lib, the lib is installed to the system
+				// partition.
+				//
+				// Always include if we are a host-apex however since those won't have any
+				// system libraries.
+				if !am.DirectlyInAnyApex() {
+					// we need a module name for Make
+					name := ch.ImplementationModuleNameForMake(ctx) + ch.Properties.SubName
+					if !android.InList(name, a.requiredDeps) {
+						a.requiredDeps = append(a.requiredDeps, name)
+					}
+				}
+				vctx.requireNativeLibs = append(vctx.requireNativeLibs, af.stem())
+				// Don't track further
+				return false
+			}
+
+			// If the dep is not considered to be in the same
+			// apex, don't add it to filesInfo so that it is not
+			// included in this APEX.
+			// TODO(jiyong): move this to at the top of the
+			// else-if clause for the indirect dependencies.
+			// Currently, that's impossible because we would
+			// like to record requiredNativeLibs even when
+			// DepIsInSameAPex is false. We also shouldn't do
+			// this for host.
+			//
+			// TODO(jiyong): explain why the same module is passed in twice.
+			// Switching the first am to parent breaks lots of tests.
+			if !android.IsDepInSameApex(ctx, am, am) {
+				return false
+			}
+
+			vctx.filesInfo = append(vctx.filesInfo, af)
+			return true // track transitive dependencies
+		} else if rm, ok := child.(*rust.Module); ok {
+			af := apexFileForRustLibrary(ctx, rm)
+			af.transitiveDep = true
+			vctx.filesInfo = append(vctx.filesInfo, af)
+			return true // track transitive dependencies
+		}
+	} else if cc.IsTestPerSrcDepTag(depTag) {
+		if ch, ok := child.(*cc.Module); ok {
+			af := apexFileForExecutable(ctx, ch)
+			// Handle modules created as `test_per_src` variations of a single test module:
+			// use the name of the generated test binary (`fileToCopy`) instead of the name
+			// of the original test module (`depName`, shared by all `test_per_src`
+			// variations of that module).
+			af.androidMkModuleName = filepath.Base(af.builtFile.String())
+			// these are not considered transitive dep
+			af.transitiveDep = false
+			vctx.filesInfo = append(vctx.filesInfo, af)
+			return true // track transitive dependencies
+		}
+	} else if cc.IsHeaderDepTag(depTag) {
+		// nothing
+	} else if java.IsJniDepTag(depTag) {
+		// Because APK-in-APEX embeds jni_libs transitively, we don't need to track transitive deps
+	} else if java.IsXmlPermissionsFileDepTag(depTag) {
+		if prebuilt, ok := child.(prebuilt_etc.PrebuiltEtcModule); ok {
+			vctx.filesInfo = append(vctx.filesInfo, apexFileForPrebuiltEtc(ctx, prebuilt, depName))
+		}
+	} else if rust.IsDylibDepTag(depTag) {
+		if rustm, ok := child.(*rust.Module); ok && rustm.IsInstallableToApex() {
+			af := apexFileForRustLibrary(ctx, rustm)
+			af.transitiveDep = true
+			vctx.filesInfo = append(vctx.filesInfo, af)
+			return true // track transitive dependencies
+		}
+	} else if rust.IsRlibDepTag(depTag) {
+		// Rlib is statically linked, but it might have shared lib
+		// dependencies. Track them.
+		return true
+	} else if java.IsBootclasspathFragmentContentDepTag(depTag) {
+		// Add the contents of the bootclasspath fragment to the apex.
+		switch child.(type) {
+		case *java.Library, *java.SdkLibrary:
+			javaModule := child.(javaModule)
+			af := apexFileForBootclasspathFragmentContentModule(ctx, parent, javaModule)
+			if !af.ok() {
+				ctx.PropertyErrorf("bootclasspath_fragments",
+					"bootclasspath_fragment content %q is not configured to be compiled into dex", depName)
+				return false
+			}
+			vctx.filesInfo = append(vctx.filesInfo, af)
+			return true // track transitive dependencies
+		default:
+			ctx.PropertyErrorf("bootclasspath_fragments",
+				"bootclasspath_fragment content %q of type %q is not supported", depName, ctx.OtherModuleType(child))
+		}
+	} else if java.IsSystemServerClasspathFragmentContentDepTag(depTag) {
+		// Add the contents of the systemserverclasspath fragment to the apex.
+		switch child.(type) {
+		case *java.Library, *java.SdkLibrary:
+			af := apexFileForJavaModule(ctx, child.(javaModule))
+			vctx.filesInfo = append(vctx.filesInfo, af)
+			return true // track transitive dependencies
+		default:
+			ctx.PropertyErrorf("systemserverclasspath_fragments",
+				"systemserverclasspath_fragment content %q of type %q is not supported", depName, ctx.OtherModuleType(child))
+		}
+	} else if _, ok := depTag.(android.CopyDirectlyInAnyApexTag); ok {
+		// nothing
+	} else if depTag == android.DarwinUniversalVariantTag {
+		// nothing
+	} else if am.CanHaveApexVariants() && am.IsInstallableToApex() {
+		ctx.ModuleErrorf("unexpected tag %s for indirect dependency %q", android.PrettyPrintTag(depTag), depName)
+	}
+	return false
+}
+
 // Creates build rules for an APEX. It consists of the following major steps:
 //
 // 1) do some validity checks such as apex_available, min_sdk_version, etc.
@@ -1812,386 +2188,23 @@
 	////////////////////////////////////////////////////////////////////////////////////////////
 	// 2) traverse the dependency tree to collect apexFile structs from them.
 
-	// all the files that will be included in this APEX
-	var filesInfo []apexFile
-
-	// native lib dependencies
-	var provideNativeLibs []string
-	var requireNativeLibs []string
-
-	handleSpecialLibs := !android.Bool(a.properties.Ignore_system_library_special_case)
-
 	// Collect the module directory for IDE info in java/jdeps.go.
 	a.modulePaths = append(a.modulePaths, ctx.ModuleDir())
 
 	// TODO(jiyong): do this using WalkPayloadDeps
 	// TODO(jiyong): make this clean!!!
-	ctx.WalkDepsBlueprint(func(child, parent blueprint.Module) bool {
-		depTag := ctx.OtherModuleDependencyTag(child)
-		if _, ok := depTag.(android.ExcludeFromApexContentsTag); ok {
-			return false
-		}
-		if mod, ok := child.(android.Module); ok && !mod.Enabled() {
-			return false
-		}
-		depName := ctx.OtherModuleName(child)
-		if _, isDirectDep := parent.(*apexBundle); isDirectDep {
-			switch depTag {
-			case sharedLibTag, jniLibTag:
-				isJniLib := depTag == jniLibTag
-				if c, ok := child.(*cc.Module); ok {
-					fi := apexFileForNativeLibrary(ctx, c, handleSpecialLibs)
-					fi.isJniLib = isJniLib
-					filesInfo = append(filesInfo, fi)
-					// Collect the list of stub-providing libs except:
-					// - VNDK libs are only for vendors
-					// - bootstrap bionic libs are treated as provided by system
-					if c.HasStubsVariants() && !a.vndkApex && !cc.InstallToBootstrap(c.BaseModuleName(), ctx.Config()) {
-						provideNativeLibs = append(provideNativeLibs, fi.stem())
-					}
-					return true // track transitive dependencies
-				} else if r, ok := child.(*rust.Module); ok {
-					fi := apexFileForRustLibrary(ctx, r)
-					fi.isJniLib = isJniLib
-					filesInfo = append(filesInfo, fi)
-					return true // track transitive dependencies
-				} else {
-					propertyName := "native_shared_libs"
-					if isJniLib {
-						propertyName = "jni_libs"
-					}
-					ctx.PropertyErrorf(propertyName, "%q is not a cc_library or cc_library_shared module", depName)
-				}
-			case executableTag:
-				if cc, ok := child.(*cc.Module); ok {
-					filesInfo = append(filesInfo, apexFileForExecutable(ctx, cc))
-					return true // track transitive dependencies
-				} else if py, ok := child.(*python.Module); ok && py.HostToolPath().Valid() {
-					filesInfo = append(filesInfo, apexFileForPyBinary(ctx, py))
-				} else if gb, ok := child.(bootstrap.GoBinaryTool); ok && a.Host() {
-					filesInfo = append(filesInfo, apexFileForGoBinary(ctx, depName, gb))
-				} else if rust, ok := child.(*rust.Module); ok {
-					filesInfo = append(filesInfo, apexFileForRustExecutable(ctx, rust))
-					return true // track transitive dependencies
-				} else {
-					ctx.PropertyErrorf("binaries", "%q is neither cc_binary, rust_binary, (embedded) py_binary, (host) blueprint_go_binary, nor (host) bootstrap_go_binary", depName)
-				}
-			case shBinaryTag:
-				if sh, ok := child.(*sh.ShBinary); ok {
-					filesInfo = append(filesInfo, apexFileForShBinary(ctx, sh))
-				} else {
-					ctx.PropertyErrorf("sh_binaries", "%q is not a sh_binary module", depName)
-				}
-			case bcpfTag:
-				{
-					bcpfModule, ok := child.(*java.BootclasspathFragmentModule)
-					if !ok {
-						ctx.PropertyErrorf("bootclasspath_fragments", "%q is not a bootclasspath_fragment module", depName)
-						return false
-					}
-
-					filesToAdd := apexBootclasspathFragmentFiles(ctx, child)
-					filesInfo = append(filesInfo, filesToAdd...)
-					for _, makeModuleName := range bcpfModule.BootImageDeviceInstallMakeModules() {
-						a.requiredDeps = append(a.requiredDeps, makeModuleName)
-					}
-					return true
-				}
-			case sscpfTag:
-				{
-					if _, ok := child.(*java.SystemServerClasspathModule); !ok {
-						ctx.PropertyErrorf("systemserverclasspath_fragments", "%q is not a systemserverclasspath_fragment module", depName)
-						return false
-					}
-					if af := apexClasspathFragmentProtoFile(ctx, child); af != nil {
-						filesInfo = append(filesInfo, *af)
-					}
-					return true
-				}
-			case javaLibTag:
-				switch child.(type) {
-				case *java.Library, *java.SdkLibrary, *java.DexImport, *java.SdkLibraryImport, *java.Import:
-					af := apexFileForJavaModule(ctx, child.(javaModule))
-					if !af.ok() {
-						ctx.PropertyErrorf("java_libs", "%q is not configured to be compiled into dex", depName)
-						return false
-					}
-					filesInfo = append(filesInfo, af)
-					return true // track transitive dependencies
-				default:
-					ctx.PropertyErrorf("java_libs", "%q of type %q is not supported", depName, ctx.OtherModuleType(child))
-				}
-			case androidAppTag:
-				if ap, ok := child.(*java.AndroidApp); ok {
-					filesInfo = append(filesInfo, apexFileForAndroidApp(ctx, ap))
-					return true // track transitive dependencies
-				} else if ap, ok := child.(*java.AndroidAppImport); ok {
-					filesInfo = append(filesInfo, apexFileForAndroidApp(ctx, ap))
-				} else if ap, ok := child.(*java.AndroidTestHelperApp); ok {
-					filesInfo = append(filesInfo, apexFileForAndroidApp(ctx, ap))
-				} else if ap, ok := child.(*java.AndroidAppSet); ok {
-					appDir := "app"
-					if ap.Privileged() {
-						appDir = "priv-app"
-					}
-					// TODO(b/224589412, b/226559955): Ensure that the dirname is
-					// suffixed so that PackageManager correctly invalidates the
-					// existing installed apk in favour of the new APK-in-APEX.
-					// See bugs for more information.
-					appDirName := filepath.Join(appDir, ap.BaseModuleName()+"@"+sanitizedBuildIdForPath(ctx))
-					af := newApexFile(ctx, ap.OutputFile(), ap.BaseModuleName(), appDirName, appSet, ap)
-					af.certificate = java.PresignedCertificate
-					filesInfo = append(filesInfo, af)
-				} else {
-					ctx.PropertyErrorf("apps", "%q is not an android_app module", depName)
-				}
-			case rroTag:
-				if rro, ok := child.(java.RuntimeResourceOverlayModule); ok {
-					filesInfo = append(filesInfo, apexFileForRuntimeResourceOverlay(ctx, rro))
-				} else {
-					ctx.PropertyErrorf("rros", "%q is not an runtime_resource_overlay module", depName)
-				}
-			case bpfTag:
-				if bpfProgram, ok := child.(bpf.BpfModule); ok {
-					filesToCopy, _ := bpfProgram.OutputFiles("")
-					apex_sub_dir := bpfProgram.SubDir()
-					for _, bpfFile := range filesToCopy {
-						filesInfo = append(filesInfo, apexFileForBpfProgram(ctx, bpfFile, apex_sub_dir, bpfProgram))
-					}
-				} else {
-					ctx.PropertyErrorf("bpfs", "%q is not a bpf module", depName)
-				}
-			case fsTag:
-				if fs, ok := child.(filesystem.Filesystem); ok {
-					filesInfo = append(filesInfo, apexFileForFilesystem(ctx, fs.OutputPath(), fs))
-				} else {
-					ctx.PropertyErrorf("filesystems", "%q is not a filesystem module", depName)
-				}
-			case prebuiltTag:
-				if prebuilt, ok := child.(prebuilt_etc.PrebuiltEtcModule); ok {
-					filesInfo = append(filesInfo, apexFileForPrebuiltEtc(ctx, prebuilt, depName))
-				} else {
-					ctx.PropertyErrorf("prebuilts", "%q is not a prebuilt_etc module", depName)
-				}
-			case compatConfigTag:
-				if compatConfig, ok := child.(java.PlatformCompatConfigIntf); ok {
-					filesInfo = append(filesInfo, apexFileForCompatConfig(ctx, compatConfig, depName))
-				} else {
-					ctx.PropertyErrorf("compat_configs", "%q is not a platform_compat_config module", depName)
-				}
-			case testTag:
-				if ccTest, ok := child.(*cc.Module); ok {
-					if ccTest.IsTestPerSrcAllTestsVariation() {
-						// Multiple-output test module (where `test_per_src: true`).
-						//
-						// `ccTest` is the "" ("all tests") variation of a `test_per_src` module.
-						// We do not add this variation to `filesInfo`, as it has no output;
-						// however, we do add the other variations of this module as indirect
-						// dependencies (see below).
-					} else {
-						// Single-output test module (where `test_per_src: false`).
-						af := apexFileForExecutable(ctx, ccTest)
-						af.class = nativeTest
-						filesInfo = append(filesInfo, af)
-					}
-					return true // track transitive dependencies
-				} else {
-					ctx.PropertyErrorf("tests", "%q is not a cc module", depName)
-				}
-			case keyTag:
-				if key, ok := child.(*apexKey); ok {
-					a.privateKeyFile = key.privateKeyFile
-					a.publicKeyFile = key.publicKeyFile
-				} else {
-					ctx.PropertyErrorf("key", "%q is not an apex_key module", depName)
-				}
-				return false
-			case certificateTag:
-				if dep, ok := child.(*java.AndroidAppCertificate); ok {
-					a.containerCertificateFile = dep.Certificate.Pem
-					a.containerPrivateKeyFile = dep.Certificate.Key
-				} else {
-					ctx.ModuleErrorf("certificate dependency %q must be an android_app_certificate module", depName)
-				}
-			case android.PrebuiltDepTag:
-				// If the prebuilt is force disabled, remember to delete the prebuilt file
-				// that might have been installed in the previous builds
-				if prebuilt, ok := child.(prebuilt); ok && prebuilt.isForceDisabled() {
-					a.prebuiltFileToDelete = prebuilt.InstallFilename()
-				}
-			}
-		} else if !a.vndkApex {
-			// indirect dependencies
-			if am, ok := child.(android.ApexModule); ok {
-				// We cannot use a switch statement on `depTag` here as the checked
-				// tags used below are private (e.g. `cc.sharedDepTag`).
-				if cc.IsSharedDepTag(depTag) || cc.IsRuntimeDepTag(depTag) {
-					if cc, ok := child.(*cc.Module); ok {
-						if cc.UseVndk() && proptools.Bool(a.properties.Use_vndk_as_stable) && cc.IsVndk() {
-							requireNativeLibs = append(requireNativeLibs, ":vndk")
-							return false
-						}
-						af := apexFileForNativeLibrary(ctx, cc, handleSpecialLibs)
-						af.transitiveDep = true
-
-						// Always track transitive dependencies for host.
-						if a.Host() {
-							filesInfo = append(filesInfo, af)
-							return true
-						}
-
-						abInfo := ctx.Provider(ApexBundleInfoProvider).(ApexBundleInfo)
-						if !abInfo.Contents.DirectlyInApex(depName) && (cc.IsStubs() || cc.HasStubsVariants()) {
-							// If the dependency is a stubs lib, don't include it in this APEX,
-							// but make sure that the lib is installed on the device.
-							// In case no APEX is having the lib, the lib is installed to the system
-							// partition.
-							//
-							// Always include if we are a host-apex however since those won't have any
-							// system libraries.
-							if !am.DirectlyInAnyApex() {
-								// we need a module name for Make
-								name := cc.ImplementationModuleNameForMake(ctx) + cc.Properties.SubName
-								if !android.InList(name, a.requiredDeps) {
-									a.requiredDeps = append(a.requiredDeps, name)
-								}
-							}
-							requireNativeLibs = append(requireNativeLibs, af.stem())
-							// Don't track further
-							return false
-						}
-
-						// If the dep is not considered to be in the same
-						// apex, don't add it to filesInfo so that it is not
-						// included in this APEX.
-						// TODO(jiyong): move this to at the top of the
-						// else-if clause for the indirect dependencies.
-						// Currently, that's impossible because we would
-						// like to record requiredNativeLibs even when
-						// DepIsInSameAPex is false. We also shouldn't do
-						// this for host.
-						//
-						// TODO(jiyong): explain why the same module is passed in twice.
-						// Switching the first am to parent breaks lots of tests.
-						if !android.IsDepInSameApex(ctx, am, am) {
-							return false
-						}
-
-						filesInfo = append(filesInfo, af)
-						return true // track transitive dependencies
-					} else if rm, ok := child.(*rust.Module); ok {
-						af := apexFileForRustLibrary(ctx, rm)
-						af.transitiveDep = true
-						filesInfo = append(filesInfo, af)
-						return true // track transitive dependencies
-					}
-				} else if cc.IsTestPerSrcDepTag(depTag) {
-					if cc, ok := child.(*cc.Module); ok {
-						af := apexFileForExecutable(ctx, cc)
-						// Handle modules created as `test_per_src` variations of a single test module:
-						// use the name of the generated test binary (`fileToCopy`) instead of the name
-						// of the original test module (`depName`, shared by all `test_per_src`
-						// variations of that module).
-						af.androidMkModuleName = filepath.Base(af.builtFile.String())
-						// these are not considered transitive dep
-						af.transitiveDep = false
-						filesInfo = append(filesInfo, af)
-						return true // track transitive dependencies
-					}
-				} else if cc.IsHeaderDepTag(depTag) {
-					// nothing
-				} else if java.IsJniDepTag(depTag) {
-					// Because APK-in-APEX embeds jni_libs transitively, we don't need to track transitive deps
-					return false
-				} else if java.IsXmlPermissionsFileDepTag(depTag) {
-					if prebuilt, ok := child.(prebuilt_etc.PrebuiltEtcModule); ok {
-						filesInfo = append(filesInfo, apexFileForPrebuiltEtc(ctx, prebuilt, depName))
-					}
-				} else if rust.IsDylibDepTag(depTag) {
-					if rustm, ok := child.(*rust.Module); ok && rustm.IsInstallableToApex() {
-						af := apexFileForRustLibrary(ctx, rustm)
-						af.transitiveDep = true
-						filesInfo = append(filesInfo, af)
-						return true // track transitive dependencies
-					}
-				} else if rust.IsRlibDepTag(depTag) {
-					// Rlib is statically linked, but it might have shared lib
-					// dependencies. Track them.
-					return true
-				} else if java.IsBootclasspathFragmentContentDepTag(depTag) {
-					// Add the contents of the bootclasspath fragment to the apex.
-					switch child.(type) {
-					case *java.Library, *java.SdkLibrary:
-						javaModule := child.(javaModule)
-						af := apexFileForBootclasspathFragmentContentModule(ctx, parent, javaModule)
-						if !af.ok() {
-							ctx.PropertyErrorf("bootclasspath_fragments", "bootclasspath_fragment content %q is not configured to be compiled into dex", depName)
-							return false
-						}
-						filesInfo = append(filesInfo, af)
-						return true // track transitive dependencies
-					default:
-						ctx.PropertyErrorf("bootclasspath_fragments", "bootclasspath_fragment content %q of type %q is not supported", depName, ctx.OtherModuleType(child))
-					}
-				} else if java.IsSystemServerClasspathFragmentContentDepTag(depTag) {
-					// Add the contents of the systemserverclasspath fragment to the apex.
-					switch child.(type) {
-					case *java.Library, *java.SdkLibrary:
-						af := apexFileForJavaModule(ctx, child.(javaModule))
-						filesInfo = append(filesInfo, af)
-						return true // track transitive dependencies
-					default:
-						ctx.PropertyErrorf("systemserverclasspath_fragments", "systemserverclasspath_fragment content %q of type %q is not supported", depName, ctx.OtherModuleType(child))
-					}
-				} else if _, ok := depTag.(android.CopyDirectlyInAnyApexTag); ok {
-					// nothing
-				} else if depTag == android.DarwinUniversalVariantTag {
-					// nothing
-				} else if am.CanHaveApexVariants() && am.IsInstallableToApex() {
-					ctx.ModuleErrorf("unexpected tag %s for indirect dependency %q", android.PrettyPrintTag(depTag), depName)
-				}
-			}
-		}
-		return false
-	})
+	vctx := visitorContext{handleSpecialLibs: !android.Bool(a.properties.Ignore_system_library_special_case)}
+	ctx.WalkDepsBlueprint(func(child, parent blueprint.Module) bool { return a.depVisitor(&vctx, ctx, child, parent) })
+	vctx.normalizeFileInfo()
 	if a.privateKeyFile == nil {
 		ctx.PropertyErrorf("key", "private_key for %q could not be found", String(a.overridableProperties.Key))
 		return
 	}
 
-	// Remove duplicates in filesInfo
-	removeDup := func(filesInfo []apexFile) []apexFile {
-		encountered := make(map[string]apexFile)
-		for _, f := range filesInfo {
-			dest := filepath.Join(f.installDir, f.builtFile.Base())
-			if e, ok := encountered[dest]; !ok {
-				encountered[dest] = f
-			} else {
-				// If a module is directly included and also transitively depended on
-				// consider it as directly included.
-				e.transitiveDep = e.transitiveDep && f.transitiveDep
-				encountered[dest] = e
-			}
-		}
-		var result []apexFile
-		for _, v := range encountered {
-			result = append(result, v)
-		}
-		return result
-	}
-	filesInfo = removeDup(filesInfo)
-
-	// Sort to have consistent build rules
-	sort.Slice(filesInfo, func(i, j int) bool {
-		// Sort by destination path so as to ensure consistent ordering even if the source of the files
-		// changes.
-		return filesInfo[i].path() < filesInfo[j].path()
-	})
-
 	////////////////////////////////////////////////////////////////////////////////////////////
 	// 3) some fields in apexBundle struct are configured
 	a.installDir = android.PathForModuleInstall(ctx, "apex")
-	a.filesInfo = filesInfo
+	a.filesInfo = vctx.filesInfo
 
 	// Set suffix and primaryApexType depending on the ApexType
 	buildFlattenedAsDefault := ctx.Config().FlattenApex()
@@ -2267,7 +2280,7 @@
 
 	////////////////////////////////////////////////////////////////////////////////////////////
 	// 4) generate the build rules to create the APEX. This is done in builder.go.
-	a.buildManifest(ctx, provideNativeLibs, requireNativeLibs)
+	a.buildManifest(ctx, vctx.provideNativeLibs, vctx.requireNativeLibs)
 	if a.properties.ApexType == flattenedApex {
 		a.buildFlattenedApex(ctx)
 	} else {
@@ -2458,7 +2471,7 @@
 	android.BazelModuleBase
 }
 
-func (o *OverrideApex) GenerateAndroidBuildActions(ctx android.ModuleContext) {
+func (o *OverrideApex) GenerateAndroidBuildActions(_ android.ModuleContext) {
 	// All the overrides happen in the base module.
 }