Allow calling ctx.DistForGoal from Singletons

Right now singletons have to dist files in either in MakeVars providers.
Allow dist from GenerateBuildActions so that we can further remove our
reliance on make.

Bug: 395184523
Test: m nothing --no-skip-soong-tests
Change-Id: Ie3004c2b038b45754279dfadcb7f7455ae740136
diff --git a/android/androidmk.go b/android/androidmk.go
index 9c72606..db838f0 100644
--- a/android/androidmk.go
+++ b/android/androidmk.go
@@ -791,6 +791,13 @@
 		}
 	}
 
+	singletonDists := getSingletonDists(ctx.Config())
+	singletonDists.lock.Lock()
+	if contribution := distsToDistContributions(singletonDists.dists); contribution != nil {
+		allDistContributions = append(allDistContributions, *contribution)
+	}
+	singletonDists.lock.Unlock()
+
 	// Build module-info.json. Only in builds with HasDeviceProduct(), as we need a named
 	// device to have a TARGET_OUT folder.
 	if ctx.Config().HasDeviceProduct() {
diff --git a/android/makevars.go b/android/makevars.go
index 3a60bbb..45fd0d0 100644
--- a/android/makevars.go
+++ b/android/makevars.go
@@ -266,6 +266,11 @@
 		dists = append(dists, mctx.dists...)
 	}
 
+	singletonDists := getSingletonDists(ctx.Config())
+	singletonDists.lock.Lock()
+	dists = append(dists, singletonDists.dists...)
+	singletonDists.lock.Unlock()
+
 	ctx.VisitAllModules(func(m Module) {
 		if provider, ok := m.(ModuleMakeVarsProvider); ok && m.Enabled(ctx) {
 			mctx := &makeVarsContext{
diff --git a/android/makevars_test.go b/android/makevars_test.go
index 5e4499f..95e4b59 100644
--- a/android/makevars_test.go
+++ b/android/makevars_test.go
@@ -9,6 +9,8 @@
 	result := GroupFixturePreparers(
 		FixtureRegisterWithContext(func(ctx RegistrationContext) {
 			ctx.RegisterModuleType("my_module_type", newDistFileModule)
+			ctx.RegisterParallelSingletonType("my_singleton", newDistFileSingleton)
+			ctx.RegisterParallelSingletonModuleType("my_singleton_module", newDistFileSingletonModule)
 		}),
 		FixtureModifyConfig(SetKatiEnabledForTests),
 		PrepareForTestWithMakevars,
@@ -16,12 +18,27 @@
 	my_module_type {
 		name: "foo",
 	}
+	my_singleton_module {
+		name: "bar"
+	}
 	`)
 
 	lateContents := string(result.SingletonForTests("makevars").Singleton().(*makeVarsSingleton).lateForTesting)
 	matched, err := regexp.MatchString(`call dist-for-goals,my_goal,.*/my_file.txt:my_file.txt\)`, lateContents)
 	if err != nil || !matched {
-		t.Fatalf("Expected a dist, but got: %s", lateContents)
+		t.Fatalf("Expected a dist of my_file.txt, but got: %s", lateContents)
+	}
+	matched, err = regexp.MatchString(`call dist-for-goals,my_singleton_goal,.*/my_singleton_file.txt:my_singleton_file.txt\)`, lateContents)
+	if err != nil || !matched {
+		t.Fatalf("Expected a dist of my_singleton_file.txt, but got: %s", lateContents)
+	}
+	matched, err = regexp.MatchString(`call dist-for-goals,my_singleton_module_module_goal,.*/my_singleton_module_module_file.txt:my_singleton_module_module_file.txt\)`, lateContents)
+	if err != nil || !matched {
+		t.Fatalf("Expected a dist of my_singleton_module_module_file.txt, but got: %s", lateContents)
+	}
+	matched, err = regexp.MatchString(`call dist-for-goals,my_singleton_module_singleton_goal,.*/my_singleton_module_singleton_file.txt:my_singleton_module_singleton_file.txt\)`, lateContents)
+	if err != nil || !matched {
+		t.Fatalf("Expected a dist of my_singleton_module_singleton_file.txt, but got: %s", lateContents)
 	}
 }
 
@@ -40,3 +57,40 @@
 	WriteFileRule(ctx, out, "Hello, world!")
 	ctx.DistForGoal("my_goal", out)
 }
+
+type distFileSingleton struct {
+}
+
+func newDistFileSingleton() Singleton {
+	return &distFileSingleton{}
+}
+
+func (d *distFileSingleton) GenerateBuildActions(ctx SingletonContext) {
+	out := PathForOutput(ctx, "my_singleton_file.txt")
+	WriteFileRule(ctx, out, "Hello, world!")
+	ctx.DistForGoal("my_singleton_goal", out)
+}
+
+type distFileSingletonModule struct {
+	SingletonModuleBase
+}
+
+func newDistFileSingletonModule() SingletonModule {
+	sm := &distFileSingletonModule{}
+	InitAndroidSingletonModule(sm)
+	return sm
+}
+
+// GenerateAndroidBuildActions implements SingletonModule.
+func (d *distFileSingletonModule) GenerateAndroidBuildActions(ctx ModuleContext) {
+	out := PathForModuleOut(ctx, "my_singleton_module_module_file.txt")
+	WriteFileRule(ctx, out, "Hello, world!")
+	ctx.DistForGoal("my_singleton_module_module_goal", out)
+}
+
+// GenerateSingletonBuildActions implements SingletonModule.
+func (d *distFileSingletonModule) GenerateSingletonBuildActions(ctx SingletonContext) {
+	out := PathForOutput(ctx, "my_singleton_module_singleton_file.txt")
+	WriteFileRule(ctx, out, "Hello, world!")
+	ctx.DistForGoal("my_singleton_module_singleton_goal", out)
+}
diff --git a/android/singleton.go b/android/singleton.go
index 0754b0c..df22045 100644
--- a/android/singleton.go
+++ b/android/singleton.go
@@ -15,6 +15,9 @@
 package android
 
 import (
+	"slices"
+	"sync"
+
 	"github.com/google/blueprint"
 )
 
@@ -97,6 +100,24 @@
 	// HasMutatorFinished returns true if the given mutator has finished running.
 	// It will panic if given an invalid mutator name.
 	HasMutatorFinished(mutatorName string) bool
+
+	// DistForGoals creates a rule to copy one or more Paths to the artifacts
+	// directory on the build server when any of the specified goals are built.
+	DistForGoal(goal string, paths ...Path)
+
+	// DistForGoalWithFilename creates a rule to copy a Path to the artifacts
+	// directory on the build server with the given filename when the specified
+	// goal is built.
+	DistForGoalWithFilename(goal string, path Path, filename string)
+
+	// DistForGoals creates a rule to copy one or more Paths to the artifacts
+	// directory on the build server when any of the specified goals are built.
+	DistForGoals(goals []string, paths ...Path)
+
+	// DistForGoalsWithFilename creates a rule to copy a Path to the artifacts
+	// directory on the build server with the given filename when any of the
+	// specified goals are built.
+	DistForGoalsWithFilename(goals []string, path Path, filename string)
 }
 
 type singletonAdaptor struct {
@@ -118,6 +139,13 @@
 
 	s.buildParams = sctx.buildParams
 	s.ruleParams = sctx.ruleParams
+
+	if len(sctx.dists) > 0 {
+		dists := getSingletonDists(sctx.Config())
+		dists.lock.Lock()
+		defer dists.lock.Unlock()
+		dists.dists = append(dists.dists, sctx.dists...)
+	}
 }
 
 func (s *singletonAdaptor) BuildParamsForTests() []BuildParams {
@@ -128,6 +156,19 @@
 	return s.ruleParams
 }
 
+var singletonDistsKey = NewOnceKey("singletonDistsKey")
+
+type singletonDistsAndLock struct {
+	dists []dist
+	lock  sync.Mutex
+}
+
+func getSingletonDists(config Config) *singletonDistsAndLock {
+	return config.Once(singletonDistsKey, func() interface{} {
+		return &singletonDistsAndLock{}
+	}).(*singletonDistsAndLock)
+}
+
 type Singleton interface {
 	GenerateBuildActions(SingletonContext)
 }
@@ -137,6 +178,7 @@
 
 	buildParams []BuildParams
 	ruleParams  map[blueprint.Rule]blueprint.RuleParams
+	dists       []dist
 }
 
 func (s *singletonContextAdaptor) blueprintSingletonContext() blueprint.SingletonContext {
@@ -315,3 +357,31 @@
 func (s *singletonContextAdaptor) HasMutatorFinished(mutatorName string) bool {
 	return s.blueprintSingletonContext().HasMutatorFinished(mutatorName)
 }
+func (s *singletonContextAdaptor) DistForGoal(goal string, paths ...Path) {
+	s.DistForGoals([]string{goal}, paths...)
+}
+
+func (s *singletonContextAdaptor) DistForGoalWithFilename(goal string, path Path, filename string) {
+	s.DistForGoalsWithFilename([]string{goal}, path, filename)
+}
+
+func (s *singletonContextAdaptor) DistForGoals(goals []string, paths ...Path) {
+	var copies distCopies
+	for _, path := range paths {
+		copies = append(copies, distCopy{
+			from: path,
+			dest: path.Base(),
+		})
+	}
+	s.dists = append(s.dists, dist{
+		goals: slices.Clone(goals),
+		paths: copies,
+	})
+}
+
+func (s *singletonContextAdaptor) DistForGoalsWithFilename(goals []string, path Path, filename string) {
+	s.dists = append(s.dists, dist{
+		goals: slices.Clone(goals),
+		paths: distCopies{{from: path, dest: filename}},
+	})
+}