Support multiple dists per Android.bp module, and dist output selection.

This CL adds "dists" to the base property struct to support multiple
dist file configurations, and generic tag support to dist tagged outputs
of modules.

Fixes: b/152834186
Test: soong tests and `m sdk dist`

Change-Id: I80c86bc9b7b09e671f640a4480c45d438bdd9a2a
Signed-off-by: Jingwen Chen <jingwen@google.com>
diff --git a/android/androidmk.go b/android/androidmk.go
index d579e30..3487b28 100644
--- a/android/androidmk.go
+++ b/android/androidmk.go
@@ -46,7 +46,7 @@
 type AndroidMkData struct {
 	Class           string
 	SubName         string
-	DistFile        OptionalPath
+	DistFiles       TaggedDistFiles
 	OutputFile      OptionalPath
 	Disabled        bool
 	Include         string
@@ -72,7 +72,7 @@
 type AndroidMkEntries struct {
 	Class           string
 	SubName         string
-	DistFile        OptionalPath
+	DistFiles       TaggedDistFiles
 	OutputFile      OptionalPath
 	Disabled        bool
 	Include         string
@@ -137,6 +137,96 @@
 	a.EntryMap[name] = append(a.EntryMap[name], value...)
 }
 
+// Compute the list of Make strings to declare phone goals and dist-for-goals
+// calls from the module's dist and dists properties.
+func (a *AndroidMkEntries) GetDistForGoals(mod blueprint.Module) []string {
+	amod := mod.(Module).base()
+	name := amod.BaseModuleName()
+
+	var ret []string
+
+	availableTaggedDists := TaggedDistFiles{}
+	if a.DistFiles != nil && len(a.DistFiles[""]) > 0 {
+		availableTaggedDists = a.DistFiles
+	} else if a.OutputFile.Valid() {
+		availableTaggedDists = MakeDefaultDistFiles(a.OutputFile.Path())
+	}
+
+	// Iterate over this module's dist structs, merged from the dist and dists properties.
+	for _, dist := range amod.Dists() {
+		// Get the list of goals this dist should be enabled for. e.g. sdk, droidcore
+		goals := strings.Join(dist.Targets, " ")
+
+		// Get the tag representing the output files to be dist'd. e.g. ".jar", ".proguard_map"
+		var tag string
+		if dist.Tag == nil {
+			// If the dist struct does not specify a tag, use the default output files tag.
+			tag = ""
+		} else {
+			tag = *dist.Tag
+		}
+
+		// Get the paths of the output files to be dist'd, represented by the tag.
+		// Can be an empty list.
+		tagPaths := availableTaggedDists[tag]
+		if len(tagPaths) == 0 {
+			// Nothing to dist for this tag, continue to the next dist.
+			continue
+		}
+
+		if len(tagPaths) > 1 && (dist.Dest != nil || dist.Suffix != nil) {
+			errorMessage := "Cannot apply dest/suffix for more than one dist " +
+				"file for %s goals in module %s. The list of dist files, " +
+				"which should have a single element, is:\n%s"
+			panic(fmt.Errorf(errorMessage, goals, name, tagPaths))
+		}
+
+		ret = append(ret, fmt.Sprintf(".PHONY: %s\n", goals))
+
+		// Create dist-for-goals calls for each path in the dist'd files.
+		for _, path := range tagPaths {
+			// It's possible that the Path is nil from errant modules. Be defensive here.
+			if path == nil {
+				tagName := "default" // for error message readability
+				if dist.Tag != nil {
+					tagName = *dist.Tag
+				}
+				panic(fmt.Errorf("Dist file should not be nil for the %s tag in %s", tagName, name))
+			}
+
+			dest := filepath.Base(path.String())
+
+			if dist.Dest != nil {
+				var err error
+				if dest, err = validateSafePath(*dist.Dest); err != nil {
+					// This was checked in ModuleBase.GenerateBuildActions
+					panic(err)
+				}
+			}
+
+			if dist.Suffix != nil {
+				ext := filepath.Ext(dest)
+				suffix := *dist.Suffix
+				dest = strings.TrimSuffix(dest, ext) + suffix + ext
+			}
+
+			if dist.Dir != nil {
+				var err error
+				if dest, err = validateSafePath(*dist.Dir, dest); err != nil {
+					// This was checked in ModuleBase.GenerateBuildActions
+					panic(err)
+				}
+			}
+
+			ret = append(
+				ret,
+				fmt.Sprintf("$(call dist-for-goals,%s,%s:%s)\n", goals, path.String(), dest))
+		}
+	}
+
+	return ret
+}
+
 func (a *AndroidMkEntries) fillInEntries(config Config, bpPath string, mod blueprint.Module) {
 	a.EntryMap = make(map[string][]string)
 	amod := mod.(Module).base()
@@ -149,42 +239,8 @@
 	a.Host_required = append(a.Host_required, amod.commonProperties.Host_required...)
 	a.Target_required = append(a.Target_required, amod.commonProperties.Target_required...)
 
-	// Fill in the header part.
-	if len(amod.commonProperties.Dist.Targets) > 0 {
-		distFile := a.DistFile
-		if !distFile.Valid() {
-			distFile = a.OutputFile
-		}
-		if distFile.Valid() {
-			dest := filepath.Base(distFile.String())
-
-			if amod.commonProperties.Dist.Dest != nil {
-				var err error
-				if dest, err = validateSafePath(*amod.commonProperties.Dist.Dest); err != nil {
-					// This was checked in ModuleBase.GenerateBuildActions
-					panic(err)
-				}
-			}
-
-			if amod.commonProperties.Dist.Suffix != nil {
-				ext := filepath.Ext(dest)
-				suffix := *amod.commonProperties.Dist.Suffix
-				dest = strings.TrimSuffix(dest, ext) + suffix + ext
-			}
-
-			if amod.commonProperties.Dist.Dir != nil {
-				var err error
-				if dest, err = validateSafePath(*amod.commonProperties.Dist.Dir, dest); err != nil {
-					// This was checked in ModuleBase.GenerateBuildActions
-					panic(err)
-				}
-			}
-
-			goals := strings.Join(amod.commonProperties.Dist.Targets, " ")
-			fmt.Fprintln(&a.header, ".PHONY:", goals)
-			fmt.Fprintf(&a.header, "$(call dist-for-goals,%s,%s:%s)\n",
-				goals, distFile.String(), dest)
-		}
+	for _, distString := range a.GetDistForGoals(mod) {
+		fmt.Fprintf(&a.header, distString)
 	}
 
 	fmt.Fprintln(&a.header, "\ninclude $(CLEAR_VARS)")
@@ -430,7 +486,7 @@
 	entries := AndroidMkEntries{
 		Class:           data.Class,
 		SubName:         data.SubName,
-		DistFile:        data.DistFile,
+		DistFiles:       data.DistFiles,
 		OutputFile:      data.OutputFile,
 		Disabled:        data.Disabled,
 		Include:         data.Include,
diff --git a/android/androidmk_test.go b/android/androidmk_test.go
index 71f8020..250f086 100644
--- a/android/androidmk_test.go
+++ b/android/androidmk_test.go
@@ -15,6 +15,7 @@
 package android
 
 import (
+	"fmt"
 	"io"
 	"reflect"
 	"testing"
@@ -22,10 +23,12 @@
 
 type customModule struct {
 	ModuleBase
-	data AndroidMkData
+	data      AndroidMkData
+	distFiles TaggedDistFiles
 }
 
 func (m *customModule) GenerateAndroidBuildActions(ctx ModuleContext) {
+	m.distFiles = m.GenerateTaggedDistFiles(ctx)
 }
 
 func (m *customModule) AndroidMk() AndroidMkData {
@@ -36,6 +39,26 @@
 	}
 }
 
+func (m *customModule) OutputFiles(tag string) (Paths, error) {
+	switch tag {
+	case "":
+		return PathsForTesting("one.out"), nil
+	case ".multiple":
+		return PathsForTesting("two.out", "three/four.out"), nil
+	default:
+		return nil, fmt.Errorf("unsupported module reference tag %q", tag)
+	}
+}
+
+func (m *customModule) AndroidMkEntries() []AndroidMkEntries {
+	return []AndroidMkEntries{
+		{
+			Class:     "CUSTOM_MODULE",
+			DistFiles: m.distFiles,
+		},
+	}
+}
+
 func customModuleFactory() Module {
 	module := &customModule{}
 	InitAndroidModule(module)
@@ -76,3 +99,159 @@
 	assertEqual([]string{"baz"}, m.data.Host_required)
 	assertEqual([]string{"qux"}, m.data.Target_required)
 }
+
+func TestGetDistForGoals(t *testing.T) {
+	testCases := []struct {
+		bp                     string
+		expectedAndroidMkLines []string
+	}{
+		{
+			bp: `
+			custom {
+				name: "foo",
+				dist: {
+					targets: ["my_goal"]
+				}
+			}
+			`,
+			expectedAndroidMkLines: []string{
+				".PHONY: my_goal\n",
+				"$(call dist-for-goals,my_goal,one.out:one.out)\n",
+			},
+		},
+		{
+			bp: `
+			custom {
+				name: "foo",
+				dists: [
+					{
+						targets: ["my_goal"],
+					},
+					{
+						targets: ["my_second_goal", "my_third_goal"],
+					},
+				],
+			}
+			`,
+			expectedAndroidMkLines: []string{
+				".PHONY: my_goal\n",
+				"$(call dist-for-goals,my_goal,one.out:one.out)\n",
+				".PHONY: my_second_goal my_third_goal\n",
+				"$(call dist-for-goals,my_second_goal my_third_goal,one.out:one.out)\n",
+			},
+		},
+		{
+			bp: `
+			custom {
+				name: "foo",
+				dist: {
+					targets: ["my_goal"],
+				},
+				dists: [
+					{
+						targets: ["my_second_goal", "my_third_goal"],
+					},
+				],
+			}
+			`,
+			expectedAndroidMkLines: []string{
+				".PHONY: my_second_goal my_third_goal\n",
+				"$(call dist-for-goals,my_second_goal my_third_goal,one.out:one.out)\n",
+				".PHONY: my_goal\n",
+				"$(call dist-for-goals,my_goal,one.out:one.out)\n",
+			},
+		},
+		{
+			bp: `
+			custom {
+				name: "foo",
+				dist: {
+					targets: ["my_goal", "my_other_goal"],
+					tag: ".multiple",
+				},
+				dists: [
+					{
+						targets: ["my_second_goal"],
+						tag: ".multiple",
+					},
+					{
+						targets: ["my_third_goal"],
+						dir: "test/dir",
+					},
+					{
+						targets: ["my_fourth_goal"],
+						suffix: ".suffix",
+					},
+					{
+						targets: ["my_fifth_goal"],
+						dest: "new-name",
+					},
+					{
+						targets: ["my_sixth_goal"],
+						dest: "new-name",
+						dir: "some/dir",
+						suffix: ".suffix",
+					},
+				],
+			}
+			`,
+			expectedAndroidMkLines: []string{
+				".PHONY: my_second_goal\n",
+				"$(call dist-for-goals,my_second_goal,two.out:two.out)\n",
+				"$(call dist-for-goals,my_second_goal,three/four.out:four.out)\n",
+				".PHONY: my_third_goal\n",
+				"$(call dist-for-goals,my_third_goal,one.out:test/dir/one.out)\n",
+				".PHONY: my_fourth_goal\n",
+				"$(call dist-for-goals,my_fourth_goal,one.out:one.suffix.out)\n",
+				".PHONY: my_fifth_goal\n",
+				"$(call dist-for-goals,my_fifth_goal,one.out:new-name)\n",
+				".PHONY: my_sixth_goal\n",
+				"$(call dist-for-goals,my_sixth_goal,one.out:some/dir/new-name.suffix)\n",
+				".PHONY: my_goal my_other_goal\n",
+				"$(call dist-for-goals,my_goal my_other_goal,two.out:two.out)\n",
+				"$(call dist-for-goals,my_goal my_other_goal,three/four.out:four.out)\n",
+			},
+		},
+	}
+
+	for _, testCase := range testCases {
+		config := TestConfig(buildDir, nil, testCase.bp, nil)
+		config.inMake = true // Enable androidmk Singleton
+
+		ctx := NewTestContext()
+		ctx.RegisterSingletonType("androidmk", AndroidMkSingleton)
+		ctx.RegisterModuleType("custom", customModuleFactory)
+		ctx.Register(config)
+
+		_, errs := ctx.ParseFileList(".", []string{"Android.bp"})
+		FailIfErrored(t, errs)
+		_, errs = ctx.PrepareBuildActions(config)
+		FailIfErrored(t, errs)
+
+		module := ctx.ModuleForTests("foo", "").Module().(*customModule)
+		entries := AndroidMkEntriesForTest(t, config, "", module)
+		if len(entries) != 1 {
+			t.Errorf("Expected a single AndroidMk entry, got %d", len(entries))
+		}
+		androidMkLines := entries[0].GetDistForGoals(module)
+
+		if len(androidMkLines) != len(testCase.expectedAndroidMkLines) {
+			t.Errorf(
+				"Expected %d AndroidMk lines, got %d:\n%v",
+				len(testCase.expectedAndroidMkLines),
+				len(androidMkLines),
+				androidMkLines,
+			)
+		}
+		for idx, line := range androidMkLines {
+			expectedLine := testCase.expectedAndroidMkLines[idx]
+			if line != expectedLine {
+				t.Errorf(
+					"Expected AndroidMk line to be '%s', got '%s'",
+					line,
+					expectedLine,
+				)
+			}
+		}
+	}
+}
diff --git a/android/module.go b/android/module.go
index ac3394d..06079ca 100644
--- a/android/module.go
+++ b/android/module.go
@@ -315,6 +315,28 @@
 	return qualifiedModuleName{pkg: pkg, name: ""}
 }
 
+type Dist struct {
+	// Copy the output of this module to the $DIST_DIR when `dist` is specified on the
+	// command line and any of these targets are also on the command line, or otherwise
+	// built
+	Targets []string `android:"arch_variant"`
+
+	// The name of the output artifact. This defaults to the basename of the output of
+	// the module.
+	Dest *string `android:"arch_variant"`
+
+	// The directory within the dist directory to store the artifact. Defaults to the
+	// top level directory ("").
+	Dir *string `android:"arch_variant"`
+
+	// A suffix to add to the artifact file name (before any extension).
+	Suffix *string `android:"arch_variant"`
+
+	// A string tag to select the OutputFiles associated with the tag. Defaults to the
+	// the empty "" string.
+	Tag *string `android:"arch_variant"`
+}
+
 type nameProperties struct {
 	// The name of the module.  Must be unique across all modules.
 	Name *string
@@ -454,23 +476,13 @@
 	// relative path to a file to include in the list of notices for the device
 	Notice *string `android:"path"`
 
-	Dist struct {
-		// copy the output of this module to the $DIST_DIR when `dist` is specified on the
-		// command line and  any of these targets are also on the command line, or otherwise
-		// built
-		Targets []string `android:"arch_variant"`
+	// configuration to distribute output files from this module to the distribution
+	// directory (default: $OUT/dist, configurable with $DIST_DIR)
+	Dist Dist `android:"arch_variant"`
 
-		// The name of the output artifact. This defaults to the basename of the output of
-		// the module.
-		Dest *string `android:"arch_variant"`
-
-		// The directory within the dist directory to store the artifact. Defaults to the
-		// top level directory ("").
-		Dir *string `android:"arch_variant"`
-
-		// A suffix to add to the artifact file name (before any extension).
-		Suffix *string `android:"arch_variant"`
-	} `android:"arch_variant"`
+	// a list of configurations to distribute output files from this module to the
+	// distribution directory (default: $OUT/dist, configurable with $DIST_DIR)
+	Dists []Dist `android:"arch_variant"`
 
 	// The OsType of artifacts that this module variant is responsible for creating.
 	//
@@ -537,6 +549,14 @@
 	ImageVariation string `blueprint:"mutated"`
 }
 
+// A map of OutputFile tag keys to Paths, for disting purposes.
+type TaggedDistFiles map[string]Paths
+
+func MakeDefaultDistFiles(paths ...Path) TaggedDistFiles {
+	// The default OutputFile tag is the empty "" string.
+	return TaggedDistFiles{"": paths}
+}
+
 type hostAndDeviceProperties struct {
 	// If set to true, build a variant of the module for the host.  Defaults to false.
 	Host_supported *bool
@@ -815,6 +835,41 @@
 	return m.visibilityPropertyInfo
 }
 
+func (m *ModuleBase) Dists() []Dist {
+	if len(m.commonProperties.Dist.Targets) > 0 {
+		// Make a copy of the underlying Dists slice to protect against
+		// backing array modifications with repeated calls to this method.
+		distsCopy := append([]Dist(nil), m.commonProperties.Dists...)
+		return append(distsCopy, m.commonProperties.Dist)
+	} else {
+		return m.commonProperties.Dists
+	}
+}
+
+func (m *ModuleBase) GenerateTaggedDistFiles(ctx BaseModuleContext) TaggedDistFiles {
+	distFiles := make(TaggedDistFiles)
+	for _, dist := range m.Dists() {
+		var tag string
+		var distFilesForTag Paths
+		if dist.Tag == nil {
+			tag = ""
+		} else {
+			tag = *dist.Tag
+		}
+		distFilesForTag, err := m.base().module.(OutputFileProducer).OutputFiles(tag)
+		if err != nil {
+			ctx.PropertyErrorf("dist.tag", "%s", err.Error())
+		}
+		for _, distFile := range distFilesForTag {
+			if distFile != nil && !distFiles[tag].containsPath(distFile) {
+				distFiles[tag] = append(distFiles[tag], distFile)
+			}
+		}
+	}
+
+	return distFiles
+}
+
 func (m *ModuleBase) Target() Target {
 	return m.commonProperties.CompileTarget
 }
diff --git a/android/paths.go b/android/paths.go
index bed6f3f..d8d51a7 100644
--- a/android/paths.go
+++ b/android/paths.go
@@ -220,6 +220,15 @@
 // Paths is a slice of Path objects, with helpers to operate on the collection.
 type Paths []Path
 
+func (paths Paths) containsPath(path Path) bool {
+	for _, p := range paths {
+		if p == path {
+			return true
+		}
+	}
+	return false
+}
+
 // PathsForSource returns Paths rooted from SrcDir
 func PathsForSource(ctx PathContext, paths []string) Paths {
 	ret := make(Paths, len(paths))
diff --git a/cc/androidmk.go b/cc/androidmk.go
index b3ad610..3f812c2 100644
--- a/cc/androidmk.go
+++ b/cc/androidmk.go
@@ -249,7 +249,10 @@
 		entries.Class = "HEADER_LIBRARIES"
 	}
 
-	entries.DistFile = library.distFile
+	if library.distFile != nil {
+		entries.DistFiles = android.MakeDefaultDistFiles(library.distFile)
+	}
+
 	entries.ExtraEntries = append(entries.ExtraEntries, func(entries *android.AndroidMkEntries) {
 		library.androidMkWriteExportedFlags(entries)
 		library.androidMkEntriesWriteAdditionalDependenciesForSourceAbiDiff(entries)
@@ -318,7 +321,7 @@
 	ctx.subAndroidMk(entries, binary.baseInstaller)
 
 	entries.Class = "EXECUTABLES"
-	entries.DistFile = binary.distFile
+	entries.DistFiles = binary.distFiles
 	entries.ExtraEntries = append(entries.ExtraEntries, func(entries *android.AndroidMkEntries) {
 		entries.SetString("LOCAL_SOONG_UNSTRIPPED_BINARY", binary.unstrippedOutputFile.String())
 		if len(binary.symlinks) > 0 {
diff --git a/cc/binary.go b/cc/binary.go
index 251b7f0..565cb8a 100644
--- a/cc/binary.go
+++ b/cc/binary.go
@@ -98,8 +98,8 @@
 	// Output archive of gcno coverage information
 	coverageOutputFile android.OptionalPath
 
-	// Location of the file that should be copied to dist dir when requested
-	distFile android.OptionalPath
+	// Location of the files that should be copied to dist dir when requested
+	distFiles android.TaggedDistFiles
 
 	post_install_cmds []string
 }
@@ -367,11 +367,11 @@
 			binary.injectVersionSymbol(ctx, outputFile, versionedOutputFile)
 		} else {
 			versionedOutputFile := android.PathForModuleOut(ctx, "versioned", fileName)
-			binary.distFile = android.OptionalPathForPath(versionedOutputFile)
+			binary.distFiles = android.MakeDefaultDistFiles(versionedOutputFile)
 
 			if binary.stripper.needsStrip(ctx) {
 				out := android.PathForModuleOut(ctx, "versioned-stripped", fileName)
-				binary.distFile = android.OptionalPathForPath(out)
+				binary.distFiles = android.MakeDefaultDistFiles(out)
 				binary.stripper.stripExecutableOrSharedLib(ctx, versionedOutputFile, out, builderFlags)
 			}
 
diff --git a/cc/library.go b/cc/library.go
index 968702e..98f4d48 100644
--- a/cc/library.go
+++ b/cc/library.go
@@ -369,7 +369,7 @@
 	unstrippedOutputFile android.Path
 
 	// Location of the file that should be copied to dist dir when requested
-	distFile android.OptionalPath
+	distFile android.Path
 
 	versionScriptPath android.ModuleGenPath
 
@@ -894,7 +894,7 @@
 			library.injectVersionSymbol(ctx, outputFile, versionedOutputFile)
 		} else {
 			versionedOutputFile := android.PathForModuleOut(ctx, "versioned", fileName)
-			library.distFile = android.OptionalPathForPath(versionedOutputFile)
+			library.distFile = versionedOutputFile
 			library.injectVersionSymbol(ctx, outputFile, versionedOutputFile)
 		}
 	}
@@ -988,11 +988,11 @@
 			library.injectVersionSymbol(ctx, outputFile, versionedOutputFile)
 		} else {
 			versionedOutputFile := android.PathForModuleOut(ctx, "versioned", fileName)
-			library.distFile = android.OptionalPathForPath(versionedOutputFile)
+			library.distFile = versionedOutputFile
 
 			if library.stripper.needsStrip(ctx) {
 				out := android.PathForModuleOut(ctx, "versioned-stripped", fileName)
-				library.distFile = android.OptionalPathForPath(out)
+				library.distFile = out
 				library.stripper.stripExecutableOrSharedLib(ctx, versionedOutputFile, out, builderFlags)
 			}
 
diff --git a/java/androidmk.go b/java/androidmk.go
index 62cf169..e73b030 100644
--- a/java/androidmk.go
+++ b/java/androidmk.go
@@ -91,7 +91,7 @@
 	} else {
 		mainEntries = android.AndroidMkEntries{
 			Class:      "JAVA_LIBRARIES",
-			DistFile:   android.OptionalPathForPath(library.distFile),
+			DistFiles:  library.distFiles,
 			OutputFile: android.OptionalPathForPath(library.outputFile),
 			Include:    "$(BUILD_SYSTEM)/soong_java_prebuilt.mk",
 			ExtraEntries: []android.AndroidMkExtraEntriesFunc{
@@ -550,14 +550,14 @@
 	// needed because an invalid output file would prevent the make entries from
 	// being written.
 	// TODO(b/146727827): Revert when we do not need to generate stubs and API separately.
-	distFile := android.OptionalPathForPath(dstubs.apiFile)
+	distFile := dstubs.apiFile
 	outputFile := android.OptionalPathForPath(dstubs.stubsSrcJar)
 	if !outputFile.Valid() {
-		outputFile = distFile
+		outputFile = android.OptionalPathForPath(distFile)
 	}
 	return []android.AndroidMkEntries{android.AndroidMkEntries{
 		Class:      "JAVA_LIBRARIES",
-		DistFile:   distFile,
+		DistFiles:  android.MakeDefaultDistFiles(distFile),
 		OutputFile: outputFile,
 		Include:    "$(BUILD_SYSTEM)/soong_droiddoc_prebuilt.mk",
 		ExtraEntries: []android.AndroidMkExtraEntriesFunc{
diff --git a/java/androidmk_test.go b/java/androidmk_test.go
index d471fb7..075b7aa 100644
--- a/java/androidmk_test.go
+++ b/java/androidmk_test.go
@@ -156,17 +156,158 @@
 		}
 	`)
 
-	without_tag_entries := android.AndroidMkEntriesForTest(t, config, "", ctx.ModuleForTests("foo_without_tag", "android_common").Module())
-	with_tag_entries := android.AndroidMkEntriesForTest(t, config, "", ctx.ModuleForTests("foo_with_tag", "android_common").Module())
+	withoutTagEntries := android.AndroidMkEntriesForTest(t, config, "", ctx.ModuleForTests("foo_without_tag", "android_common").Module())
+	withTagEntries := android.AndroidMkEntriesForTest(t, config, "", ctx.ModuleForTests("foo_with_tag", "android_common").Module())
 
-	if len(without_tag_entries) != 2 || len(with_tag_entries) != 2 {
-		t.Errorf("two mk entries per module expected, got %d and %d", len(without_tag_entries), len(with_tag_entries))
+	if len(withoutTagEntries) != 2 || len(withTagEntries) != 2 {
+		t.Errorf("two mk entries per module expected, got %d and %d", len(withoutTagEntries), len(withTagEntries))
 	}
-	if !with_tag_entries[0].DistFile.Valid() || !strings.Contains(with_tag_entries[0].DistFile.String(), "/javac/foo_with_tag.jar") {
-		t.Errorf("expected classes.jar DistFile, got %v", with_tag_entries[0].DistFile)
+	if len(withTagEntries[0].DistFiles[".jar"]) != 1 ||
+		!strings.Contains(withTagEntries[0].DistFiles[".jar"][0].String(), "/javac/foo_with_tag.jar") {
+		t.Errorf("expected DistFiles to contain classes.jar, got %v", withTagEntries[0].DistFiles)
 	}
-	if without_tag_entries[0].DistFile.Valid() {
-		t.Errorf("did not expect explicit DistFile, got %v", without_tag_entries[0].DistFile)
+	if len(withoutTagEntries[0].DistFiles[".jar"]) > 0 {
+		t.Errorf("did not expect explicit DistFile for .jar tag, got %v", withoutTagEntries[0].DistFiles[".jar"])
+	}
+}
+
+func TestDistWithDest(t *testing.T) {
+	ctx, config := testJava(t, `
+		java_library {
+			name: "foo",
+			srcs: ["a.java"],
+			compile_dex: true,
+			dist: {
+				targets: ["my_goal"],
+				dest: "my/custom/dest/dir",
+			},
+		}
+	`)
+
+	module := ctx.ModuleForTests("foo", "android_common").Module()
+	entries := android.AndroidMkEntriesForTest(t, config, "", module)
+	if len(entries) != 2 {
+		t.Errorf("Expected 2 AndroidMk entries, got %d", len(entries))
+	}
+
+	distStrings := entries[0].GetDistForGoals(module)
+
+	if len(distStrings) != 2 {
+		t.Errorf("Expected 2 entries for dist: PHONY and dist-for-goals, but got %q", distStrings)
+	}
+
+	if distStrings[0] != ".PHONY: my_goal\n" {
+		t.Errorf("Expected .PHONY entry to declare my_goal, but got: %s", distStrings[0])
+	}
+
+	if !strings.Contains(distStrings[1], "$(call dist-for-goals,my_goal") ||
+		!strings.Contains(distStrings[1], ".intermediates/foo/android_common/dex/foo.jar:my/custom/dest/dir") {
+		t.Errorf(
+			"Expected dist-for-goals entry to contain my_goal and new dest dir, but got: %s", distStrings[1])
+	}
+}
+
+func TestDistsWithAllProperties(t *testing.T) {
+	ctx, config := testJava(t, `
+		java_library {
+			name: "foo",
+			srcs: ["a.java"],
+			compile_dex: true,
+			dist: {
+				targets: ["baz"],
+			},
+			dists: [
+				{
+					targets: ["bar"],
+					tag: ".jar",
+					dest: "bar.jar",
+					dir: "bar/dir",
+					suffix: ".qux",
+				},
+			]
+		}
+	`)
+
+	module := ctx.ModuleForTests("foo", "android_common").Module()
+	entries := android.AndroidMkEntriesForTest(t, config, "", module)
+	if len(entries) != 2 {
+		t.Errorf("Expected 2 AndroidMk entries, got %d", len(entries))
+	}
+
+	distStrings := entries[0].GetDistForGoals(module)
+
+	if len(distStrings) != 4 {
+		t.Errorf("Expected 4 entries for dist: PHONY and dist-for-goals, but got %d", len(distStrings))
+	}
+
+	if distStrings[0] != ".PHONY: bar\n" {
+		t.Errorf("Expected .PHONY entry to declare bar, but got: %s", distStrings[0])
+	}
+
+	if !strings.Contains(distStrings[1], "$(call dist-for-goals,bar") ||
+		!strings.Contains(
+			distStrings[1],
+			".intermediates/foo/android_common/javac/foo.jar:bar/dir/bar.qux.jar") {
+		t.Errorf(
+			"Expected dist-for-goals entry to contain bar and new dest dir, but got: %s", distStrings[1])
+	}
+
+	if distStrings[2] != ".PHONY: baz\n" {
+		t.Errorf("Expected .PHONY entry to declare baz, but got: %s", distStrings[2])
+	}
+
+	if !strings.Contains(distStrings[3], "$(call dist-for-goals,baz") ||
+		!strings.Contains(distStrings[3], ".intermediates/foo/android_common/dex/foo.jar:foo.jar") {
+		t.Errorf(
+			"Expected dist-for-goals entry to contain my_other_goal and new dest dir, but got: %s",
+			distStrings[3])
+	}
+}
+
+func TestDistsWithTag(t *testing.T) {
+	ctx, config := testJava(t, `
+		java_library {
+			name: "foo_without_tag",
+			srcs: ["a.java"],
+			compile_dex: true,
+			dists: [
+				{
+					targets: ["hi"],
+				},
+			],
+		}
+		java_library {
+			name: "foo_with_tag",
+			srcs: ["a.java"],
+			compile_dex: true,
+			dists: [
+				{
+					targets: ["hi"],
+					tag: ".jar",
+				},
+			],
+		}
+	`)
+
+	moduleWithoutTag := ctx.ModuleForTests("foo_without_tag", "android_common").Module()
+	moduleWithTag := ctx.ModuleForTests("foo_with_tag", "android_common").Module()
+
+	withoutTagEntries := android.AndroidMkEntriesForTest(t, config, "", moduleWithoutTag)
+	withTagEntries := android.AndroidMkEntriesForTest(t, config, "", moduleWithTag)
+
+	if len(withoutTagEntries) != 2 || len(withTagEntries) != 2 {
+		t.Errorf("two mk entries per module expected, got %d and %d", len(withoutTagEntries), len(withTagEntries))
+	}
+
+	distFilesWithoutTag := withoutTagEntries[0].DistFiles
+	distFilesWithTag := withTagEntries[0].DistFiles
+
+	if len(distFilesWithTag[".jar"]) != 1 ||
+		!strings.Contains(distFilesWithTag[".jar"][0].String(), "/javac/foo_with_tag.jar") {
+		t.Errorf("expected foo_with_tag's .jar-tagged DistFiles to contain classes.jar, got %v", distFilesWithTag[".jar"])
+	}
+	if len(distFilesWithoutTag[".jar"]) > 0 {
+		t.Errorf("did not expect foo_without_tag's .jar-tagged DistFiles to contain files, but got %v", distFilesWithoutTag[".jar"])
 	}
 }
 
diff --git a/java/java.go b/java/java.go
index 5b31630..0a8eb99 100644
--- a/java/java.go
+++ b/java/java.go
@@ -479,7 +479,7 @@
 	// list of the xref extraction files
 	kytheFiles android.Paths
 
-	distFile android.Path
+	distFiles android.TaggedDistFiles
 
 	// Collect the module directory for IDE info in java/jdeps.go.
 	modulePaths []string
@@ -1921,18 +1921,9 @@
 // Java libraries (.jar file)
 //
 
-type LibraryProperties struct {
-	Dist struct {
-		// The tag of the output of this module that should be output.
-		Tag *string `android:"arch_variant"`
-	} `android:"arch_variant"`
-}
-
 type Library struct {
 	Module
 
-	libraryProperties LibraryProperties
-
 	InstallMixin func(ctx android.ModuleContext, installPath android.Path) (extraInstallDeps android.Paths)
 }
 
@@ -1994,14 +1985,7 @@
 			j.Stem()+".jar", j.outputFile, extraInstallDeps...)
 	}
 
-	// Verify Dist.Tag is set to a supported output
-	if j.libraryProperties.Dist.Tag != nil {
-		distFiles, err := j.OutputFiles(*j.libraryProperties.Dist.Tag)
-		if err != nil {
-			ctx.PropertyErrorf("dist.tag", "%s", err.Error())
-		}
-		j.distFile = distFiles[0]
-	}
+	j.distFiles = j.GenerateTaggedDistFiles(ctx)
 }
 
 func (j *Library) DepsMutator(ctx android.BottomUpMutatorContext) {
@@ -2119,7 +2103,6 @@
 	module := &Library{}
 
 	module.addHostAndDeviceProperties()
-	module.AddProperties(&module.libraryProperties)
 
 	module.initModuleAndImport(&module.ModuleBase)
 
diff --git a/rust/androidmk.go b/rust/androidmk.go
index 69d0df5..aea899b 100644
--- a/rust/androidmk.go
+++ b/rust/androidmk.go
@@ -86,8 +86,11 @@
 func (binary *binaryDecorator) AndroidMk(ctx AndroidMkContext, ret *android.AndroidMkData) {
 	ctx.subAndroidMk(ret, binary.baseCompiler)
 
+	if binary.distFile.Valid() {
+		ret.DistFiles = android.MakeDefaultDistFiles(binary.distFile.Path())
+	}
+
 	ret.Class = "EXECUTABLES"
-	ret.DistFile = binary.distFile
 	ret.Extra = append(ret.Extra, func(w io.Writer, outputFile android.Path) {
 		fmt.Fprintln(w, "LOCAL_SOONG_UNSTRIPPED_BINARY :=", binary.unstrippedOutputFile.String())
 		if binary.coverageOutputZipFile.Valid() {
@@ -127,7 +130,10 @@
 		ret.Class = "SHARED_LIBRARIES"
 	}
 
-	ret.DistFile = library.distFile
+	if library.distFile.Valid() {
+		ret.DistFiles = android.MakeDefaultDistFiles(library.distFile.Path())
+	}
+
 	ret.Extra = append(ret.Extra, func(w io.Writer, outputFile android.Path) {
 		if !library.rlib() {
 			fmt.Fprintln(w, "LOCAL_SOONG_UNSTRIPPED_BINARY :=", library.unstrippedOutputFile.String())
@@ -143,7 +149,9 @@
 	ctx.subAndroidMk(ret, procMacro.baseCompiler)
 
 	ret.Class = "PROC_MACRO_LIBRARIES"
-	ret.DistFile = procMacro.distFile
+	if procMacro.distFile.Valid() {
+		ret.DistFiles = android.MakeDefaultDistFiles(procMacro.distFile.Path())
+	}
 
 }
 
diff --git a/sdk/sdk.go b/sdk/sdk.go
index cb5a605..b9b8199 100644
--- a/sdk/sdk.go
+++ b/sdk/sdk.go
@@ -291,7 +291,7 @@
 	return []android.AndroidMkEntries{android.AndroidMkEntries{
 		Class:      "FAKE",
 		OutputFile: s.snapshotFile,
-		DistFile:   s.snapshotFile,
+		DistFiles:  android.MakeDefaultDistFiles(s.snapshotFile.Path()),
 		Include:    "$(BUILD_PHONY_PACKAGE)",
 		ExtraFooters: []android.AndroidMkExtraFootersFunc{
 			func(w io.Writer, name, prefix, moduleDir string, entries *android.AndroidMkEntries) {