1// Copyright 2021 Google LLC 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 15// The application to convert product configuration makefiles to Starlark. 16// Converts either given list of files (and optionally the dependent files 17// of the same kind), or all all product configuration makefiles in the 18// given source tree. 19// Previous version of a converted file can be backed up. 20// Optionally prints detailed statistics at the end. 21package main 22 23import ( 24 "bufio" 25 "flag" 26 "fmt" 27 "io/ioutil" 28 "os" 29 "os/exec" 30 "path/filepath" 31 "regexp" 32 "runtime/debug" 33 "runtime/pprof" 34 "sort" 35 "strings" 36 "time" 37 38 "android/soong/androidmk/parser" 39 "android/soong/mk2rbc" 40) 41 42var ( 43 // TODO(asmundak): remove this option once there is a consensus on suffix 44 suffix = flag.String("suffix", ".rbc", "generated files' suffix") 45 dryRun = flag.Bool("dry_run", false, "dry run") 46 recurse = flag.Bool("convert_dependents", false, "convert all dependent files") 47 mode = flag.String("mode", "", `"backup" to back up existing files, "write" to overwrite them`) 48 errstat = flag.Bool("error_stat", false, "print error statistics") 49 traceVar = flag.String("trace", "", "comma-separated list of variables to trace") 50 // TODO(asmundak): this option is for debugging 51 allInSource = flag.Bool("all", false, "convert all product config makefiles in the tree under //") 52 outputTop = flag.String("outdir", "", "write output files into this directory hierarchy") 53 launcher = flag.String("launcher", "", "generated launcher path.") 54 boardlauncher = flag.String("boardlauncher", "", "generated board configuration launcher path.") 55 printProductConfigMap = flag.Bool("print_product_config_map", false, "print product config map and exit") 56 cpuProfile = flag.String("cpu_profile", "", "write cpu profile to file") 57 traceCalls = flag.Bool("trace_calls", false, "trace function calls") 58 inputVariables = flag.String("input_variables", "", "starlark file containing product config and global variables") 59 makefileList = flag.String("makefile_list", "", "path to a list of all makefiles in the source tree, generated by soong's finder. If not provided, mk2rbc will find the makefiles itself (more slowly than if this flag was provided)") 60) 61 62func init() { 63 // Simplistic flag aliasing: works, but the usage string is ugly and 64 // both flag and its alias can be present on the command line 65 flagAlias := func(target string, alias string) { 66 if f := flag.Lookup(target); f != nil { 67 flag.Var(f.Value, alias, "alias for --"+f.Name) 68 return 69 } 70 quit("cannot alias unknown flag " + target) 71 } 72 flagAlias("suffix", "s") 73 flagAlias("dry_run", "n") 74 flagAlias("convert_dependents", "r") 75 flagAlias("error_stat", "e") 76} 77 78var backupSuffix string 79var tracedVariables []string 80var errorLogger = errorSink{data: make(map[string]datum)} 81var makefileFinder mk2rbc.MakefileFinder 82 83func main() { 84 flag.Usage = func() { 85 cmd := filepath.Base(os.Args[0]) 86 fmt.Fprintf(flag.CommandLine.Output(), 87 "Usage: %[1]s flags file...\n", cmd) 88 flag.PrintDefaults() 89 } 90 flag.Parse() 91 92 if _, err := os.Stat("build/soong/mk2rbc"); err != nil { 93 quit("Must be run from the root of the android tree. (build/soong/mk2rbc does not exist)") 94 } 95 96 // Delouse 97 if *suffix == ".mk" { 98 quit("cannot use .mk as generated file suffix") 99 } 100 if *suffix == "" { 101 quit("suffix cannot be empty") 102 } 103 if *outputTop != "" { 104 if err := os.MkdirAll(*outputTop, os.ModeDir+os.ModePerm); err != nil { 105 quit(err) 106 } 107 s, err := filepath.Abs(*outputTop) 108 if err != nil { 109 quit(err) 110 } 111 *outputTop = s 112 } 113 if *allInSource && len(flag.Args()) > 0 { 114 quit("file list cannot be specified when -all is present") 115 } 116 if *allInSource && *launcher != "" { 117 quit("--all and --launcher are mutually exclusive") 118 } 119 120 // Flag-driven adjustments 121 if (*suffix)[0] != '.' { 122 *suffix = "." + *suffix 123 } 124 if *mode == "backup" { 125 backupSuffix = time.Now().Format("20060102150405") 126 } 127 if *traceVar != "" { 128 tracedVariables = strings.Split(*traceVar, ",") 129 } 130 131 if *cpuProfile != "" { 132 f, err := os.Create(*cpuProfile) 133 if err != nil { 134 quit(err) 135 } 136 pprof.StartCPUProfile(f) 137 defer pprof.StopCPUProfile() 138 } 139 140 if *makefileList != "" { 141 makefileFinder = &FileListMakefileFinder{ 142 cachedMakefiles: nil, 143 filePath: *makefileList, 144 } 145 } else { 146 makefileFinder = &FindCommandMakefileFinder{} 147 } 148 149 // Find out global variables 150 getConfigVariables() 151 getSoongVariables() 152 153 if *printProductConfigMap { 154 productConfigMap := buildProductConfigMap() 155 var products []string 156 for p := range productConfigMap { 157 products = append(products, p) 158 } 159 sort.Strings(products) 160 for _, p := range products { 161 fmt.Println(p, productConfigMap[p]) 162 } 163 os.Exit(0) 164 } 165 166 // Convert! 167 files := flag.Args() 168 if *allInSource { 169 productConfigMap := buildProductConfigMap() 170 for _, path := range productConfigMap { 171 files = append(files, path) 172 } 173 } 174 ok := true 175 for _, mkFile := range files { 176 ok = convertOne(mkFile, []string{}) && ok 177 } 178 179 if *launcher != "" { 180 if len(files) != 1 { 181 quit(fmt.Errorf("a launcher can be generated only for a single product")) 182 } 183 if *inputVariables == "" { 184 quit(fmt.Errorf("the product launcher requires an input variables file")) 185 } 186 if !convertOne(*inputVariables, []string{}) { 187 quit(fmt.Errorf("the product launcher input variables file failed to convert")) 188 } 189 190 err := writeGenerated(*launcher, mk2rbc.Launcher(outputModulePath(files[0]), outputModulePath(*inputVariables), 191 mk2rbc.MakePath2ModuleName(files[0]))) 192 if err != nil { 193 fmt.Fprintf(os.Stderr, "%s: %s", files[0], err) 194 ok = false 195 } 196 } 197 if *boardlauncher != "" { 198 if len(files) != 1 { 199 quit(fmt.Errorf("a launcher can be generated only for a single product")) 200 } 201 if *inputVariables == "" { 202 quit(fmt.Errorf("the board launcher requires an input variables file")) 203 } 204 if !convertOne(*inputVariables, []string{}) { 205 quit(fmt.Errorf("the board launcher input variables file failed to convert")) 206 } 207 err := writeGenerated(*boardlauncher, mk2rbc.BoardLauncher( 208 outputModulePath(files[0]), outputModulePath(*inputVariables))) 209 if err != nil { 210 fmt.Fprintf(os.Stderr, "%s: %s", files[0], err) 211 ok = false 212 } 213 } 214 215 if *errstat { 216 errorLogger.printStatistics() 217 printStats() 218 } 219 if !ok { 220 os.Exit(1) 221 } 222} 223 224func quit(s interface{}) { 225 fmt.Fprintln(os.Stderr, s) 226 os.Exit(2) 227} 228 229func buildProductConfigMap() map[string]string { 230 const androidProductsMk = "AndroidProducts.mk" 231 // Build the list of AndroidProducts.mk files: it's 232 // build/make/target/product/AndroidProducts.mk + device/**/AndroidProducts.mk plus + vendor/**/AndroidProducts.mk 233 targetAndroidProductsFile := filepath.Join("build", "make", "target", "product", androidProductsMk) 234 if _, err := os.Stat(targetAndroidProductsFile); err != nil { 235 fmt.Fprintf(os.Stderr, "%s: %s\n", targetAndroidProductsFile, err) 236 } 237 productConfigMap := make(map[string]string) 238 if err := mk2rbc.UpdateProductConfigMap(productConfigMap, targetAndroidProductsFile); err != nil { 239 fmt.Fprintf(os.Stderr, "%s: %s\n", targetAndroidProductsFile, err) 240 } 241 for _, t := range []string{"device", "vendor"} { 242 _ = filepath.WalkDir(t, 243 func(path string, d os.DirEntry, err error) error { 244 if err != nil || d.IsDir() || filepath.Base(path) != androidProductsMk { 245 return nil 246 } 247 if err2 := mk2rbc.UpdateProductConfigMap(productConfigMap, path); err2 != nil { 248 fmt.Fprintf(os.Stderr, "%s: %s\n", path, err) 249 // Keep going, we want to find all such errors in a single run 250 } 251 return nil 252 }) 253 } 254 return productConfigMap 255} 256 257func getConfigVariables() { 258 path := filepath.Join("build", "make", "core", "product.mk") 259 if err := mk2rbc.FindConfigVariables(path, mk2rbc.KnownVariables); err != nil { 260 quit(err) 261 } 262} 263 264// Implements mkparser.Scope, to be used by mkparser.Value.Value() 265type fileNameScope struct { 266 mk2rbc.ScopeBase 267} 268 269func (s fileNameScope) Get(name string) string { 270 if name != "BUILD_SYSTEM" { 271 return fmt.Sprintf("$(%s)", name) 272 } 273 return filepath.Join("build", "make", "core") 274} 275 276func getSoongVariables() { 277 path := filepath.Join("build", "make", "core", "soong_config.mk") 278 err := mk2rbc.FindSoongVariables(path, fileNameScope{}, mk2rbc.KnownVariables) 279 if err != nil { 280 quit(err) 281 } 282} 283 284var converted = make(map[string]*mk2rbc.StarlarkScript) 285 286//goland:noinspection RegExpRepeatedSpace 287var cpNormalizer = regexp.MustCompile( 288 "# Copyright \\(C\\) 20.. The Android Open Source Project") 289 290const cpNormalizedCopyright = "# Copyright (C) 20xx The Android Open Source Project" 291const copyright = `# 292# Copyright (C) 20xx The Android Open Source Project 293# 294# Licensed under the Apache License, Version 2.0 (the "License"); 295# you may not use this file except in compliance with the License. 296# You may obtain a copy of the License at 297# 298# http://www.apache.org/licenses/LICENSE-2.0 299# 300# Unless required by applicable law or agreed to in writing, software 301# distributed under the License is distributed on an "AS IS" BASIS, 302# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 303# See the License for the specific language governing permissions and 304# limitations under the License. 305# 306` 307 308// Convert a single file. 309// Write the result either to the same directory, to the same place in 310// the output hierarchy, or to the stdout. 311// Optionally, recursively convert the files this one includes by 312// $(call inherit-product) or an include statement. 313func convertOne(mkFile string, loadStack []string) (ok bool) { 314 if v, ok := converted[mkFile]; ok { 315 if v == nil { 316 fmt.Fprintf(os.Stderr, "Cycle in load graph:\n%s\n%s\n\n", strings.Join(loadStack, "\n"), mkFile) 317 return false 318 } 319 return true 320 } 321 converted[mkFile] = nil 322 defer func() { 323 if r := recover(); r != nil { 324 ok = false 325 fmt.Fprintf(os.Stderr, "%s: panic while converting: %s\n%s\n", mkFile, r, debug.Stack()) 326 } 327 }() 328 329 mk2starRequest := mk2rbc.Request{ 330 MkFile: mkFile, 331 Reader: nil, 332 OutputDir: *outputTop, 333 OutputSuffix: *suffix, 334 TracedVariables: tracedVariables, 335 TraceCalls: *traceCalls, 336 SourceFS: os.DirFS("."), 337 MakefileFinder: makefileFinder, 338 ErrorLogger: errorLogger, 339 } 340 ss, err := mk2rbc.Convert(mk2starRequest) 341 if err != nil { 342 fmt.Fprintln(os.Stderr, mkFile, ": ", err) 343 return false 344 } 345 script := ss.String() 346 outputPath := outputFilePath(mkFile) 347 348 if *dryRun { 349 fmt.Printf("==== %s ====\n", outputPath) 350 // Print generated script after removing the copyright header 351 outText := cpNormalizer.ReplaceAllString(script, cpNormalizedCopyright) 352 fmt.Println(strings.TrimPrefix(outText, copyright)) 353 } else { 354 if err := maybeBackup(outputPath); err != nil { 355 fmt.Fprintln(os.Stderr, err) 356 return false 357 } 358 if err := writeGenerated(outputPath, script); err != nil { 359 fmt.Fprintln(os.Stderr, err) 360 return false 361 } 362 } 363 loadStack = append(loadStack, mkFile) 364 ok = true 365 if *recurse { 366 for _, sub := range ss.SubConfigFiles() { 367 // File may be absent if it is a conditional load 368 if _, err := os.Stat(sub); os.IsNotExist(err) { 369 continue 370 } 371 ok = convertOne(sub, loadStack) && ok 372 } 373 } 374 converted[mkFile] = ss 375 return ok 376} 377 378// Optionally saves the previous version of the generated file 379func maybeBackup(filename string) error { 380 stat, err := os.Stat(filename) 381 if os.IsNotExist(err) { 382 return nil 383 } 384 if !stat.Mode().IsRegular() { 385 return fmt.Errorf("%s exists and is not a regular file", filename) 386 } 387 switch *mode { 388 case "backup": 389 return os.Rename(filename, filename+backupSuffix) 390 case "write": 391 return os.Remove(filename) 392 default: 393 return fmt.Errorf("%s already exists, use --mode option", filename) 394 } 395} 396 397func outputFilePath(mkFile string) string { 398 path := strings.TrimSuffix(mkFile, filepath.Ext(mkFile)) + *suffix 399 if *outputTop != "" { 400 path = filepath.Join(*outputTop, path) 401 } 402 return path 403} 404 405func outputModulePath(mkFile string) string { 406 path := outputFilePath(mkFile) 407 path, err := mk2rbc.RelativeToCwd(path) 408 if err != nil { 409 panic(err) 410 } 411 return "//" + path 412} 413 414func writeGenerated(path string, contents string) error { 415 if err := os.MkdirAll(filepath.Dir(path), os.ModeDir|os.ModePerm); err != nil { 416 return err 417 } 418 if err := ioutil.WriteFile(path, []byte(contents), 0644); err != nil { 419 return err 420 } 421 return nil 422} 423 424func printStats() { 425 var sortedFiles []string 426 for p := range converted { 427 sortedFiles = append(sortedFiles, p) 428 } 429 sort.Strings(sortedFiles) 430 431 nOk, nPartial, nFailed := 0, 0, 0 432 for _, f := range sortedFiles { 433 if converted[f] == nil { 434 nFailed++ 435 } else if converted[f].HasErrors() { 436 nPartial++ 437 } else { 438 nOk++ 439 } 440 } 441 if nPartial > 0 { 442 fmt.Fprintf(os.Stderr, "Conversion was partially successful for:\n") 443 for _, f := range sortedFiles { 444 if ss := converted[f]; ss != nil && ss.HasErrors() { 445 fmt.Fprintln(os.Stderr, " ", f) 446 } 447 } 448 } 449 450 if nFailed > 0 { 451 fmt.Fprintf(os.Stderr, "Conversion failed for files:\n") 452 for _, f := range sortedFiles { 453 if converted[f] == nil { 454 fmt.Fprintln(os.Stderr, " ", f) 455 } 456 } 457 } 458} 459 460type datum struct { 461 count int 462 formattingArgs []string 463} 464 465type errorSink struct { 466 data map[string]datum 467} 468 469func (ebt errorSink) NewError(el mk2rbc.ErrorLocation, node parser.Node, message string, args ...interface{}) { 470 fmt.Fprint(os.Stderr, el, ": ") 471 fmt.Fprintf(os.Stderr, message, args...) 472 fmt.Fprintln(os.Stderr) 473 if !*errstat { 474 return 475 } 476 477 v, exists := ebt.data[message] 478 if exists { 479 v.count++ 480 } else { 481 v = datum{1, nil} 482 } 483 if strings.Contains(message, "%s") { 484 var newArg1 string 485 if len(args) == 0 { 486 panic(fmt.Errorf(`%s has %%s but args are missing`, message)) 487 } 488 newArg1 = fmt.Sprint(args[0]) 489 if message == "unsupported line" { 490 newArg1 = node.Dump() 491 } else if message == "unsupported directive %s" { 492 if newArg1 == "include" || newArg1 == "-include" { 493 newArg1 = node.Dump() 494 } 495 } 496 v.formattingArgs = append(v.formattingArgs, newArg1) 497 } 498 ebt.data[message] = v 499} 500 501func (ebt errorSink) printStatistics() { 502 if len(ebt.data) > 0 { 503 fmt.Fprintln(os.Stderr, "Error counts:") 504 } 505 for message, data := range ebt.data { 506 if len(data.formattingArgs) == 0 { 507 fmt.Fprintf(os.Stderr, "%4d %s\n", data.count, message) 508 continue 509 } 510 itemsByFreq, count := stringsWithFreq(data.formattingArgs, 30) 511 fmt.Fprintf(os.Stderr, "%4d %s [%d unique items]:\n", data.count, message, count) 512 fmt.Fprintln(os.Stderr, " ", itemsByFreq) 513 } 514} 515 516func stringsWithFreq(items []string, topN int) (string, int) { 517 freq := make(map[string]int) 518 for _, item := range items { 519 freq[strings.TrimPrefix(strings.TrimSuffix(item, "]"), "[")]++ 520 } 521 var sorted []string 522 for item := range freq { 523 sorted = append(sorted, item) 524 } 525 sort.Slice(sorted, func(i int, j int) bool { 526 return freq[sorted[i]] > freq[sorted[j]] 527 }) 528 sep := "" 529 res := "" 530 for i, item := range sorted { 531 if i >= topN { 532 res += " ..." 533 break 534 } 535 count := freq[item] 536 if count > 1 { 537 res += fmt.Sprintf("%s%s(%d)", sep, item, count) 538 } else { 539 res += fmt.Sprintf("%s%s", sep, item) 540 } 541 sep = ", " 542 } 543 return res, len(sorted) 544} 545 546// FindCommandMakefileFinder is an implementation of mk2rbc.MakefileFinder that 547// runs the unix find command to find all the makefiles in the source tree. 548type FindCommandMakefileFinder struct { 549 cachedRoot string 550 cachedMakefiles []string 551} 552 553func (l *FindCommandMakefileFinder) Find(root string) []string { 554 if l.cachedMakefiles != nil && l.cachedRoot == root { 555 return l.cachedMakefiles 556 } 557 558 // Return all *.mk files but not in hidden directories. 559 560 // NOTE(asmundak): as it turns out, even the WalkDir (which is an _optimized_ directory tree walker) 561 // is about twice slower than running `find` command (14s vs 6s on the internal Android source tree). 562 common_args := []string{"!", "-type", "d", "-name", "*.mk", "!", "-path", "*/.*/*"} 563 if root != "" { 564 common_args = append([]string{root}, common_args...) 565 } 566 cmd := exec.Command("/usr/bin/find", common_args...) 567 stdout, err := cmd.StdoutPipe() 568 if err == nil { 569 err = cmd.Start() 570 } 571 if err != nil { 572 panic(fmt.Errorf("cannot get the output from %s: %s", cmd, err)) 573 } 574 scanner := bufio.NewScanner(stdout) 575 result := make([]string, 0) 576 for scanner.Scan() { 577 result = append(result, strings.TrimPrefix(scanner.Text(), "./")) 578 } 579 stdout.Close() 580 err = scanner.Err() 581 if err != nil { 582 panic(fmt.Errorf("cannot get the output from %s: %s", cmd, err)) 583 } 584 l.cachedRoot = root 585 l.cachedMakefiles = result 586 return l.cachedMakefiles 587} 588 589// FileListMakefileFinder is an implementation of mk2rbc.MakefileFinder that 590// reads a file containing the list of makefiles in the android source tree. 591// This file is generated by soong's finder, so that it can be computed while 592// soong is already walking the source tree looking for other files. If the root 593// to find makefiles under is not the root of the android source tree, it will 594// fall back to using FindCommandMakefileFinder. 595type FileListMakefileFinder struct { 596 FindCommandMakefileFinder 597 cachedMakefiles []string 598 filePath string 599} 600 601func (l *FileListMakefileFinder) Find(root string) []string { 602 root, err1 := filepath.Abs(root) 603 wd, err2 := os.Getwd() 604 if root != wd || err1 != nil || err2 != nil { 605 return l.FindCommandMakefileFinder.Find(root) 606 } 607 if l.cachedMakefiles != nil { 608 return l.cachedMakefiles 609 } 610 611 file, err := os.Open(l.filePath) 612 if err != nil { 613 panic(fmt.Errorf("Cannot read makefile list: %s\n", err)) 614 } 615 defer file.Close() 616 617 result := make([]string, 0) 618 scanner := bufio.NewScanner(file) 619 for scanner.Scan() { 620 line := scanner.Text() 621 if len(line) > 0 { 622 result = append(result, line) 623 } 624 } 625 626 if err = scanner.Err(); err != nil { 627 panic(fmt.Errorf("Cannot read makefile list: %s\n", err)) 628 } 629 l.cachedMakefiles = result 630 return l.cachedMakefiles 631} 632