Add release_config_contributions modules

These are used to enumerate what directories may contribute to release
configs, whether or not they are actually used in the build.

Bug: b/370544058
Test: manual, TH
Change-Id: I1509eb3795a9b51b29995b182b77ade76021bb52
diff --git a/aconfig/build_flags/Android.bp b/aconfig/build_flags/Android.bp
index be1b872..139aeac 100644
--- a/aconfig/build_flags/Android.bp
+++ b/aconfig/build_flags/Android.bp
@@ -17,6 +17,7 @@
         "build_flags_singleton.go",
         "declarations.go",
         "init.go",
+        "release_configs.go",
     ],
     testSrcs: [
     ],
diff --git a/aconfig/build_flags/build_flags_singleton.go b/aconfig/build_flags/build_flags_singleton.go
index 5f02912..ba27a85 100644
--- a/aconfig/build_flags/build_flags_singleton.go
+++ b/aconfig/build_flags/build_flags_singleton.go
@@ -30,49 +30,94 @@
 }
 
 type allBuildFlagDeclarationsSingleton struct {
-	intermediateBinaryProtoPath android.OutputPath
-	intermediateTextProtoPath   android.OutputPath
+	flagsBinaryProtoPath   android.OutputPath
+	flagsTextProtoPath     android.OutputPath
+	configsBinaryProtoPath android.OutputPath
+	configsTextProtoPath   android.OutputPath
 }
 
 func (this *allBuildFlagDeclarationsSingleton) GenerateBuildActions(ctx android.SingletonContext) {
 	// Find all of the build_flag_declarations modules
-	var intermediateFiles android.Paths
+	var flagsFiles android.Paths
+	// Find all of the release_config_contribution modules
+	var contributionDirs android.Paths
 	ctx.VisitAllModules(func(module android.Module) {
 		decl, ok := android.OtherModuleProvider(ctx, module, BuildFlagDeclarationsProviderKey)
-		if !ok {
-			return
+		if ok {
+			flagsFiles = append(flagsFiles, decl.IntermediateCacheOutputPath)
 		}
-		intermediateFiles = append(intermediateFiles, decl.IntermediateCacheOutputPath)
+
+		contrib, ok := android.OtherModuleProvider(ctx, module, ReleaseConfigContributionsProviderKey)
+		if ok {
+			contributionDirs = append(contributionDirs, contrib.ContributionDir)
+		}
 	})
 
 	// Generate build action for build_flag (binary proto output)
-	this.intermediateBinaryProtoPath = android.PathForIntermediates(ctx, "all_build_flag_declarations.pb")
+	this.flagsBinaryProtoPath = android.PathForIntermediates(ctx, "all_build_flag_declarations.pb")
 	ctx.Build(pctx, android.BuildParams{
 		Rule:        allDeclarationsRule,
-		Inputs:      intermediateFiles,
-		Output:      this.intermediateBinaryProtoPath,
+		Inputs:      flagsFiles,
+		Output:      this.flagsBinaryProtoPath,
 		Description: "all_build_flag_declarations",
 		Args: map[string]string{
-			"intermediates": android.JoinPathsWithPrefix(intermediateFiles, "--intermediate "),
+			"intermediates": android.JoinPathsWithPrefix(flagsFiles, "--intermediate "),
 		},
 	})
-	ctx.Phony("all_build_flag_declarations", this.intermediateBinaryProtoPath)
+	ctx.Phony("all_build_flag_declarations", this.flagsBinaryProtoPath)
 
 	// Generate build action for build_flag (text proto output)
-	this.intermediateTextProtoPath = android.PathForIntermediates(ctx, "all_build_flag_declarations.textproto")
+	this.flagsTextProtoPath = android.PathForIntermediates(ctx, "all_build_flag_declarations.textproto")
 	ctx.Build(pctx, android.BuildParams{
 		Rule:        allDeclarationsRuleTextProto,
-		Input:       this.intermediateBinaryProtoPath,
-		Output:      this.intermediateTextProtoPath,
+		Input:       this.flagsBinaryProtoPath,
+		Output:      this.flagsTextProtoPath,
 		Description: "all_build_flag_declarations_textproto",
 	})
-	ctx.Phony("all_build_flag_declarations_textproto", this.intermediateTextProtoPath)
+	ctx.Phony("all_build_flag_declarations_textproto", this.flagsTextProtoPath)
+
+	// Generate build action for release_configs (binary proto output)
+	this.configsBinaryProtoPath = android.PathForIntermediates(ctx, "all_release_config_contributions.pb")
+	ctx.Build(pctx, android.BuildParams{
+		Rule:        allReleaseConfigContributionsRule,
+		Inputs:      contributionDirs,
+		Output:      this.configsBinaryProtoPath,
+		Description: "all_release_config_contributions",
+		Args: map[string]string{
+			"dirs":   android.JoinPathsWithPrefix(contributionDirs, "--dir "),
+			"format": "pb",
+		},
+	})
+	ctx.Phony("all_release_config_contributions", this.configsBinaryProtoPath)
+
+	this.configsTextProtoPath = android.PathForIntermediates(ctx, "all_release_config_contributions.textproto")
+	ctx.Build(pctx, android.BuildParams{
+		Rule:        allReleaseConfigContributionsRule,
+		Inputs:      contributionDirs,
+		Output:      this.configsTextProtoPath,
+		Description: "all_release_config_contributions_textproto",
+		Args: map[string]string{
+			"dirs":   android.JoinPathsWithPrefix(contributionDirs, "--dir "),
+			"format": "textproto",
+		},
+	})
+	ctx.Phony("all_release_config_contributions_textproto", this.configsTextProtoPath)
+
+	// Add a simple target for ci/build_metadata to use.
+	ctx.Phony("release_config_metadata",
+		this.flagsBinaryProtoPath,
+		this.flagsTextProtoPath,
+		this.configsBinaryProtoPath,
+		this.configsTextProtoPath,
+	)
 }
 
 func (this *allBuildFlagDeclarationsSingleton) MakeVars(ctx android.MakeVarsContext) {
-	ctx.DistForGoal("droid", this.intermediateBinaryProtoPath)
+	ctx.DistForGoal("droid", this.flagsBinaryProtoPath)
 	for _, goal := range []string{"docs", "droid", "sdk"} {
-		ctx.DistForGoalWithFilename(goal, this.intermediateBinaryProtoPath, "build_flags/all_flags.pb")
-		ctx.DistForGoalWithFilename(goal, this.intermediateTextProtoPath, "build_flags/all_flags.textproto")
+		ctx.DistForGoalWithFilename(goal, this.flagsBinaryProtoPath, "build_flags/all_flags.pb")
+		ctx.DistForGoalWithFilename(goal, this.flagsTextProtoPath, "build_flags/all_flags.textproto")
+		ctx.DistForGoalWithFilename(goal, this.configsBinaryProtoPath, "build_flags/all_release_config_contributions.pb")
+		ctx.DistForGoalWithFilename(goal, this.configsTextProtoPath, "build_flags/all_release_config_contributions.textproto")
 	}
 }
diff --git a/aconfig/build_flags/declarations.go b/aconfig/build_flags/declarations.go
index e927db2..4a54269 100644
--- a/aconfig/build_flags/declarations.go
+++ b/aconfig/build_flags/declarations.go
@@ -35,7 +35,7 @@
 
 	// Properties for "aconfig_declarations"
 	properties struct {
-		// aconfig files, relative to this Android.bp file
+		// build flag declaration files, relative to this Android.bp file
 		Srcs []string `android:"path"`
 	}
 }
diff --git a/aconfig/build_flags/init.go b/aconfig/build_flags/init.go
index dc1369c..a7575e8 100644
--- a/aconfig/build_flags/init.go
+++ b/aconfig/build_flags/init.go
@@ -65,15 +65,32 @@
 				"${buildFlagDeclarations}",
 			},
 		})
+
+	allReleaseConfigContributionsRule = pctx.AndroidStaticRule("all-release-config-contributions-dump",
+		blueprint.RuleParams{
+			Command: `${releaseConfigContributions} ${dirs} --format ${format} --output ${out}`,
+			CommandDeps: []string{
+				"${releaseConfigContributions}",
+			},
+		}, "dirs", "format")
+	allReleaseConfigContributionsRuleText = pctx.AndroidStaticRule("all-release-config-contributions-dumptext",
+		blueprint.RuleParams{
+			Command: `${releaseConfigContributions} ${dirs} --format ${format} --output ${out}`,
+			CommandDeps: []string{
+				"${releaseConfigContributions}",
+			},
+		}, "dirs", "format")
 )
 
 func init() {
 	RegisterBuildComponents(android.InitRegistrationContext)
 	pctx.Import("android/soong/android")
 	pctx.HostBinToolVariable("buildFlagDeclarations", "build-flag-declarations")
+	pctx.HostBinToolVariable("releaseConfigContributions", "release-config-contributions")
 }
 
 func RegisterBuildComponents(ctx android.RegistrationContext) {
 	ctx.RegisterModuleType("build_flag_declarations", DeclarationsFactory)
+	ctx.RegisterModuleType("release_config_contributions", ReleaseConfigContributionsFactory)
 	ctx.RegisterParallelSingletonType("all_build_flag_declarations", AllBuildFlagDeclarationsFactory)
 }
diff --git a/aconfig/build_flags/release_configs.go b/aconfig/build_flags/release_configs.go
new file mode 100644
index 0000000..3fa8a7c
--- /dev/null
+++ b/aconfig/build_flags/release_configs.go
@@ -0,0 +1,78 @@
+// Copyright 2023 Google Inc. All rights reserved.
+//
+// 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 build_flags
+
+import (
+	"path/filepath"
+
+	"android/soong/android"
+
+	"github.com/google/blueprint"
+)
+
+type ReleaseConfigContributionsProviderData struct {
+	ContributionDir android.SourcePath
+}
+
+var ReleaseConfigContributionsProviderKey = blueprint.NewProvider[ReleaseConfigContributionsProviderData]()
+
+// Soong uses `release_config_contributions` modules to produce the
+// `build_flags/all_release_config_contributions.*` artifacts, listing *all* of
+// the directories in the source tree that contribute to each release config,
+// whether or not they are actually used for the lunch product.
+//
+// This artifact helps flagging automation determine in which directory a flag
+// should be placed by default.
+type ReleaseConfigContributionsModule struct {
+	android.ModuleBase
+	android.DefaultableModuleBase
+
+	// Properties for "release_config_contributions"
+	properties struct {
+		// The `release_configs/*.textproto` files provided by this
+		// directory, relative to this Android.bp file
+		Srcs []string `android:"path"`
+	}
+}
+
+func ReleaseConfigContributionsFactory() android.Module {
+	module := &ReleaseConfigContributionsModule{}
+
+	android.InitAndroidModule(module)
+	android.InitDefaultableModule(module)
+	module.AddProperties(&module.properties)
+
+	return module
+}
+
+func (module *ReleaseConfigContributionsModule) GenerateAndroidBuildActions(ctx android.ModuleContext) {
+	srcs := android.PathsForModuleSrc(ctx, module.properties.Srcs)
+	if len(srcs) == 0 {
+		return
+	}
+	contributionDir := filepath.Dir(filepath.Dir(srcs[0].String()))
+	for _, file := range srcs {
+		if filepath.Dir(filepath.Dir(file.String())) != contributionDir {
+			ctx.ModuleErrorf("Cannot include %s with %s contributions", file, contributionDir)
+		}
+		if filepath.Base(filepath.Dir(file.String())) != "release_configs" || file.Ext() != ".textproto" {
+			ctx.ModuleErrorf("Invalid contribution file %s", file)
+		}
+	}
+	android.SetProvider(ctx, ReleaseConfigContributionsProviderKey, ReleaseConfigContributionsProviderData{
+		ContributionDir: android.PathForSource(ctx, contributionDir),
+	})
+
+}
diff --git a/cmd/release_config/release_config_contributions/Android.bp b/cmd/release_config/release_config_contributions/Android.bp
new file mode 100644
index 0000000..6882ea2
--- /dev/null
+++ b/cmd/release_config/release_config_contributions/Android.bp
@@ -0,0 +1,32 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+blueprint_go_binary {
+    name: "release-config-contributions",
+    deps: [
+        "golang-protobuf-encoding-prototext",
+        "golang-protobuf-reflect-protoreflect",
+        "golang-protobuf-runtime-protoimpl",
+        "soong-cmd-release_config-proto",
+        "soong-cmd-release_config-lib",
+    ],
+    srcs: [
+        "main.go",
+    ],
+}
+
+bootstrap_go_package {
+    name: "soong-cmd-release_config-release_config_contributions",
+    pkgPath: "android/soong/cmd/release_config/release_config_contributions",
+    deps: [
+        "golang-protobuf-encoding-prototext",
+        "golang-protobuf-reflect-protoreflect",
+        "golang-protobuf-runtime-protoimpl",
+        "soong-cmd-release_config-proto",
+        "soong-cmd-release_config-lib",
+    ],
+    srcs: [
+        "main.go",
+    ],
+}
diff --git a/cmd/release_config/release_config_contributions/main.go b/cmd/release_config/release_config_contributions/main.go
new file mode 100644
index 0000000..6abf768
--- /dev/null
+++ b/cmd/release_config/release_config_contributions/main.go
@@ -0,0 +1,130 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"os"
+	"slices"
+	"strings"
+
+	rc_lib "android/soong/cmd/release_config/release_config_lib"
+	rc_proto "android/soong/cmd/release_config/release_config_proto"
+)
+
+type Flags struct {
+	// The path to the top of the workspace.  Default: ".".
+	top string
+
+	// Output file.
+	output string
+
+	// Format for output file
+	format string
+
+	// List of release config directories to process.
+	dirs rc_lib.StringList
+
+	// Disable warning messages
+	quiet bool
+
+	// Panic on errors.
+	debug bool
+}
+
+func sortDirectories(dirList []string) {
+	order := func(dir string) int {
+		switch {
+		// These three are always in this order.
+		case dir == "build/release":
+			return 1
+		case dir == "vendor/google_shared/build/release":
+			return 2
+		case dir == "vendor/google/release":
+			return 3
+		// Keep their subdirs in the same order.
+		case strings.HasPrefix(dir, "build/release/"):
+			return 21
+		case strings.HasPrefix(dir, "vendor/google_shared/build/release/"):
+			return 22
+		case strings.HasPrefix(dir, "vendor/google/release/"):
+			return 23
+		// Everything else sorts by directory path.
+		default:
+			return 99
+		}
+	}
+
+	slices.SortFunc(dirList, func(a, b string) int {
+		aOrder, bOrder := order(a), order(b)
+		if aOrder != bOrder {
+			return aOrder - bOrder
+		}
+		return strings.Compare(a, b)
+	})
+}
+
+func main() {
+	var flags Flags
+	topDir, err := rc_lib.GetTopDir()
+
+	// Handle the common arguments
+	flag.StringVar(&flags.top, "top", topDir, "path to top of workspace")
+	flag.Var(&flags.dirs, "dir", "path to a release config contribution directory. May be repeated")
+	flag.StringVar(&flags.format, "format", "pb", "output file format")
+	flag.StringVar(&flags.output, "output", "release_config_contributions.pb", "output file")
+	flag.BoolVar(&flags.debug, "debug", false, "turn on debugging output for errors")
+	flag.BoolVar(&flags.quiet, "quiet", false, "disable warning messages")
+	flag.Parse()
+
+	errorExit := func(err error) {
+		if flags.debug {
+			panic(err)
+		}
+		fmt.Fprintf(os.Stderr, "%s\n", err)
+		os.Exit(1)
+	}
+
+	if flags.quiet {
+		rc_lib.DisableWarnings()
+	}
+
+	if err = os.Chdir(flags.top); err != nil {
+		errorExit(err)
+	}
+
+	contributingDirsMap := make(map[string][]string)
+	for _, dir := range flags.dirs {
+		contributions, err := rc_lib.EnumerateReleaseConfigs(dir)
+		if err != nil {
+			errorExit(err)
+		}
+		for _, name := range contributions {
+			contributingDirsMap[name] = append(contributingDirsMap[name], dir)
+		}
+	}
+
+	releaseConfigNames := []string{}
+	for name := range contributingDirsMap {
+		releaseConfigNames = append(releaseConfigNames, name)
+	}
+	slices.Sort(releaseConfigNames)
+
+	message := &rc_proto.ReleaseConfigContributionsArtifacts{
+		ReleaseConfigContributionsArtifactList: []*rc_proto.ReleaseConfigContributionsArtifact{},
+	}
+	for _, name := range releaseConfigNames {
+		dirs := contributingDirsMap[name]
+		slices.Sort(dirs)
+		message.ReleaseConfigContributionsArtifactList = append(
+			message.ReleaseConfigContributionsArtifactList,
+			&rc_proto.ReleaseConfigContributionsArtifact{
+				Name:                    &name,
+				ContributingDirectories: dirs,
+			})
+	}
+
+	err = rc_lib.WriteFormattedMessage(flags.output, flags.format, message)
+	if err != nil {
+		errorExit(err)
+	}
+}
diff --git a/cmd/release_config/release_config_lib/release_configs.go b/cmd/release_config/release_config_lib/release_configs.go
index 97eb8f1..831ec02 100644
--- a/cmd/release_config/release_config_lib/release_configs.go
+++ b/cmd/release_config/release_config_lib/release_configs.go
@@ -248,6 +248,18 @@
 	return configs.configDirs[index], nil
 }
 
+// Return the (unsorted) release configs contributed to by `dir`.
+func EnumerateReleaseConfigs(dir string) ([]string, error) {
+	var ret []string
+	err := WalkTextprotoFiles(dir, "release_configs", func(path string, d fs.DirEntry, err error) error {
+		// Strip off the trailing `.textproto` from the name.
+		name := filepath.Base(path)
+		ret = append(ret, name[:len(name)-10])
+		return err
+	})
+	return ret, err
+}
+
 func (configs *ReleaseConfigs) LoadReleaseConfigMap(path string, ConfigDirIndex int) error {
 	if _, err := os.Stat(path); err != nil {
 		return fmt.Errorf("%s does not exist\n", path)
diff --git a/cmd/release_config/release_config_proto/Android.bp b/cmd/release_config/release_config_proto/Android.bp
index c34d203..c6869b1 100644
--- a/cmd/release_config/release_config_proto/Android.bp
+++ b/cmd/release_config/release_config_proto/Android.bp
@@ -28,5 +28,6 @@
         "build_flags_declarations.pb.go",
         "build_flags_src.pb.go",
         "build_flags_out.pb.go",
+        "release_configs_contributions.pb.go",
     ],
 }