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