Support disting in soong-only builds

Disting was normally handled by the packaging kati step, which was
not run in soong-only builds. Disting needs to be done in a separate
ninja file, so that if you toggle dist on/off you only need to
regenerate a small ninja file instead of the main one.

This change makes it so that the kati packaging step is also run in
soong-only builds. (which makes them not technically soong-only, but
the packaging step is very fast so it's fine)

Phonies in soong were normally exported to the main make invocation
and then make would emit the ninja rules for them. In soong-only builds
soong would emit the ninja phone rules directly. This is problematic
when supporting disting in soong, because some of the phony rules have
the same name as dist targets, causing ninja to complain about duplicate
rules. To resolve this, make soong export its phonies to the packaging
step, which will dedup them with the dist phonies.

Bug: 388312357
Test: m --soong-only dist
Change-Id: If6de8c28274fe96dac674f449b9bc0b488540655
diff --git a/android/androidmk.go b/android/androidmk.go
index 590cce3..9e721e1 100644
--- a/android/androidmk.go
+++ b/android/androidmk.go
@@ -701,11 +701,6 @@
 type androidMkSingleton struct{}
 
 func (c *androidMkSingleton) GenerateBuildActions(ctx SingletonContext) {
-	// Skip if Soong wasn't invoked from Make.
-	if !ctx.Config().KatiEnabled() {
-		return
-	}
-
 	var androidMkModulesList []blueprint.Module
 
 	ctx.VisitAllModulesBlueprint(func(module blueprint.Module) {
@@ -718,6 +713,12 @@
 		return ctx.ModuleName(androidMkModulesList[i]) < ctx.ModuleName(androidMkModulesList[j])
 	})
 
+	// If running in soong-only mode, do a different, more limited version of this singleton
+	if !ctx.Config().KatiEnabled() {
+		c.soongOnlyBuildActions(ctx, androidMkModulesList)
+		return
+	}
+
 	transMk := PathForOutput(ctx, "Android"+String(ctx.Config().productVariables.Make_suffix)+".mk")
 	if ctx.Failed() {
 		return
@@ -736,6 +737,97 @@
 	})
 }
 
+// In soong-only mode, we don't do most of the androidmk stuff. But disted files are still largely
+// defined through the androidmk mechanisms, so this function is an alternate implementation of
+// the androidmk singleton that just focuses on getting the dist contributions
+func (c *androidMkSingleton) soongOnlyBuildActions(ctx SingletonContext, mods []blueprint.Module) {
+	allDistContributions := getDistContributionsFromMods(ctx, mods)
+
+	distMkFile := absolutePath(filepath.Join(ctx.Config().katiPackageMkDir(), "dist.mk"))
+
+	var goalOutputPairs []string
+	var buf strings.Builder
+	buf.WriteString("DIST_SRC_DST_PAIRS := $(sort")
+	for _, contributions := range allDistContributions {
+		for _, copiesForGoal := range contributions.copiesForGoals {
+			goals := strings.Fields(copiesForGoal.goals)
+			for _, copy := range copiesForGoal.copies {
+				for _, goal := range goals {
+					goalOutputPairs = append(goalOutputPairs, fmt.Sprintf("%s:%s", goal, copy.dest))
+				}
+				buf.WriteString(fmt.Sprintf(" %s:%s", copy.from.String(), copy.dest))
+			}
+		}
+	}
+	buf.WriteString(")\nDIST_GOAL_OUTPUT_PAIRS := $(sort ")
+	buf.WriteString(strings.Join(goalOutputPairs, " "))
+	buf.WriteString(")\n")
+
+	writeValueIfChanged(ctx, distMkFile, buf.String())
+}
+
+func writeValueIfChanged(ctx SingletonContext, path string, value string) {
+	if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
+		ctx.Errorf("%s\n", err)
+		return
+	}
+	previousValue := ""
+	rawPreviousValue, err := os.ReadFile(path)
+	if err == nil {
+		previousValue = string(rawPreviousValue)
+	}
+
+	if previousValue != value {
+		if err = os.WriteFile(path, []byte(value), 0666); err != nil {
+			ctx.Errorf("Failed to write: %v", err)
+		}
+	}
+}
+
+func getDistContributionsFromMods(ctx fillInEntriesContext, mods []blueprint.Module) []distContributions {
+	var allDistContributions []distContributions
+	for _, mod := range mods {
+		if amod, ok := mod.(Module); ok && shouldSkipAndroidMkProcessing(ctx, amod.base()) {
+			continue
+		}
+		if info, ok := OtherModuleProvider(ctx, mod, AndroidMkInfoProvider); ok {
+			if contribution := info.PrimaryInfo.getDistContributions(ctx, mod); contribution != nil {
+				allDistContributions = append(allDistContributions, *contribution)
+			}
+			for _, ei := range info.ExtraInfo {
+				if contribution := ei.getDistContributions(ctx, mod); contribution != nil {
+					allDistContributions = append(allDistContributions, *contribution)
+				}
+			}
+		} else {
+			switch x := mod.(type) {
+			case AndroidMkDataProvider:
+				data := x.AndroidMk()
+
+				if data.Include == "" {
+					data.Include = "$(BUILD_PREBUILT)"
+				}
+
+				data.fillInData(ctx, mod)
+				if contribution := data.Entries.getDistContributions(mod); contribution != nil {
+					allDistContributions = append(allDistContributions, *contribution)
+				}
+			case AndroidMkEntriesProvider:
+				entriesList := x.AndroidMkEntries()
+				for _, entries := range entriesList {
+					entries.fillInEntries(ctx, mod)
+					if contribution := entries.getDistContributions(mod); contribution != nil {
+						allDistContributions = append(allDistContributions, *contribution)
+					}
+				}
+			default:
+				// Not exported to make so no make variables to set.
+			}
+		}
+	}
+	return allDistContributions
+}
+
 func translateAndroidMk(ctx SingletonContext, absMkFile string, moduleInfoJSONPath WritablePath, mods []blueprint.Module) error {
 	buf := &bytes.Buffer{}
 
diff --git a/android/config.go b/android/config.go
index e9cb2cd..70c5e4f 100644
--- a/android/config.go
+++ b/android/config.go
@@ -83,6 +83,7 @@
 	OutDir         string
 	SoongOutDir    string
 	SoongVariables string
+	KatiSuffix     string
 
 	ModuleGraphFile   string
 	ModuleActionsFile string
@@ -349,6 +350,7 @@
 	// Changes behavior based on whether Kati runs after soong_build, or if soong_build
 	// runs standalone.
 	katiEnabled bool
+	katiSuffix  string
 
 	captureBuild      bool // true for tests, saves build parameters for each module
 	ignoreEnvironment bool // true for tests, returns empty from all Getenv calls
@@ -620,6 +622,7 @@
 		outDir:            cmdArgs.OutDir,
 		soongOutDir:       cmdArgs.SoongOutDir,
 		runGoTests:        cmdArgs.RunGoTests,
+		katiSuffix:        cmdArgs.KatiSuffix,
 		multilibConflicts: make(map[ArchType]bool),
 
 		moduleListFile: cmdArgs.ModuleListFile,
@@ -1512,6 +1515,10 @@
 	return c.productVariables.GetBuildFlagBool("RELEASE_BOARD_API_LEVEL_FROZEN")
 }
 
+func (c *config) katiPackageMkDir() string {
+	return filepath.Join(c.soongOutDir, "kati_packaging"+c.katiSuffix)
+}
+
 func (c *deviceConfig) Arches() []Arch {
 	var arches []Arch
 	for _, target := range c.config.Targets[Android] {
diff --git a/android/module.go b/android/module.go
index b8f2cae..2f505aa 100644
--- a/android/module.go
+++ b/android/module.go
@@ -1679,14 +1679,13 @@
 		}
 	})
 
-	var deps Paths
-
 	var namespacePrefix string
 	nameSpace := ctx.Namespace().Path
 	if nameSpace != "." {
 		namespacePrefix = strings.ReplaceAll(nameSpace, "/", ".") + "-"
 	}
 
+	var deps Paths
 	var info FinalModuleBuildTargetsInfo
 
 	if len(allInstalledFiles) > 0 {
@@ -1853,9 +1852,9 @@
 
 var SourceFilesInfoKey = blueprint.NewProvider[SourceFilesInfo]()
 
+// FinalModuleBuildTargetsInfo is used by buildTargetSingleton to create checkbuild and
+// per-directory build targets. Only set on the final variant of each module
 type FinalModuleBuildTargetsInfo struct {
-	// Used by buildTargetSingleton to create checkbuild and per-directory build targets
-	// Only set on the final variant of each module
 	InstallTarget    WritablePath
 	CheckbuildTarget WritablePath
 	BlueprintDir     string
diff --git a/android/phony.go b/android/phony.go
index f8db88d..7bdd9d3 100644
--- a/android/phony.go
+++ b/android/phony.go
@@ -15,6 +15,7 @@
 package android
 
 import (
+	"strings"
 	"sync"
 
 	"github.com/google/blueprint"
@@ -68,13 +69,25 @@
 	}
 
 	if !ctx.Config().KatiEnabled() {
+		// In soong-only builds, the phonies can conflict with dist targets that will
+		// be generated in the packaging step. Instead of emitting a blueprint/ninja phony directly,
+		// create a makefile that defines the phonies that will be included in the packaging step.
+		// Make will dedup the phonies there.
+		var buildPhonyFileContents strings.Builder
 		for _, phony := range p.phonyList {
-			ctx.Build(pctx, BuildParams{
-				Rule:      blueprint.Phony,
-				Outputs:   []WritablePath{PathForPhony(ctx, phony)},
-				Implicits: p.phonyMap[phony],
-			})
+			buildPhonyFileContents.WriteString(".PHONY: ")
+			buildPhonyFileContents.WriteString(phony)
+			buildPhonyFileContents.WriteString("\n")
+			buildPhonyFileContents.WriteString(phony)
+			buildPhonyFileContents.WriteString(":")
+			for _, dep := range p.phonyMap[phony] {
+				buildPhonyFileContents.WriteString(" ")
+				buildPhonyFileContents.WriteString(dep.String())
+			}
+			buildPhonyFileContents.WriteString("\n")
 		}
+		buildPhonyFile := PathForOutput(ctx, "soong_phony_targets.mk")
+		writeValueIfChanged(ctx, absolutePath(buildPhonyFile.String()), buildPhonyFileContents.String())
 	}
 }