• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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	// Potential aconfig and build flag contributions in this map directory.
47	// This is used to detect errors.
48	FlagValueDirs map[string][]string
49}
50
51type ReleaseConfigDirMap map[string]int
52
53// The generated release configs.
54type ReleaseConfigs struct {
55	// Ordered list of release config maps processed.
56	ReleaseConfigMaps []*ReleaseConfigMap
57
58	// Aliases
59	Aliases map[string]*string
60
61	// Dictionary of flag_name:FlagDeclaration, with no overrides applied.
62	FlagArtifacts FlagArtifacts
63
64	// Generated release configs artifact
65	Artifact rc_proto.ReleaseConfigsArtifact
66
67	// Dictionary of name:ReleaseConfig
68	// Use `GetReleaseConfigs(name)` to get a release config.
69	ReleaseConfigs map[string]*ReleaseConfig
70
71	// Map of directory to *ReleaseConfigMap
72	releaseConfigMapsMap map[string]*ReleaseConfigMap
73
74	// The files used by all release configs
75	FilesUsedMap map[string]bool
76
77	// The list of config directories used.
78	configDirs []string
79
80	// A map from the config directory to its order in the list of config
81	// directories.
82	configDirIndexes ReleaseConfigDirMap
83
84	// True if we should allow a missing primary release config.  In this
85	// case, we will substitute `trunk_staging` values, but the release
86	// config will not be in ALL_RELEASE_CONFIGS_FOR_PRODUCT.
87	allowMissing bool
88}
89
90func (configs *ReleaseConfigs) WriteInheritanceGraph(outFile string) error {
91	data := []string{}
92	usedAliases := make(map[string]bool)
93	priorStages := make(map[string][]string)
94	for _, config := range configs.ReleaseConfigs {
95		if config.Name == "root" {
96			continue
97		}
98		var fillColor string
99		inherits := []string{}
100		for _, inherit := range config.InheritNames {
101			if inherit == "root" {
102				continue
103			}
104			data = append(data, fmt.Sprintf(`"%s" -> "%s"`, config.Name, inherit))
105			inherits = append(inherits, inherit)
106			// If inheriting an alias, add a link from the alias to that release config.
107			if name, found := configs.Aliases[inherit]; found {
108				if !usedAliases[inherit] {
109					usedAliases[inherit] = true
110					data = append(data, fmt.Sprintf(`"%s" -> "%s"`, inherit, *name))
111					data = append(data,
112						fmt.Sprintf(`"%s" [ label="%s\ncurrently: %s" shape=oval ]`,
113							inherit, inherit, *name))
114				}
115			}
116		}
117		// Add links for all of the advancement progressions.
118		for priorStage := range config.PriorStagesMap {
119			data = append(data, fmt.Sprintf(`"%s" -> "%s" [ style=dashed color="#81c995" ]`,
120				priorStage, config.Name))
121			priorStages[config.Name] = append(priorStages[config.Name], priorStage)
122		}
123		label := config.Name
124		if len(inherits) > 0 {
125			label += "\\ninherits: " + strings.Join(inherits, " ")
126		}
127		if len(config.OtherNames) > 0 {
128			label += "\\nother names: " + strings.Join(config.OtherNames, " ")
129		}
130		switch config.Name {
131		case *configs.Artifact.ReleaseConfig.Name:
132			// The active release config has a light blue fill.
133			fillColor = `fillcolor="#d2e3fc" `
134		case "trunk", "trunk_staging":
135			// Certain workflow stages have a light green fill.
136			fillColor = `fillcolor="#ceead6" `
137		default:
138			// Look for "next" and "*_next", make them light green as well.
139			for _, n := range config.OtherNames {
140				if n == "next" || strings.HasSuffix(n, "_next") {
141					fillColor = `fillcolor="#ceead6" `
142				}
143			}
144		}
145		data = append(data,
146			fmt.Sprintf(`"%s" [ label="%s" %s]`, config.Name, label, fillColor))
147	}
148	slices.Sort(data)
149	data = append([]string{
150		"digraph {",
151		"graph [ ratio=.5 ]",
152		"node [ shape=box style=filled fillcolor=white colorscheme=svg fontcolor=black ]",
153	}, data...)
154	data = append(data, "}")
155	return os.WriteFile(outFile, []byte(strings.Join(data, "\n")), 0644)
156}
157
158// Write the "all_release_configs" artifact.
159//
160// The file will be in "{outDir}/all_release_configs-{product}.{format}"
161//
162// Args:
163//
164//	outDir string: directory path. Will be created if not present.
165//	product string: TARGET_PRODUCT for the release_configs.
166//	format string: one of "json", "pb", or "textproto"
167//
168// Returns:
169//
170//	error: Any error encountered.
171func (configs *ReleaseConfigs) WriteArtifact(outDir, product, format string) error {
172	return WriteMessage(
173		filepath.Join(outDir, fmt.Sprintf("all_release_configs-%s.%s", product, format)),
174		&configs.Artifact)
175}
176
177func ReleaseConfigsFactory() (c *ReleaseConfigs) {
178	configs := ReleaseConfigs{
179		Aliases:              make(map[string]*string),
180		FlagArtifacts:        make(map[string]*FlagArtifact),
181		ReleaseConfigs:       make(map[string]*ReleaseConfig),
182		releaseConfigMapsMap: make(map[string]*ReleaseConfigMap),
183		configDirs:           []string{},
184		configDirIndexes:     make(ReleaseConfigDirMap),
185		FilesUsedMap:         make(map[string]bool),
186	}
187	workflowManual := rc_proto.Workflow(rc_proto.Workflow_MANUAL)
188	releaseAconfigValueSets := FlagArtifact{
189		FlagDeclaration: &rc_proto.FlagDeclaration{
190			Name:        proto.String("RELEASE_ACONFIG_VALUE_SETS"),
191			Namespace:   proto.String("android_UNKNOWN"),
192			Description: proto.String("Aconfig value sets assembled by release-config"),
193			Workflow:    &workflowManual,
194			Containers:  []string{"system", "system_ext", "product", "vendor"},
195			Value:       &rc_proto.Value{Val: &rc_proto.Value_UnspecifiedValue{false}},
196		},
197		DeclarationIndex: -1,
198		Traces:           []*rc_proto.Tracepoint{},
199	}
200	configs.FlagArtifacts["RELEASE_ACONFIG_VALUE_SETS"] = &releaseAconfigValueSets
201	return &configs
202}
203
204func (configs *ReleaseConfigs) GetSortedReleaseConfigs() (ret []*ReleaseConfig) {
205	for _, config := range configs.ReleaseConfigs {
206		ret = append(ret, config)
207	}
208	slices.SortFunc(ret, func(a, b *ReleaseConfig) int {
209		return cmp.Compare(a.Name, b.Name)
210	})
211	return ret
212}
213
214func ReleaseConfigMapFactory(protoPath string) (m *ReleaseConfigMap) {
215	m = &ReleaseConfigMap{
216		path:                       protoPath,
217		ReleaseConfigContributions: make(map[string]*ReleaseConfigContribution),
218	}
219	if protoPath != "" {
220		LoadMessage(protoPath, &m.proto)
221	}
222	return m
223}
224
225// Find the top of the release config contribution directory.
226// Returns the parent of the flag_declarations and flag_values directories.
227func (configs *ReleaseConfigs) GetDirIndex(path string) (int, error) {
228	for p := path; p != "."; p = filepath.Dir(p) {
229		if idx, ok := configs.configDirIndexes[p]; ok {
230			return idx, nil
231		}
232	}
233	return -1, fmt.Errorf("Could not determine release config directory from %s", path)
234}
235
236// Determine the default directory for writing a flag value.
237//
238// Returns the path of the highest-Indexed one of:
239//   - Where the flag is declared
240//   - Where the release config is first declared
241//   - The last place the value is being written.
242func (configs *ReleaseConfigs) GetFlagValueDirectory(config *ReleaseConfig, flag *FlagArtifact) (string, error) {
243	current, err := configs.GetDirIndex(*flag.Traces[len(flag.Traces)-1].Source)
244	if err != nil {
245		return "", err
246	}
247	index := max(flag.DeclarationIndex, config.DeclarationIndex, current)
248	return configs.configDirs[index], nil
249}
250
251// Return the (unsorted) release configs contributed to by `dir`.
252func EnumerateReleaseConfigs(dir string) ([]string, error) {
253	var ret []string
254	err := WalkTextprotoFiles(dir, "release_configs", func(path string, d fs.DirEntry, err error) error {
255		// Strip off the trailing `.textproto` from the name.
256		name := filepath.Base(path)
257		ret = append(ret, name[:len(name)-10])
258		return err
259	})
260	return ret, err
261}
262
263func (configs *ReleaseConfigs) LoadReleaseConfigMap(path string, ConfigDirIndex int) error {
264	if _, err := os.Stat(path); err != nil {
265		return fmt.Errorf("%s does not exist\n", path)
266	}
267	m := ReleaseConfigMapFactory(path)
268	if m.proto.DefaultContainers == nil {
269		return fmt.Errorf("Release config map %s lacks default_containers", path)
270	}
271	for _, container := range m.proto.DefaultContainers {
272		if !validContainer(container) {
273			return fmt.Errorf("Release config map %s has invalid container %s", path, container)
274		}
275	}
276	configs.FilesUsedMap[path] = true
277	dir := filepath.Dir(path)
278	// Record any aliases, checking for duplicates.
279	for _, alias := range m.proto.Aliases {
280		name := *alias.Name
281		oldTarget, ok := configs.Aliases[name]
282		if ok {
283			if *oldTarget != *alias.Target {
284				return fmt.Errorf("Conflicting alias declarations: %s vs %s",
285					*oldTarget, *alias.Target)
286			}
287		}
288		configs.Aliases[name] = alias.Target
289	}
290	var err error
291	// Temporarily allowlist duplicate flag declaration files to prevent
292	// more from entering the tree while we work to clean up the duplicates
293	// that already exist.
294	dupFlagFile := filepath.Join(dir, "duplicate_allowlist.txt")
295	data, err := os.ReadFile(dupFlagFile)
296	if err == nil {
297		for _, flag := range strings.Split(string(data), "\n") {
298			flag = strings.TrimSpace(flag)
299			if strings.HasPrefix(flag, "//") || strings.HasPrefix(flag, "#") {
300				continue
301			}
302			DuplicateDeclarationAllowlist[flag] = true
303		}
304	}
305	err = WalkTextprotoFiles(dir, "flag_declarations", func(path string, d fs.DirEntry, err error) error {
306		flagDeclaration := FlagDeclarationFactory(path)
307		// Container must be specified.
308		if flagDeclaration.Containers == nil {
309			flagDeclaration.Containers = m.proto.DefaultContainers
310		} else {
311			for _, container := range flagDeclaration.Containers {
312				if !validContainer(container) {
313					return fmt.Errorf("Flag declaration %s has invalid container %s", path, container)
314				}
315			}
316		}
317		if flagDeclaration.Namespace == nil {
318			return fmt.Errorf("Flag declaration %s has no namespace.", path)
319		}
320
321		m.FlagDeclarations = append(m.FlagDeclarations, *flagDeclaration)
322		name := *flagDeclaration.Name
323		if name == "RELEASE_ACONFIG_VALUE_SETS" {
324			return fmt.Errorf("%s: %s is a reserved build flag", path, name)
325		}
326		if def, ok := configs.FlagArtifacts[name]; !ok {
327			configs.FlagArtifacts[name] = &FlagArtifact{FlagDeclaration: flagDeclaration, DeclarationIndex: ConfigDirIndex}
328		} else if !proto.Equal(def.FlagDeclaration, flagDeclaration) || !DuplicateDeclarationAllowlist[name] {
329			return fmt.Errorf("Duplicate definition of %s in %s", *flagDeclaration.Name, path)
330		}
331		// Set the initial value in the flag artifact.
332		configs.FilesUsedMap[path] = true
333		configs.FlagArtifacts[name].UpdateValue(
334			FlagValue{path: path, proto: rc_proto.FlagValue{
335				Name: proto.String(name), Value: flagDeclaration.Value}})
336		if configs.FlagArtifacts[name].Redacted {
337			return fmt.Errorf("%s may not be redacted by default.", name)
338		}
339		return nil
340	})
341	if err != nil {
342		return err
343	}
344
345	subDirs := func(subdir string) (ret []string) {
346		if flagVersions, err := os.ReadDir(filepath.Join(dir, subdir)); err == nil {
347			for _, e := range flagVersions {
348				if e.IsDir() && validReleaseConfigName(e.Name()) {
349					ret = append(ret, e.Name())
350				}
351			}
352		}
353		return
354	}
355	m.FlagValueDirs = map[string][]string{
356		"aconfig":     subDirs("aconfig"),
357		"flag_values": subDirs("flag_values"),
358	}
359
360	err = WalkTextprotoFiles(dir, "release_configs", func(path string, d fs.DirEntry, err error) error {
361		releaseConfigContribution := &ReleaseConfigContribution{path: path, DeclarationIndex: ConfigDirIndex}
362		LoadMessage(path, &releaseConfigContribution.proto)
363		name := *releaseConfigContribution.proto.Name
364		if fmt.Sprintf("%s.textproto", name) != filepath.Base(path) {
365			return fmt.Errorf("%s incorrectly declares release config %s", path, name)
366		}
367		releaseConfigType := releaseConfigContribution.proto.ReleaseConfigType
368		if releaseConfigType == nil {
369			if name == "root" {
370				releaseConfigType = rc_proto.ReleaseConfigType_EXPLICIT_INHERITANCE_CONFIG.Enum()
371			} else {
372				releaseConfigType = rc_proto.ReleaseConfigType_RELEASE_CONFIG.Enum()
373			}
374		}
375		if _, ok := configs.ReleaseConfigs[name]; !ok {
376			configs.ReleaseConfigs[name] = ReleaseConfigFactory(name, ConfigDirIndex)
377			configs.ReleaseConfigs[name].ReleaseConfigType = *releaseConfigType
378		}
379		config := configs.ReleaseConfigs[name]
380		if config.ReleaseConfigType != *releaseConfigType {
381			return fmt.Errorf("%s mismatching ReleaseConfigType value %s", path, *releaseConfigType)
382		}
383		config.FilesUsedMap[path] = true
384		config.DisallowLunchUse = config.DisallowLunchUse || releaseConfigContribution.proto.GetDisallowLunchUse()
385		inheritNames := make(map[string]bool)
386		for _, inh := range config.InheritNames {
387			inheritNames[inh] = true
388		}
389		// If this contribution says to inherit something we already inherited, we do not want the duplicate.
390		for _, cInh := range releaseConfigContribution.proto.Inherits {
391			if !inheritNames[cInh] {
392				config.InheritNames = append(config.InheritNames, cInh)
393				inheritNames[cInh] = true
394			}
395		}
396
397		// Only walk flag_values/{RELEASE} for defined releases.
398		err2 := WalkTextprotoFiles(dir, filepath.Join("flag_values", name), func(path string, d fs.DirEntry, err error) error {
399			flagValue := FlagValueFactory(path)
400			if fmt.Sprintf("%s.textproto", *flagValue.proto.Name) != filepath.Base(path) {
401				return fmt.Errorf("%s incorrectly sets value for flag %s", path, *flagValue.proto.Name)
402			}
403			if *flagValue.proto.Name == "RELEASE_ACONFIG_VALUE_SETS" {
404				return fmt.Errorf("%s: %s is a reserved build flag", path, *flagValue.proto.Name)
405			}
406			config.FilesUsedMap[path] = true
407			releaseConfigContribution.FlagValues = append(releaseConfigContribution.FlagValues, flagValue)
408			return nil
409		})
410		if err2 != nil {
411			return err2
412		}
413		if releaseConfigContribution.proto.GetAconfigFlagsOnly() {
414			config.AconfigFlagsOnly = true
415		}
416		m.ReleaseConfigContributions[name] = releaseConfigContribution
417		config.Contributions = append(config.Contributions, releaseConfigContribution)
418		return nil
419	})
420	if err != nil {
421		return err
422	}
423	configs.ReleaseConfigMaps = append(configs.ReleaseConfigMaps, m)
424	configs.releaseConfigMapsMap[dir] = m
425	return nil
426}
427
428func (configs *ReleaseConfigs) GetReleaseConfig(name string) (*ReleaseConfig, error) {
429	return configs.getReleaseConfig(name, configs.allowMissing)
430}
431
432func (configs *ReleaseConfigs) GetReleaseConfigStrict(name string) (*ReleaseConfig, error) {
433	return configs.getReleaseConfig(name, false)
434}
435
436func (configs *ReleaseConfigs) getReleaseConfig(name string, allow_missing bool) (*ReleaseConfig, error) {
437	trace := []string{name}
438	for target, ok := configs.Aliases[name]; ok; target, ok = configs.Aliases[name] {
439		name = *target
440		trace = append(trace, name)
441	}
442	if config, ok := configs.ReleaseConfigs[name]; ok {
443		return config, nil
444	}
445	if allow_missing {
446		if config, ok := configs.ReleaseConfigs["trunk_staging"]; ok {
447			return config, nil
448		}
449	}
450	return nil, fmt.Errorf("Missing config %s.  Trace=%v", name, trace)
451}
452
453func (configs *ReleaseConfigs) GetAllReleaseNames() []string {
454	var allReleaseNames []string
455	for _, v := range configs.ReleaseConfigs {
456		if v.isConfigListable() {
457			allReleaseNames = append(allReleaseNames, v.Name)
458			allReleaseNames = append(allReleaseNames, v.OtherNames...)
459		}
460	}
461	slices.Sort(allReleaseNames)
462	return allReleaseNames
463}
464
465func (configs *ReleaseConfigs) GenerateReleaseConfigs(targetRelease string) error {
466	otherNames := make(map[string][]string)
467	for aliasName, aliasTarget := range configs.Aliases {
468		if _, ok := configs.ReleaseConfigs[aliasName]; ok {
469			return fmt.Errorf("Alias %s is a declared release config", aliasName)
470		}
471		if _, ok := configs.ReleaseConfigs[*aliasTarget]; !ok {
472			if _, ok2 := configs.Aliases[*aliasTarget]; !ok2 {
473				return fmt.Errorf("Alias %s points to non-existing config %s", aliasName, *aliasTarget)
474			}
475		}
476		otherNames[*aliasTarget] = append(otherNames[*aliasTarget], aliasName)
477	}
478	for name, aliases := range otherNames {
479		configs.ReleaseConfigs[name].OtherNames = aliases
480	}
481
482	sortedReleaseConfigs := configs.GetSortedReleaseConfigs()
483	for _, c := range sortedReleaseConfigs {
484		err := c.GenerateReleaseConfig(configs)
485		if err != nil {
486			return err
487		}
488	}
489
490	// Look for ignored flagging values.  Gather the entire list to make it easier to fix them.
491	errors := []string{}
492	for _, contrib := range configs.ReleaseConfigMaps {
493		dirName := filepath.Dir(contrib.path)
494		for k, names := range contrib.FlagValueDirs {
495			for _, rcName := range names {
496				if config, err := configs.getReleaseConfig(rcName, false); err == nil {
497					rcPath := filepath.Join(dirName, "release_configs", fmt.Sprintf("%s.textproto", config.Name))
498					if _, err := os.Stat(rcPath); err != nil {
499						errors = append(errors, fmt.Sprintf("%s exists but %s does not contribute to %s",
500							filepath.Join(dirName, k, rcName), dirName, config.Name))
501					}
502				}
503
504			}
505		}
506	}
507	if len(errors) > 0 {
508		return fmt.Errorf("%s", strings.Join(errors, "\n"))
509	}
510
511	releaseConfig, err := configs.GetReleaseConfig(targetRelease)
512	if err != nil {
513		return err
514	}
515	orc := []*rc_proto.ReleaseConfigArtifact{}
516	for _, c := range sortedReleaseConfigs {
517		if c.Name != releaseConfig.Name {
518			orc = append(orc, c.ReleaseConfigArtifact)
519		}
520	}
521
522	configs.Artifact = rc_proto.ReleaseConfigsArtifact{
523		ReleaseConfig:       releaseConfig.ReleaseConfigArtifact,
524		OtherReleaseConfigs: orc,
525		ReleaseConfigMapsMap: func() map[string]*rc_proto.ReleaseConfigMap {
526			ret := make(map[string]*rc_proto.ReleaseConfigMap)
527			for k, v := range configs.releaseConfigMapsMap {
528				ret[k] = &v.proto
529			}
530			return ret
531		}(),
532	}
533	return nil
534}
535
536func ReadReleaseConfigMaps(releaseConfigMapPaths StringList, targetRelease string, useBuildVar, allowMissing bool) (*ReleaseConfigs, error) {
537	var err error
538
539	if len(releaseConfigMapPaths) == 0 {
540		releaseConfigMapPaths, err = GetDefaultMapPaths(useBuildVar)
541		if err != nil {
542			return nil, err
543		}
544		if len(releaseConfigMapPaths) == 0 {
545			return nil, fmt.Errorf("No maps found")
546		}
547		if !useBuildVar {
548			warnf("No --map argument provided.  Using: --map %s\n", strings.Join(releaseConfigMapPaths, " --map "))
549		}
550	}
551
552	configs := ReleaseConfigsFactory()
553	configs.allowMissing = allowMissing
554	mapsRead := make(map[string]bool)
555	var idx int
556	for _, releaseConfigMapPath := range releaseConfigMapPaths {
557		// Maintain an ordered list of release config directories.
558		configDir := filepath.Dir(releaseConfigMapPath)
559		if mapsRead[configDir] {
560			continue
561		}
562		mapsRead[configDir] = true
563		configs.configDirIndexes[configDir] = idx
564		configs.configDirs = append(configs.configDirs, configDir)
565		// Force the path to be the textproto path, so that both the scl and textproto formats can coexist.
566		releaseConfigMapPath = filepath.Join(configDir, "release_config_map.textproto")
567		err = configs.LoadReleaseConfigMap(releaseConfigMapPath, idx)
568		if err != nil {
569			return nil, err
570		}
571		idx += 1
572	}
573
574	// Now that we have all of the release config maps, can meld them and generate the artifacts.
575	err = configs.GenerateReleaseConfigs(targetRelease)
576	return configs, err
577}
578