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) && 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) { 187 quit(fmt.Errorf("the product launcher input variables file failed to convert")) 188 } 189 190 err := writeGenerated(*launcher, mk2rbc.Launcher(outputFilePath(files[0]), outputFilePath(*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) { 205 quit(fmt.Errorf("the board launcher input variables file failed to convert")) 206 } 207 err := writeGenerated(*boardlauncher, mk2rbc.BoardLauncher( 208 outputFilePath(files[0]), outputFilePath(*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) (ok bool) { 314 if v, ok := converted[mkFile]; ok { 315 return v != nil 316 } 317 converted[mkFile] = nil 318 defer func() { 319 if r := recover(); r != nil { 320 ok = false 321 fmt.Fprintf(os.Stderr, "%s: panic while converting: %s\n%s\n", mkFile, r, debug.Stack()) 322 } 323 }() 324 325 mk2starRequest := mk2rbc.Request{ 326 MkFile: mkFile, 327 Reader: nil, 328 OutputDir: *outputTop, 329 OutputSuffix: *suffix, 330 TracedVariables: tracedVariables, 331 TraceCalls: *traceCalls, 332 SourceFS: os.DirFS("."), 333 MakefileFinder: makefileFinder, 334 ErrorLogger: errorLogger, 335 } 336 ss, err := mk2rbc.Convert(mk2starRequest) 337 if err != nil { 338 fmt.Fprintln(os.Stderr, mkFile, ": ", err) 339 return false 340 } 341 script := ss.String() 342 outputPath := outputFilePath(mkFile) 343 344 if *dryRun { 345 fmt.Printf("==== %s ====\n", outputPath) 346 // Print generated script after removing the copyright header 347 outText := cpNormalizer.ReplaceAllString(script, cpNormalizedCopyright) 348 fmt.Println(strings.TrimPrefix(outText, copyright)) 349 } else { 350 if err := maybeBackup(outputPath); err != nil { 351 fmt.Fprintln(os.Stderr, err) 352 return false 353 } 354 if err := writeGenerated(outputPath, script); err != nil { 355 fmt.Fprintln(os.Stderr, err) 356 return false 357 } 358 } 359 ok = true 360 if *recurse { 361 for _, sub := range ss.SubConfigFiles() { 362 // File may be absent if it is a conditional load 363 if _, err := os.Stat(sub); os.IsNotExist(err) { 364 continue 365 } 366 ok = convertOne(sub) && ok 367 } 368 } 369 converted[mkFile] = ss 370 return ok 371} 372 373// Optionally saves the previous version of the generated file 374func maybeBackup(filename string) error { 375 stat, err := os.Stat(filename) 376 if os.IsNotExist(err) { 377 return nil 378 } 379 if !stat.Mode().IsRegular() { 380 return fmt.Errorf("%s exists and is not a regular file", filename) 381 } 382 switch *mode { 383 case "backup": 384 return os.Rename(filename, filename+backupSuffix) 385 case "write": 386 return os.Remove(filename) 387 default: 388 return fmt.Errorf("%s already exists, use --mode option", filename) 389 } 390} 391 392func outputFilePath(mkFile string) string { 393 path := strings.TrimSuffix(mkFile, filepath.Ext(mkFile)) + *suffix 394 if *outputTop != "" { 395 path = filepath.Join(*outputTop, path) 396 } 397 return path 398} 399 400func writeGenerated(path string, contents string) error { 401 if err := os.MkdirAll(filepath.Dir(path), os.ModeDir|os.ModePerm); err != nil { 402 return err 403 } 404 if err := ioutil.WriteFile(path, []byte(contents), 0644); err != nil { 405 return err 406 } 407 return nil 408} 409 410func printStats() { 411 var sortedFiles []string 412 for p := range converted { 413 sortedFiles = append(sortedFiles, p) 414 } 415 sort.Strings(sortedFiles) 416 417 nOk, nPartial, nFailed := 0, 0, 0 418 for _, f := range sortedFiles { 419 if converted[f] == nil { 420 nFailed++ 421 } else if converted[f].HasErrors() { 422 nPartial++ 423 } else { 424 nOk++ 425 } 426 } 427 if nPartial > 0 { 428 fmt.Fprintf(os.Stderr, "Conversion was partially successful for:\n") 429 for _, f := range sortedFiles { 430 if ss := converted[f]; ss != nil && ss.HasErrors() { 431 fmt.Fprintln(os.Stderr, " ", f) 432 } 433 } 434 } 435 436 if nFailed > 0 { 437 fmt.Fprintf(os.Stderr, "Conversion failed for files:\n") 438 for _, f := range sortedFiles { 439 if converted[f] == nil { 440 fmt.Fprintln(os.Stderr, " ", f) 441 } 442 } 443 } 444} 445 446type datum struct { 447 count int 448 formattingArgs []string 449} 450 451type errorSink struct { 452 data map[string]datum 453} 454 455func (ebt errorSink) NewError(el mk2rbc.ErrorLocation, node parser.Node, message string, args ...interface{}) { 456 fmt.Fprint(os.Stderr, el, ": ") 457 fmt.Fprintf(os.Stderr, message, args...) 458 fmt.Fprintln(os.Stderr) 459 if !*errstat { 460 return 461 } 462 463 v, exists := ebt.data[message] 464 if exists { 465 v.count++ 466 } else { 467 v = datum{1, nil} 468 } 469 if strings.Contains(message, "%s") { 470 var newArg1 string 471 if len(args) == 0 { 472 panic(fmt.Errorf(`%s has %%s but args are missing`, message)) 473 } 474 newArg1 = fmt.Sprint(args[0]) 475 if message == "unsupported line" { 476 newArg1 = node.Dump() 477 } else if message == "unsupported directive %s" { 478 if newArg1 == "include" || newArg1 == "-include" { 479 newArg1 = node.Dump() 480 } 481 } 482 v.formattingArgs = append(v.formattingArgs, newArg1) 483 } 484 ebt.data[message] = v 485} 486 487func (ebt errorSink) printStatistics() { 488 if len(ebt.data) > 0 { 489 fmt.Fprintln(os.Stderr, "Error counts:") 490 } 491 for message, data := range ebt.data { 492 if len(data.formattingArgs) == 0 { 493 fmt.Fprintf(os.Stderr, "%4d %s\n", data.count, message) 494 continue 495 } 496 itemsByFreq, count := stringsWithFreq(data.formattingArgs, 30) 497 fmt.Fprintf(os.Stderr, "%4d %s [%d unique items]:\n", data.count, message, count) 498 fmt.Fprintln(os.Stderr, " ", itemsByFreq) 499 } 500} 501 502func stringsWithFreq(items []string, topN int) (string, int) { 503 freq := make(map[string]int) 504 for _, item := range items { 505 freq[strings.TrimPrefix(strings.TrimSuffix(item, "]"), "[")]++ 506 } 507 var sorted []string 508 for item := range freq { 509 sorted = append(sorted, item) 510 } 511 sort.Slice(sorted, func(i int, j int) bool { 512 return freq[sorted[i]] > freq[sorted[j]] 513 }) 514 sep := "" 515 res := "" 516 for i, item := range sorted { 517 if i >= topN { 518 res += " ..." 519 break 520 } 521 count := freq[item] 522 if count > 1 { 523 res += fmt.Sprintf("%s%s(%d)", sep, item, count) 524 } else { 525 res += fmt.Sprintf("%s%s", sep, item) 526 } 527 sep = ", " 528 } 529 return res, len(sorted) 530} 531 532// FindCommandMakefileFinder is an implementation of mk2rbc.MakefileFinder that 533// runs the unix find command to find all the makefiles in the source tree. 534type FindCommandMakefileFinder struct { 535 cachedRoot string 536 cachedMakefiles []string 537} 538 539func (l *FindCommandMakefileFinder) Find(root string) []string { 540 if l.cachedMakefiles != nil && l.cachedRoot == root { 541 return l.cachedMakefiles 542 } 543 544 // Return all *.mk files but not in hidden directories. 545 546 // NOTE(asmundak): as it turns out, even the WalkDir (which is an _optimized_ directory tree walker) 547 // is about twice slower than running `find` command (14s vs 6s on the internal Android source tree). 548 common_args := []string{"!", "-type", "d", "-name", "*.mk", "!", "-path", "*/.*/*"} 549 if root != "" { 550 common_args = append([]string{root}, common_args...) 551 } 552 cmd := exec.Command("/usr/bin/find", common_args...) 553 stdout, err := cmd.StdoutPipe() 554 if err == nil { 555 err = cmd.Start() 556 } 557 if err != nil { 558 panic(fmt.Errorf("cannot get the output from %s: %s", cmd, err)) 559 } 560 scanner := bufio.NewScanner(stdout) 561 result := make([]string, 0) 562 for scanner.Scan() { 563 result = append(result, strings.TrimPrefix(scanner.Text(), "./")) 564 } 565 stdout.Close() 566 err = scanner.Err() 567 if err != nil { 568 panic(fmt.Errorf("cannot get the output from %s: %s", cmd, err)) 569 } 570 l.cachedRoot = root 571 l.cachedMakefiles = result 572 return l.cachedMakefiles 573} 574 575// FileListMakefileFinder is an implementation of mk2rbc.MakefileFinder that 576// reads a file containing the list of makefiles in the android source tree. 577// This file is generated by soong's finder, so that it can be computed while 578// soong is already walking the source tree looking for other files. If the root 579// to find makefiles under is not the root of the android source tree, it will 580// fall back to using FindCommandMakefileFinder. 581type FileListMakefileFinder struct { 582 FindCommandMakefileFinder 583 cachedMakefiles []string 584 filePath string 585} 586 587func (l *FileListMakefileFinder) Find(root string) []string { 588 root, err1 := filepath.Abs(root) 589 wd, err2 := os.Getwd() 590 if root != wd || err1 != nil || err2 != nil { 591 return l.FindCommandMakefileFinder.Find(root) 592 } 593 if l.cachedMakefiles != nil { 594 return l.cachedMakefiles 595 } 596 597 file, err := os.Open(l.filePath) 598 if err != nil { 599 panic(fmt.Errorf("Cannot read makefile list: %s\n", err)) 600 } 601 defer file.Close() 602 603 result := make([]string, 0) 604 scanner := bufio.NewScanner(file) 605 for scanner.Scan() { 606 line := scanner.Text() 607 if len(line) > 0 { 608 result = append(result, line) 609 } 610 } 611 612 if err = scanner.Err(); err != nil { 613 panic(fmt.Errorf("Cannot read makefile list: %s\n", err)) 614 } 615 l.cachedMakefiles = result 616 return l.cachedMakefiles 617} 618