blob: bb5a680331f30245404d3e4130e91fc2e9e8b737 [file] [log] [blame]
Sasha Smundakb051c4e2020-11-05 20:45:07 -08001// 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 (
Sasha Smundak6609ba72021-07-22 18:32:56 -070024 "bufio"
Sasha Smundakb051c4e2020-11-05 20:45:07 -080025 "flag"
26 "fmt"
27 "io/ioutil"
28 "os"
Sasha Smundak6609ba72021-07-22 18:32:56 -070029 "os/exec"
Sasha Smundakb051c4e2020-11-05 20:45:07 -080030 "path/filepath"
31 "regexp"
32 "runtime/debug"
Sasha Smundak38802792021-08-01 14:38:07 -070033 "runtime/pprof"
Sasha Smundakb051c4e2020-11-05 20:45:07 -080034 "sort"
35 "strings"
36 "time"
37
38 "android/soong/androidmk/parser"
39 "android/soong/mk2rbc"
40)
41
42var (
43 rootDir = flag.String("root", ".", "the value of // for load paths")
44 // TODO(asmundak): remove this option once there is a consensus on suffix
45 suffix = flag.String("suffix", ".rbc", "generated files' suffix")
46 dryRun = flag.Bool("dry_run", false, "dry run")
47 recurse = flag.Bool("convert_dependents", false, "convert all dependent files")
48 mode = flag.String("mode", "", `"backup" to back up existing files, "write" to overwrite them`)
Sasha Smundakb051c4e2020-11-05 20:45:07 -080049 errstat = flag.Bool("error_stat", false, "print error statistics")
50 traceVar = flag.String("trace", "", "comma-separated list of variables to trace")
51 // TODO(asmundak): this option is for debugging
52 allInSource = flag.Bool("all", false, "convert all product config makefiles in the tree under //")
53 outputTop = flag.String("outdir", "", "write output files into this directory hierarchy")
Cole Faust6ed7cb42021-10-07 17:08:46 -070054 launcher = flag.String("launcher", "", "generated launcher path.")
55 boardlauncher = flag.String("boardlauncher", "", "generated board configuration launcher path.")
Sasha Smundakb051c4e2020-11-05 20:45:07 -080056 printProductConfigMap = flag.Bool("print_product_config_map", false, "print product config map and exit")
Sasha Smundak38802792021-08-01 14:38:07 -070057 cpuProfile = flag.String("cpu_profile", "", "write cpu profile to file")
Sasha Smundakb051c4e2020-11-05 20:45:07 -080058 traceCalls = flag.Bool("trace_calls", false, "trace function calls")
Cole Faust6ed7cb42021-10-07 17:08:46 -070059 inputVariables = flag.String("input_variables", "", "starlark file containing product config and global variables")
Sasha Smundakb051c4e2020-11-05 20:45:07 -080060)
61
62func init() {
Joe Onoratob4638c12021-10-27 15:47:06 -070063 // Simplistic flag aliasing: works, but the usage string is ugly and
Sasha Smundakb051c4e2020-11-05 20:45:07 -080064 // 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("root", "d")
74 flagAlias("dry_run", "n")
75 flagAlias("convert_dependents", "r")
Sasha Smundakb051c4e2020-11-05 20:45:07 -080076 flagAlias("error_stat", "e")
77}
78
79var backupSuffix string
80var tracedVariables []string
Sasha Smundak7d934b92021-11-10 12:20:01 -080081var errorLogger = errorSink{data: make(map[string]datum)}
Sasha Smundak6609ba72021-07-22 18:32:56 -070082var makefileFinder = &LinuxMakefileFinder{}
Sasha Smundakd7d07ad2021-09-10 15:42:34 -070083var versionDefaultsMk = filepath.Join("build", "make", "core", "version_defaults.mk")
Sasha Smundakb051c4e2020-11-05 20:45:07 -080084
85func main() {
86 flag.Usage = func() {
87 cmd := filepath.Base(os.Args[0])
88 fmt.Fprintf(flag.CommandLine.Output(),
Cole Faust6ed7cb42021-10-07 17:08:46 -070089 "Usage: %[1]s flags file...\n", cmd)
Sasha Smundakb051c4e2020-11-05 20:45:07 -080090 flag.PrintDefaults()
91 }
92 flag.Parse()
93
94 // Delouse
95 if *suffix == ".mk" {
96 quit("cannot use .mk as generated file suffix")
97 }
98 if *suffix == "" {
99 quit("suffix cannot be empty")
100 }
101 if *outputTop != "" {
102 if err := os.MkdirAll(*outputTop, os.ModeDir+os.ModePerm); err != nil {
103 quit(err)
104 }
105 s, err := filepath.Abs(*outputTop)
106 if err != nil {
107 quit(err)
108 }
109 *outputTop = s
110 }
111 if *allInSource && len(flag.Args()) > 0 {
112 quit("file list cannot be specified when -all is present")
113 }
114 if *allInSource && *launcher != "" {
115 quit("--all and --launcher are mutually exclusive")
116 }
117
118 // Flag-driven adjustments
119 if (*suffix)[0] != '.' {
120 *suffix = "." + *suffix
121 }
122 if *mode == "backup" {
123 backupSuffix = time.Now().Format("20060102150405")
124 }
125 if *traceVar != "" {
126 tracedVariables = strings.Split(*traceVar, ",")
127 }
128
Sasha Smundak38802792021-08-01 14:38:07 -0700129 if *cpuProfile != "" {
130 f, err := os.Create(*cpuProfile)
131 if err != nil {
132 quit(err)
133 }
134 pprof.StartCPUProfile(f)
135 defer pprof.StopCPUProfile()
136 }
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800137 // Find out global variables
138 getConfigVariables()
139 getSoongVariables()
140
141 if *printProductConfigMap {
142 productConfigMap := buildProductConfigMap()
143 var products []string
144 for p := range productConfigMap {
145 products = append(products, p)
146 }
147 sort.Strings(products)
148 for _, p := range products {
149 fmt.Println(p, productConfigMap[p])
150 }
151 os.Exit(0)
152 }
Sasha Smundakb2ac8592021-07-26 09:27:22 -0700153
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800154 // Convert!
Cole Faust07ea5032021-09-30 16:11:16 -0700155 files := flag.Args()
Cole Faust07ea5032021-09-30 16:11:16 -0700156 if *allInSource {
Cole Faustebf79bf2021-10-05 11:05:02 -0700157 productConfigMap := buildProductConfigMap()
Cole Faust07ea5032021-09-30 16:11:16 -0700158 for _, path := range productConfigMap {
159 files = append(files, path)
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800160 }
Cole Faust07ea5032021-09-30 16:11:16 -0700161 }
Cole Faust07ea5032021-09-30 16:11:16 -0700162 ok := true
163 for _, mkFile := range files {
164 ok = convertOne(mkFile) && ok
165 }
166
167 if *launcher != "" {
168 if len(files) != 1 {
169 quit(fmt.Errorf("a launcher can be generated only for a single product"))
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800170 }
Sasha Smundakd7d07ad2021-09-10 15:42:34 -0700171 versionDefaults, err := generateVersionDefaults()
172 if err != nil {
173 quit(err)
174 }
Sasha Smundakd7d07ad2021-09-10 15:42:34 -0700175 versionDefaultsPath := outputFilePath(versionDefaultsMk)
176 err = writeGenerated(versionDefaultsPath, versionDefaults)
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800177 if err != nil {
Cole Faust6ed7cb42021-10-07 17:08:46 -0700178 fmt.Fprintf(os.Stderr, "%s: %s", files[0], err)
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800179 ok = false
180 }
181
Cole Faust07ea5032021-09-30 16:11:16 -0700182 err = writeGenerated(*launcher, mk2rbc.Launcher(outputFilePath(files[0]), versionDefaultsPath,
183 mk2rbc.MakePath2ModuleName(files[0])))
Sasha Smundakd7d07ad2021-09-10 15:42:34 -0700184 if err != nil {
Cole Faust6ed7cb42021-10-07 17:08:46 -0700185 fmt.Fprintf(os.Stderr, "%s: %s", files[0], err)
186 ok = false
187 }
188 }
189 if *boardlauncher != "" {
190 if len(files) != 1 {
191 quit(fmt.Errorf("a launcher can be generated only for a single product"))
192 }
193 if *inputVariables == "" {
194 quit(fmt.Errorf("the board launcher requires an input variables file"))
195 }
196 if !convertOne(*inputVariables) {
197 quit(fmt.Errorf("the board launcher input variables file failed to convert"))
198 }
199 err := writeGenerated(*boardlauncher, mk2rbc.BoardLauncher(
200 outputFilePath(files[0]), outputFilePath(*inputVariables)))
201 if err != nil {
202 fmt.Fprintf(os.Stderr, "%s: %s", files[0], err)
Sasha Smundakd7d07ad2021-09-10 15:42:34 -0700203 ok = false
204 }
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800205 }
206
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800207 if *errstat {
208 errorLogger.printStatistics()
Sasha Smundak422b6142021-11-11 18:31:59 -0800209 printStats()
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800210 }
211 if !ok {
212 os.Exit(1)
213 }
214}
215
Sasha Smundakd7d07ad2021-09-10 15:42:34 -0700216func generateVersionDefaults() (string, error) {
217 versionSettings, err := mk2rbc.ParseVersionDefaults(filepath.Join(*rootDir, versionDefaultsMk))
218 if err != nil {
219 return "", err
220 }
221 return mk2rbc.VersionDefaults(versionSettings), nil
222
223}
224
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800225func quit(s interface{}) {
226 fmt.Fprintln(os.Stderr, s)
227 os.Exit(2)
228}
229
230func buildProductConfigMap() map[string]string {
231 const androidProductsMk = "AndroidProducts.mk"
232 // Build the list of AndroidProducts.mk files: it's
Sasha Smundak70f17452021-09-01 19:14:24 -0700233 // build/make/target/product/AndroidProducts.mk + device/**/AndroidProducts.mk plus + vendor/**/AndroidProducts.mk
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800234 targetAndroidProductsFile := filepath.Join(*rootDir, "build", "make", "target", "product", androidProductsMk)
235 if _, err := os.Stat(targetAndroidProductsFile); err != nil {
236 fmt.Fprintf(os.Stderr, "%s: %s\n(hint: %s is not a source tree root)\n",
237 targetAndroidProductsFile, err, *rootDir)
238 }
239 productConfigMap := make(map[string]string)
240 if err := mk2rbc.UpdateProductConfigMap(productConfigMap, targetAndroidProductsFile); err != nil {
241 fmt.Fprintf(os.Stderr, "%s: %s\n", targetAndroidProductsFile, err)
242 }
Sasha Smundak70f17452021-09-01 19:14:24 -0700243 for _, t := range []string{"device", "vendor"} {
244 _ = filepath.WalkDir(filepath.Join(*rootDir, t),
245 func(path string, d os.DirEntry, err error) error {
246 if err != nil || d.IsDir() || filepath.Base(path) != androidProductsMk {
247 return nil
248 }
249 if err2 := mk2rbc.UpdateProductConfigMap(productConfigMap, path); err2 != nil {
250 fmt.Fprintf(os.Stderr, "%s: %s\n", path, err)
251 // Keep going, we want to find all such errors in a single run
252 }
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800253 return nil
Sasha Smundak70f17452021-09-01 19:14:24 -0700254 })
255 }
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800256 return productConfigMap
257}
258
259func getConfigVariables() {
260 path := filepath.Join(*rootDir, "build", "make", "core", "product.mk")
261 if err := mk2rbc.FindConfigVariables(path, mk2rbc.KnownVariables); err != nil {
262 quit(fmt.Errorf("%s\n(check --root[=%s], it should point to the source root)",
263 err, *rootDir))
264 }
265}
266
267// Implements mkparser.Scope, to be used by mkparser.Value.Value()
268type fileNameScope struct {
269 mk2rbc.ScopeBase
270}
271
272func (s fileNameScope) Get(name string) string {
273 if name != "BUILD_SYSTEM" {
274 return fmt.Sprintf("$(%s)", name)
275 }
276 return filepath.Join(*rootDir, "build", "make", "core")
277}
278
279func getSoongVariables() {
280 path := filepath.Join(*rootDir, "build", "make", "core", "soong_config.mk")
281 err := mk2rbc.FindSoongVariables(path, fileNameScope{}, mk2rbc.KnownVariables)
282 if err != nil {
283 quit(err)
284 }
285}
286
287var converted = make(map[string]*mk2rbc.StarlarkScript)
288
289//goland:noinspection RegExpRepeatedSpace
290var cpNormalizer = regexp.MustCompile(
291 "# Copyright \\(C\\) 20.. The Android Open Source Project")
292
293const cpNormalizedCopyright = "# Copyright (C) 20xx The Android Open Source Project"
294const copyright = `#
295# Copyright (C) 20xx The Android Open Source Project
296#
297# Licensed under the Apache License, Version 2.0 (the "License");
298# you may not use this file except in compliance with the License.
299# You may obtain a copy of the License at
300#
301# http://www.apache.org/licenses/LICENSE-2.0
302#
303# Unless required by applicable law or agreed to in writing, software
304# distributed under the License is distributed on an "AS IS" BASIS,
305# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
306# See the License for the specific language governing permissions and
307# limitations under the License.
308#
309`
310
311// Convert a single file.
312// Write the result either to the same directory, to the same place in
313// the output hierarchy, or to the stdout.
314// Optionally, recursively convert the files this one includes by
315// $(call inherit-product) or an include statement.
316func convertOne(mkFile string) (ok bool) {
317 if v, ok := converted[mkFile]; ok {
318 return v != nil
319 }
320 converted[mkFile] = nil
321 defer func() {
322 if r := recover(); r != nil {
323 ok = false
324 fmt.Fprintf(os.Stderr, "%s: panic while converting: %s\n%s\n", mkFile, r, debug.Stack())
325 }
326 }()
327
328 mk2starRequest := mk2rbc.Request{
Sasha Smundak422b6142021-11-11 18:31:59 -0800329 MkFile: mkFile,
330 Reader: nil,
331 RootDir: *rootDir,
332 OutputDir: *outputTop,
333 OutputSuffix: *suffix,
334 TracedVariables: tracedVariables,
335 TraceCalls: *traceCalls,
336 SourceFS: os.DirFS(*rootDir),
337 MakefileFinder: makefileFinder,
338 ErrorLogger: errorLogger,
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800339 }
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 ok = true
364 if *recurse {
365 for _, sub := range ss.SubConfigFiles() {
366 // File may be absent if it is a conditional load
367 if _, err := os.Stat(sub); os.IsNotExist(err) {
368 continue
369 }
370 ok = convertOne(sub) && ok
371 }
372 }
373 converted[mkFile] = ss
374 return ok
375}
376
377// Optionally saves the previous version of the generated file
378func maybeBackup(filename string) error {
379 stat, err := os.Stat(filename)
380 if os.IsNotExist(err) {
381 return nil
382 }
383 if !stat.Mode().IsRegular() {
384 return fmt.Errorf("%s exists and is not a regular file", filename)
385 }
386 switch *mode {
387 case "backup":
388 return os.Rename(filename, filename+backupSuffix)
389 case "write":
390 return os.Remove(filename)
391 default:
392 return fmt.Errorf("%s already exists, use --mode option", filename)
393 }
394}
395
396func outputFilePath(mkFile string) string {
397 path := strings.TrimSuffix(mkFile, filepath.Ext(mkFile)) + *suffix
398 if *outputTop != "" {
399 path = filepath.Join(*outputTop, path)
400 }
401 return path
402}
403
404func writeGenerated(path string, contents string) error {
405 if err := os.MkdirAll(filepath.Dir(path), os.ModeDir|os.ModePerm); err != nil {
406 return err
407 }
408 if err := ioutil.WriteFile(path, []byte(contents), 0644); err != nil {
409 return err
410 }
411 return nil
412}
413
414func printStats() {
415 var sortedFiles []string
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800416 for p := range converted {
417 sortedFiles = append(sortedFiles, p)
418 }
419 sort.Strings(sortedFiles)
420
421 nOk, nPartial, nFailed := 0, 0, 0
422 for _, f := range sortedFiles {
423 if converted[f] == nil {
424 nFailed++
425 } else if converted[f].HasErrors() {
426 nPartial++
427 } else {
428 nOk++
429 }
430 }
Sasha Smundak422b6142021-11-11 18:31:59 -0800431 if nPartial > 0 {
432 fmt.Fprintf(os.Stderr, "Conversion was partially successful for:\n")
433 for _, f := range sortedFiles {
434 if ss := converted[f]; ss != nil && ss.HasErrors() {
435 fmt.Fprintln(os.Stderr, " ", f)
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800436 }
437 }
438 }
Sasha Smundak422b6142021-11-11 18:31:59 -0800439
440 if nFailed > 0 {
441 fmt.Fprintf(os.Stderr, "Conversion failed for files:\n")
442 for _, f := range sortedFiles {
443 if converted[f] == nil {
444 fmt.Fprintln(os.Stderr, " ", f)
445 }
446 }
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800447 }
448}
449
450type datum struct {
451 count int
452 formattingArgs []string
453}
454
Sasha Smundak7d934b92021-11-10 12:20:01 -0800455type errorSink struct {
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800456 data map[string]datum
457}
458
Sasha Smundak422b6142021-11-11 18:31:59 -0800459func (ebt errorSink) NewError(el mk2rbc.ErrorLocation, node parser.Node, message string, args ...interface{}) {
460 fmt.Fprint(os.Stderr, el, ": ")
Sasha Smundak7d934b92021-11-10 12:20:01 -0800461 fmt.Fprintf(os.Stderr, message, args...)
462 fmt.Fprintln(os.Stderr)
463 if !*errstat {
464 return
465 }
466
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800467 v, exists := ebt.data[message]
468 if exists {
469 v.count++
470 } else {
471 v = datum{1, nil}
472 }
473 if strings.Contains(message, "%s") {
474 var newArg1 string
475 if len(args) == 0 {
476 panic(fmt.Errorf(`%s has %%s but args are missing`, message))
477 }
478 newArg1 = fmt.Sprint(args[0])
479 if message == "unsupported line" {
480 newArg1 = node.Dump()
481 } else if message == "unsupported directive %s" {
482 if newArg1 == "include" || newArg1 == "-include" {
483 newArg1 = node.Dump()
484 }
485 }
486 v.formattingArgs = append(v.formattingArgs, newArg1)
487 }
488 ebt.data[message] = v
489}
490
Sasha Smundak7d934b92021-11-10 12:20:01 -0800491func (ebt errorSink) printStatistics() {
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800492 if len(ebt.data) > 0 {
493 fmt.Fprintln(os.Stderr, "Error counts:")
494 }
495 for message, data := range ebt.data {
496 if len(data.formattingArgs) == 0 {
497 fmt.Fprintf(os.Stderr, "%4d %s\n", data.count, message)
498 continue
499 }
500 itemsByFreq, count := stringsWithFreq(data.formattingArgs, 30)
501 fmt.Fprintf(os.Stderr, "%4d %s [%d unique items]:\n", data.count, message, count)
502 fmt.Fprintln(os.Stderr, " ", itemsByFreq)
503 }
504}
505
506func stringsWithFreq(items []string, topN int) (string, int) {
507 freq := make(map[string]int)
508 for _, item := range items {
509 freq[strings.TrimPrefix(strings.TrimSuffix(item, "]"), "[")]++
510 }
511 var sorted []string
512 for item := range freq {
513 sorted = append(sorted, item)
514 }
515 sort.Slice(sorted, func(i int, j int) bool {
516 return freq[sorted[i]] > freq[sorted[j]]
517 })
518 sep := ""
519 res := ""
520 for i, item := range sorted {
521 if i >= topN {
522 res += " ..."
523 break
524 }
525 count := freq[item]
526 if count > 1 {
527 res += fmt.Sprintf("%s%s(%d)", sep, item, count)
528 } else {
529 res += fmt.Sprintf("%s%s", sep, item)
530 }
531 sep = ", "
532 }
533 return res, len(sorted)
534}
Sasha Smundak6609ba72021-07-22 18:32:56 -0700535
536type LinuxMakefileFinder struct {
537 cachedRoot string
538 cachedMakefiles []string
539}
540
541func (l *LinuxMakefileFinder) Find(root string) []string {
542 if l.cachedMakefiles != nil && l.cachedRoot == root {
543 return l.cachedMakefiles
544 }
545 l.cachedRoot = root
546 l.cachedMakefiles = make([]string, 0)
547
548 // Return all *.mk files but not in hidden directories.
549
550 // NOTE(asmundak): as it turns out, even the WalkDir (which is an _optimized_ directory tree walker)
551 // is about twice slower than running `find` command (14s vs 6s on the internal Android source tree).
552 common_args := []string{"!", "-type", "d", "-name", "*.mk", "!", "-path", "*/.*/*"}
553 if root != "" {
554 common_args = append([]string{root}, common_args...)
555 }
556 cmd := exec.Command("/usr/bin/find", common_args...)
557 stdout, err := cmd.StdoutPipe()
558 if err == nil {
559 err = cmd.Start()
560 }
561 if err != nil {
562 panic(fmt.Errorf("cannot get the output from %s: %s", cmd, err))
563 }
564 scanner := bufio.NewScanner(stdout)
565 for scanner.Scan() {
566 l.cachedMakefiles = append(l.cachedMakefiles, strings.TrimPrefix(scanner.Text(), "./"))
567 }
568 stdout.Close()
569 return l.cachedMakefiles
570}