Merge "Allow Rust for tombstoned client."
diff --git a/android/bazel.go b/android/bazel.go
index 7e2727c..fafb68b 100644
--- a/android/bazel.go
+++ b/android/bazel.go
@@ -357,6 +357,7 @@
 		"packages/services/Car/tests/SampleRearViewCamera":   Bp2BuildDefaultTrue,
 		"prebuilts/clang/host/linux-x86":                     Bp2BuildDefaultTrueRecursively,
 		"prebuilts/tools/common/m2":                          Bp2BuildDefaultTrue,
+		"prebuilts/sdk/tools/jetifier/jetifier-standalone":   Bp2BuildDefaultTrue,
 		"system/apex":                                        Bp2BuildDefaultFalse, // TODO(b/207466993): flaky failures
 		"system/apex/proto":                                  Bp2BuildDefaultTrueRecursively,
 		"system/apex/libs":                                   Bp2BuildDefaultTrueRecursively,
@@ -393,8 +394,6 @@
 	// A module can either be in this list or its directory allowlisted entirely
 	// in bp2buildDefaultConfig, but not both at the same time.
 	bp2buildModuleAlwaysConvertList = []string{
-		"prebuilt_junit-params-assertj-core",
-
 		//external/avb
 		"avbtool",
 		"libavb",
@@ -417,11 +416,22 @@
 
 		//system/extras/verity/fec
 		"fec",
+
+		//packages/apps/Car/libs/car-ui-lib/car-ui-androidx
+		// genrule dependencies for java_imports
+		"car-ui-androidx-annotation-nodeps",
+		"car-ui-androidx-collection-nodeps",
+		"car-ui-androidx-core-common-nodeps",
+		"car-ui-androidx-lifecycle-common-nodeps",
+		"car-ui-androidx-constraintlayout-solver-nodeps",
 	}
 
-	// Per-module-type allowlist to always opt modules in of both bp2build and mixed builds
+	// Per-module-type allowlist to always opt modules in to both bp2build and mixed builds
 	// when they have the same type as one listed.
-	bp2buildModuleTypeAlwaysConvertList = []string{}
+	bp2buildModuleTypeAlwaysConvertList = []string{
+		"java_import",
+		"java_import_host",
+	}
 
 	// Per-module denylist to always opt modules out of both bp2build and mixed builds.
 	bp2buildModuleDoNotConvertList = []string{
@@ -485,18 +495,12 @@
 		"libprotobuf-java-full",            // b/210751803, we don't handle path property for filegroups
 		"host-libprotobuf-java-full",       // b/210751803, we don't handle path property for filegroups
 		"libprotobuf-java-util-full",       // b/210751803, we don't handle path property for filegroups
+		"apex_manifest_proto_java",         // b/210751803, depends on libprotobuf-java-full
+		"conscrypt",                        // b/210751803, we don't handle path property for filegroups
+		"conscrypt-for-host",               // b/210751803, we don't handle path property for filegroups
 
-		"conscrypt",          // b/210751803, we don't handle path property for filegroups
-		"conscrypt-for-host", // b/210751803, we don't handle path property for filegroups
-
-		"host-libprotobuf-java-lite",   // b/217236083, java_library cannot have deps without srcs
-		"host-libprotobuf-java-micro",  // b/217236083, java_library cannot have deps without srcs
-		"host-libprotobuf-java-nano",   // b/217236083, java_library cannot have deps without srcs
-		"error_prone_core",             // b/217236083, java_library cannot have deps without srcs
-		"bouncycastle-host",            // b/217236083, java_library cannot have deps without srcs
-		"mockito-robolectric-prebuilt", // b/217236083, java_library cannot have deps without srcs
-
-		"apex_manifest_proto_java", // b/215230097, we don't handle .proto files in java_library srcs attribute
+		"libprotobuf-java-nano",      // b/220869005, depends on non-public_current SDK
+		"host-libprotobuf-java-nano", // b/220869005, depends on libprotobuf-java-nano
 
 		"libc_musl_sysroot_bionic_arch_headers", // b/218405924, depends on soong_zip
 		"libc_musl_sysroot_bionic_headers",      // b/218405924, depends on soong_zip and generates duplicate srcs
@@ -509,6 +513,9 @@
 
 		"brotli-fuzzer-corpus", // b/202015218: outputs are in location incompatible with bazel genrule handling.
 
+		// python modules
+		"analyze_bcpf", // depends on bpmodify a blueprint_go_binary.
+
 		// b/203369847: multiple genrules in the same package creating the same file
 		// //development/sdk/...
 		"platform_tools_properties",
@@ -563,9 +570,12 @@
 		"art-script",     // depends on unconverted modules: dalvikvm, dex2oat
 		"dex2oat-script", // depends on unconverted modules: dex2oat
 
-		"error_prone_checkerframework_dataflow_nullaway", // TODO(b/219908977): "Error in fail: deps not allowed without srcs; move to runtime_deps?"
-
-		"libprotobuf-java-nano", // b/220869005, depends on non-public_current SDK
+		"prebuilt_car-ui-androidx-core-common",                               // b/224773339, genrule dependency creates an .aar, not a .jar
+		"prebuilt_art-module-host-exports_okhttp-norepackage@current",        // aosp/1999250, needs Jars (arch variant)
+		"prebuilt_conscrypt-unbundled",                                       // aosp/1999250, needs Jars (arch variant)
+		"prebuilt_conscrypt-module-host-exports_conscrypt-unbundled@current", // aosp/1999250, needs Jars (arch variant)
+		"prebuilt_platform-robolectric-4.4-prebuilt",                         // aosp/1999250, needs .aar support in Jars
+		"prebuilt_platform-robolectric-4.5.1-prebuilt",                       // aosp/1999250, needs .aar support in Jars
 	}
 
 	// Per-module denylist of cc_library modules to only generate the static
diff --git a/bazel/properties.go b/bazel/properties.go
index 1300a53..f956031 100644
--- a/bazel/properties.go
+++ b/bazel/properties.go
@@ -65,6 +65,14 @@
 	Excludes []Label
 }
 
+// MakeLabelList creates a LabelList from a list Label
+func MakeLabelList(labels []Label) LabelList {
+	return LabelList{
+		Includes: labels,
+		Excludes: nil,
+	}
+}
+
 func (ll *LabelList) Equals(other LabelList) bool {
 	if len(ll.Includes) != len(other.Includes) || len(ll.Excludes) != len(other.Excludes) {
 		return false
@@ -354,6 +362,15 @@
 	return keys
 }
 
+// MakeLabelAttribute turns a string into a LabelAttribute
+func MakeLabelAttribute(label string) *LabelAttribute {
+	return &LabelAttribute{
+		Value: &Label{
+			Label: label,
+		},
+	}
+}
+
 type configToBools map[string]bool
 
 func (ctb configToBools) setValue(config string, value *bool) {
diff --git a/bp2build/android_app_conversion_test.go b/bp2build/android_app_conversion_test.go
index b6095b2..3824586 100644
--- a/bp2build/android_app_conversion_test.go
+++ b/bp2build/android_app_conversion_test.go
@@ -51,7 +51,6 @@
 				"srcs":           `["app.java"]`,
 				"manifest":       `"AndroidManifest.xml"`,
 				"resource_files": `["res/res.png"]`,
-				"deps":           `["//prebuilts/sdk:public_current_android_sdk_java_import"]`,
 			}),
 		}})
 }
@@ -87,10 +86,7 @@
         "resb/res.png",
     ]`,
 				"custom_package": `"com.google"`,
-				"deps": `[
-        "//prebuilts/sdk:public_current_android_sdk_java_import",
-        ":static_lib_dep",
-    ]`,
+				"deps":           `[":static_lib_dep"]`,
 			}),
 		}})
 }
@@ -129,7 +125,6 @@
     })`,
 				"manifest":       `"AndroidManifest.xml"`,
 				"resource_files": `["res/res.png"]`,
-				"deps":           `["//prebuilts/sdk:public_current_android_sdk_java_import"]`,
 			}),
 		}})
 }
diff --git a/bp2build/java_binary_host_conversion_test.go b/bp2build/java_binary_host_conversion_test.go
index 65136d9..4fc07e0 100644
--- a/bp2build/java_binary_host_conversion_test.go
+++ b/bp2build/java_binary_host_conversion_test.go
@@ -28,6 +28,7 @@
 	(&tc).moduleTypeUnderTestFactory = java.BinaryHostFactory
 	runBp2BuildTestCase(t, func(ctx android.RegistrationContext) {
 		ctx.RegisterModuleType("cc_library_host_shared", cc.LibraryHostSharedFactory)
+		ctx.RegisterModuleType("java_library", java.LibraryFactory)
 	}, tc)
 }
 
@@ -67,3 +68,33 @@
 		},
 	})
 }
+
+func TestJavaBinaryHostRuntimeDeps(t *testing.T) {
+	runJavaBinaryHostTestCase(t, bp2buildTestCase{
+		description: "java_binary_host with srcs, exclude_srcs, jni_libs, javacflags, and manifest.",
+		filesystem:  fs,
+		blueprint: `java_binary_host {
+    name: "java-binary-host-1",
+    static_libs: ["java-dep-1"],
+    manifest: "test.mf",
+    bazel_module: { bp2build_available: true },
+}
+
+java_library {
+    name: "java-dep-1",
+    srcs: ["a.java"],
+    bazel_module: { bp2build_available: false },
+}
+`,
+		expectedBazelTargets: []string{
+			makeBazelTarget("java_binary", "java-binary-host-1", attrNameToString{
+				"main_class":   `"com.android.test.MainClass"`,
+				"runtime_deps": `[":java-dep-1"]`,
+				"target_compatible_with": `select({
+        "//build/bazel/platforms/os:android": ["@platforms//:incompatible"],
+        "//conditions:default": [],
+    })`,
+			}),
+		},
+	})
+}
diff --git a/bp2build/java_library_conversion_test.go b/bp2build/java_library_conversion_test.go
index 5c65ec2..2f6bce2 100644
--- a/bp2build/java_library_conversion_test.go
+++ b/bp2build/java_library_conversion_test.go
@@ -15,17 +15,22 @@
 package bp2build
 
 import (
+	"fmt"
 	"testing"
 
 	"android/soong/android"
 	"android/soong/java"
 )
 
-func runJavaLibraryTestCase(t *testing.T, tc bp2buildTestCase) {
+func runJavaLibraryTestCaseWithRegistrationCtxFunc(t *testing.T, tc bp2buildTestCase, registrationCtxFunc func(ctx android.RegistrationContext)) {
 	t.Helper()
 	(&tc).moduleTypeUnderTest = "java_library"
 	(&tc).moduleTypeUnderTestFactory = java.LibraryFactory
-	runBp2BuildTestCase(t, func(ctx android.RegistrationContext) {}, tc)
+	runBp2BuildTestCase(t, registrationCtxFunc, tc)
+}
+
+func runJavaLibraryTestCase(t *testing.T, tc bp2buildTestCase) {
+	runJavaLibraryTestCaseWithRegistrationCtxFunc(t, tc, func(ctx android.RegistrationContext) {})
 }
 
 func TestJavaLibrary(t *testing.T) {
@@ -55,3 +60,99 @@
 		},
 	})
 }
+
+func TestJavaLibraryConvertsStaticLibsToDepsAndExports(t *testing.T) {
+	runJavaLibraryTestCase(t, bp2buildTestCase{
+		blueprint: `java_library {
+    name: "java-lib-1",
+    srcs: ["a.java"],
+    libs: ["java-lib-2"],
+    static_libs: ["java-lib-3"],
+    bazel_module: { bp2build_available: true },
+}
+
+java_library {
+    name: "java-lib-2",
+    srcs: ["b.java"],
+    bazel_module: { bp2build_available: false },
+}
+
+java_library {
+    name: "java-lib-3",
+    srcs: ["c.java"],
+    bazel_module: { bp2build_available: false },
+}`,
+		expectedBazelTargets: []string{
+			makeBazelTarget("java_library", "java-lib-1", attrNameToString{
+				"srcs": `["a.java"]`,
+				"deps": `[
+        ":java-lib-2",
+        ":java-lib-3",
+    ]`,
+				"exports": `[":java-lib-3"]`,
+			}),
+		},
+	})
+}
+
+func TestJavaLibraryConvertsStaticLibsToExportsIfNoSrcs(t *testing.T) {
+	runJavaLibraryTestCase(t, bp2buildTestCase{
+		blueprint: `java_library {
+    name: "java-lib-1",
+    static_libs: ["java-lib-2"],
+    bazel_module: { bp2build_available: true },
+}
+
+java_library {
+    name: "java-lib-2",
+    srcs: ["a.java"],
+    bazel_module: { bp2build_available: false },
+}`,
+		expectedBazelTargets: []string{
+			makeBazelTarget("java_library", "java-lib-1", attrNameToString{
+				"exports": `[":java-lib-2"]`,
+			}),
+		},
+	})
+}
+
+func TestJavaLibraryFailsToConvertLibsWithNoSrcs(t *testing.T) {
+	runJavaLibraryTestCase(t, bp2buildTestCase{
+		expectedErr: fmt.Errorf("Module has direct dependencies but no sources. Bazel will not allow this."),
+		blueprint: `java_library {
+    name: "java-lib-1",
+    libs: ["java-lib-2"],
+    bazel_module: { bp2build_available: true },
+}
+
+java_library {
+    name: "java-lib-2",
+    srcs: ["a.java"],
+    bazel_module: { bp2build_available: false },
+}`,
+		expectedBazelTargets: []string{},
+	})
+}
+
+func TestJavaLibraryPlugins(t *testing.T) {
+	runJavaLibraryTestCaseWithRegistrationCtxFunc(t, bp2buildTestCase{
+		blueprint: `java_library {
+    name: "java-lib-1",
+    plugins: ["java-plugin-1"],
+    bazel_module: { bp2build_available: true },
+}
+
+java_plugin {
+    name: "java-plugin-1",
+    srcs: ["a.java"],
+    bazel_module: { bp2build_available: false },
+}`,
+		expectedBazelTargets: []string{
+			makeBazelTarget("java_library", "java-lib-1", attrNameToString{
+				"plugins": `[":java-plugin-1"]`,
+			}),
+		},
+	}, func(ctx android.RegistrationContext) {
+		ctx.RegisterModuleType("java_plugin", java.PluginFactory)
+	})
+}
diff --git a/bp2build/java_plugin_conversion_test.go b/bp2build/java_plugin_conversion_test.go
index ff13bb0..c2a2182 100644
--- a/bp2build/java_plugin_conversion_test.go
+++ b/bp2build/java_plugin_conversion_test.go
@@ -70,3 +70,39 @@
 		},
 	})
 }
+
+func TestJavaPluginNoSrcs(t *testing.T) {
+	runJavaPluginTestCase(t, bp2buildTestCase{
+		description: "java_plugin without srcs converts (static) libs to deps",
+		blueprint: `java_plugin {
+    name: "java-plug-1",
+    libs: ["java-lib-1"],
+    static_libs: ["java-lib-2"],
+    bazel_module: { bp2build_available: true },
+}
+
+java_library {
+    name: "java-lib-1",
+    srcs: ["b.java"],
+    bazel_module: { bp2build_available: false },
+}
+
+java_library {
+    name: "java-lib-2",
+    srcs: ["c.java"],
+    bazel_module: { bp2build_available: false },
+}`,
+		expectedBazelTargets: []string{
+			makeBazelTarget("java_plugin", "java-plug-1", attrNameToString{
+				"target_compatible_with": `select({
+        "//build/bazel/platforms/os:android": ["@platforms//:incompatible"],
+        "//conditions:default": [],
+    })`,
+				"deps": `[
+        ":java-lib-1",
+        ":java-lib-2",
+    ]`,
+			}),
+		},
+	})
+}
diff --git a/bp2build/java_proto_conversion_test.go b/bp2build/java_proto_conversion_test.go
index 61a398c..67f8044 100644
--- a/bp2build/java_proto_conversion_test.go
+++ b/bp2build/java_proto_conversion_test.go
@@ -89,7 +89,7 @@
 						"deps": `[":java-protos_proto"]`,
 					}),
 				makeBazelTarget("java_library", "java-protos", attrNameToString{
-					"deps": fmt.Sprintf(`[":%s"]`, javaLibraryName),
+					"exports": fmt.Sprintf(`[":%s"]`, javaLibraryName),
 				}),
 			},
 		})
@@ -98,7 +98,7 @@
 
 func TestJavaProtoDefault(t *testing.T) {
 	runJavaProtoTestCase(t, bp2buildTestCase{
-		description: "java_proto",
+		description: "java_library proto default",
 		blueprint: `java_library_static {
     name: "java-protos",
     srcs: ["a.proto"],
@@ -115,7 +115,7 @@
 					"deps": `[":java-protos_proto"]`,
 				}),
 			makeBazelTarget("java_library", "java-protos", attrNameToString{
-				"deps": `[":java-protos_java_proto_lite"]`,
+				"exports": `[":java-protos_java_proto_lite"]`,
 			}),
 		},
 	})
diff --git a/java/app.go b/java/app.go
index 96fd61a..8728df6 100755
--- a/java/app.go
+++ b/java/app.go
@@ -1450,7 +1450,8 @@
 }
 
 type bazelAndroidAppAttributes struct {
-	*javaLibraryAttributes
+	*javaCommonAttributes
+	Deps             bazel.LabelListAttribute
 	Manifest         bazel.Label
 	Custom_package   *string
 	Resource_files   bazel.LabelListAttribute
@@ -1460,7 +1461,16 @@
 
 // ConvertWithBp2build is used to convert android_app to Bazel.
 func (a *AndroidApp) ConvertWithBp2build(ctx android.TopDownMutatorContext) {
-	libAttrs := a.convertLibraryAttrsBp2Build(ctx)
+	commonAttrs, depLabels := a.convertLibraryAttrsBp2Build(ctx)
+
+	deps := depLabels.Deps
+	if !commonAttrs.Srcs.IsEmpty() {
+		deps.Append(depLabels.StaticDeps) // we should only append these if there are sources to use them
+	} else if !deps.IsEmpty() || !depLabels.StaticDeps.IsEmpty() {
+		ctx.ModuleErrorf("android_app has dynamic or static dependencies but no sources." +
+			" Bazel does not allow direct dependencies without sources nor exported" +
+			" dependencies on android_binary rule.")
+	}
 
 	manifest := proptools.StringDefault(a.aaptProperties.Manifest, "AndroidManifest.xml")
 
@@ -1483,7 +1493,8 @@
 	}
 
 	attrs := &bazelAndroidAppAttributes{
-		libAttrs,
+		commonAttrs,
+		deps,
 		android.BazelLabelForModuleSrcSingle(ctx, manifest),
 		// TODO(b/209576404): handle package name override by product variable PRODUCT_MANIFEST_PACKAGE_NAME_OVERRIDES
 		a.overridableAppProperties.Package_name,
diff --git a/java/java.go b/java/java.go
index ddef34d..1b6e559 100644
--- a/java/java.go
+++ b/java/java.go
@@ -1725,6 +1725,7 @@
 
 	android.InitPrebuiltModule(module, &module.properties.Jars)
 	android.InitApexModule(module)
+	android.InitBazelModule(module)
 	InitJavaModule(module, android.HostSupported)
 	return module
 }
@@ -2023,13 +2024,24 @@
 	}
 }
 
-type javaLibraryAttributes struct {
+type javaCommonAttributes struct {
 	Srcs      bazel.LabelListAttribute
-	Deps      bazel.LabelListAttribute
+	Plugins   bazel.LabelListAttribute
 	Javacopts bazel.StringListAttribute
 }
 
-func (m *Library) convertLibraryAttrsBp2Build(ctx android.TopDownMutatorContext) *javaLibraryAttributes {
+type javaDependencyLabels struct {
+	// Dependencies which DO NOT contribute to the API visible to upstream dependencies.
+	Deps bazel.LabelListAttribute
+	// Dependencies which DO contribute to the API visible to upstream dependencies.
+	StaticDeps bazel.LabelListAttribute
+}
+
+// convertLibraryAttrsBp2Build converts a few shared attributes from java_* modules
+// and also separates dependencies into dynamic dependencies and static dependencies.
+// Each corresponding Bazel target type, can have a different method for handling
+// dynamic vs. static dependencies, and so these are returned to the calling function.
+func (m *Library) convertLibraryAttrsBp2Build(ctx android.TopDownMutatorContext) (*javaCommonAttributes, *javaDependencyLabels) {
 	var srcs bazel.LabelListAttribute
 	archVariantProps := m.GetArchVariantProperties(ctx, &CommonProperties{})
 	for axis, configToProps := range archVariantProps {
@@ -2048,40 +2060,71 @@
 		protoSrcPartition: android.ProtoSrcLabelPartition,
 	})
 
-	attrs := &javaLibraryAttributes{
+	commonAttrs := &javaCommonAttributes{
 		Srcs: srcPartitions[javaSrcPartition],
+		Plugins: bazel.MakeLabelListAttribute(
+			android.BazelLabelForModuleDeps(ctx, m.properties.Plugins),
+		),
 	}
 
 	if m.properties.Javacflags != nil {
-		attrs.Javacopts = bazel.MakeStringListAttribute(m.properties.Javacflags)
+		commonAttrs.Javacopts = bazel.MakeStringListAttribute(m.properties.Javacflags)
 	}
 
+	depLabels := &javaDependencyLabels{}
+
 	var deps bazel.LabelList
-	sdkVersion := m.SdkVersion(ctx)
-	if sdkVersion.Kind == android.SdkPublic && sdkVersion.ApiLevel == android.FutureApiLevel {
-		// TODO(b/220869005) remove forced dependency on current public android.jar
-		deps.Add(&bazel.Label{Label: "//prebuilts/sdk:public_current_android_sdk_java_import"})
-	}
 	if m.properties.Libs != nil {
 		deps.Append(android.BazelLabelForModuleDeps(ctx, m.properties.Libs))
 	}
+
+	var staticDeps bazel.LabelList
 	if m.properties.Static_libs != nil {
-		//TODO(b/217236083) handle static libs similarly to Soong
-		deps.Append(android.BazelLabelForModuleDeps(ctx, m.properties.Static_libs))
+		staticDeps.Append(android.BazelLabelForModuleDeps(ctx, m.properties.Static_libs))
 	}
 
-	protoDeps := bp2buildProto(ctx, &m.Module, srcPartitions[protoSrcPartition])
-	if protoDeps != nil {
-		deps.Add(protoDeps)
-	}
+	protoDepLabel := bp2buildProto(ctx, &m.Module, srcPartitions[protoSrcPartition])
+	// Soong does not differentiate between a java_library and the Bazel equivalent of
+	// a java_proto_library + proto_library pair. Instead, in Soong proto sources are
+	// listed directly in the srcs of a java_library, and the classes produced
+	// by protoc are included directly in the resulting JAR. Thus upstream dependencies
+	// that depend on a java_library with proto sources can link directly to the protobuf API,
+	// and so this should be a static dependency.
+	staticDeps.Add(protoDepLabel)
 
-	attrs.Deps = bazel.MakeLabelListAttribute(deps)
+	depLabels.Deps = bazel.MakeLabelListAttribute(deps)
+	depLabels.StaticDeps = bazel.MakeLabelListAttribute(staticDeps)
 
-	return attrs
+	return commonAttrs, depLabels
+}
+
+type javaLibraryAttributes struct {
+	*javaCommonAttributes
+	Deps    bazel.LabelListAttribute
+	Exports bazel.LabelListAttribute
 }
 
 func javaLibraryBp2Build(ctx android.TopDownMutatorContext, m *Library) {
-	attrs := m.convertLibraryAttrsBp2Build(ctx)
+	commonAttrs, depLabels := m.convertLibraryAttrsBp2Build(ctx)
+
+	deps := depLabels.Deps
+	if !commonAttrs.Srcs.IsEmpty() {
+		deps.Append(depLabels.StaticDeps) // we should only append these if there are sources to use them
+
+		sdkVersion := m.SdkVersion(ctx)
+		if sdkVersion.Kind == android.SdkPublic && sdkVersion.ApiLevel == android.FutureApiLevel {
+			// TODO(b/220869005) remove forced dependency on current public android.jar
+			deps.Add(bazel.MakeLabelAttribute("//prebuilts/sdk:public_current_android_sdk_java_import"))
+		}
+	} else if !depLabels.Deps.IsEmpty() {
+		ctx.ModuleErrorf("Module has direct dependencies but no sources. Bazel will not allow this.")
+	}
+
+	attrs := &javaLibraryAttributes{
+		javaCommonAttributes: commonAttrs,
+		Deps:                 deps,
+		Exports:              depLabels.StaticDeps,
+	}
 
 	props := bazel.BazelTargetModuleProperties{
 		Rule_class:        "java_library",
@@ -2092,15 +2135,30 @@
 }
 
 type javaBinaryHostAttributes struct {
-	Srcs       bazel.LabelListAttribute
-	Deps       bazel.LabelListAttribute
-	Main_class string
-	Jvm_flags  bazel.StringListAttribute
-	Javacopts  bazel.StringListAttribute
+	*javaCommonAttributes
+	Deps         bazel.LabelListAttribute
+	Runtime_deps bazel.LabelListAttribute
+	Main_class   string
+	Jvm_flags    bazel.StringListAttribute
 }
 
 // JavaBinaryHostBp2Build is for java_binary_host bp2build.
 func javaBinaryHostBp2Build(ctx android.TopDownMutatorContext, m *Binary) {
+	commonAttrs, depLabels := m.convertLibraryAttrsBp2Build(ctx)
+
+	deps := depLabels.Deps
+	deps.Append(depLabels.StaticDeps)
+	if m.binaryProperties.Jni_libs != nil {
+		deps.Append(bazel.MakeLabelListAttribute(android.BazelLabelForModuleDeps(ctx, m.binaryProperties.Jni_libs)))
+	}
+
+	var runtimeDeps bazel.LabelListAttribute
+	if commonAttrs.Srcs.IsEmpty() {
+		// if there are no sources, then the dependencies can only be used at runtime
+		runtimeDeps = deps
+		deps = bazel.LabelListAttribute{}
+	}
+
 	mainClass := ""
 	if m.binaryProperties.Main_class != nil {
 		mainClass = *m.binaryProperties.Main_class
@@ -2112,26 +2170,12 @@
 		}
 		mainClass = mainClassInManifest
 	}
-	srcs := bazel.MakeLabelListAttribute(android.BazelLabelForModuleSrcExcludes(ctx, m.properties.Srcs, m.properties.Exclude_srcs))
+
 	attrs := &javaBinaryHostAttributes{
-		Srcs:       srcs,
-		Main_class: mainClass,
-	}
-
-	if m.properties.Javacflags != nil {
-		attrs.Javacopts = bazel.MakeStringListAttribute(m.properties.Javacflags)
-	}
-
-	// Attribute deps
-	deps := []string{}
-	if m.properties.Static_libs != nil {
-		deps = append(deps, m.properties.Static_libs...)
-	}
-	if m.binaryProperties.Jni_libs != nil {
-		deps = append(deps, m.binaryProperties.Jni_libs...)
-	}
-	if len(deps) > 0 {
-		attrs.Deps = bazel.MakeLabelListAttribute(android.BazelLabelForModuleDeps(ctx, deps))
+		javaCommonAttributes: commonAttrs,
+		Deps:                 deps,
+		Runtime_deps:         runtimeDeps,
+		Main_class:           mainClass,
 	}
 
 	// Attribute jvm_flags
diff --git a/java/plugin.go b/java/plugin.go
index 4b174b9..123dbd4 100644
--- a/java/plugin.go
+++ b/java/plugin.go
@@ -58,27 +58,32 @@
 }
 
 type pluginAttributes struct {
-	*javaLibraryAttributes
-	Processor_class        *string
-	Target_compatible_with bazel.LabelListAttribute
+	*javaCommonAttributes
+	Deps            bazel.LabelListAttribute
+	Processor_class *string
 }
 
 // ConvertWithBp2build is used to convert android_app to Bazel.
 func (p *Plugin) ConvertWithBp2build(ctx android.TopDownMutatorContext) {
-	libAttrs := p.convertLibraryAttrsBp2Build(ctx)
-	attrs := &pluginAttributes{
-		libAttrs,
-		nil,
-		bazel.LabelListAttribute{},
+	pluginName := p.Name()
+	commonAttrs, depLabels := p.convertLibraryAttrsBp2Build(ctx)
+
+	deps := depLabels.Deps
+	deps.Append(depLabels.StaticDeps)
+
+	var processorClass *string
+	if p.pluginProperties.Processor_class != nil {
+		processorClass = p.pluginProperties.Processor_class
 	}
 
-	if p.pluginProperties.Processor_class != nil {
-		attrs.Processor_class = p.pluginProperties.Processor_class
+	attrs := &pluginAttributes{
+		javaCommonAttributes: commonAttrs,
+		Deps:                 deps,
+		Processor_class:      processorClass,
 	}
 
 	props := bazel.BazelTargetModuleProperties{
 		Rule_class: "java_plugin",
 	}
-
-	ctx.CreateBazelTargetModule(props, android.CommonAttributes{Name: p.Name()}, attrs)
+	ctx.CreateBazelTargetModule(props, android.CommonAttributes{Name: pluginName}, attrs)
 }
diff --git a/scripts/hiddenapi/Android.bp b/scripts/hiddenapi/Android.bp
index 8a47c5d..07878f9 100644
--- a/scripts/hiddenapi/Android.bp
+++ b/scripts/hiddenapi/Android.bp
@@ -19,6 +19,52 @@
 }
 
 python_binary_host {
+    name: "analyze_bcpf",
+    main: "analyze_bcpf.py",
+    srcs: ["analyze_bcpf.py"],
+    // Make sure that the bpmodify tool is built.
+    data: [":bpmodify"],
+    libs: [
+        "signature_trie",
+    ],
+    version: {
+        py2: {
+            enabled: false,
+        },
+        py3: {
+            enabled: true,
+            embedded_launcher: true,
+        },
+    },
+}
+
+python_test_host {
+    name: "analyze_bcpf_test",
+    main: "analyze_bcpf_test.py",
+    srcs: [
+        "analyze_bcpf.py",
+        "analyze_bcpf_test.py",
+    ],
+    // Make sure that the bpmodify tool is built.
+    data: [":bpmodify"],
+    libs: [
+        "signature_trie",
+    ],
+    version: {
+        py2: {
+            enabled: false,
+        },
+        py3: {
+            enabled: true,
+            embedded_launcher: true,
+        },
+    },
+    test_options: {
+        unit_test: true,
+    },
+}
+
+python_binary_host {
     name: "merge_csv",
     main: "merge_csv.py",
     srcs: ["merge_csv.py"],
diff --git a/scripts/hiddenapi/analyze_bcpf.py b/scripts/hiddenapi/analyze_bcpf.py
new file mode 100644
index 0000000..1ad8d07
--- /dev/null
+++ b/scripts/hiddenapi/analyze_bcpf.py
@@ -0,0 +1,1336 @@
+#!/usr/bin/env -S python -u
+#
+# Copyright (C) 2022 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.
+"""Analyze bootclasspath_fragment usage."""
+import argparse
+import dataclasses
+import enum
+import json
+import logging
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+import textwrap
+import typing
+from enum import Enum
+
+import sys
+
+from signature_trie import signature_trie
+
+_STUB_FLAGS_FILE = "out/soong/hiddenapi/hiddenapi-stub-flags.txt"
+
+_FLAGS_FILE = "out/soong/hiddenapi/hiddenapi-flags.csv"
+
+_INCONSISTENT_FLAGS = "ERROR: Hidden API flags are inconsistent:"
+
+
+class BuildOperation:
+
+    def __init__(self, popen):
+        self.popen = popen
+        self.returncode = None
+
+    def lines(self):
+        """Return an iterator over the lines output by the build operation.
+
+        The lines have had any trailing white space, including the newline
+        stripped.
+        """
+        return newline_stripping_iter(self.popen.stdout.readline)
+
+    def wait(self, *args, **kwargs):
+        self.popen.wait(*args, **kwargs)
+        self.returncode = self.popen.returncode
+
+
+@dataclasses.dataclass()
+class FlagDiffs:
+    """Encapsulates differences in flags reported by the build"""
+
+    # Map from member signature to the (module flags, monolithic flags)
+    diffs: typing.Dict[str, typing.Tuple[str, str]]
+
+
+@dataclasses.dataclass()
+class ModuleInfo:
+    """Provides access to the generated module-info.json file.
+
+    This is used to find the location of the file within which specific modules
+    are defined.
+    """
+
+    modules: typing.Dict[str, typing.Dict[str, typing.Any]]
+
+    @staticmethod
+    def load(filename):
+        with open(filename, "r", encoding="utf8") as f:
+            j = json.load(f)
+            return ModuleInfo(j)
+
+    def _module(self, module_name):
+        """Find module by name in module-info.json file"""
+        if module_name in self.modules:
+            return self.modules[module_name]
+
+        raise Exception(f"Module {module_name} could not be found")
+
+    def module_path(self, module_name):
+        module = self._module(module_name)
+        # The "path" is actually a list of paths, one for each class of module
+        # but as the modules are all created from bp files if a module does
+        # create multiple classes of make modules they should all have the same
+        # path.
+        paths = module["path"]
+        unique_paths = set(paths)
+        if len(unique_paths) != 1:
+            raise Exception(f"Expected module '{module_name}' to have a "
+                            f"single unique path but found {unique_paths}")
+        return paths[0]
+
+
+def extract_indent(line):
+    return re.match(r"([ \t]*)", line).group(1)
+
+
+_SPECIAL_PLACEHOLDER: str = "SPECIAL_PLACEHOLDER"
+
+
+@dataclasses.dataclass
+class BpModifyRunner:
+
+    bpmodify_path: str
+
+    def add_values_to_property(self, property_name, values, module_name,
+                               bp_file):
+        cmd = [
+            self.bpmodify_path, "-a", values, "-property", property_name, "-m",
+            module_name, "-w", bp_file, bp_file
+        ]
+
+        logging.debug(" ".join(cmd))
+        subprocess.run(
+            cmd,
+            stderr=subprocess.STDOUT,
+            stdout=log_stream_for_subprocess(),
+            check=True)
+
+
+@dataclasses.dataclass
+class FileChange:
+    path: str
+
+    description: str
+
+    def __lt__(self, other):
+        return self.path < other.path
+
+
+class PropertyChangeAction(Enum):
+    """Allowable actions that are supported by HiddenApiPropertyChange."""
+
+    # New values are appended to any existing values.
+    APPEND = 1
+
+    # New values replace any existing values.
+    REPLACE = 2
+
+
+@dataclasses.dataclass
+class HiddenApiPropertyChange:
+
+    property_name: str
+
+    values: typing.List[str]
+
+    property_comment: str = ""
+
+    # The action that indicates how this change is applied.
+    action: PropertyChangeAction = PropertyChangeAction.APPEND
+
+    def snippet(self, indent):
+        snippet = "\n"
+        snippet += format_comment_as_text(self.property_comment, indent)
+        snippet += f"{indent}{self.property_name}: ["
+        if self.values:
+            snippet += "\n"
+            for value in self.values:
+                snippet += f'{indent}    "{value}",\n'
+            snippet += f"{indent}"
+        snippet += "],\n"
+        return snippet
+
+    def fix_bp_file(self, bcpf_bp_file, bcpf, bpmodify_runner: BpModifyRunner):
+        # Add an additional placeholder value to identify the modification that
+        # bpmodify makes.
+        bpmodify_values = [_SPECIAL_PLACEHOLDER]
+
+        if self.action == PropertyChangeAction.APPEND:
+            # If adding the values to the existing values then pass the new
+            # values to bpmodify.
+            bpmodify_values.extend(self.values)
+        elif self.action == PropertyChangeAction.REPLACE:
+            # If replacing the existing values then it is not possible to use
+            # bpmodify for that directly. It could be used twice to remove the
+            # existing property and then add a new one but that does not remove
+            # any related comments and loses the position of the existing
+            # property as the new property is always added to the end of the
+            # containing block.
+            #
+            # So, instead of passing the new values to bpmodify this this just
+            # adds an extra placeholder to force bpmodify to format the list
+            # across multiple lines to ensure a consistent structure for the
+            # code that removes all the existing values and adds the new ones.
+            #
+            # This placeholder has to be different to the other placeholder as
+            # bpmodify dedups values.
+            bpmodify_values.append(_SPECIAL_PLACEHOLDER + "_REPLACE")
+        else:
+            raise ValueError(f"unknown action {self.action}")
+
+        packages = ",".join(bpmodify_values)
+        bpmodify_runner.add_values_to_property(
+            f"hidden_api.{self.property_name}", packages, bcpf, bcpf_bp_file)
+
+        with open(bcpf_bp_file, "r", encoding="utf8") as tio:
+            lines = tio.readlines()
+            lines = [line.rstrip("\n") for line in lines]
+
+        if self.fixup_bpmodify_changes(bcpf_bp_file, lines):
+            with open(bcpf_bp_file, "w", encoding="utf8") as tio:
+                for line in lines:
+                    print(line, file=tio)
+
+    def fixup_bpmodify_changes(self, bcpf_bp_file, lines):
+        """Fixup the output of bpmodify.
+
+        The bpmodify tool does not support all the capabilities that this needs
+        so it is used to do what it can, including marking the place in the
+        Android.bp file where it makes its changes and then this gets passed a
+        list of lines from that file which it then modifies to complete the
+        change.
+
+        This analyzes the list of lines to find the indices of the significant
+        lines and then applies some changes. As those changes can insert and
+        delete lines (changing the indices of following lines) the changes are
+        generally done in reverse order starting from the end and working
+        towards the beginning. That ensures that the changes do not invalidate
+        the indices of following lines.
+        """
+
+        # Find the line containing the placeholder that has been inserted.
+        place_holder_index = -1
+        for i, line in enumerate(lines):
+            if _SPECIAL_PLACEHOLDER in line:
+                place_holder_index = i
+                break
+        if place_holder_index == -1:
+            logging.debug("Could not find %s in %s", _SPECIAL_PLACEHOLDER,
+                          bcpf_bp_file)
+            return False
+
+        # Remove the place holder. Do this before inserting the comment as that
+        # would change the location of the place holder in the list.
+        place_holder_line = lines[place_holder_index]
+        if place_holder_line.endswith("],"):
+            place_holder_line = place_holder_line.replace(
+                f'"{_SPECIAL_PLACEHOLDER}"', "")
+            lines[place_holder_index] = place_holder_line
+        else:
+            del lines[place_holder_index]
+
+        # Scan forward to the end of the property block to remove a blank line
+        # that bpmodify inserts.
+        end_property_array_index = -1
+        for i in range(place_holder_index, len(lines)):
+            line = lines[i]
+            if line.endswith("],"):
+                end_property_array_index = i
+                break
+        if end_property_array_index == -1:
+            logging.debug("Could not find end of property array in %s",
+                          bcpf_bp_file)
+            return False
+
+        # If bdmodify inserted a blank line afterwards then remove it.
+        if (not lines[end_property_array_index + 1] and
+                lines[end_property_array_index + 2].endswith("},")):
+            del lines[end_property_array_index + 1]
+
+        # Scan back to find the preceding property line.
+        property_line_index = -1
+        for i in range(place_holder_index, 0, -1):
+            line = lines[i]
+            if line.lstrip().startswith(f"{self.property_name}: ["):
+                property_line_index = i
+                break
+        if property_line_index == -1:
+            logging.debug("Could not find property line in %s", bcpf_bp_file)
+            return False
+
+        # If this change is replacing the existing values then they need to be
+        # removed and replaced with the new values. That will change the lines
+        # after the property but it is necessary to do here as the following
+        # code operates on earlier lines.
+        if self.action == PropertyChangeAction.REPLACE:
+            # This removes the existing values and replaces them with the new
+            # values.
+            indent = extract_indent(lines[property_line_index + 1])
+            insert = [f'{indent}"{x}",' for x in self.values]
+            lines[property_line_index + 1:end_property_array_index] = insert
+            if not self.values:
+                # If the property has no values then merge the ], onto the
+                # same line as the property name.
+                del lines[property_line_index + 1]
+                lines[property_line_index] = lines[property_line_index] + "],"
+
+        # Only insert a comment if the property does not already have a comment.
+        line_preceding_property = lines[(property_line_index - 1)]
+        if (self.property_comment and
+                not re.match("([ \t]+)// ", line_preceding_property)):
+            # Extract the indent from the property line and use it to format the
+            # comment.
+            indent = extract_indent(lines[property_line_index])
+            comment_lines = format_comment_as_lines(self.property_comment,
+                                                    indent)
+
+            # If the line before the comment is not blank then insert an extra
+            # blank line at the beginning of the comment.
+            if line_preceding_property:
+                comment_lines.insert(0, "")
+
+            # Insert the comment before the property.
+            lines[property_line_index:property_line_index] = comment_lines
+        return True
+
+
+@dataclasses.dataclass()
+class Result:
+    """Encapsulates the result of the analysis."""
+
+    # The diffs in the flags.
+    diffs: typing.Optional[FlagDiffs] = None
+
+    # The bootclasspath_fragment hidden API properties changes.
+    property_changes: typing.List[HiddenApiPropertyChange] = dataclasses.field(
+        default_factory=list)
+
+    # The list of file changes.
+    file_changes: typing.List[FileChange] = dataclasses.field(
+        default_factory=list)
+
+
+class ClassProvider(enum.Enum):
+    """The source of a class found during the hidden API processing"""
+    BCPF = "bcpf"
+    OTHER = "other"
+
+
+# A fake member to use when using the signature trie to compute the package
+# properties from hidden API flags. This is needed because while that
+# computation only cares about classes the trie expects a class to be an
+# interior node but without a member it makes the class a leaf node. That causes
+# problems when analyzing inner classes as the outer class is a leaf node for
+# its own entry but is used as an interior node for inner classes.
+_FAKE_MEMBER = ";->fake()V"
+
+
+@dataclasses.dataclass()
+class BcpfAnalyzer:
+    # Path to this tool.
+    tool_path: str
+
+    # Directory pointed to by ANDROID_BUILD_OUT
+    top_dir: str
+
+    # Directory pointed to by OUT_DIR of {top_dir}/out if that is not set.
+    out_dir: str
+
+    # Directory pointed to by ANDROID_PRODUCT_OUT.
+    product_out_dir: str
+
+    # The name of the bootclasspath_fragment module.
+    bcpf: str
+
+    # The name of the apex module containing {bcpf}, only used for
+    # informational purposes.
+    apex: str
+
+    # The name of the sdk module containing {bcpf}, only used for
+    # informational purposes.
+    sdk: str
+
+    # If true then this will attempt to automatically fix any issues that are
+    # found.
+    fix: bool = False
+
+    # All the signatures, loaded from all-flags.csv, initialized by
+    # load_all_flags().
+    _signatures: typing.Set[str] = dataclasses.field(default_factory=set)
+
+    # All the classes, loaded from all-flags.csv, initialized by
+    # load_all_flags().
+    _classes: typing.Set[str] = dataclasses.field(default_factory=set)
+
+    # Information loaded from module-info.json, initialized by
+    # load_module_info().
+    module_info: ModuleInfo = None
+
+    @staticmethod
+    def reformat_report_test(text):
+        return re.sub(r"(.)\n([^\s])", r"\1 \2", text)
+
+    def report(self, text, **kwargs):
+        # Concatenate lines that are not separated by a blank line together to
+        # eliminate formatting applied to the supplied text to adhere to python
+        # line length limitations.
+        text = self.reformat_report_test(text)
+        logging.info("%s", text, **kwargs)
+
+    def run_command(self, cmd, *args, **kwargs):
+        cmd_line = " ".join(cmd)
+        logging.debug("Running %s", cmd_line)
+        subprocess.run(
+            cmd,
+            *args,
+            check=True,
+            cwd=self.top_dir,
+            stderr=subprocess.STDOUT,
+            stdout=log_stream_for_subprocess(),
+            text=True,
+            **kwargs)
+
+    @property
+    def signatures(self):
+        if not self._signatures:
+            raise Exception("signatures has not been initialized")
+        return self._signatures
+
+    @property
+    def classes(self):
+        if not self._classes:
+            raise Exception("classes has not been initialized")
+        return self._classes
+
+    def load_all_flags(self):
+        all_flags = self.find_bootclasspath_fragment_output_file(
+            "all-flags.csv")
+
+        # Extract the set of signatures and a separate set of classes produced
+        # by the bootclasspath_fragment.
+        with open(all_flags, "r", encoding="utf8") as f:
+            for line in newline_stripping_iter(f.readline):
+                signature = self.line_to_signature(line)
+                self._signatures.add(signature)
+                class_name = self.signature_to_class(signature)
+                self._classes.add(class_name)
+
+    def load_module_info(self):
+        module_info_file = os.path.join(self.product_out_dir,
+                                        "module-info.json")
+        self.report(f"""
+Making sure that {module_info_file} is up to date.
+""")
+        output = self.build_file_read_output(module_info_file)
+        lines = output.lines()
+        for line in lines:
+            logging.debug("%s", line)
+        output.wait(timeout=10)
+        if output.returncode:
+            raise Exception(f"Error building {module_info_file}")
+        abs_module_info_file = os.path.join(self.top_dir, module_info_file)
+        self.module_info = ModuleInfo.load(abs_module_info_file)
+
+    @staticmethod
+    def line_to_signature(line):
+        return line.split(",")[0]
+
+    @staticmethod
+    def signature_to_class(signature):
+        return signature.split(";->")[0]
+
+    @staticmethod
+    def to_parent_package(pkg_or_class):
+        return pkg_or_class.rsplit("/", 1)[0]
+
+    def module_path(self, module_name):
+        return self.module_info.module_path(module_name)
+
+    def module_out_dir(self, module_name):
+        module_path = self.module_path(module_name)
+        return os.path.join(self.out_dir, "soong/.intermediates", module_path,
+                            module_name)
+
+    def find_bootclasspath_fragment_output_file(self, basename, required=True):
+        # Find the output file of the bootclasspath_fragment with the specified
+        # base name.
+        found_file = ""
+        bcpf_out_dir = self.module_out_dir(self.bcpf)
+        for (dirpath, _, filenames) in os.walk(bcpf_out_dir):
+            for f in filenames:
+                if f == basename:
+                    found_file = os.path.join(dirpath, f)
+                    break
+        if not found_file and required:
+            raise Exception(f"Could not find {basename} in {bcpf_out_dir}")
+        return found_file
+
+    def analyze(self):
+        """Analyze a bootclasspath_fragment module.
+
+        Provides help in resolving any existing issues and provides
+        optimizations that can be applied.
+        """
+        self.report(f"Analyzing bootclasspath_fragment module {self.bcpf}")
+        self.report(f"""
+Run this tool to help initialize a bootclasspath_fragment module. Before you
+start make sure that:
+
+1. The current checkout is up to date.
+
+2. The environment has been initialized using lunch, e.g.
+   lunch aosp_arm64-userdebug
+
+3. You have added a bootclasspath_fragment module to the appropriate Android.bp
+file. Something like this:
+
+   bootclasspath_fragment {{
+     name: "{self.bcpf}",
+     contents: [
+       "...",
+     ],
+
+     // The bootclasspath_fragments that provide APIs on which this depends.
+     fragments: [
+       {{
+         apex: "com.android.art",
+         module: "art-bootclasspath-fragment",
+       }},
+     ],
+   }}
+
+4. You have added it to the platform_bootclasspath module in
+frameworks/base/boot/Android.bp. Something like this:
+
+   platform_bootclasspath {{
+     name: "platform-bootclasspath",
+     fragments: [
+       ...
+       {{
+         apex: "{self.apex}",
+         module: "{self.bcpf}",
+       }},
+     ],
+   }}
+
+5. You have added an sdk module. Something like this:
+
+   sdk {{
+     name: "{self.sdk}",
+     bootclasspath_fragments: ["{self.bcpf}"],
+   }}
+""")
+
+        # Make sure that the module-info.json file is up to date.
+        self.load_module_info()
+
+        self.report("""
+Cleaning potentially stale files.
+""")
+        # Remove the out/soong/hiddenapi files.
+        shutil.rmtree(f"{self.out_dir}/soong/hiddenapi", ignore_errors=True)
+
+        # Remove any bootclasspath_fragment output files.
+        shutil.rmtree(self.module_out_dir(self.bcpf), ignore_errors=True)
+
+        self.build_monolithic_stubs_flags()
+
+        result = Result()
+
+        self.build_monolithic_flags(result)
+        self.analyze_hiddenapi_package_properties(result)
+        self.explain_how_to_check_signature_patterns()
+
+        # If there were any changes that need to be made to the Android.bp
+        # file then either apply or report them.
+        if result.property_changes:
+            bcpf_dir = self.module_info.module_path(self.bcpf)
+            bcpf_bp_file = os.path.join(self.top_dir, bcpf_dir, "Android.bp")
+            if self.fix:
+                tool_dir = os.path.dirname(self.tool_path)
+                bpmodify_path = os.path.join(tool_dir, "bpmodify")
+                bpmodify_runner = BpModifyRunner(bpmodify_path)
+                for property_change in result.property_changes:
+                    property_change.fix_bp_file(bcpf_bp_file, self.bcpf,
+                                                bpmodify_runner)
+
+                result.file_changes.append(
+                    self.new_file_change(
+                        bcpf_bp_file,
+                        f"Updated hidden_api properties of '{self.bcpf}'"))
+
+            else:
+                hiddenapi_snippet = ""
+                for property_change in result.property_changes:
+                    hiddenapi_snippet += property_change.snippet("        ")
+
+                # Remove leading and trailing blank lines.
+                hiddenapi_snippet = hiddenapi_snippet.strip("\n")
+
+                result.file_changes.append(
+                    self.new_file_change(
+                        bcpf_bp_file, f"""
+Add the following snippet into the {self.bcpf} bootclasspath_fragment module
+in the {bcpf_dir}/Android.bp file. If the hidden_api block already exists then
+merge these properties into it.
+
+    hidden_api: {{
+{hiddenapi_snippet}
+    }},
+"""))
+
+        if result.file_changes:
+            if self.fix:
+                file_change_message = """
+The following files were modified by this script:"""
+            else:
+                file_change_message = """
+The following modifications need to be made:"""
+
+            self.report(f"""
+{file_change_message}""")
+            result.file_changes.sort()
+            for file_change in result.file_changes:
+                self.report(f"""
+    {file_change.path}
+        {file_change.description}
+""".lstrip("\n"))
+
+            if not self.fix:
+                self.report("""
+Run the command again with the --fix option to automatically make the above
+changes.
+""".lstrip())
+
+    def new_file_change(self, file, description):
+        return FileChange(
+            path=os.path.relpath(file, self.top_dir), description=description)
+
+    def check_inconsistent_flag_lines(self, significant, module_line,
+                                      monolithic_line, separator_line):
+        if not (module_line.startswith("< ") and
+                monolithic_line.startswith("> ") and not separator_line):
+            # Something went wrong.
+            self.report(f"""Invalid build output detected:
+  module_line: "{module_line}"
+  monolithic_line: "{monolithic_line}"
+  separator_line: "{separator_line}"
+""")
+            sys.exit(1)
+
+        if significant:
+            logging.debug("%s", module_line)
+            logging.debug("%s", monolithic_line)
+            logging.debug("%s", separator_line)
+
+    def scan_inconsistent_flags_report(self, lines):
+        """Scans a hidden API flags report
+
+        The hidden API inconsistent flags report which looks something like
+        this.
+
+        < out/soong/.intermediates/.../filtered-stub-flags.csv
+        > out/soong/hiddenapi/hiddenapi-stub-flags.txt
+
+        < Landroid/compat/Compatibility;->clearOverrides()V
+        > Landroid/compat/Compatibility;->clearOverrides()V,core-platform-api
+
+        """
+
+        # The basic format of an entry in the inconsistent flags report is:
+        #   <module specific flag>
+        #   <monolithic flag>
+        #   <separator>
+        #
+        # Wrap the lines iterator in an iterator which returns a tuple
+        # consisting of the three separate lines.
+        triples = zip(lines, lines, lines)
+
+        module_line, monolithic_line, separator_line = next(triples)
+        significant = False
+        bcpf_dir = self.module_info.module_path(self.bcpf)
+        if os.path.join(bcpf_dir, self.bcpf) in module_line:
+            # These errors are related to the bcpf being analyzed so
+            # keep them.
+            significant = True
+        else:
+            self.report(f"Filtering out errors related to {module_line}")
+
+        self.check_inconsistent_flag_lines(significant, module_line,
+                                           monolithic_line, separator_line)
+
+        diffs = {}
+        for module_line, monolithic_line, separator_line in triples:
+            self.check_inconsistent_flag_lines(significant, module_line,
+                                               monolithic_line, "")
+
+            module_parts = module_line.removeprefix("< ").split(",")
+            module_signature = module_parts[0]
+            module_flags = module_parts[1:]
+
+            monolithic_parts = monolithic_line.removeprefix("> ").split(",")
+            monolithic_signature = monolithic_parts[0]
+            monolithic_flags = monolithic_parts[1:]
+
+            if module_signature != monolithic_signature:
+                # Something went wrong.
+                self.report(f"""Inconsistent signatures detected:
+  module_signature: "{module_signature}"
+  monolithic_signature: "{monolithic_signature}"
+""")
+                sys.exit(1)
+
+            diffs[module_signature] = (module_flags, monolithic_flags)
+
+            if separator_line:
+                # If the separator line is not blank then it is the end of the
+                # current report, and possibly the start of another.
+                return separator_line, diffs
+
+        return "", diffs
+
+    def build_file_read_output(self, filename):
+        # Make sure the filename is relative to top if possible as the build
+        # may be using relative paths as the target.
+        rel_filename = filename.removeprefix(self.top_dir)
+        cmd = ["build/soong/soong_ui.bash", "--make-mode", rel_filename]
+        cmd_line = " ".join(cmd)
+        logging.debug("%s", cmd_line)
+        # pylint: disable=consider-using-with
+        output = subprocess.Popen(
+            cmd,
+            cwd=self.top_dir,
+            stderr=subprocess.STDOUT,
+            stdout=subprocess.PIPE,
+            text=True,
+        )
+        return BuildOperation(popen=output)
+
+    def build_hiddenapi_flags(self, filename):
+        output = self.build_file_read_output(filename)
+
+        lines = output.lines()
+        diffs = None
+        for line in lines:
+            logging.debug("%s", line)
+            while line == _INCONSISTENT_FLAGS:
+                line, diffs = self.scan_inconsistent_flags_report(lines)
+
+        output.wait(timeout=10)
+        if output.returncode != 0:
+            logging.debug("Command failed with %s", output.returncode)
+        else:
+            logging.debug("Command succeeded")
+
+        return diffs
+
+    def build_monolithic_stubs_flags(self):
+        self.report(f"""
+Attempting to build {_STUB_FLAGS_FILE} to verify that the
+bootclasspath_fragment has the correct API stubs available...
+""")
+
+        # Build the hiddenapi-stubs-flags.txt file.
+        diffs = self.build_hiddenapi_flags(_STUB_FLAGS_FILE)
+        if diffs:
+            self.report(f"""
+There is a discrepancy between the stub API derived flags created by the
+bootclasspath_fragment and the platform_bootclasspath. See preceding error
+messages to see which flags are inconsistent. The inconsistencies can occur for
+a couple of reasons:
+
+If you are building against prebuilts of the Android SDK, e.g. by using
+TARGET_BUILD_APPS then the prebuilt versions of the APIs this
+bootclasspath_fragment depends upon are out of date and need updating. See
+go/update-prebuilts for help.
+
+Otherwise, this is happening because there are some stub APIs that are either
+provided by or used by the contents of the bootclasspath_fragment but which are
+not available to it. There are 4 ways to handle this:
+
+1. A java_sdk_library in the contents property will automatically make its stub
+   APIs available to the bootclasspath_fragment so nothing needs to be done.
+
+2. If the API provided by the bootclasspath_fragment is created by an api_only
+   java_sdk_library (or a java_library that compiles files generated by a
+   separate droidstubs module then it cannot be added to the contents and
+   instead must be added to the api.stubs property, e.g.
+
+   bootclasspath_fragment {{
+     name: "{self.bcpf}",
+     ...
+     api: {{
+       stubs: ["$MODULE-api-only"],"
+     }},
+   }}
+
+3. If the contents use APIs provided by another bootclasspath_fragment then
+   it needs to be added to the fragments property, e.g.
+
+   bootclasspath_fragment {{
+     name: "{self.bcpf}",
+     ...
+     // The bootclasspath_fragments that provide APIs on which this depends.
+     fragments: [
+       ...
+       {{
+         apex: "com.android.other",
+         module: "com.android.other-bootclasspath-fragment",
+       }},
+     ],
+   }}
+
+4. If the contents use APIs from a module that is not part of another
+   bootclasspath_fragment then it must be added to the additional_stubs
+   property, e.g.
+
+   bootclasspath_fragment {{
+     name: "{self.bcpf}",
+     ...
+     additional_stubs: ["android-non-updatable"],
+   }}
+
+   Like the api.stubs property these are typically java_sdk_library modules but
+   can be java_library too.
+
+   Note: The "android-non-updatable" is treated as if it was a java_sdk_library
+   which it is not at the moment but will be in future.
+""")
+
+        return diffs
+
+    def build_monolithic_flags(self, result):
+        self.report(f"""
+Attempting to build {_FLAGS_FILE} to verify that the
+bootclasspath_fragment has the correct hidden API flags...
+""")
+
+        # Build the hiddenapi-flags.csv file and extract any differences in
+        # the flags between this bootclasspath_fragment and the monolithic
+        # files.
+        result.diffs = self.build_hiddenapi_flags(_FLAGS_FILE)
+
+        # Load information from the bootclasspath_fragment's all-flags.csv file.
+        self.load_all_flags()
+
+        if result.diffs:
+            self.report(f"""
+There is a discrepancy between the hidden API flags created by the
+bootclasspath_fragment and the platform_bootclasspath. See preceding error
+messages to see which flags are inconsistent. The inconsistencies can occur for
+a couple of reasons:
+
+If you are building against prebuilts of this bootclasspath_fragment then the
+prebuilt version of the sdk snapshot (specifically the hidden API flag files)
+are inconsistent with the prebuilt version of the apex {self.apex}. Please
+ensure that they are both updated from the same build.
+
+1. There are custom hidden API flags specified in the one of the files in
+   frameworks/base/boot/hiddenapi which apply to the bootclasspath_fragment but
+   which are not supplied to the bootclasspath_fragment module.
+
+2. The bootclasspath_fragment specifies invalid "package_prefixes" or
+   "split_packages" properties that match packages and classes that it does not
+   provide.
+
+""")
+
+            # Check to see if there are any hiddenapi related properties that
+            # need to be added to the
+            self.report("""
+Checking custom hidden API flags....
+""")
+            self.check_frameworks_base_boot_hidden_api_files(result)
+
+    def report_hidden_api_flag_file_changes(self, result, property_name,
+                                            flags_file, rel_bcpf_flags_file,
+                                            bcpf_flags_file):
+        matched_signatures = set()
+        # Open the flags file to read the flags from.
+        with open(flags_file, "r", encoding="utf8") as f:
+            for signature in newline_stripping_iter(f.readline):
+                if signature in self.signatures:
+                    # The signature is provided by the bootclasspath_fragment so
+                    # it will need to be moved to the bootclasspath_fragment
+                    # specific file.
+                    matched_signatures.add(signature)
+
+        # If the bootclasspath_fragment specific flags file is not empty
+        # then it contains flags. That could either be new flags just moved
+        # from frameworks/base or previous contents of the file. In either
+        # case the file must not be removed.
+        if matched_signatures:
+            insert = textwrap.indent("\n".join(matched_signatures),
+                                     "            ")
+            result.file_changes.append(
+                self.new_file_change(
+                    flags_file, f"""Remove the following entries:
+{insert}
+"""))
+
+            result.file_changes.append(
+                self.new_file_change(
+                    bcpf_flags_file, f"""Add the following entries:
+{insert}
+"""))
+
+            result.property_changes.append(
+                HiddenApiPropertyChange(
+                    property_name=property_name,
+                    values=[rel_bcpf_flags_file],
+                ))
+
+    def fix_hidden_api_flag_files(self, result, property_name, flags_file,
+                                  rel_bcpf_flags_file, bcpf_flags_file):
+        # Read the file in frameworks/base/boot/hiddenapi/<file> copy any
+        # flags that relate to the bootclasspath_fragment into a local
+        # file in the hiddenapi subdirectory.
+        tmp_flags_file = flags_file + ".tmp"
+
+        # Make sure the directory containing the bootclasspath_fragment specific
+        # hidden api flags exists.
+        os.makedirs(os.path.dirname(bcpf_flags_file), exist_ok=True)
+
+        bcpf_flags_file_exists = os.path.exists(bcpf_flags_file)
+
+        matched_signatures = set()
+        # Open the flags file to read the flags from.
+        with open(flags_file, "r", encoding="utf8") as f:
+            # Open a temporary file to write the flags (minus any removed
+            # flags).
+            with open(tmp_flags_file, "w", encoding="utf8") as t:
+                # Open the bootclasspath_fragment file for append just in
+                # case it already exists.
+                with open(bcpf_flags_file, "a", encoding="utf8") as b:
+                    for line in iter(f.readline, ""):
+                        signature = line.rstrip()
+                        if signature in self.signatures:
+                            # The signature is provided by the
+                            # bootclasspath_fragment so write it to the new
+                            # bootclasspath_fragment specific file.
+                            print(line, file=b, end="")
+                            matched_signatures.add(signature)
+                        else:
+                            # The signature is NOT provided by the
+                            # bootclasspath_fragment. Copy it to the new
+                            # monolithic file.
+                            print(line, file=t, end="")
+
+        # If the bootclasspath_fragment specific flags file is not empty
+        # then it contains flags. That could either be new flags just moved
+        # from frameworks/base or previous contents of the file. In either
+        # case the file must not be removed.
+        if matched_signatures:
+            # There are custom flags related to the bootclasspath_fragment
+            # so replace the frameworks/base/boot/hiddenapi file with the
+            # file that does not contain those flags.
+            shutil.move(tmp_flags_file, flags_file)
+
+            result.file_changes.append(
+                self.new_file_change(flags_file,
+                                     f"Removed '{self.bcpf}' specific entries"))
+
+            result.property_changes.append(
+                HiddenApiPropertyChange(
+                    property_name=property_name,
+                    values=[rel_bcpf_flags_file],
+                ))
+
+            # Make sure that the files are sorted.
+            self.run_command([
+                "tools/platform-compat/hiddenapi/sort_api.sh",
+                bcpf_flags_file,
+            ])
+
+            if bcpf_flags_file_exists:
+                desc = f"Added '{self.bcpf}' specific entries"
+            else:
+                desc = f"Created with '{self.bcpf}' specific entries"
+            result.file_changes.append(
+                self.new_file_change(bcpf_flags_file, desc))
+        else:
+            # There are no custom flags related to the
+            # bootclasspath_fragment so clean up the working files.
+            os.remove(tmp_flags_file)
+            if not bcpf_flags_file_exists:
+                os.remove(bcpf_flags_file)
+
+    def check_frameworks_base_boot_hidden_api_files(self, result):
+        hiddenapi_dir = os.path.join(self.top_dir,
+                                     "frameworks/base/boot/hiddenapi")
+        for basename in sorted(os.listdir(hiddenapi_dir)):
+            if not (basename.startswith("hiddenapi-") and
+                    basename.endswith(".txt")):
+                continue
+
+            flags_file = os.path.join(hiddenapi_dir, basename)
+
+            logging.debug("Checking %s for flags related to %s", flags_file,
+                          self.bcpf)
+
+            # Map the file name in frameworks/base/boot/hiddenapi into a
+            # slightly more meaningful name for use by the
+            # bootclasspath_fragment.
+            if basename == "hiddenapi-max-target-o.txt":
+                basename = "hiddenapi-max-target-o-low-priority.txt"
+            elif basename == "hiddenapi-max-target-r-loprio.txt":
+                basename = "hiddenapi-max-target-r-low-priority.txt"
+
+            property_name = basename.removeprefix("hiddenapi-")
+            property_name = property_name.removesuffix(".txt")
+            property_name = property_name.replace("-", "_")
+
+            rel_bcpf_flags_file = f"hiddenapi/{basename}"
+            bcpf_dir = self.module_info.module_path(self.bcpf)
+            bcpf_flags_file = os.path.join(self.top_dir, bcpf_dir,
+                                           rel_bcpf_flags_file)
+
+            if self.fix:
+                self.fix_hidden_api_flag_files(result, property_name,
+                                               flags_file, rel_bcpf_flags_file,
+                                               bcpf_flags_file)
+            else:
+                self.report_hidden_api_flag_file_changes(
+                    result, property_name, flags_file, rel_bcpf_flags_file,
+                    bcpf_flags_file)
+
+    @staticmethod
+    def split_package_comment(split_packages):
+        if split_packages:
+            return textwrap.dedent("""
+                The following packages contain classes from other modules on the
+                bootclasspath. That means that the hidden API flags for this
+                module has to explicitly list every single class this module
+                provides in that package to differentiate them from the classes
+                provided by other modules. That can include private classes that
+                are not part of the API.
+            """).strip("\n")
+
+        return "This module does not contain any split packages."
+
+    @staticmethod
+    def package_prefixes_comment():
+        return textwrap.dedent("""
+            The following packages and all their subpackages currently only
+            contain classes from this bootclasspath_fragment. Listing a package
+            here won't prevent other bootclasspath modules from adding classes
+            in any of those packages but it will prevent them from adding those
+            classes into an API surface, e.g. public, system, etc.. Doing so
+            will result in a build failure due to inconsistent flags.
+        """).strip("\n")
+
+    def analyze_hiddenapi_package_properties(self, result):
+        split_packages, single_packages, package_prefixes = \
+            self.compute_hiddenapi_package_properties()
+
+        # TODO(b/202154151): Find those classes in split packages that are not
+        #  part of an API, i.e. are an internal implementation class, and so
+        #  can, and should, be safely moved out of the split packages.
+
+        result.property_changes.append(
+            HiddenApiPropertyChange(
+                property_name="split_packages",
+                values=split_packages,
+                property_comment=self.split_package_comment(split_packages),
+                action=PropertyChangeAction.REPLACE,
+            ))
+
+        if split_packages:
+            self.report(f"""
+bootclasspath_fragment {self.bcpf} contains classes in packages that also
+contain classes provided by other sources, those packages are called split
+packages. Split packages should be avoided where possible but are often
+unavoidable when modularizing existing code.
+
+The hidden api processing needs to know which packages are split (and conversely
+which are not) so that it can optimize the hidden API flags to remove
+unnecessary implementation details.
+""")
+
+        self.report("""
+By default (for backwards compatibility) the bootclasspath_fragment assumes that
+all packages are split unless one of the package_prefixes or split_packages
+properties are specified. While that is safe it is not optimal and can lead to
+unnecessary implementation details leaking into the hidden API flags. Adding an
+empty split_packages property allows the flags to be optimized and remove any
+unnecessary implementation details.
+""")
+
+        if single_packages:
+            result.property_changes.append(
+                HiddenApiPropertyChange(
+                    property_name="single_packages",
+                    values=single_packages,
+                    property_comment=textwrap.dedent("""
+                    The following packages currently only contain classes from
+                    this bootclasspath_fragment but some of their sub-packages
+                    contain classes from other bootclasspath modules. Packages
+                    should only be listed here when necessary for legacy
+                    purposes, new packages should match a package prefix.
+                """),
+                    action=PropertyChangeAction.REPLACE,
+                ))
+
+        if package_prefixes:
+            result.property_changes.append(
+                HiddenApiPropertyChange(
+                    property_name="package_prefixes",
+                    values=package_prefixes,
+                    property_comment=self.package_prefixes_comment(),
+                    action=PropertyChangeAction.REPLACE,
+                ))
+
+    def explain_how_to_check_signature_patterns(self):
+        signature_patterns_files = self.find_bootclasspath_fragment_output_file(
+            "signature-patterns.csv", required=False)
+        if signature_patterns_files:
+            signature_patterns_files = signature_patterns_files.removeprefix(
+                self.top_dir)
+
+            self.report(f"""
+The purpose of the hiddenapi split_packages and package_prefixes properties is
+to allow the removal of implementation details from the hidden API flags to
+reduce the coupling between sdk snapshots and the APEX runtime. It cannot
+eliminate that coupling completely though. Doing so may require changes to the
+code.
+
+This tool provides support for managing those properties but it cannot decide
+whether the set of package prefixes suggested is appropriate that needs the
+input of the developer.
+
+Please run the following command:
+    m {signature_patterns_files}
+
+And then check the '{signature_patterns_files}' for any mention of
+implementation classes and packages (i.e. those classes/packages that do not
+contain any part of an API surface, including the hidden API). If they are
+found then the code should ideally be moved to a package unique to this module
+that is contained within a package that is part of an API surface.
+
+The format of the file is a list of patterns:
+
+* Patterns for split packages will list every class in that package.
+
+* Patterns for package prefixes will end with .../**.
+
+* Patterns for packages which are not split but cannot use a package prefix
+because there are sub-packages which are provided by another module will end
+with .../*.
+""")
+
+    def compute_hiddenapi_package_properties(self):
+        trie = signature_trie()
+        # Populate the trie with the classes that are provided by the
+        # bootclasspath_fragment tagging them to make it clear where they
+        # are from.
+        sorted_classes = sorted(self.classes)
+        for class_name in sorted_classes:
+            trie.add(class_name + _FAKE_MEMBER, ClassProvider.BCPF)
+
+        monolithic_classes = set()
+        abs_flags_file = os.path.join(self.top_dir, _FLAGS_FILE)
+        with open(abs_flags_file, "r", encoding="utf8") as f:
+            for line in iter(f.readline, ""):
+                signature = self.line_to_signature(line)
+                class_name = self.signature_to_class(signature)
+                if (class_name not in monolithic_classes and
+                        class_name not in self.classes):
+                    trie.add(
+                        class_name + _FAKE_MEMBER,
+                        ClassProvider.OTHER,
+                        only_if_matches=True)
+                    monolithic_classes.add(class_name)
+
+        split_packages = []
+        single_packages = []
+        package_prefixes = []
+        self.recurse_hiddenapi_packages_trie(trie, split_packages,
+                                             single_packages, package_prefixes)
+        return split_packages, single_packages, package_prefixes
+
+    def recurse_hiddenapi_packages_trie(self, node, split_packages,
+                                        single_packages, package_prefixes):
+        nodes = node.child_nodes()
+        if nodes:
+            for child in nodes:
+                # Ignore any non-package nodes.
+                if child.type != "package":
+                    continue
+
+                package = child.selector.replace("/", ".")
+
+                providers = set(child.get_matching_rows("**"))
+                if not providers:
+                    # The package and all its sub packages contain no
+                    # classes. This should never happen.
+                    pass
+                elif providers == {ClassProvider.BCPF}:
+                    # The package and all its sub packages only contain
+                    # classes provided by the bootclasspath_fragment.
+                    logging.debug("Package '%s.**' is not split", package)
+                    package_prefixes.append(package)
+                    # There is no point traversing into the sub packages.
+                    continue
+                elif providers == {ClassProvider.OTHER}:
+                    # The package and all its sub packages contain no
+                    # classes provided by the bootclasspath_fragment.
+                    # There is no point traversing into the sub packages.
+                    logging.debug("Package '%s.**' contains no classes from %s",
+                                  package, self.bcpf)
+                    continue
+                elif ClassProvider.BCPF in providers:
+                    # The package and all its sub packages contain classes
+                    # provided by the bootclasspath_fragment and other
+                    # sources.
+                    logging.debug(
+                        "Package '%s.**' contains classes from "
+                        "%s and other sources", package, self.bcpf)
+
+                providers = set(child.get_matching_rows("*"))
+                if not providers:
+                    # The package contains no classes.
+                    logging.debug("Package: %s contains no classes", package)
+                elif providers == {ClassProvider.BCPF}:
+                    # The package only contains classes provided by the
+                    # bootclasspath_fragment.
+                    logging.debug("Package '%s.*' is not split", package)
+                    single_packages.append(package)
+                elif providers == {ClassProvider.OTHER}:
+                    # The package contains no classes provided by the
+                    # bootclasspath_fragment. Child nodes make contain such
+                    # classes.
+                    logging.debug("Package '%s.*' contains no classes from %s",
+                                  package, self.bcpf)
+                elif ClassProvider.BCPF in providers:
+                    # The package contains classes provided by both the
+                    # bootclasspath_fragment and some other source.
+                    logging.debug("Package '%s.*' is split", package)
+                    split_packages.append(package)
+
+                self.recurse_hiddenapi_packages_trie(child, split_packages,
+                                                     single_packages,
+                                                     package_prefixes)
+
+
+def newline_stripping_iter(iterator):
+    """Return an iterator over the iterator that strips trailing white space."""
+    lines = iter(iterator, "")
+    lines = (line.rstrip() for line in lines)
+    return lines
+
+
+def format_comment_as_text(text, indent):
+    return "".join(
+        [f"{line}\n" for line in format_comment_as_lines(text, indent)])
+
+
+def format_comment_as_lines(text, indent):
+    lines = textwrap.wrap(text.strip("\n"), width=77 - len(indent))
+    lines = [f"{indent}// {line}" for line in lines]
+    return lines
+
+
+def log_stream_for_subprocess():
+    stream = subprocess.DEVNULL
+    for handler in logging.root.handlers:
+        if handler.level == logging.DEBUG:
+            if isinstance(handler, logging.StreamHandler):
+                stream = handler.stream
+    return stream
+
+
+def main(argv):
+    args_parser = argparse.ArgumentParser(
+        description="Analyze a bootclasspath_fragment module.")
+    args_parser.add_argument(
+        "--bcpf",
+        help="The bootclasspath_fragment module to analyze",
+        required=True,
+    )
+    args_parser.add_argument(
+        "--apex",
+        help="The apex module to which the bootclasspath_fragment belongs. It "
+        "is not strictly necessary at the moment but providing it will "
+        "allow this script to give more useful messages and it may be"
+        "required in future.",
+        default="SPECIFY-APEX-OPTION")
+    args_parser.add_argument(
+        "--sdk",
+        help="The sdk module to which the bootclasspath_fragment belongs. It "
+        "is not strictly necessary at the moment but providing it will "
+        "allow this script to give more useful messages and it may be"
+        "required in future.",
+        default="SPECIFY-SDK-OPTION")
+    args_parser.add_argument(
+        "--fix",
+        help="Attempt to fix any issues found automatically.",
+        action="store_true",
+        default=False)
+    args = args_parser.parse_args(argv[1:])
+    top_dir = os.environ["ANDROID_BUILD_TOP"] + "/"
+    out_dir = os.environ.get("OUT_DIR", os.path.join(top_dir, "out"))
+    product_out_dir = os.environ.get("ANDROID_PRODUCT_OUT", top_dir)
+    # Make product_out_dir relative to the top so it can be used as part of a
+    # build target.
+    product_out_dir = product_out_dir.removeprefix(top_dir)
+    log_fd, abs_log_file = tempfile.mkstemp(
+        suffix="_analyze_bcpf.log", text=True)
+
+    with os.fdopen(log_fd, "w") as log_file:
+        # Set up debug logging to the log file.
+        logging.basicConfig(
+            level=logging.DEBUG,
+            format="%(levelname)-8s %(message)s",
+            stream=log_file)
+
+        # define a Handler which writes INFO messages or higher to the
+        # sys.stdout with just the message.
+        console = logging.StreamHandler()
+        console.setLevel(logging.INFO)
+        console.setFormatter(logging.Formatter("%(message)s"))
+        # add the handler to the root logger
+        logging.getLogger("").addHandler(console)
+
+        print(f"Writing log to {abs_log_file}")
+        try:
+            analyzer = BcpfAnalyzer(
+                tool_path=argv[0],
+                top_dir=top_dir,
+                out_dir=out_dir,
+                product_out_dir=product_out_dir,
+                bcpf=args.bcpf,
+                apex=args.apex,
+                sdk=args.sdk,
+                fix=args.fix,
+            )
+            analyzer.analyze()
+        finally:
+            print(f"Log written to {abs_log_file}")
+
+
+if __name__ == "__main__":
+    main(sys.argv)
diff --git a/scripts/hiddenapi/analyze_bcpf_test.py b/scripts/hiddenapi/analyze_bcpf_test.py
new file mode 100644
index 0000000..650dd54
--- /dev/null
+++ b/scripts/hiddenapi/analyze_bcpf_test.py
@@ -0,0 +1,650 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2022 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.
+"""Unit tests for analyzing bootclasspath_fragment modules."""
+import os.path
+import shutil
+import tempfile
+import unittest
+import unittest.mock
+
+import sys
+
+import analyze_bcpf as ab
+
+_FRAMEWORK_HIDDENAPI = "frameworks/base/boot/hiddenapi"
+_MAX_TARGET_O = f"{_FRAMEWORK_HIDDENAPI}/hiddenapi-max-target-o.txt"
+_MAX_TARGET_P = f"{_FRAMEWORK_HIDDENAPI}/hiddenapi-max-target-p.txt"
+_MAX_TARGET_Q = f"{_FRAMEWORK_HIDDENAPI}/hiddenapi-max-target-q.txt"
+_MAX_TARGET_R = f"{_FRAMEWORK_HIDDENAPI}/hiddenapi-max-target-r-loprio.txt"
+
+_MULTI_LINE_COMMENT = """
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut arcu justo,
+bibendum eu malesuada vel, fringilla in odio. Etiam gravida ultricies sem
+tincidunt luctus.""".replace("\n", " ").strip()
+
+
+class FakeBuildOperation(ab.BuildOperation):
+
+    def __init__(self, lines, return_code):
+        ab.BuildOperation.__init__(self, None)
+        self._lines = lines
+        self.returncode = return_code
+
+    def lines(self):
+        return iter(self._lines)
+
+    def wait(self, *args, **kwargs):
+        return
+
+
+class TestAnalyzeBcpf(unittest.TestCase):
+
+    def setUp(self):
+        # Create a temporary directory
+        self.test_dir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        # Remove the directory after the test
+        shutil.rmtree(self.test_dir)
+
+    @staticmethod
+    def write_abs_file(abs_path, contents):
+        os.makedirs(os.path.dirname(abs_path), exist_ok=True)
+        with open(abs_path, "w", encoding="utf8") as f:
+            print(contents.removeprefix("\n"), file=f, end="")
+
+    def populate_fs(self, fs):
+        for path, contents in fs.items():
+            abs_path = os.path.join(self.test_dir, path)
+            self.write_abs_file(abs_path, contents)
+
+    def create_analyzer_for_test(self,
+                                 fs=None,
+                                 bcpf="bcpf",
+                                 apex="apex",
+                                 sdk="sdk",
+                                 fix=False):
+        if fs:
+            self.populate_fs(fs)
+
+        top_dir = self.test_dir
+        out_dir = os.path.join(self.test_dir, "out")
+        product_out_dir = "out/product"
+
+        bcpf_dir = f"{bcpf}-dir"
+        modules = {bcpf: {"path": [bcpf_dir]}}
+        module_info = ab.ModuleInfo(modules)
+
+        analyzer = ab.BcpfAnalyzer(
+            tool_path=os.path.join(out_dir, "bin"),
+            top_dir=top_dir,
+            out_dir=out_dir,
+            product_out_dir=product_out_dir,
+            bcpf=bcpf,
+            apex=apex,
+            sdk=sdk,
+            fix=fix,
+            module_info=module_info,
+        )
+        analyzer.load_all_flags()
+        return analyzer
+
+    def test_reformat_report_text(self):
+        lines = """
+99. An item in a numbered list
+that traverses multiple lines.
+
+   An indented example
+   that should not be reformatted.
+"""
+        reformatted = ab.BcpfAnalyzer.reformat_report_test(lines)
+        self.assertEqual(
+            """
+99. An item in a numbered list that traverses multiple lines.
+
+   An indented example
+   that should not be reformatted.
+""", reformatted)
+
+    def do_test_build_flags(self, fix):
+        lines = """
+ERROR: Hidden API flags are inconsistent:
+< out/soong/.intermediates/bcpf-dir/bcpf-dir/filtered-flags.csv
+> out/soong/hiddenapi/hiddenapi-flags.csv
+
+< Lacme/test/Class;-><init>()V,blocked
+> Lacme/test/Class;-><init>()V,max-target-o
+
+< Lacme/test/Other;->getThing()Z,blocked
+> Lacme/test/Other;->getThing()Z,max-target-p
+
+< Lacme/test/Widget;-><init()V,blocked
+> Lacme/test/Widget;-><init()V,max-target-q
+
+< Lacme/test/Gadget;->NAME:Ljava/lang/String;,blocked
+> Lacme/test/Gadget;->NAME:Ljava/lang/String;,lo-prio,max-target-r
+16:37:32 ninja failed with: exit status 1
+""".strip().splitlines()
+        operation = FakeBuildOperation(lines=lines, return_code=1)
+
+        fs = {
+            _MAX_TARGET_O:
+                """
+Lacme/items/Magnet;->size:I
+Lacme/test/Class;-><init>()V
+""",
+            _MAX_TARGET_P:
+                """
+Lacme/items/Rocket;->size:I
+Lacme/test/Other;->getThing()Z
+""",
+            _MAX_TARGET_Q:
+                """
+Lacme/items/Rock;->size:I
+Lacme/test/Widget;-><init()V
+""",
+            _MAX_TARGET_R:
+                """
+Lacme/items/Lever;->size:I
+Lacme/test/Gadget;->NAME:Ljava/lang/String;
+""",
+            "bcpf-dir/hiddenapi/hiddenapi-max-target-p.txt":
+                """
+Lacme/old/Class;->getWidget()Lacme/test/Widget;
+""",
+            "out/soong/.intermediates/bcpf-dir/bcpf/all-flags.csv":
+                """
+Lacme/test/Gadget;->NAME:Ljava/lang/String;,blocked
+Lacme/test/Widget;-><init()V,blocked
+Lacme/test/Class;-><init>()V,blocked
+Lacme/test/Other;->getThing()Z,blocked
+""",
+        }
+
+        analyzer = self.create_analyzer_for_test(fs, fix=fix)
+
+        # Override the build_file_read_output() method to just return a fake
+        # build operation.
+        analyzer.build_file_read_output = unittest.mock.Mock(
+            return_value=operation)
+
+        # Override the run_command() method to do nothing.
+        analyzer.run_command = unittest.mock.Mock()
+
+        result = ab.Result()
+
+        analyzer.build_monolithic_flags(result)
+        expected_diffs = {
+            "Lacme/test/Gadget;->NAME:Ljava/lang/String;":
+                (["blocked"], ["lo-prio", "max-target-r"]),
+            "Lacme/test/Widget;-><init()V": (["blocked"], ["max-target-q"]),
+            "Lacme/test/Class;-><init>()V": (["blocked"], ["max-target-o"]),
+            "Lacme/test/Other;->getThing()Z": (["blocked"], ["max-target-p"])
+        }
+        self.assertEqual(expected_diffs, result.diffs, msg="flag differences")
+
+        expected_property_changes = [
+            ab.HiddenApiPropertyChange(
+                property_name="max_target_o_low_priority",
+                values=["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
+                property_comment=""),
+            ab.HiddenApiPropertyChange(
+                property_name="max_target_p",
+                values=["hiddenapi/hiddenapi-max-target-p.txt"],
+                property_comment=""),
+            ab.HiddenApiPropertyChange(
+                property_name="max_target_q",
+                values=["hiddenapi/hiddenapi-max-target-q.txt"],
+                property_comment=""),
+            ab.HiddenApiPropertyChange(
+                property_name="max_target_r_low_priority",
+                values=["hiddenapi/hiddenapi-max-target-r-low-priority.txt"],
+                property_comment=""),
+        ]
+        self.assertEqual(
+            expected_property_changes,
+            result.property_changes,
+            msg="property changes")
+
+        return result
+
+    def test_build_flags_report(self):
+        result = self.do_test_build_flags(fix=False)
+
+        expected_file_changes = [
+            ab.FileChange(
+                path="bcpf-dir/hiddenapi/"
+                "hiddenapi-max-target-o-low-priority.txt",
+                description="""Add the following entries:
+            Lacme/test/Class;-><init>()V
+""",
+            ),
+            ab.FileChange(
+                path="bcpf-dir/hiddenapi/hiddenapi-max-target-p.txt",
+                description="""Add the following entries:
+            Lacme/test/Other;->getThing()Z
+""",
+            ),
+            ab.FileChange(
+                path="bcpf-dir/hiddenapi/hiddenapi-max-target-q.txt",
+                description="""Add the following entries:
+            Lacme/test/Widget;-><init()V
+"""),
+            ab.FileChange(
+                path="bcpf-dir/hiddenapi/"
+                "hiddenapi-max-target-r-low-priority.txt",
+                description="""Add the following entries:
+            Lacme/test/Gadget;->NAME:Ljava/lang/String;
+"""),
+            ab.FileChange(
+                path="frameworks/base/boot/hiddenapi/"
+                "hiddenapi-max-target-o.txt",
+                description="""Remove the following entries:
+            Lacme/test/Class;-><init>()V
+"""),
+            ab.FileChange(
+                path="frameworks/base/boot/hiddenapi/"
+                "hiddenapi-max-target-p.txt",
+                description="""Remove the following entries:
+            Lacme/test/Other;->getThing()Z
+"""),
+            ab.FileChange(
+                path="frameworks/base/boot/hiddenapi/"
+                "hiddenapi-max-target-q.txt",
+                description="""Remove the following entries:
+            Lacme/test/Widget;-><init()V
+"""),
+            ab.FileChange(
+                path="frameworks/base/boot/hiddenapi/"
+                "hiddenapi-max-target-r-loprio.txt",
+                description="""Remove the following entries:
+            Lacme/test/Gadget;->NAME:Ljava/lang/String;
+""")
+        ]
+        result.file_changes.sort()
+        self.assertEqual(
+            expected_file_changes, result.file_changes, msg="file_changes")
+
+    def test_build_flags_fix(self):
+        result = self.do_test_build_flags(fix=True)
+
+        expected_file_changes = [
+            ab.FileChange(
+                path="bcpf-dir/hiddenapi/"
+                "hiddenapi-max-target-o-low-priority.txt",
+                description="Created with 'bcpf' specific entries"),
+            ab.FileChange(
+                path="bcpf-dir/hiddenapi/hiddenapi-max-target-p.txt",
+                description="Added 'bcpf' specific entries"),
+            ab.FileChange(
+                path="bcpf-dir/hiddenapi/hiddenapi-max-target-q.txt",
+                description="Created with 'bcpf' specific entries"),
+            ab.FileChange(
+                path="bcpf-dir/hiddenapi/"
+                "hiddenapi-max-target-r-low-priority.txt",
+                description="Created with 'bcpf' specific entries"),
+            ab.FileChange(
+                path=_MAX_TARGET_O,
+                description="Removed 'bcpf' specific entries"),
+            ab.FileChange(
+                path=_MAX_TARGET_P,
+                description="Removed 'bcpf' specific entries"),
+            ab.FileChange(
+                path=_MAX_TARGET_Q,
+                description="Removed 'bcpf' specific entries"),
+            ab.FileChange(
+                path=_MAX_TARGET_R,
+                description="Removed 'bcpf' specific entries")
+        ]
+
+        result.file_changes.sort()
+        self.assertEqual(
+            expected_file_changes, result.file_changes, msg="file_changes")
+
+        expected_file_contents = {
+            "bcpf-dir/hiddenapi/hiddenapi-max-target-o-low-priority.txt":
+                """
+Lacme/test/Class;-><init>()V
+""",
+            "bcpf-dir/hiddenapi/hiddenapi-max-target-p.txt":
+                """
+Lacme/old/Class;->getWidget()Lacme/test/Widget;
+Lacme/test/Other;->getThing()Z
+""",
+            "bcpf-dir/hiddenapi/hiddenapi-max-target-q.txt":
+                """
+Lacme/test/Widget;-><init()V
+""",
+            "bcpf-dir/hiddenapi/hiddenapi-max-target-r-low-priority.txt":
+                """
+Lacme/test/Gadget;->NAME:Ljava/lang/String;
+""",
+            _MAX_TARGET_O:
+                """
+Lacme/items/Magnet;->size:I
+""",
+            _MAX_TARGET_P:
+                """
+Lacme/items/Rocket;->size:I
+""",
+            _MAX_TARGET_Q:
+                """
+Lacme/items/Rock;->size:I
+""",
+            _MAX_TARGET_R:
+                """
+Lacme/items/Lever;->size:I
+""",
+        }
+        for file_change in result.file_changes:
+            path = file_change.path
+            expected_contents = expected_file_contents[path].lstrip()
+            abs_path = os.path.join(self.test_dir, path)
+            with open(abs_path, "r", encoding="utf8") as tio:
+                contents = tio.read()
+                self.assertEqual(
+                    expected_contents, contents, msg=f"{path} contents")
+
+    def test_compute_hiddenapi_package_properties(self):
+        fs = {
+            "out/soong/.intermediates/bcpf-dir/bcpf/all-flags.csv":
+                """
+La/b/C;->m()V
+La/b/c/D;->m()V
+La/b/c/E;->m()V
+Lb/c/D;->m()V
+Lb/c/E;->m()V
+Lb/c/d/E;->m()V
+""",
+            "out/soong/hiddenapi/hiddenapi-flags.csv":
+                """
+La/b/C;->m()V
+La/b/D;->m()V
+La/b/E;->m()V
+La/b/c/D;->m()V
+La/b/c/E;->m()V
+La/b/c/d/E;->m()V
+Lb/c/D;->m()V
+Lb/c/E;->m()V
+Lb/c/d/E;->m()V
+"""
+        }
+        analyzer = self.create_analyzer_for_test(fs)
+        analyzer.load_all_flags()
+
+        split_packages, single_packages, package_prefixes = \
+            analyzer.compute_hiddenapi_package_properties()
+        self.assertEqual(["a.b"], split_packages)
+        self.assertEqual(["a.b.c"], single_packages)
+        self.assertEqual(["b"], package_prefixes)
+
+
+class TestHiddenApiPropertyChange(unittest.TestCase):
+
+    def setUp(self):
+        # Create a temporary directory
+        self.test_dir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        # Remove the directory after the test
+        shutil.rmtree(self.test_dir)
+
+    def check_change_fix(self, change, bpmodify_output, expected):
+        file = os.path.join(self.test_dir, "Android.bp")
+
+        with open(file, "w", encoding="utf8") as tio:
+            tio.write(bpmodify_output.strip("\n"))
+
+        bpmodify_runner = ab.BpModifyRunner(
+            os.path.join(os.path.dirname(sys.argv[0]), "bpmodify"))
+        change.fix_bp_file(file, "bcpf", bpmodify_runner)
+
+        with open(file, "r", encoding="utf8") as tio:
+            contents = tio.read()
+            self.assertEqual(expected.lstrip("\n"), contents)
+
+    def check_change_snippet(self, change, expected):
+        snippet = change.snippet("        ")
+        self.assertEqual(expected, snippet)
+
+    def test_change_property_with_value_no_comment(self):
+        change = ab.HiddenApiPropertyChange(
+            property_name="split_packages",
+            values=["android.provider"],
+        )
+
+        self.check_change_snippet(
+            change, """
+        split_packages: [
+            "android.provider",
+        ],
+""")
+
+        self.check_change_fix(
+            change, """
+bootclasspath_fragment {
+    name: "bcpf",
+
+    // modified by the Soong or platform compat team.
+    hidden_api: {
+        max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
+        split_packages: [
+            "android.provider",
+        ],
+    },
+}
+""", """
+bootclasspath_fragment {
+    name: "bcpf",
+
+    // modified by the Soong or platform compat team.
+    hidden_api: {
+        max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
+        split_packages: [
+            "android.provider",
+        ],
+    },
+}
+""")
+
+    def test_change_property_with_value_and_comment(self):
+        change = ab.HiddenApiPropertyChange(
+            property_name="split_packages",
+            values=["android.provider"],
+            property_comment=_MULTI_LINE_COMMENT,
+        )
+
+        self.check_change_snippet(
+            change, """
+        // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut arcu
+        // justo, bibendum eu malesuada vel, fringilla in odio. Etiam gravida
+        // ultricies sem tincidunt luctus.
+        split_packages: [
+            "android.provider",
+        ],
+""")
+
+        self.check_change_fix(
+            change, """
+bootclasspath_fragment {
+    name: "bcpf",
+
+    // modified by the Soong or platform compat team.
+    hidden_api: {
+        max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
+        split_packages: [
+            "android.provider",
+        ],
+
+        single_packages: [
+            "android.system",
+        ],
+
+    },
+}
+""", """
+bootclasspath_fragment {
+    name: "bcpf",
+
+    // modified by the Soong or platform compat team.
+    hidden_api: {
+        max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
+
+        // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut arcu
+        // justo, bibendum eu malesuada vel, fringilla in odio. Etiam gravida
+        // ultricies sem tincidunt luctus.
+        split_packages: [
+            "android.provider",
+        ],
+
+        single_packages: [
+            "android.system",
+        ],
+
+    },
+}
+""")
+
+    def test_set_property_with_value_and_comment(self):
+        change = ab.HiddenApiPropertyChange(
+            property_name="split_packages",
+            values=["another.provider", "other.system"],
+            property_comment=_MULTI_LINE_COMMENT,
+            action=ab.PropertyChangeAction.REPLACE,
+        )
+
+        self.check_change_snippet(
+            change, """
+        // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut arcu
+        // justo, bibendum eu malesuada vel, fringilla in odio. Etiam gravida
+        // ultricies sem tincidunt luctus.
+        split_packages: [
+            "another.provider",
+            "other.system",
+        ],
+""")
+
+        self.check_change_fix(
+            change, """
+bootclasspath_fragment {
+    name: "bcpf",
+
+    // modified by the Soong or platform compat team.
+    hidden_api: {
+        max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
+        split_packages: [
+            "another.provider",
+            "other.system",
+        ],
+    },
+}
+""", """
+bootclasspath_fragment {
+    name: "bcpf",
+
+    // modified by the Soong or platform compat team.
+    hidden_api: {
+        max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
+
+        // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut arcu
+        // justo, bibendum eu malesuada vel, fringilla in odio. Etiam gravida
+        // ultricies sem tincidunt luctus.
+        split_packages: [
+            "another.provider",
+            "other.system",
+        ],
+    },
+}
+""")
+
+    def test_set_property_with_no_value_or_comment(self):
+        change = ab.HiddenApiPropertyChange(
+            property_name="split_packages",
+            values=[],
+            action=ab.PropertyChangeAction.REPLACE,
+        )
+
+        self.check_change_snippet(change, """
+        split_packages: [],
+""")
+
+        self.check_change_fix(
+            change, """
+bootclasspath_fragment {
+    name: "bcpf",
+
+    // modified by the Soong or platform compat team.
+    hidden_api: {
+        max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
+        split_packages: [
+            "another.provider",
+            "other.system",
+        ],
+        package_prefixes: ["android.provider"],
+    },
+}
+""", """
+bootclasspath_fragment {
+    name: "bcpf",
+
+    // modified by the Soong or platform compat team.
+    hidden_api: {
+        max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
+        split_packages: [],
+        package_prefixes: ["android.provider"],
+    },
+}
+""")
+
+    def test_set_empty_property_with_no_value_or_comment(self):
+        change = ab.HiddenApiPropertyChange(
+            property_name="split_packages",
+            values=[],
+            action=ab.PropertyChangeAction.REPLACE,
+        )
+
+        self.check_change_snippet(change, """
+        split_packages: [],
+""")
+
+        self.check_change_fix(
+            change, """
+bootclasspath_fragment {
+    name: "bcpf",
+
+    // modified by the Soong or platform compat team.
+    hidden_api: {
+        max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
+        split_packages: [],
+        package_prefixes: ["android.provider"],
+    },
+}
+""", """
+bootclasspath_fragment {
+    name: "bcpf",
+
+    // modified by the Soong or platform compat team.
+    hidden_api: {
+        max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
+        split_packages: [],
+        package_prefixes: ["android.provider"],
+    },
+}
+""")
+
+
+if __name__ == "__main__":
+    unittest.main(verbosity=3)
diff --git a/scripts/microfactory.bash b/scripts/microfactory.bash
index 5e702e0..192b38f 100644
--- a/scripts/microfactory.bash
+++ b/scripts/microfactory.bash
@@ -59,7 +59,7 @@
     BUILDDIR=$(getoutdir) \
       SRCDIR=${TOP} \
       BLUEPRINTDIR=${TOP}/build/blueprint \
-      EXTRA_ARGS="-pkg-path android/soong=${TOP}/build/soong -pkg-path google.golang.org/protobuf=${TOP}/external/golang-protobuf" \
+      EXTRA_ARGS="-pkg-path android/soong=${TOP}/build/soong -pkg-path rbcrun=${TOP}/build/make/tools/rbcrun -pkg-path google.golang.org/protobuf=${TOP}/external/golang-protobuf -pkg-path go.starlark.net=${TOP}/external/starlark-go" \
       build_go $@
 }
 
diff --git a/scripts/rbc-run b/scripts/rbc-run
index b8a6c0c..8d93f0e 100755
--- a/scripts/rbc-run
+++ b/scripts/rbc-run
@@ -6,8 +6,8 @@
 set -eu
 
 declare -r output_root="${OUT_DIR:-out}"
-declare -r runner="${output_root}/soong/rbcrun"
-declare -r converter="${output_root}/soong/mk2rbc"
+declare -r runner="${output_root}/rbcrun"
+declare -r converter="${output_root}/mk2rbc"
 declare -r launcher="${output_root}/rbc/launcher.rbc"
 declare -r makefile_list="${output_root}/.module_paths/configuration.list"
 declare -r makefile="$1"
diff --git a/soong_ui.bash b/soong_ui.bash
index c1c236b..49c4b78 100755
--- a/soong_ui.bash
+++ b/soong_ui.bash
@@ -53,6 +53,8 @@
 source ${TOP}/build/soong/scripts/microfactory.bash
 
 soong_build_go soong_ui android/soong/cmd/soong_ui
+soong_build_go mk2rbc android/soong/mk2rbc/cmd
+soong_build_go rbcrun rbcrun/cmd
 
 cd ${TOP}
 exec "$(getoutdir)/soong_ui" "$@"
diff --git a/tests/lib.sh b/tests/lib.sh
index 55f9ab4..1bb2df9 100644
--- a/tests/lib.sh
+++ b/tests/lib.sh
@@ -83,12 +83,14 @@
 function create_mock_soong {
   copy_directory build/blueprint
   copy_directory build/soong
+  copy_directory build/make/tools/rbcrun
 
   symlink_directory prebuilts/go
   symlink_directory prebuilts/build-tools
   symlink_directory prebuilts/clang/host
   symlink_directory external/go-cmp
   symlink_directory external/golang-protobuf
+  symlink_directory external/starlark-go
 
   touch "$MOCK_TOP/Android.bp"
 }
diff --git a/ui/build/dumpvars.go b/ui/build/dumpvars.go
index 3f10f75..11311f9 100644
--- a/ui/build/dumpvars.go
+++ b/ui/build/dumpvars.go
@@ -262,12 +262,6 @@
 		"BUILD_BROKEN_USES_BUILD_STATIC_LIBRARY",
 	}, exportEnvVars...), BannerVars...)
 
-	// We need Roboleaf converter and runner in the mixed mode
-	runMicrofactory(ctx, config, "mk2rbc", "android/soong/mk2rbc/cmd",
-		map[string]string{"android/soong": "build/soong"})
-	runMicrofactory(ctx, config, "rbcrun", "rbcrun/cmd",
-		map[string]string{"go.starlark.net": "external/starlark-go", "rbcrun": "build/make/tools/rbcrun"})
-
 	makeVars, err := dumpMakeVars(ctx, config, config.Arguments(), allVars, true, "")
 	if err != nil {
 		ctx.Fatalln("Error dumping make vars:", err)