blob: 9d0f0c1b0854d456be48ccd5e920f171b58658ae [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`)
49 warn = flag.Bool("warnings", false, "warn about partially failed conversions")
50 verbose = flag.Bool("v", false, "print summary")
51 errstat = flag.Bool("error_stat", false, "print error statistics")
52 traceVar = flag.String("trace", "", "comma-separated list of variables to trace")
53 // TODO(asmundak): this option is for debugging
54 allInSource = flag.Bool("all", false, "convert all product config makefiles in the tree under //")
55 outputTop = flag.String("outdir", "", "write output files into this directory hierarchy")
56 launcher = flag.String("launcher", "", "generated launcher path. If set, the non-flag argument is _product_name_")
57 printProductConfigMap = flag.Bool("print_product_config_map", false, "print product config map and exit")
Sasha Smundak38802792021-08-01 14:38:07 -070058 cpuProfile = flag.String("cpu_profile", "", "write cpu profile to file")
Sasha Smundakb051c4e2020-11-05 20:45:07 -080059 traceCalls = flag.Bool("trace_calls", false, "trace function calls")
60)
61
62func init() {
63 // Poor man's 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("root", "d")
74 flagAlias("dry_run", "n")
75 flagAlias("convert_dependents", "r")
76 flagAlias("warnings", "w")
77 flagAlias("error_stat", "e")
78}
79
80var backupSuffix string
81var tracedVariables []string
82var errorLogger = errorsByType{data: make(map[string]datum)}
Sasha Smundak6609ba72021-07-22 18:32:56 -070083var makefileFinder = &LinuxMakefileFinder{}
Sasha Smundakd7d07ad2021-09-10 15:42:34 -070084var versionDefaultsMk = filepath.Join("build", "make", "core", "version_defaults.mk")
Sasha Smundakb051c4e2020-11-05 20:45:07 -080085
86func main() {
87 flag.Usage = func() {
88 cmd := filepath.Base(os.Args[0])
89 fmt.Fprintf(flag.CommandLine.Output(),
90 "Usage: %[1]s flags file...\n"+
91 "or: %[1]s flags --launcher=PATH PRODUCT\n", cmd)
92 flag.PrintDefaults()
93 }
94 flag.Parse()
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
Sasha Smundak38802792021-08-01 14:38:07 -0700131 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 }
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800139 // Find out global variables
140 getConfigVariables()
141 getSoongVariables()
142
143 if *printProductConfigMap {
144 productConfigMap := buildProductConfigMap()
145 var products []string
146 for p := range productConfigMap {
147 products = append(products, p)
148 }
149 sort.Strings(products)
150 for _, p := range products {
151 fmt.Println(p, productConfigMap[p])
152 }
153 os.Exit(0)
154 }
Sasha Smundakb2ac8592021-07-26 09:27:22 -0700155
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800156 // Convert!
Cole Faust07ea5032021-09-30 16:11:16 -0700157 files := flag.Args()
158 productConfigMap := buildProductConfigMap()
159 if *allInSource {
160 for _, path := range productConfigMap {
161 files = append(files, path)
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800162 }
Cole Faust07ea5032021-09-30 16:11:16 -0700163 }
164 for i, file := range files {
165 if _, err := os.Stat(file); os.IsNotExist(err) {
166 temp, ok := productConfigMap[file]
167 if ok {
168 files[i] = temp
169 } else {
170 quit(fmt.Errorf("%s is neither a product makefile nor a product name", file))
171 }
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"))
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800182 }
Sasha Smundakd7d07ad2021-09-10 15:42:34 -0700183 versionDefaults, err := generateVersionDefaults()
184 if err != nil {
185 quit(err)
186 }
Sasha Smundakd7d07ad2021-09-10 15:42:34 -0700187 versionDefaultsPath := outputFilePath(versionDefaultsMk)
188 err = writeGenerated(versionDefaultsPath, versionDefaults)
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800189 if err != nil {
Cole Faust07ea5032021-09-30 16:11:16 -0700190 fmt.Fprintf(os.Stderr, "%s:%s", files[0], err)
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800191 ok = false
192 }
193
Cole Faust07ea5032021-09-30 16:11:16 -0700194 err = writeGenerated(*launcher, mk2rbc.Launcher(outputFilePath(files[0]), versionDefaultsPath,
195 mk2rbc.MakePath2ModuleName(files[0])))
Sasha Smundakd7d07ad2021-09-10 15:42:34 -0700196 if err != nil {
Cole Faust07ea5032021-09-30 16:11:16 -0700197 fmt.Fprintf(os.Stderr, "%s:%s", files[0], err)
Sasha Smundakd7d07ad2021-09-10 15:42:34 -0700198 ok = false
199 }
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800200 }
201
202 printStats()
203 if *errstat {
204 errorLogger.printStatistics()
205 }
206 if !ok {
207 os.Exit(1)
208 }
209}
210
Sasha Smundakd7d07ad2021-09-10 15:42:34 -0700211func generateVersionDefaults() (string, error) {
212 versionSettings, err := mk2rbc.ParseVersionDefaults(filepath.Join(*rootDir, versionDefaultsMk))
213 if err != nil {
214 return "", err
215 }
216 return mk2rbc.VersionDefaults(versionSettings), nil
217
218}
219
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800220func quit(s interface{}) {
221 fmt.Fprintln(os.Stderr, s)
222 os.Exit(2)
223}
224
225func buildProductConfigMap() map[string]string {
226 const androidProductsMk = "AndroidProducts.mk"
227 // Build the list of AndroidProducts.mk files: it's
Sasha Smundak70f17452021-09-01 19:14:24 -0700228 // build/make/target/product/AndroidProducts.mk + device/**/AndroidProducts.mk plus + vendor/**/AndroidProducts.mk
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800229 targetAndroidProductsFile := filepath.Join(*rootDir, "build", "make", "target", "product", androidProductsMk)
230 if _, err := os.Stat(targetAndroidProductsFile); err != nil {
231 fmt.Fprintf(os.Stderr, "%s: %s\n(hint: %s is not a source tree root)\n",
232 targetAndroidProductsFile, err, *rootDir)
233 }
234 productConfigMap := make(map[string]string)
235 if err := mk2rbc.UpdateProductConfigMap(productConfigMap, targetAndroidProductsFile); err != nil {
236 fmt.Fprintf(os.Stderr, "%s: %s\n", targetAndroidProductsFile, err)
237 }
Sasha Smundak70f17452021-09-01 19:14:24 -0700238 for _, t := range []string{"device", "vendor"} {
239 _ = filepath.WalkDir(filepath.Join(*rootDir, t),
240 func(path string, d os.DirEntry, err error) error {
241 if err != nil || d.IsDir() || filepath.Base(path) != androidProductsMk {
242 return nil
243 }
244 if err2 := mk2rbc.UpdateProductConfigMap(productConfigMap, path); err2 != nil {
245 fmt.Fprintf(os.Stderr, "%s: %s\n", path, err)
246 // Keep going, we want to find all such errors in a single run
247 }
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800248 return nil
Sasha Smundak70f17452021-09-01 19:14:24 -0700249 })
250 }
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800251 return productConfigMap
252}
253
254func getConfigVariables() {
255 path := filepath.Join(*rootDir, "build", "make", "core", "product.mk")
256 if err := mk2rbc.FindConfigVariables(path, mk2rbc.KnownVariables); err != nil {
257 quit(fmt.Errorf("%s\n(check --root[=%s], it should point to the source root)",
258 err, *rootDir))
259 }
260}
261
262// Implements mkparser.Scope, to be used by mkparser.Value.Value()
263type fileNameScope struct {
264 mk2rbc.ScopeBase
265}
266
267func (s fileNameScope) Get(name string) string {
268 if name != "BUILD_SYSTEM" {
269 return fmt.Sprintf("$(%s)", name)
270 }
271 return filepath.Join(*rootDir, "build", "make", "core")
272}
273
274func getSoongVariables() {
275 path := filepath.Join(*rootDir, "build", "make", "core", "soong_config.mk")
276 err := mk2rbc.FindSoongVariables(path, fileNameScope{}, mk2rbc.KnownVariables)
277 if err != nil {
278 quit(err)
279 }
280}
281
282var converted = make(map[string]*mk2rbc.StarlarkScript)
283
284//goland:noinspection RegExpRepeatedSpace
285var cpNormalizer = regexp.MustCompile(
286 "# Copyright \\(C\\) 20.. The Android Open Source Project")
287
288const cpNormalizedCopyright = "# Copyright (C) 20xx The Android Open Source Project"
289const copyright = `#
290# Copyright (C) 20xx The Android Open Source Project
291#
292# Licensed under the Apache License, Version 2.0 (the "License");
293# you may not use this file except in compliance with the License.
294# You may obtain a copy of the License at
295#
296# http://www.apache.org/licenses/LICENSE-2.0
297#
298# Unless required by applicable law or agreed to in writing, software
299# distributed under the License is distributed on an "AS IS" BASIS,
300# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
301# See the License for the specific language governing permissions and
302# limitations under the License.
303#
304`
305
306// Convert a single file.
307// Write the result either to the same directory, to the same place in
308// the output hierarchy, or to the stdout.
309// Optionally, recursively convert the files this one includes by
310// $(call inherit-product) or an include statement.
311func convertOne(mkFile string) (ok bool) {
312 if v, ok := converted[mkFile]; ok {
313 return v != nil
314 }
315 converted[mkFile] = nil
316 defer func() {
317 if r := recover(); r != nil {
318 ok = false
319 fmt.Fprintf(os.Stderr, "%s: panic while converting: %s\n%s\n", mkFile, r, debug.Stack())
320 }
321 }()
322
323 mk2starRequest := mk2rbc.Request{
324 MkFile: mkFile,
325 Reader: nil,
326 RootDir: *rootDir,
327 OutputDir: *outputTop,
328 OutputSuffix: *suffix,
329 TracedVariables: tracedVariables,
330 TraceCalls: *traceCalls,
331 WarnPartialSuccess: *warn,
Sasha Smundak6609ba72021-07-22 18:32:56 -0700332 SourceFS: os.DirFS(*rootDir),
333 MakefileFinder: makefileFinder,
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800334 }
335 if *errstat {
336 mk2starRequest.ErrorLogger = errorLogger
337 }
338 ss, err := mk2rbc.Convert(mk2starRequest)
339 if err != nil {
340 fmt.Fprintln(os.Stderr, mkFile, ": ", err)
341 return false
342 }
343 script := ss.String()
344 outputPath := outputFilePath(mkFile)
345
346 if *dryRun {
347 fmt.Printf("==== %s ====\n", outputPath)
348 // Print generated script after removing the copyright header
349 outText := cpNormalizer.ReplaceAllString(script, cpNormalizedCopyright)
350 fmt.Println(strings.TrimPrefix(outText, copyright))
351 } else {
352 if err := maybeBackup(outputPath); err != nil {
353 fmt.Fprintln(os.Stderr, err)
354 return false
355 }
356 if err := writeGenerated(outputPath, script); err != nil {
357 fmt.Fprintln(os.Stderr, err)
358 return false
359 }
360 }
361 ok = true
362 if *recurse {
363 for _, sub := range ss.SubConfigFiles() {
364 // File may be absent if it is a conditional load
365 if _, err := os.Stat(sub); os.IsNotExist(err) {
366 continue
367 }
368 ok = convertOne(sub) && ok
369 }
370 }
371 converted[mkFile] = ss
372 return ok
373}
374
375// Optionally saves the previous version of the generated file
376func maybeBackup(filename string) error {
377 stat, err := os.Stat(filename)
378 if os.IsNotExist(err) {
379 return nil
380 }
381 if !stat.Mode().IsRegular() {
382 return fmt.Errorf("%s exists and is not a regular file", filename)
383 }
384 switch *mode {
385 case "backup":
386 return os.Rename(filename, filename+backupSuffix)
387 case "write":
388 return os.Remove(filename)
389 default:
390 return fmt.Errorf("%s already exists, use --mode option", filename)
391 }
392}
393
394func outputFilePath(mkFile string) string {
395 path := strings.TrimSuffix(mkFile, filepath.Ext(mkFile)) + *suffix
396 if *outputTop != "" {
397 path = filepath.Join(*outputTop, path)
398 }
399 return path
400}
401
402func writeGenerated(path string, contents string) error {
403 if err := os.MkdirAll(filepath.Dir(path), os.ModeDir|os.ModePerm); err != nil {
404 return err
405 }
406 if err := ioutil.WriteFile(path, []byte(contents), 0644); err != nil {
407 return err
408 }
409 return nil
410}
411
412func printStats() {
413 var sortedFiles []string
414 if !*warn && !*verbose {
415 return
416 }
417 for p := range converted {
418 sortedFiles = append(sortedFiles, p)
419 }
420 sort.Strings(sortedFiles)
421
422 nOk, nPartial, nFailed := 0, 0, 0
423 for _, f := range sortedFiles {
424 if converted[f] == nil {
425 nFailed++
426 } else if converted[f].HasErrors() {
427 nPartial++
428 } else {
429 nOk++
430 }
431 }
432 if *warn {
433 if nPartial > 0 {
434 fmt.Fprintf(os.Stderr, "Conversion was partially successful for:\n")
435 for _, f := range sortedFiles {
436 if ss := converted[f]; ss != nil && ss.HasErrors() {
437 fmt.Fprintln(os.Stderr, " ", f)
438 }
439 }
440 }
441
442 if nFailed > 0 {
443 fmt.Fprintf(os.Stderr, "Conversion failed for files:\n")
444 for _, f := range sortedFiles {
445 if converted[f] == nil {
446 fmt.Fprintln(os.Stderr, " ", f)
447 }
448 }
449 }
450 }
451 if *verbose {
452 fmt.Fprintf(os.Stderr, "%-16s%5d\n", "Succeeded:", nOk)
453 fmt.Fprintf(os.Stderr, "%-16s%5d\n", "Partial:", nPartial)
454 fmt.Fprintf(os.Stderr, "%-16s%5d\n", "Failed:", nFailed)
455 }
456}
457
458type datum struct {
459 count int
460 formattingArgs []string
461}
462
463type errorsByType struct {
464 data map[string]datum
465}
466
467func (ebt errorsByType) NewError(message string, node parser.Node, args ...interface{}) {
468 v, exists := ebt.data[message]
469 if exists {
470 v.count++
471 } else {
472 v = datum{1, nil}
473 }
474 if strings.Contains(message, "%s") {
475 var newArg1 string
476 if len(args) == 0 {
477 panic(fmt.Errorf(`%s has %%s but args are missing`, message))
478 }
479 newArg1 = fmt.Sprint(args[0])
480 if message == "unsupported line" {
481 newArg1 = node.Dump()
482 } else if message == "unsupported directive %s" {
483 if newArg1 == "include" || newArg1 == "-include" {
484 newArg1 = node.Dump()
485 }
486 }
487 v.formattingArgs = append(v.formattingArgs, newArg1)
488 }
489 ebt.data[message] = v
490}
491
492func (ebt errorsByType) printStatistics() {
493 if len(ebt.data) > 0 {
494 fmt.Fprintln(os.Stderr, "Error counts:")
495 }
496 for message, data := range ebt.data {
497 if len(data.formattingArgs) == 0 {
498 fmt.Fprintf(os.Stderr, "%4d %s\n", data.count, message)
499 continue
500 }
501 itemsByFreq, count := stringsWithFreq(data.formattingArgs, 30)
502 fmt.Fprintf(os.Stderr, "%4d %s [%d unique items]:\n", data.count, message, count)
503 fmt.Fprintln(os.Stderr, " ", itemsByFreq)
504 }
505}
506
507func stringsWithFreq(items []string, topN int) (string, int) {
508 freq := make(map[string]int)
509 for _, item := range items {
510 freq[strings.TrimPrefix(strings.TrimSuffix(item, "]"), "[")]++
511 }
512 var sorted []string
513 for item := range freq {
514 sorted = append(sorted, item)
515 }
516 sort.Slice(sorted, func(i int, j int) bool {
517 return freq[sorted[i]] > freq[sorted[j]]
518 })
519 sep := ""
520 res := ""
521 for i, item := range sorted {
522 if i >= topN {
523 res += " ..."
524 break
525 }
526 count := freq[item]
527 if count > 1 {
528 res += fmt.Sprintf("%s%s(%d)", sep, item, count)
529 } else {
530 res += fmt.Sprintf("%s%s", sep, item)
531 }
532 sep = ", "
533 }
534 return res, len(sorted)
535}
Sasha Smundak6609ba72021-07-22 18:32:56 -0700536
537type LinuxMakefileFinder struct {
538 cachedRoot string
539 cachedMakefiles []string
540}
541
542func (l *LinuxMakefileFinder) Find(root string) []string {
543 if l.cachedMakefiles != nil && l.cachedRoot == root {
544 return l.cachedMakefiles
545 }
546 l.cachedRoot = root
547 l.cachedMakefiles = make([]string, 0)
548
549 // Return all *.mk files but not in hidden directories.
550
551 // NOTE(asmundak): as it turns out, even the WalkDir (which is an _optimized_ directory tree walker)
552 // is about twice slower than running `find` command (14s vs 6s on the internal Android source tree).
553 common_args := []string{"!", "-type", "d", "-name", "*.mk", "!", "-path", "*/.*/*"}
554 if root != "" {
555 common_args = append([]string{root}, common_args...)
556 }
557 cmd := exec.Command("/usr/bin/find", common_args...)
558 stdout, err := cmd.StdoutPipe()
559 if err == nil {
560 err = cmd.Start()
561 }
562 if err != nil {
563 panic(fmt.Errorf("cannot get the output from %s: %s", cmd, err))
564 }
565 scanner := bufio.NewScanner(stdout)
566 for scanner.Scan() {
567 l.cachedMakefiles = append(l.cachedMakefiles, strings.TrimPrefix(scanner.Text(), "./"))
568 }
569 stdout.Close()
570 return l.cachedMakefiles
571}