1package main 2 3import ( 4 "flag" 5 "fmt" 6 "io/fs" 7 "os" 8 "path/filepath" 9 "regexp" 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 "google.golang.org/protobuf/encoding/prototext" 15 "google.golang.org/protobuf/proto" 16) 17 18var ( 19 // When a flag declaration has an initial value that is a string, the default workflow is PREBUILT. 20 // If the flag name starts with any of prefixes in manualFlagNamePrefixes, it is MANUAL. 21 manualFlagNamePrefixes []string = []string{ 22 "RELEASE_ACONFIG_", 23 "RELEASE_PLATFORM_", 24 "RELEASE_BUILD_FLAGS_", 25 } 26 27 // Set `aconfig_flags_only: true` in these release configs. 28 aconfigFlagsOnlyConfigs map[string]bool = map[string]bool{ 29 "trunk_food": true, 30 } 31 32 // Default namespace value. This is intentionally invalid. 33 defaultFlagNamespace string = "android_UNKNOWN" 34 35 // What is the current name for "next". 36 nextName string = "ap3a" 37) 38 39func RenameNext(name string) string { 40 if name == "next" { 41 return nextName 42 } 43 return name 44} 45 46func WriteFile(path string, message proto.Message) error { 47 data, err := prototext.MarshalOptions{Multiline: true}.Marshal(message) 48 if err != nil { 49 return err 50 } 51 52 err = os.MkdirAll(filepath.Dir(path), 0775) 53 if err != nil { 54 return err 55 } 56 return os.WriteFile(path, data, 0644) 57} 58 59func WalkValueFiles(dir string, Func fs.WalkDirFunc) error { 60 valPath := filepath.Join(dir, "build_config") 61 if _, err := os.Stat(valPath); err != nil { 62 fmt.Printf("%s not found, ignoring.\n", valPath) 63 return nil 64 } 65 66 return filepath.WalkDir(valPath, func(path string, d fs.DirEntry, err error) error { 67 if err != nil { 68 return err 69 } 70 if strings.HasSuffix(d.Name(), ".scl") && d.Type().IsRegular() { 71 return Func(path, d, err) 72 } 73 return nil 74 }) 75} 76 77func ProcessBuildFlags(dir string, namespaceMap map[string]string) error { 78 var rootAconfigModule string 79 80 path := filepath.Join(dir, "build_flags.scl") 81 if _, err := os.Stat(path); err != nil { 82 fmt.Printf("%s not found, ignoring.\n", path) 83 return nil 84 } else { 85 fmt.Printf("Processing %s\n", path) 86 } 87 commentRegexp, err := regexp.Compile("^[[:space:]]*#(?<comment>.+)") 88 if err != nil { 89 return err 90 } 91 declRegexp, err := regexp.Compile("^[[:space:]]*flag.\"(?<name>[A-Z_0-9]+)\",[[:space:]]*(?<container>[_A-Z]*),[[:space:]]*(?<value>(\"[^\"]*\"|[^\",)]*))") 92 if err != nil { 93 return err 94 } 95 declIn, err := os.ReadFile(path) 96 if err != nil { 97 return err 98 } 99 lines := strings.Split(string(declIn), "\n") 100 var description string 101 for _, line := range lines { 102 if comment := commentRegexp.FindStringSubmatch(commentRegexp.FindString(line)); comment != nil { 103 // Description is the text from any contiguous series of lines before a `flag()` call. 104 descLine := strings.TrimSpace(comment[commentRegexp.SubexpIndex("comment")]) 105 if !strings.HasPrefix(descLine, "keep-sorted") { 106 description += fmt.Sprintf(" %s", descLine) 107 } 108 continue 109 } 110 matches := declRegexp.FindStringSubmatch(declRegexp.FindString(line)) 111 if matches == nil { 112 // The line is neither a comment nor a `flag()` call. 113 // Discard any description we have gathered and process the next line. 114 description = "" 115 continue 116 } 117 declName := matches[declRegexp.SubexpIndex("name")] 118 declValue := matches[declRegexp.SubexpIndex("value")] 119 description = strings.TrimSpace(description) 120 containers := []string{strings.ToLower(matches[declRegexp.SubexpIndex("container")])} 121 if containers[0] == "all" { 122 containers = []string{"product", "system", "system_ext", "vendor"} 123 } 124 var namespace string 125 var ok bool 126 if namespace, ok = namespaceMap[declName]; !ok { 127 namespace = defaultFlagNamespace 128 } 129 flagDeclaration := &rc_proto.FlagDeclaration{ 130 Name: proto.String(declName), 131 Namespace: proto.String(namespace), 132 Description: proto.String(description), 133 Containers: containers, 134 } 135 description = "" 136 // Most build flags are `workflow: PREBUILT`. 137 workflow := rc_proto.Workflow(rc_proto.Workflow_PREBUILT) 138 switch { 139 case declName == "RELEASE_ACONFIG_VALUE_SETS": 140 if strings.HasPrefix(declValue, "\"") { 141 rootAconfigModule = declValue[1 : len(declValue)-1] 142 } 143 continue 144 case strings.HasPrefix(declValue, "\""): 145 // String values mean that the flag workflow is (most likely) either MANUAL or PREBUILT. 146 declValue = declValue[1 : len(declValue)-1] 147 flagDeclaration.Value = &rc_proto.Value{Val: &rc_proto.Value_StringValue{declValue}} 148 for _, prefix := range manualFlagNamePrefixes { 149 if strings.HasPrefix(declName, prefix) { 150 workflow = rc_proto.Workflow(rc_proto.Workflow_MANUAL) 151 break 152 } 153 } 154 case declValue == "False" || declValue == "True": 155 // Boolean values are LAUNCH flags. 156 flagDeclaration.Value = &rc_proto.Value{Val: &rc_proto.Value_BoolValue{declValue == "True"}} 157 workflow = rc_proto.Workflow(rc_proto.Workflow_LAUNCH) 158 case declValue == "None": 159 // Use PREBUILT workflow with no initial value. 160 default: 161 fmt.Printf("%s: Unexpected value %s=%s\n", path, declName, declValue) 162 } 163 flagDeclaration.Workflow = &workflow 164 if flagDeclaration != nil { 165 declPath := filepath.Join(dir, "flag_declarations", fmt.Sprintf("%s.textproto", declName)) 166 err := WriteFile(declPath, flagDeclaration) 167 if err != nil { 168 return err 169 } 170 } 171 } 172 if rootAconfigModule != "" { 173 rootProto := &rc_proto.ReleaseConfig{ 174 Name: proto.String("root"), 175 AconfigValueSets: []string{rootAconfigModule}, 176 } 177 return WriteFile(filepath.Join(dir, "release_configs", "root.textproto"), rootProto) 178 } 179 return nil 180} 181 182func ProcessBuildConfigs(dir, name string, paths []string, releaseProto *rc_proto.ReleaseConfig) error { 183 valRegexp, err := regexp.Compile("[[:space:]]+value.\"(?<name>[A-Z_0-9]+)\",[[:space:]]*(?<value>(\"[^\"]*\"|[^\",)]*))") 184 if err != nil { 185 return err 186 } 187 for _, path := range paths { 188 fmt.Printf("Processing %s\n", path) 189 valIn, err := os.ReadFile(path) 190 if err != nil { 191 fmt.Printf("%s: error: %v\n", path, err) 192 return err 193 } 194 vals := valRegexp.FindAllString(string(valIn), -1) 195 for _, val := range vals { 196 matches := valRegexp.FindStringSubmatch(val) 197 valValue := matches[valRegexp.SubexpIndex("value")] 198 valName := matches[valRegexp.SubexpIndex("name")] 199 flagValue := &rc_proto.FlagValue{ 200 Name: proto.String(valName), 201 } 202 switch { 203 case valName == "RELEASE_ACONFIG_VALUE_SETS": 204 flagValue = nil 205 if releaseProto.AconfigValueSets == nil { 206 releaseProto.AconfigValueSets = []string{} 207 } 208 releaseProto.AconfigValueSets = append(releaseProto.AconfigValueSets, valValue[1:len(valValue)-1]) 209 case strings.HasPrefix(valValue, "\""): 210 valValue = valValue[1 : len(valValue)-1] 211 flagValue.Value = &rc_proto.Value{Val: &rc_proto.Value_StringValue{valValue}} 212 case valValue == "None": 213 // nothing to do here. 214 case valValue == "True": 215 flagValue.Value = &rc_proto.Value{Val: &rc_proto.Value_BoolValue{true}} 216 case valValue == "False": 217 flagValue.Value = &rc_proto.Value{Val: &rc_proto.Value_BoolValue{false}} 218 default: 219 fmt.Printf("%s: Unexpected value %s=%s\n", path, valName, valValue) 220 } 221 if flagValue != nil { 222 if releaseProto.GetAconfigFlagsOnly() { 223 return fmt.Errorf("%s does not allow build flag overrides", RenameNext(name)) 224 } 225 valPath := filepath.Join(dir, "flag_values", RenameNext(name), fmt.Sprintf("%s.textproto", valName)) 226 err := WriteFile(valPath, flagValue) 227 if err != nil { 228 return err 229 } 230 } 231 } 232 } 233 return err 234} 235 236var ( 237 allContainers = func() []string { 238 return []string{"product", "system", "system_ext", "vendor"} 239 }() 240) 241 242func ProcessReleaseConfigMap(dir string, descriptionMap map[string]string) error { 243 path := filepath.Join(dir, "release_config_map.mk") 244 if _, err := os.Stat(path); err != nil { 245 fmt.Printf("%s not found, ignoring.\n", path) 246 return nil 247 } else { 248 fmt.Printf("Processing %s\n", path) 249 } 250 configRegexp, err := regexp.Compile("^..call[[:space:]]+declare-release-config,[[:space:]]+(?<name>[_a-z0-9A-Z]+),[[:space:]]+(?<files>[^,]*)(,[[:space:]]*(?<inherits>.*)|[[:space:]]*)[)]$") 251 if err != nil { 252 return err 253 } 254 aliasRegexp, err := regexp.Compile("^..call[[:space:]]+alias-release-config,[[:space:]]+(?<name>[_a-z0-9A-Z]+),[[:space:]]+(?<target>[_a-z0-9A-Z]+)") 255 if err != nil { 256 return err 257 } 258 259 mapIn, err := os.ReadFile(path) 260 if err != nil { 261 return err 262 } 263 cleanDir := strings.TrimLeft(dir, "../") 264 var defaultContainers []string 265 switch { 266 case strings.HasPrefix(cleanDir, "build/") || cleanDir == "vendor/google_shared/build": 267 defaultContainers = allContainers 268 case cleanDir == "vendor/google/release": 269 defaultContainers = allContainers 270 default: 271 defaultContainers = []string{"vendor"} 272 } 273 releaseConfigMap := &rc_proto.ReleaseConfigMap{DefaultContainers: defaultContainers} 274 // If we find a description for the directory, include it. 275 if description, ok := descriptionMap[cleanDir]; ok { 276 releaseConfigMap.Description = proto.String(description) 277 } 278 lines := strings.Split(string(mapIn), "\n") 279 for _, line := range lines { 280 alias := aliasRegexp.FindStringSubmatch(aliasRegexp.FindString(line)) 281 if alias != nil { 282 fmt.Printf("processing alias %s\n", line) 283 name := alias[aliasRegexp.SubexpIndex("name")] 284 target := alias[aliasRegexp.SubexpIndex("target")] 285 if target == "next" { 286 if RenameNext(target) != name { 287 return fmt.Errorf("Unexpected name for next (%s)", RenameNext(target)) 288 } 289 target, name = name, target 290 } 291 releaseConfigMap.Aliases = append(releaseConfigMap.Aliases, 292 &rc_proto.ReleaseAlias{ 293 Name: proto.String(name), 294 Target: proto.String(target), 295 }) 296 } 297 config := configRegexp.FindStringSubmatch(configRegexp.FindString(line)) 298 if config == nil { 299 continue 300 } 301 name := config[configRegexp.SubexpIndex("name")] 302 releaseConfig := &rc_proto.ReleaseConfig{ 303 Name: proto.String(RenameNext(name)), 304 } 305 if aconfigFlagsOnlyConfigs[name] { 306 releaseConfig.AconfigFlagsOnly = proto.Bool(true) 307 } 308 configFiles := config[configRegexp.SubexpIndex("files")] 309 files := strings.Split(strings.ReplaceAll(configFiles, "$(local_dir)", dir+"/"), " ") 310 configInherits := config[configRegexp.SubexpIndex("inherits")] 311 if len(configInherits) > 0 { 312 releaseConfig.Inherits = strings.Split(configInherits, " ") 313 } 314 err := ProcessBuildConfigs(dir, name, files, releaseConfig) 315 if err != nil { 316 return err 317 } 318 319 releasePath := filepath.Join(dir, "release_configs", fmt.Sprintf("%s.textproto", RenameNext(name))) 320 err = WriteFile(releasePath, releaseConfig) 321 if err != nil { 322 return err 323 } 324 } 325 return WriteFile(filepath.Join(dir, "release_config_map.textproto"), releaseConfigMap) 326} 327 328func main() { 329 var err error 330 var top string 331 var dirs rc_lib.StringList 332 var namespacesFile string 333 var descriptionsFile string 334 var debug bool 335 defaultTopDir, err := rc_lib.GetTopDir() 336 337 flag.StringVar(&top, "top", defaultTopDir, "path to top of workspace") 338 flag.Var(&dirs, "dir", "directory to process, relative to the top of the workspace") 339 flag.StringVar(&namespacesFile, "namespaces", "", "location of file with 'flag_name namespace' information") 340 flag.StringVar(&descriptionsFile, "descriptions", "", "location of file with 'directory description' information") 341 flag.BoolVar(&debug, "debug", false, "turn on debugging output for errors") 342 flag.Parse() 343 344 errorExit := func(err error) { 345 if debug { 346 panic(err) 347 } 348 fmt.Fprintf(os.Stderr, "%s\n", err) 349 os.Exit(1) 350 } 351 352 if err = os.Chdir(top); err != nil { 353 errorExit(err) 354 } 355 if len(dirs) == 0 { 356 dirs = rc_lib.StringList{"build/release", "vendor/google_shared/build/release", "vendor/google/release"} 357 } 358 359 namespaceMap := make(map[string]string) 360 if namespacesFile != "" { 361 data, err := os.ReadFile(namespacesFile) 362 if err != nil { 363 errorExit(err) 364 } 365 for idx, line := range strings.Split(string(data), "\n") { 366 fields := strings.Split(line, " ") 367 if len(fields) > 2 { 368 errorExit(fmt.Errorf("line %d: too many fields: %s", idx, line)) 369 } 370 namespaceMap[fields[0]] = fields[1] 371 } 372 373 } 374 375 descriptionMap := make(map[string]string) 376 descriptionMap["build/release"] = "Published open-source flags and declarations" 377 if descriptionsFile != "" { 378 data, err := os.ReadFile(descriptionsFile) 379 if err != nil { 380 errorExit(err) 381 } 382 for _, line := range strings.Split(string(data), "\n") { 383 if strings.TrimSpace(line) != "" { 384 fields := strings.SplitN(line, " ", 2) 385 descriptionMap[fields[0]] = fields[1] 386 } 387 } 388 389 } 390 391 for _, dir := range dirs { 392 err = ProcessBuildFlags(dir, namespaceMap) 393 if err != nil { 394 errorExit(err) 395 } 396 397 err = ProcessReleaseConfigMap(dir, descriptionMap) 398 if err != nil { 399 errorExit(err) 400 } 401 } 402} 403