1// Copyright 2024 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package release_config_lib
16
17import (
18	"cmp"
19	"fmt"
20	"io/fs"
21	"os"
22	"path/filepath"
23	"slices"
24	"strings"
25
26	rc_proto "android/soong/cmd/release_config/release_config_proto"
27
28	"google.golang.org/protobuf/proto"
29)
30
31// A single release_config_map.textproto and its associated data.
32// Used primarily for debugging.
33type ReleaseConfigMap struct {
34	// The path to this release_config_map file.
35	path string
36
37	// Data received
38	proto rc_proto.ReleaseConfigMap
39
40	// Map of name:contribution for release config contributions.
41	ReleaseConfigContributions map[string]*ReleaseConfigContribution
42
43	// Flags declared this directory's flag_declarations/*.textproto
44	FlagDeclarations []rc_proto.FlagDeclaration
45}
46
47type ReleaseConfigDirMap map[string]int
48
49// The generated release configs.
50type ReleaseConfigs struct {
51	// Ordered list of release config maps processed.
52	ReleaseConfigMaps []*ReleaseConfigMap
53
54	// Aliases
55	Aliases map[string]*string
56
57	// Dictionary of flag_name:FlagDeclaration, with no overrides applied.
58	FlagArtifacts FlagArtifacts
59
60	// Generated release configs artifact
61	Artifact rc_proto.ReleaseConfigsArtifact
62
63	// Dictionary of name:ReleaseConfig
64	// Use `GetReleaseConfigs(name)` to get a release config.
65	ReleaseConfigs map[string]*ReleaseConfig
66
67	// Map of directory to *ReleaseConfigMap
68	releaseConfigMapsMap map[string]*ReleaseConfigMap
69
70	// The files used by all release configs
71	FilesUsedMap map[string]bool
72
73	// The list of config directories used.
74	configDirs []string
75
76	// A map from the config directory to its order in the list of config
77	// directories.
78	configDirIndexes ReleaseConfigDirMap
79
80	// True if we should allow a missing primary release config.  In this
81	// case, we will substitute `trunk_staging` values, but the release
82	// config will not be in ALL_RELEASE_CONFIGS_FOR_PRODUCT.
83	allowMissing bool
84}
85
86func (configs *ReleaseConfigs) WriteInheritanceGraph(outFile string) error {
87	data := []string{}
88	usedAliases := make(map[string]bool)
89	priorStages := make(map[string][]string)
90	for _, config := range configs.ReleaseConfigs {
91		if config.Name == "root" {
92			continue
93		}
94		var fillColor string
95		inherits := []string{}
96		for _, inherit := range config.InheritNames {
97			if inherit == "root" {
98				continue
99			}
100			data = append(data, fmt.Sprintf(`"%s" -> "%s"`, config.Name, inherit))
101			inherits = append(inherits, inherit)
102			// If inheriting an alias, add a link from the alias to that release config.
103			if name, found := configs.Aliases[inherit]; found {
104				if !usedAliases[inherit] {
105					usedAliases[inherit] = true
106					data = append(data, fmt.Sprintf(`"%s" -> "%s"`, inherit, *name))
107					data = append(data,
108						fmt.Sprintf(`"%s" [ label="%s\ncurrently: %s" shape=oval ]`,
109							inherit, inherit, *name))
110				}
111			}
112		}
113		// Add links for all of the advancement progressions.
114		for priorStage := range config.PriorStagesMap {
115			data = append(data, fmt.Sprintf(`"%s" -> "%s" [ style=dashed color="#81c995" ]`,
116				priorStage, config.Name))
117			priorStages[config.Name] = append(priorStages[config.Name], priorStage)
118		}
119		label := config.Name
120		if len(inherits) > 0 {
121			label += "\\ninherits: " + strings.Join(inherits, " ")
122		}
123		if len(config.OtherNames) > 0 {
124			label += "\\nother names: " + strings.Join(config.OtherNames, " ")
125		}
126		switch config.Name {
127		case *configs.Artifact.ReleaseConfig.Name:
128			// The active release config has a light blue fill.
129			fillColor = `fillcolor="#d2e3fc" `
130		case "trunk", "trunk_staging":
131			// Certain workflow stages have a light green fill.
132			fillColor = `fillcolor="#ceead6" `
133		default:
134			// Look for "next" and "*_next", make them light green as well.
135			for _, n := range config.OtherNames {
136				if n == "next" || strings.HasSuffix(n, "_next") {
137					fillColor = `fillcolor="#ceead6" `
138				}
139			}
140		}
141		data = append(data,
142			fmt.Sprintf(`"%s" [ label="%s" %s]`, config.Name, label, fillColor))
143	}
144	slices.Sort(data)
145	data = append([]string{
146		"digraph {",
147		"graph [ ratio=.5 ]",
148		"node [ shape=box style=filled fillcolor=white colorscheme=svg fontcolor=black ]",
149	}, data...)
150	data = append(data, "}")
151	return os.WriteFile(outFile, []byte(strings.Join(data, "\n")), 0644)
152}
153
154// Write the "all_release_configs" artifact.
155//
156// The file will be in "{outDir}/all_release_configs-{product}.{format}"
157//
158// Args:
159//
160//	outDir string: directory path. Will be created if not present.
161//	product string: TARGET_PRODUCT for the release_configs.
162//	format string: one of "json", "pb", or "textproto"
163//
164// Returns:
165//
166//	error: Any error encountered.
167func (configs *ReleaseConfigs) WriteArtifact(outDir, product, format string) error {
168	return WriteMessage(
169		filepath.Join(outDir, fmt.Sprintf("all_release_configs-%s.%s", product, format)),
170		&configs.Artifact)
171}
172
173func ReleaseConfigsFactory() (c *ReleaseConfigs) {
174	configs := ReleaseConfigs{
175		Aliases:              make(map[string]*string),
176		FlagArtifacts:        make(map[string]*FlagArtifact),
177		ReleaseConfigs:       make(map[string]*ReleaseConfig),
178		releaseConfigMapsMap: make(map[string]*ReleaseConfigMap),
179		configDirs:           []string{},
180		configDirIndexes:     make(ReleaseConfigDirMap),
181		FilesUsedMap:         make(map[string]bool),
182	}
183	workflowManual := rc_proto.Workflow(rc_proto.Workflow_MANUAL)
184	releaseAconfigValueSets := FlagArtifact{
185		FlagDeclaration: &rc_proto.FlagDeclaration{
186			Name:        proto.String("RELEASE_ACONFIG_VALUE_SETS"),
187			Namespace:   proto.String("android_UNKNOWN"),
188			Description: proto.String("Aconfig value sets assembled by release-config"),
189			Workflow:    &workflowManual,
190			Containers:  []string{"system", "system_ext", "product", "vendor"},
191			Value:       &rc_proto.Value{Val: &rc_proto.Value_UnspecifiedValue{false}},
192		},
193		DeclarationIndex: -1,
194		Traces:           []*rc_proto.Tracepoint{},
195	}
196	configs.FlagArtifacts["RELEASE_ACONFIG_VALUE_SETS"] = &releaseAconfigValueSets
197	return &configs
198}
199
200func (configs *ReleaseConfigs) GetSortedReleaseConfigs() (ret []*ReleaseConfig) {
201	for _, config := range configs.ReleaseConfigs {
202		ret = append(ret, config)
203	}
204	slices.SortFunc(ret, func(a, b *ReleaseConfig) int {
205		return cmp.Compare(a.Name, b.Name)
206	})
207	return ret
208}
209
210func ReleaseConfigMapFactory(protoPath string) (m *ReleaseConfigMap) {
211	m = &ReleaseConfigMap{
212		path:                       protoPath,
213		ReleaseConfigContributions: make(map[string]*ReleaseConfigContribution),
214	}
215	if protoPath != "" {
216		LoadMessage(protoPath, &m.proto)
217	}
218	return m
219}
220
221// Find the top of the release config contribution directory.
222// Returns the parent of the flag_declarations and flag_values directories.
223func (configs *ReleaseConfigs) GetDirIndex(path string) (int, error) {
224	for p := path; p != "."; p = filepath.Dir(p) {
225		if idx, ok := configs.configDirIndexes[p]; ok {
226			return idx, nil
227		}
228	}
229	return -1, fmt.Errorf("Could not determine release config directory from %s", path)
230}
231
232// Determine the default directory for writing a flag value.
233//
234// Returns the path of the highest-Indexed one of:
235//   - Where the flag is declared
236//   - Where the release config is first declared
237//   - The last place the value is being written.
238func (configs *ReleaseConfigs) GetFlagValueDirectory(config *ReleaseConfig, flag *FlagArtifact) (string, error) {
239	current, err := configs.GetDirIndex(*flag.Traces[len(flag.Traces)-1].Source)
240	if err != nil {
241		return "", err
242	}
243	index := max(flag.DeclarationIndex, config.DeclarationIndex, current)
244	return configs.configDirs[index], nil
245}
246
247func (configs *ReleaseConfigs) LoadReleaseConfigMap(path string, ConfigDirIndex int) error {
248	if _, err := os.Stat(path); err != nil {
249		return fmt.Errorf("%s does not exist\n", path)
250	}
251	m := ReleaseConfigMapFactory(path)
252	if m.proto.DefaultContainers == nil {
253		return fmt.Errorf("Release config map %s lacks default_containers", path)
254	}
255	for _, container := range m.proto.DefaultContainers {
256		if !validContainer(container) {
257			return fmt.Errorf("Release config map %s has invalid container %s", path, container)
258		}
259	}
260	configs.FilesUsedMap[path] = true
261	dir := filepath.Dir(path)
262	// Record any aliases, checking for duplicates.
263	for _, alias := range m.proto.Aliases {
264		name := *alias.Name
265		oldTarget, ok := configs.Aliases[name]
266		if ok {
267			if *oldTarget != *alias.Target {
268				return fmt.Errorf("Conflicting alias declarations: %s vs %s",
269					*oldTarget, *alias.Target)
270			}
271		}
272		configs.Aliases[name] = alias.Target
273	}
274	var err error
275	err = WalkTextprotoFiles(dir, "flag_declarations", func(path string, d fs.DirEntry, err error) error {
276		flagDeclaration := FlagDeclarationFactory(path)
277		// Container must be specified.
278		if flagDeclaration.Containers == nil {
279			flagDeclaration.Containers = m.proto.DefaultContainers
280		} else {
281			for _, container := range flagDeclaration.Containers {
282				if !validContainer(container) {
283					return fmt.Errorf("Flag declaration %s has invalid container %s", path, container)
284				}
285			}
286		}
287
288		// TODO: once we have namespaces initialized, we can throw an error here.
289		if flagDeclaration.Namespace == nil {
290			flagDeclaration.Namespace = proto.String("android_UNKNOWN")
291		}
292		// If the input didn't specify a value, create one (== UnspecifiedValue).
293		if flagDeclaration.Value == nil {
294			flagDeclaration.Value = &rc_proto.Value{Val: &rc_proto.Value_UnspecifiedValue{false}}
295		}
296		m.FlagDeclarations = append(m.FlagDeclarations, *flagDeclaration)
297		name := *flagDeclaration.Name
298		if name == "RELEASE_ACONFIG_VALUE_SETS" {
299			return fmt.Errorf("%s: %s is a reserved build flag", path, name)
300		}
301		if def, ok := configs.FlagArtifacts[name]; !ok {
302			configs.FlagArtifacts[name] = &FlagArtifact{FlagDeclaration: flagDeclaration, DeclarationIndex: ConfigDirIndex}
303		} else if !proto.Equal(def.FlagDeclaration, flagDeclaration) {
304			return fmt.Errorf("Duplicate definition of %s", *flagDeclaration.Name)
305		}
306		// Set the initial value in the flag artifact.
307		configs.FilesUsedMap[path] = true
308		configs.FlagArtifacts[name].UpdateValue(
309			FlagValue{path: path, proto: rc_proto.FlagValue{
310				Name: proto.String(name), Value: flagDeclaration.Value}})
311		if configs.FlagArtifacts[name].Redacted {
312			return fmt.Errorf("%s may not be redacted by default.", name)
313		}
314		return nil
315	})
316	if err != nil {
317		return err
318	}
319
320	err = WalkTextprotoFiles(dir, "release_configs", func(path string, d fs.DirEntry, err error) error {
321		releaseConfigContribution := &ReleaseConfigContribution{path: path, DeclarationIndex: ConfigDirIndex}
322		LoadMessage(path, &releaseConfigContribution.proto)
323		name := *releaseConfigContribution.proto.Name
324		if fmt.Sprintf("%s.textproto", name) != filepath.Base(path) {
325			return fmt.Errorf("%s incorrectly declares release config %s", path, name)
326		}
327		if _, ok := configs.ReleaseConfigs[name]; !ok {
328			configs.ReleaseConfigs[name] = ReleaseConfigFactory(name, ConfigDirIndex)
329		}
330		config := configs.ReleaseConfigs[name]
331		config.FilesUsedMap[path] = true
332		inheritNames := make(map[string]bool)
333		for _, inh := range config.InheritNames {
334			inheritNames[inh] = true
335		}
336		// If this contribution says to inherit something we already inherited, we do not want the duplicate.
337		for _, cInh := range releaseConfigContribution.proto.Inherits {
338			if !inheritNames[cInh] {
339				config.InheritNames = append(config.InheritNames, cInh)
340				inheritNames[cInh] = true
341			}
342		}
343
344		// Only walk flag_values/{RELEASE} for defined releases.
345		err2 := WalkTextprotoFiles(dir, filepath.Join("flag_values", name), func(path string, d fs.DirEntry, err error) error {
346			flagValue := FlagValueFactory(path)
347			if fmt.Sprintf("%s.textproto", *flagValue.proto.Name) != filepath.Base(path) {
348				return fmt.Errorf("%s incorrectly sets value for flag %s", path, *flagValue.proto.Name)
349			}
350			if *flagValue.proto.Name == "RELEASE_ACONFIG_VALUE_SETS" {
351				return fmt.Errorf("%s: %s is a reserved build flag", path, *flagValue.proto.Name)
352			}
353			config.FilesUsedMap[path] = true
354			releaseConfigContribution.FlagValues = append(releaseConfigContribution.FlagValues, flagValue)
355			return nil
356		})
357		if err2 != nil {
358			return err2
359		}
360		if releaseConfigContribution.proto.GetAconfigFlagsOnly() {
361			config.AconfigFlagsOnly = true
362		}
363		m.ReleaseConfigContributions[name] = releaseConfigContribution
364		config.Contributions = append(config.Contributions, releaseConfigContribution)
365		return nil
366	})
367	if err != nil {
368		return err
369	}
370	configs.ReleaseConfigMaps = append(configs.ReleaseConfigMaps, m)
371	configs.releaseConfigMapsMap[dir] = m
372	return nil
373}
374
375func (configs *ReleaseConfigs) GetReleaseConfig(name string) (*ReleaseConfig, error) {
376	trace := []string{name}
377	for target, ok := configs.Aliases[name]; ok; target, ok = configs.Aliases[name] {
378		name = *target
379		trace = append(trace, name)
380	}
381	if config, ok := configs.ReleaseConfigs[name]; ok {
382		return config, nil
383	}
384	if configs.allowMissing {
385		if config, ok := configs.ReleaseConfigs["trunk_staging"]; ok {
386			return config, nil
387		}
388	}
389	return nil, fmt.Errorf("Missing config %s.  Trace=%v", name, trace)
390}
391
392func (configs *ReleaseConfigs) GetAllReleaseNames() []string {
393	var allReleaseNames []string
394	for _, v := range configs.ReleaseConfigs {
395		allReleaseNames = append(allReleaseNames, v.Name)
396		allReleaseNames = append(allReleaseNames, v.OtherNames...)
397	}
398	slices.Sort(allReleaseNames)
399	return allReleaseNames
400}
401
402func (configs *ReleaseConfigs) GenerateReleaseConfigs(targetRelease string) error {
403	otherNames := make(map[string][]string)
404	for aliasName, aliasTarget := range configs.Aliases {
405		if _, ok := configs.ReleaseConfigs[aliasName]; ok {
406			return fmt.Errorf("Alias %s is a declared release config", aliasName)
407		}
408		if _, ok := configs.ReleaseConfigs[*aliasTarget]; !ok {
409			if _, ok2 := configs.Aliases[*aliasTarget]; !ok2 {
410				return fmt.Errorf("Alias %s points to non-existing config %s", aliasName, *aliasTarget)
411			}
412		}
413		otherNames[*aliasTarget] = append(otherNames[*aliasTarget], aliasName)
414	}
415	for name, aliases := range otherNames {
416		configs.ReleaseConfigs[name].OtherNames = aliases
417	}
418
419	sortedReleaseConfigs := configs.GetSortedReleaseConfigs()
420	for _, c := range sortedReleaseConfigs {
421		err := c.GenerateReleaseConfig(configs)
422		if err != nil {
423			return err
424		}
425	}
426
427	releaseConfig, err := configs.GetReleaseConfig(targetRelease)
428	if err != nil {
429		return err
430	}
431	orc := []*rc_proto.ReleaseConfigArtifact{}
432	for _, c := range sortedReleaseConfigs {
433		if c.Name != releaseConfig.Name {
434			orc = append(orc, c.ReleaseConfigArtifact)
435		}
436	}
437
438	configs.Artifact = rc_proto.ReleaseConfigsArtifact{
439		ReleaseConfig:       releaseConfig.ReleaseConfigArtifact,
440		OtherReleaseConfigs: orc,
441		ReleaseConfigMapsMap: func() map[string]*rc_proto.ReleaseConfigMap {
442			ret := make(map[string]*rc_proto.ReleaseConfigMap)
443			for k, v := range configs.releaseConfigMapsMap {
444				ret[k] = &v.proto
445			}
446			return ret
447		}(),
448	}
449	return nil
450}
451
452func ReadReleaseConfigMaps(releaseConfigMapPaths StringList, targetRelease string, useBuildVar, allowMissing bool) (*ReleaseConfigs, error) {
453	var err error
454
455	if len(releaseConfigMapPaths) == 0 {
456		releaseConfigMapPaths, err = GetDefaultMapPaths(useBuildVar)
457		if err != nil {
458			return nil, err
459		}
460		if len(releaseConfigMapPaths) == 0 {
461			return nil, fmt.Errorf("No maps found")
462		}
463		if !useBuildVar {
464			warnf("No --map argument provided.  Using: --map %s\n", strings.Join(releaseConfigMapPaths, " --map "))
465		}
466	}
467
468	configs := ReleaseConfigsFactory()
469	configs.allowMissing = allowMissing
470	mapsRead := make(map[string]bool)
471	var idx int
472	for _, releaseConfigMapPath := range releaseConfigMapPaths {
473		// Maintain an ordered list of release config directories.
474		configDir := filepath.Dir(releaseConfigMapPath)
475		if mapsRead[configDir] {
476			continue
477		}
478		mapsRead[configDir] = true
479		configs.configDirIndexes[configDir] = idx
480		configs.configDirs = append(configs.configDirs, configDir)
481		// Force the path to be the textproto path, so that both the scl and textproto formats can coexist.
482		releaseConfigMapPath = filepath.Join(configDir, "release_config_map.textproto")
483		err = configs.LoadReleaseConfigMap(releaseConfigMapPath, idx)
484		if err != nil {
485			return nil, err
486		}
487		idx += 1
488	}
489
490	// Now that we have all of the release config maps, can meld them and generate the artifacts.
491	err = configs.GenerateReleaseConfigs(targetRelease)
492	return configs, err
493}
494