Add apex_set module.

apex_set takes an .apks file that contains a set of prebuilt apexes with
different configurations. It uses extract_apks to select and install the
best matching one for the current target.

Bug: 153456259
Test: apex_test.go
Test: com.android.media.apks
Change-Id: I1da8bbcf1611b7c580a0cb225856cbd7029cc0a7
diff --git a/apex/prebuilt.go b/apex/prebuilt.go
index d089c28..03266c5 100644
--- a/apex/prebuilt.go
+++ b/apex/prebuilt.go
@@ -16,13 +16,29 @@
 
 import (
 	"fmt"
+	"strconv"
 	"strings"
 
 	"android/soong/android"
+	"android/soong/java"
+	"github.com/google/blueprint"
 
 	"github.com/google/blueprint/proptools"
 )
 
+var (
+	extractMatchingApex = pctx.StaticRule(
+		"extractMatchingApex",
+		blueprint.RuleParams{
+			Command: `rm -rf "$out" && ` +
+				`${extract_apks} -o "${out}" -allow-prereleased=${allow-prereleased} ` +
+				`-sdk-version=${sdk-version} -abis=${abis} -screen-densities=all -extract-single ` +
+				`${in}`,
+			CommandDeps: []string{"${extract_apks}"},
+		},
+		"abis", "allow-prereleased", "sdk-version")
+)
+
 type Prebuilt struct {
 	android.ModuleBase
 	prebuilt android.Prebuilt
@@ -208,3 +224,117 @@
 		},
 	}}
 }
+
+type ApexSet struct {
+	android.ModuleBase
+	prebuilt android.Prebuilt
+
+	properties ApexSetProperties
+
+	installDir      android.InstallPath
+	installFilename string
+	outputApex      android.WritablePath
+
+	// list of commands to create symlinks for backward compatibility.
+	// these commands will be attached as LOCAL_POST_INSTALL_CMD
+	compatSymlinks []string
+}
+
+type ApexSetProperties struct {
+	// the .apks file path that contains prebuilt apex files to be extracted.
+	Set *string
+
+	// whether the extracted apex file installable.
+	Installable *bool
+
+	// optional name for the installed apex. If unspecified, name of the
+	// module is used as the file name
+	Filename *string
+
+	// names of modules to be overridden. Listed modules can only be other binaries
+	// (in Make or Soong).
+	// This does not completely prevent installation of the overridden binaries, but if both
+	// binaries would be installed by default (in PRODUCT_PACKAGES) the other binary will be removed
+	// from PRODUCT_PACKAGES.
+	Overrides []string
+
+	// apexes in this set use prerelease SDK version
+	Prerelease *bool
+}
+
+func (a *ApexSet) installable() bool {
+	return a.properties.Installable == nil || proptools.Bool(a.properties.Installable)
+}
+
+func (a *ApexSet) InstallFilename() string {
+	return proptools.StringDefault(a.properties.Filename, a.BaseModuleName()+imageApexSuffix)
+}
+
+func (a *ApexSet) Prebuilt() *android.Prebuilt {
+	return &a.prebuilt
+}
+
+func (a *ApexSet) Name() string {
+	return a.prebuilt.Name(a.ModuleBase.Name())
+}
+
+// prebuilt_apex imports an `.apex` file into the build graph as if it was built with apex.
+func apexSetFactory() android.Module {
+	module := &ApexSet{}
+	module.AddProperties(&module.properties)
+	android.InitSingleSourcePrebuiltModule(module, &module.properties, "Set")
+	android.InitAndroidMultiTargetsArchModule(module, android.DeviceSupported, android.MultilibCommon)
+	return module
+}
+
+func (a *ApexSet) GenerateAndroidBuildActions(ctx android.ModuleContext) {
+	a.installFilename = a.InstallFilename()
+	if !strings.HasSuffix(a.installFilename, imageApexSuffix) {
+		ctx.ModuleErrorf("filename should end in %s for apex_set", imageApexSuffix)
+	}
+
+	apexSet := a.prebuilt.SingleSourcePath(ctx)
+	a.outputApex = android.PathForModuleOut(ctx, a.installFilename)
+	ctx.Build(pctx,
+		android.BuildParams{
+			Rule:        extractMatchingApex,
+			Description: "Extract an apex from an apex set",
+			Inputs:      android.Paths{apexSet},
+			Output:      a.outputApex,
+			Args: map[string]string{
+				"abis":              strings.Join(java.SupportedAbis(ctx), ","),
+				"allow-prereleased": strconv.FormatBool(proptools.Bool(a.properties.Prerelease)),
+				"sdk-version":       ctx.Config().PlatformSdkVersion(),
+			},
+		})
+	a.installDir = android.PathForModuleInstall(ctx, "apex")
+	if a.installable() {
+		ctx.InstallFile(a.installDir, a.installFilename, a.outputApex)
+	}
+
+	// in case that apex_set replaces source apex (using prefer: prop)
+	a.compatSymlinks = makeCompatSymlinks(a.BaseModuleName(), ctx)
+	// or that apex_set overrides other apexes (using overrides: prop)
+	for _, overridden := range a.properties.Overrides {
+		a.compatSymlinks = append(a.compatSymlinks, makeCompatSymlinks(overridden, ctx)...)
+	}
+}
+
+func (a *ApexSet) AndroidMkEntries() []android.AndroidMkEntries {
+	return []android.AndroidMkEntries{android.AndroidMkEntries{
+		Class:      "ETC",
+		OutputFile: android.OptionalPathForPath(a.outputApex),
+		Include:    "$(BUILD_PREBUILT)",
+		ExtraEntries: []android.AndroidMkExtraEntriesFunc{
+			func(entries *android.AndroidMkEntries) {
+				entries.SetString("LOCAL_MODULE_PATH", a.installDir.ToMakePath().String())
+				entries.SetString("LOCAL_MODULE_STEM", a.installFilename)
+				entries.SetBoolIfTrue("LOCAL_UNINSTALLABLE_MODULE", !a.installable())
+				entries.AddStrings("LOCAL_OVERRIDES_MODULES", a.properties.Overrides...)
+				if len(a.compatSymlinks) > 0 {
+					entries.SetString("LOCAL_POST_INSTALL_CMD", strings.Join(a.compatSymlinks, " && "))
+				}
+			},
+		},
+	}}
+}