Add multitree apex metadata

Provide a list of the installable apexes in the tree, so that the
multi-tree orchestrator can generate rules for packaging.

Bug: b/266729952
Test: unit tests pass
Change-Id: If2b2585a7d14aed0618ddbacd116bc5728109d87
diff --git a/apex/Android.bp b/apex/Android.bp
index 018d030..7ffca0e 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -29,6 +29,7 @@
         "bp2build.go",
         "deapexer.go",
         "key.go",
+        "metadata.go",
         "prebuilt.go",
         "testing.go",
         "vndk.go",
@@ -37,6 +38,7 @@
         "apex_test.go",
         "bootclasspath_fragment_test.go",
         "classpath_element_test.go",
+        "metadata_test.go",
         "platform_bootclasspath_test.go",
         "systemserver_classpath_fragment_test.go",
         "vndk_test.go",
diff --git a/apex/metadata.go b/apex/metadata.go
new file mode 100644
index 0000000..b1dff3e
--- /dev/null
+++ b/apex/metadata.go
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package apex
+
+import (
+	"encoding/json"
+
+	"github.com/google/blueprint"
+
+	"android/soong/android"
+)
+
+var (
+	mtctx = android.NewPackageContext("android/soong/multitree_apex")
+)
+
+func init() {
+	RegisterModulesSingleton(android.InitRegistrationContext)
+}
+
+func RegisterModulesSingleton(ctx android.RegistrationContext) {
+	ctx.RegisterSingletonType("apex_multitree_singleton", multitreeAnalysisSingletonFactory)
+}
+
+var PrepareForTestWithApexMultitreeSingleton = android.FixtureRegisterWithContext(RegisterModulesSingleton)
+
+func multitreeAnalysisSingletonFactory() android.Singleton {
+	return &multitreeAnalysisSingleton{}
+}
+
+type multitreeAnalysisSingleton struct {
+	multitreeApexMetadataPath android.OutputPath
+}
+
+type ApexMultitreeMetadataEntry struct {
+	// The name of the apex.
+	Name string
+
+	// TODO: Add other properties as needed.
+}
+
+type ApexMultitreeMetadata struct {
+	// Information about the installable apexes.
+	Apexes map[string]ApexMultitreeMetadataEntry
+}
+
+func (p *multitreeAnalysisSingleton) GenerateBuildActions(context android.SingletonContext) {
+	data := ApexMultitreeMetadata{
+		Apexes: make(map[string]ApexMultitreeMetadataEntry, 0),
+	}
+	context.VisitAllModules(func(module android.Module) {
+		// If this module is not being installed, ignore it.
+		if !module.Enabled() || module.IsSkipInstall() {
+			return
+		}
+		// Actual apexes provide ApexBundleInfoProvider.
+		if _, ok := context.ModuleProvider(module, ApexBundleInfoProvider).(ApexBundleInfo); !ok {
+			return
+		}
+		bundle, ok := module.(*apexBundle)
+		if ok && !bundle.testApex && !bundle.vndkApex && bundle.primaryApexType {
+			name := module.Name()
+			entry := ApexMultitreeMetadataEntry{
+				Name: name,
+			}
+			data.Apexes[name] = entry
+		}
+	})
+	p.multitreeApexMetadataPath = android.PathForOutput(context, "multitree_apex_metadata.json")
+
+	jsonStr, err := json.Marshal(data)
+	if err != nil {
+		context.Errorf(err.Error())
+	}
+	android.WriteFileRule(context, p.multitreeApexMetadataPath, string(jsonStr))
+	// This seems cleaner, but doesn't emit the phony rule in testing.
+	// context.Phony("multitree_apex_metadata", p.multitreeApexMetadataPath)
+
+	context.Build(mtctx, android.BuildParams{
+		Rule:        blueprint.Phony,
+		Description: "phony rule for multitree_apex_metadata",
+		Inputs:      []android.Path{p.multitreeApexMetadataPath},
+		Output:      android.PathForPhony(context, "multitree_apex_metadata"),
+	})
+}
diff --git a/apex/metadata_test.go b/apex/metadata_test.go
new file mode 100644
index 0000000..f6ead42
--- /dev/null
+++ b/apex/metadata_test.go
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package apex
+
+import (
+	"strings"
+	"testing"
+
+	"android/soong/android"
+	"android/soong/java"
+)
+
+func TestModulesSingleton(t *testing.T) {
+	result := android.GroupFixturePreparers(
+		PrepareForTestWithApexMultitreeSingleton,
+		java.PrepareForTestWithDexpreopt,
+		PrepareForTestWithApexBuildComponents,
+		java.FixtureConfigureApexBootJars("myapex:foo"),
+		java.PrepareForTestWithJavaSdkLibraryFiles,
+	).RunTestWithBp(t, `
+		prebuilt_apex {
+			name: "myapex",
+			src: "myapex.apex",
+			exported_bootclasspath_fragments: ["mybootclasspath-fragment"],
+		}
+
+		// A prebuilt java_sdk_library_import that is not preferred by default but will be preferred
+		// because AlwaysUsePrebuiltSdks() is true.
+		java_sdk_library_import {
+			name: "foo",
+			prefer: false,
+			shared_library: false,
+			permitted_packages: ["foo"],
+			public: {
+				jars: ["sdk_library/public/foo-stubs.jar"],
+				stub_srcs: ["sdk_library/public/foo_stub_sources"],
+				current_api: "sdk_library/public/foo.txt",
+				removed_api: "sdk_library/public/foo-removed.txt",
+				sdk_version: "current",
+			},
+			apex_available: ["myapex"],
+		}
+
+		prebuilt_bootclasspath_fragment {
+			name: "mybootclasspath-fragment",
+			apex_available: [
+				"myapex",
+			],
+			contents: [
+				"foo",
+			],
+			hidden_api: {
+				stub_flags: "prebuilt-stub-flags.csv",
+				annotation_flags: "prebuilt-annotation-flags.csv",
+				metadata: "prebuilt-metadata.csv",
+				index: "prebuilt-index.csv",
+				all_flags: "prebuilt-all-flags.csv",
+			},
+		}
+
+		platform_bootclasspath {
+			name: "myplatform-bootclasspath",
+			fragments: [
+				{
+					apex: "myapex",
+					module:"mybootclasspath-fragment",
+				},
+			],
+		}
+`,
+	)
+
+	outputs := result.SingletonForTests("apex_multitree_singleton").AllOutputs()
+	for _, output := range outputs {
+		testingBuildParam := result.SingletonForTests("apex_multitree_singleton").Output(output)
+		switch {
+		case strings.Contains(output, "soong/multitree_apex_metadata.json"):
+			android.AssertStringEquals(t, "Invalid build rule", "android/soong/android.writeFile", testingBuildParam.Rule.String())
+			android.AssertIntEquals(t, "Invalid input", len(testingBuildParam.Inputs), 0)
+			android.AssertStringDoesContain(t, "Invalid output path", output, "soong/multitree_apex_metadata.json")
+
+		case strings.HasSuffix(output, "multitree_apex_metadata"):
+			android.AssertStringEquals(t, "Invalid build rule", "<builtin>:phony", testingBuildParam.Rule.String())
+			android.AssertStringEquals(t, "Invalid input", testingBuildParam.Inputs[0].String(), "out/soong/multitree_apex_metadata.json")
+			android.AssertStringEquals(t, "Invalid output path", output, "multitree_apex_metadata")
+			android.AssertIntEquals(t, "Invalid args", len(testingBuildParam.Args), 0)
+		}
+	}
+}