xref: /aosp_15_r20/build/soong/cmd/release_config/build_flag/main.go (revision 333d2b3687b3a337dbcca9d65000bca186795e39)
1package main
2
3import (
4	"cmp"
5	"flag"
6	"fmt"
7	"os"
8	"path/filepath"
9	"slices"
10	"strings"
11
12	rc_lib "android/soong/cmd/release_config/release_config_lib"
13	rc_proto "android/soong/cmd/release_config/release_config_proto"
14
15	"google.golang.org/protobuf/proto"
16)
17
18type Flags struct {
19	// The path to the top of the workspace.  Default: ".".
20	top string
21
22	// Pathlist of release config map textproto files.
23	// If not specified, then the value is (if present):
24	// - build/release/release_config_map.textproto
25	// - vendor/google_shared/build/release/release_config_map.textproto
26	// - vendor/google/release/release_config_map.textproto
27	//
28	// Additionally, any maps specified in the environment variable
29	// `PRODUCT_RELEASE_CONFIG_MAPS` are used.
30	maps rc_lib.StringList
31
32	// Output directory (relative to `top`).
33	outDir string
34
35	// Which $TARGET_RELEASE(s) should we use.  Some commands will only
36	// accept one value, others also accept `--release --all`.
37	targetReleases rc_lib.StringList
38
39	// Disable warning messages
40	quiet bool
41
42	// Show all release configs
43	allReleases bool
44
45	// Call get_build_var PRODUCT_RELEASE_CONFIG_MAPS to get the
46	// product-specific map directories.
47	useGetBuildVar bool
48
49	// Panic on errors.
50	debug bool
51
52	// Allow missing release config.
53	// If true, and we cannot find the named release config, values for
54	// `trunk_staging` will be used.
55	allowMissing bool
56}
57
58type CommandFunc func(*rc_lib.ReleaseConfigs, Flags, string, []string) error
59
60var commandMap map[string]CommandFunc = map[string]CommandFunc{
61	"get":   GetCommand,
62	"set":   SetCommand,
63	"trace": GetCommand, // Also handled by GetCommand
64}
65
66// Find the top of the release config contribution directory.
67// Returns the parent of the flag_declarations and flag_values directories.
68func GetMapDir(path string) (string, error) {
69	for p := path; p != "."; p = filepath.Dir(p) {
70		switch filepath.Base(p) {
71		case "flag_declarations":
72			return filepath.Dir(p), nil
73		case "flag_values":
74			return filepath.Dir(p), nil
75		}
76	}
77	return "", fmt.Errorf("Could not determine directory from %s", path)
78}
79
80func MarshalFlagDefaultValue(config *rc_lib.ReleaseConfig, name string) (ret string, err error) {
81	fa, ok := config.FlagArtifacts[name]
82	if !ok {
83		return "", fmt.Errorf("%s not found in %s", name, config.Name)
84	}
85	return rc_lib.MarshalValue(fa.Traces[0].Value), nil
86}
87
88func MarshalFlagValue(config *rc_lib.ReleaseConfig, name string) (ret string, err error) {
89	fa, ok := config.FlagArtifacts[name]
90	if !ok {
91		return "", fmt.Errorf("%s not found in %s", name, config.Name)
92	}
93	if fa.Redacted {
94		return "==REDACTED==", nil
95	}
96	return rc_lib.MarshalValue(fa.Value), nil
97}
98
99// Returns a list of ReleaseConfig objects for which to process flags.
100func GetReleaseArgs(configs *rc_lib.ReleaseConfigs, commonFlags Flags) ([]*rc_lib.ReleaseConfig, error) {
101	var all bool
102	relFlags := flag.NewFlagSet("releaseFlags", flag.ExitOnError)
103	relFlags.BoolVar(&all, "all", false, "Display all releases")
104	relFlags.Parse(commonFlags.targetReleases)
105	var ret []*rc_lib.ReleaseConfig
106	if all || commonFlags.allReleases {
107		sortMap := map[string]int{
108			"trunk_staging": 0,
109			"trunk_food":    10,
110			"trunk":         20,
111			// Anything not listed above, uses this for key 1 in the sort.
112			"-default": 100,
113		}
114
115		for _, config := range configs.ReleaseConfigs {
116			ret = append(ret, config)
117		}
118		slices.SortFunc(ret, func(a, b *rc_lib.ReleaseConfig) int {
119			mapValue := func(v *rc_lib.ReleaseConfig) int {
120				if v, ok := sortMap[v.Name]; ok {
121					return v
122				}
123				return sortMap["-default"]
124			}
125			if n := cmp.Compare(mapValue(a), mapValue(b)); n != 0 {
126				return n
127			}
128			return cmp.Compare(a.Name, b.Name)
129		})
130		return ret, nil
131	}
132	for _, arg := range relFlags.Args() {
133		// Return releases in the order that they were given.
134		config, err := configs.GetReleaseConfig(arg)
135		if err != nil {
136			return nil, err
137		}
138		ret = append(ret, config)
139	}
140	return ret, nil
141}
142
143func GetCommand(configs *rc_lib.ReleaseConfigs, commonFlags Flags, cmd string, args []string) error {
144	isTrace := cmd == "trace"
145	isSet := cmd == "set"
146
147	var all bool
148	getFlags := flag.NewFlagSet("get", flag.ExitOnError)
149	getFlags.BoolVar(&all, "all", false, "Display all flags")
150	getFlags.Parse(args)
151	args = getFlags.Args()
152
153	if isSet {
154		commonFlags.allReleases = true
155	}
156	releaseConfigList, err := GetReleaseArgs(configs, commonFlags)
157	if err != nil {
158		return err
159	}
160	if isTrace && len(releaseConfigList) > 1 {
161		return fmt.Errorf("trace command only allows one --release argument.  Got: %s", strings.Join(commonFlags.targetReleases, " "))
162	}
163
164	if all {
165		args = []string{}
166		for _, fa := range configs.FlagArtifacts {
167			args = append(args, *fa.FlagDeclaration.Name)
168		}
169		slices.Sort(args)
170	}
171
172	var maxVariableNameLen, maxReleaseNameLen int
173	var releaseNameFormat, variableNameFormat string
174	valueFormat := "%s"
175	showReleaseName := len(releaseConfigList) > 1
176	showVariableName := len(args) > 1
177	if showVariableName {
178		for _, arg := range args {
179			maxVariableNameLen = max(len(arg), maxVariableNameLen)
180		}
181		variableNameFormat = fmt.Sprintf("%%-%ds ", maxVariableNameLen)
182		valueFormat = "'%s'"
183	}
184	if showReleaseName {
185		for _, config := range releaseConfigList {
186			maxReleaseNameLen = max(len(config.Name), maxReleaseNameLen)
187		}
188		releaseNameFormat = fmt.Sprintf("%%-%ds ", maxReleaseNameLen)
189		valueFormat = "'%s'"
190	}
191
192	outputOneLine := func(variable, release, value, valueFormat string) {
193		var outStr string
194		if showVariableName {
195			outStr += fmt.Sprintf(variableNameFormat, variable)
196		}
197		if showReleaseName {
198			outStr += fmt.Sprintf(releaseNameFormat, release)
199		}
200		outStr += fmt.Sprintf(valueFormat, value)
201		fmt.Println(outStr)
202	}
203
204	for _, arg := range args {
205		if _, ok := configs.FlagArtifacts[arg]; !ok {
206			return fmt.Errorf("%s is not a defined build flag", arg)
207		}
208	}
209
210	for _, arg := range args {
211		for _, config := range releaseConfigList {
212			if isSet {
213				// If this is from the set command, format the output as:
214				// <default>           ""
215				// trunk_staging       ""
216				// trunk               ""
217				//
218				// ap1a                ""
219				// ...
220				switch {
221				case config.Name == "trunk_staging":
222					defaultValue, err := MarshalFlagDefaultValue(config, arg)
223					if err != nil {
224						return err
225					}
226					outputOneLine(arg, "<default>", defaultValue, valueFormat)
227				case config.AconfigFlagsOnly:
228					continue
229				case config.Name == "trunk":
230					fmt.Println()
231				}
232			}
233			val, err := MarshalFlagValue(config, arg)
234			if err == nil {
235				outputOneLine(arg, config.Name, val, valueFormat)
236			} else {
237				outputOneLine(arg, config.Name, "REDACTED", "%s")
238			}
239			if err == nil && isTrace {
240				for _, trace := range config.FlagArtifacts[arg].Traces {
241					fmt.Printf("  => \"%s\" in %s\n", rc_lib.MarshalValue(trace.Value), *trace.Source)
242				}
243			}
244		}
245	}
246	return nil
247}
248
249func SetCommand(configs *rc_lib.ReleaseConfigs, commonFlags Flags, cmd string, args []string) error {
250	var valueDir string
251	var redacted bool
252	var value string
253	if len(commonFlags.targetReleases) > 1 {
254		return fmt.Errorf("set command only allows one --release argument.  Got: %s", strings.Join(commonFlags.targetReleases, " "))
255	}
256	targetRelease := commonFlags.targetReleases[0]
257
258	setFlags := flag.NewFlagSet("set", flag.ExitOnError)
259	setFlags.StringVar(&valueDir, "dir", "", "Directory in which to place the value")
260	setFlags.BoolVar(&redacted, "redacted", false, "Whether the flag should be redacted")
261	setFlags.Parse(args)
262	setArgs := setFlags.Args()
263	if redacted {
264		if len(setArgs) != 1 {
265			return fmt.Errorf("set command expected '--redacted=true flag', got: --redacted=true %s", strings.Join(setArgs, " "))
266		}
267	} else if len(setArgs) != 2 {
268		return fmt.Errorf("set command expected flag and value, got: %s", strings.Join(setArgs, " "))
269	}
270	name := setArgs[0]
271	if !redacted {
272		value = setArgs[1]
273	}
274	release, err := configs.GetReleaseConfig(targetRelease)
275	targetRelease = release.Name
276	if err != nil {
277		return err
278	}
279	if release.AconfigFlagsOnly {
280		return fmt.Errorf("%s does not allow build flag overrides", targetRelease)
281	}
282	flagArtifact, ok := release.FlagArtifacts[name]
283	if !ok {
284		return fmt.Errorf("Unknown build flag %s", name)
285	}
286	if valueDir == "" {
287		mapDir, err := configs.GetFlagValueDirectory(release, flagArtifact)
288		if err != nil {
289			return err
290		}
291		valueDir = mapDir
292	}
293
294	var updatedFiles []string
295	rcPath := filepath.Join(valueDir, "release_configs", fmt.Sprintf("%s.textproto", targetRelease))
296	// Create the release config declaration only if necessary.
297	if _, err = os.Stat(rcPath); err != nil {
298		if err = os.MkdirAll(filepath.Dir(rcPath), 0775); err != nil {
299			return err
300		}
301		rcValue := &rc_proto.ReleaseConfig{
302			Name: proto.String(targetRelease),
303		}
304		err = rc_lib.WriteMessage(rcPath, rcValue)
305		if err != nil {
306			return err
307		}
308		updatedFiles = append(updatedFiles, rcPath)
309	}
310
311	flagValue := &rc_proto.FlagValue{
312		Name: proto.String(name),
313	}
314	if redacted {
315		flagValue.Redacted = proto.Bool(true)
316	} else {
317		flagValue.Value = rc_lib.UnmarshalValue(value)
318	}
319	flagPath := filepath.Join(valueDir, "flag_values", targetRelease, fmt.Sprintf("%s.textproto", name))
320	err = rc_lib.WriteMessage(flagPath, flagValue)
321	if err != nil {
322		return err
323	}
324
325	// Reload the release configs.
326	configs, err = rc_lib.ReadReleaseConfigMaps(commonFlags.maps, commonFlags.targetReleases[0], commonFlags.useGetBuildVar, commonFlags.allowMissing)
327	if err != nil {
328		return err
329	}
330	err = GetCommand(configs, commonFlags, cmd, []string{name})
331	if err != nil {
332		return err
333	}
334	updatedFiles = append(updatedFiles, flagPath)
335	fmt.Printf("\033[1mAdded/Updated: %s\033[0m\n", strings.Join(updatedFiles, " "))
336	return nil
337}
338
339func main() {
340	var commonFlags Flags
341	var configs *rc_lib.ReleaseConfigs
342	topDir, err := rc_lib.GetTopDir()
343
344	// Handle the common arguments
345	flag.StringVar(&commonFlags.top, "top", topDir, "path to top of workspace")
346	flag.BoolVar(&commonFlags.quiet, "quiet", false, "disable warning messages")
347	flag.Var(&commonFlags.maps, "map", "path to a release_config_map.textproto. may be repeated")
348	flag.StringVar(&commonFlags.outDir, "out-dir", rc_lib.GetDefaultOutDir(), "basepath for the output. Multiple formats are created")
349	flag.Var(&commonFlags.targetReleases, "release", "TARGET_RELEASE for this build")
350	flag.BoolVar(&commonFlags.allowMissing, "allow-missing", false, "Use trunk_staging values if release not found")
351	flag.BoolVar(&commonFlags.allReleases, "all-releases", false, "operate on all releases. (Ignored for set command)")
352	flag.BoolVar(&commonFlags.useGetBuildVar, "use-get-build-var", true, "use get_build_var PRODUCT_RELEASE_CONFIG_MAPS to get needed maps")
353	flag.BoolVar(&commonFlags.debug, "debug", false, "turn on debugging output for errors")
354	flag.Parse()
355
356	errorExit := func(err error) {
357		if commonFlags.debug {
358			panic(err)
359		}
360		fmt.Fprintf(os.Stderr, "%s\n", err)
361		os.Exit(1)
362	}
363
364	if commonFlags.quiet {
365		rc_lib.DisableWarnings()
366	}
367
368	if len(commonFlags.targetReleases) == 0 {
369		release, ok := os.LookupEnv("TARGET_RELEASE")
370		if ok {
371			commonFlags.targetReleases = rc_lib.StringList{release}
372		} else {
373			commonFlags.targetReleases = rc_lib.StringList{"trunk_staging"}
374		}
375	}
376
377	if err = os.Chdir(commonFlags.top); err != nil {
378		errorExit(err)
379	}
380
381	// Get the current state of flagging.
382	relName := commonFlags.targetReleases[0]
383	if relName == "--all" || relName == "-all" {
384		commonFlags.allReleases = true
385	}
386	configs, err = rc_lib.ReadReleaseConfigMaps(commonFlags.maps, relName, commonFlags.useGetBuildVar, commonFlags.allowMissing)
387	if err != nil {
388		errorExit(err)
389	}
390
391	if cmd, ok := commandMap[flag.Arg(0)]; ok {
392		args := flag.Args()
393		if err = cmd(configs, commonFlags, args[0], args[1:]); err != nil {
394			errorExit(err)
395		}
396	}
397}
398