blob: 5a4ab06ce63d44a200c6c0563e2a1340a5d7e15d [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 (
24 "flag"
25 "fmt"
26 "io/ioutil"
27 "os"
28 "path/filepath"
29 "regexp"
30 "runtime/debug"
Sasha Smundak38802792021-08-01 14:38:07 -070031 "runtime/pprof"
Sasha Smundakb051c4e2020-11-05 20:45:07 -080032 "sort"
33 "strings"
34 "time"
35
36 "android/soong/androidmk/parser"
37 "android/soong/mk2rbc"
38)
39
40var (
41 rootDir = flag.String("root", ".", "the value of // for load paths")
42 // TODO(asmundak): remove this option once there is a consensus on suffix
43 suffix = flag.String("suffix", ".rbc", "generated files' suffix")
44 dryRun = flag.Bool("dry_run", false, "dry run")
45 recurse = flag.Bool("convert_dependents", false, "convert all dependent files")
46 mode = flag.String("mode", "", `"backup" to back up existing files, "write" to overwrite them`)
47 warn = flag.Bool("warnings", false, "warn about partially failed conversions")
48 verbose = flag.Bool("v", false, "print summary")
49 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")
54 launcher = flag.String("launcher", "", "generated launcher path. If set, the non-flag argument is _product_name_")
55 printProductConfigMap = flag.Bool("print_product_config_map", false, "print product config map and exit")
Sasha Smundak38802792021-08-01 14:38:07 -070056 cpuProfile = flag.String("cpu_profile", "", "write cpu profile to file")
Sasha Smundakb051c4e2020-11-05 20:45:07 -080057 traceCalls = flag.Bool("trace_calls", false, "trace function calls")
58)
59
60func init() {
61 // Poor man's flag aliasing: works, but the usage string is ugly and
62 // both flag and its alias can be present on the command line
63 flagAlias := func(target string, alias string) {
64 if f := flag.Lookup(target); f != nil {
65 flag.Var(f.Value, alias, "alias for --"+f.Name)
66 return
67 }
68 quit("cannot alias unknown flag " + target)
69 }
70 flagAlias("suffix", "s")
71 flagAlias("root", "d")
72 flagAlias("dry_run", "n")
73 flagAlias("convert_dependents", "r")
74 flagAlias("warnings", "w")
75 flagAlias("error_stat", "e")
76}
77
78var backupSuffix string
79var tracedVariables []string
80var errorLogger = errorsByType{data: make(map[string]datum)}
81
82func main() {
83 flag.Usage = func() {
84 cmd := filepath.Base(os.Args[0])
85 fmt.Fprintf(flag.CommandLine.Output(),
86 "Usage: %[1]s flags file...\n"+
87 "or: %[1]s flags --launcher=PATH PRODUCT\n", cmd)
88 flag.PrintDefaults()
89 }
90 flag.Parse()
91
92 // Delouse
93 if *suffix == ".mk" {
94 quit("cannot use .mk as generated file suffix")
95 }
96 if *suffix == "" {
97 quit("suffix cannot be empty")
98 }
99 if *outputTop != "" {
100 if err := os.MkdirAll(*outputTop, os.ModeDir+os.ModePerm); err != nil {
101 quit(err)
102 }
103 s, err := filepath.Abs(*outputTop)
104 if err != nil {
105 quit(err)
106 }
107 *outputTop = s
108 }
109 if *allInSource && len(flag.Args()) > 0 {
110 quit("file list cannot be specified when -all is present")
111 }
112 if *allInSource && *launcher != "" {
113 quit("--all and --launcher are mutually exclusive")
114 }
115
116 // Flag-driven adjustments
117 if (*suffix)[0] != '.' {
118 *suffix = "." + *suffix
119 }
120 if *mode == "backup" {
121 backupSuffix = time.Now().Format("20060102150405")
122 }
123 if *traceVar != "" {
124 tracedVariables = strings.Split(*traceVar, ",")
125 }
126
Sasha Smundak38802792021-08-01 14:38:07 -0700127 if *cpuProfile != "" {
128 f, err := os.Create(*cpuProfile)
129 if err != nil {
130 quit(err)
131 }
132 pprof.StartCPUProfile(f)
133 defer pprof.StopCPUProfile()
134 }
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800135 // Find out global variables
136 getConfigVariables()
137 getSoongVariables()
138
139 if *printProductConfigMap {
140 productConfigMap := buildProductConfigMap()
141 var products []string
142 for p := range productConfigMap {
143 products = append(products, p)
144 }
145 sort.Strings(products)
146 for _, p := range products {
147 fmt.Println(p, productConfigMap[p])
148 }
149 os.Exit(0)
150 }
Sasha Smundakb2ac8592021-07-26 09:27:22 -0700151
Sasha Smundakb051c4e2020-11-05 20:45:07 -0800152 // Convert!
153 ok := true
154 if *launcher != "" {
155 if len(flag.Args()) != 1 {
156 quit(fmt.Errorf("a launcher can be generated only for a single product"))
157 }
158 product := flag.Args()[0]
159 productConfigMap := buildProductConfigMap()
160 path, found := productConfigMap[product]
161 if !found {
162 quit(fmt.Errorf("cannot generate configuration launcher for %s, it is not a known product",
163 product))
164 }
165 ok = convertOne(path) && ok
166 err := writeGenerated(*launcher, mk2rbc.Launcher(outputFilePath(path), mk2rbc.MakePath2ModuleName(path)))
167 if err != nil {
168 fmt.Fprintf(os.Stderr, "%s:%s", path, err)
169 ok = false
170 }
171
172 } else {
173 files := flag.Args()
174 if *allInSource {
175 productConfigMap := buildProductConfigMap()
176 for _, path := range productConfigMap {
177 files = append(files, path)
178 }
179 }
180 for _, mkFile := range files {
181 ok = convertOne(mkFile) && ok
182 }
183 }
184
185 printStats()
186 if *errstat {
187 errorLogger.printStatistics()
188 }
189 if !ok {
190 os.Exit(1)
191 }
192}
193
194func quit(s interface{}) {
195 fmt.Fprintln(os.Stderr, s)
196 os.Exit(2)
197}
198
199func buildProductConfigMap() map[string]string {
200 const androidProductsMk = "AndroidProducts.mk"
201 // Build the list of AndroidProducts.mk files: it's
202 // build/make/target/product/AndroidProducts.mk plus
203 // device/**/AndroidProducts.mk
204 targetAndroidProductsFile := filepath.Join(*rootDir, "build", "make", "target", "product", androidProductsMk)
205 if _, err := os.Stat(targetAndroidProductsFile); err != nil {
206 fmt.Fprintf(os.Stderr, "%s: %s\n(hint: %s is not a source tree root)\n",
207 targetAndroidProductsFile, err, *rootDir)
208 }
209 productConfigMap := make(map[string]string)
210 if err := mk2rbc.UpdateProductConfigMap(productConfigMap, targetAndroidProductsFile); err != nil {
211 fmt.Fprintf(os.Stderr, "%s: %s\n", targetAndroidProductsFile, err)
212 }
213 _ = filepath.Walk(filepath.Join(*rootDir, "device"),
214 func(path string, info os.FileInfo, err error) error {
215 if info.IsDir() || filepath.Base(path) != androidProductsMk {
216 return nil
217 }
218 if err2 := mk2rbc.UpdateProductConfigMap(productConfigMap, path); err2 != nil {
219 fmt.Fprintf(os.Stderr, "%s: %s\n", path, err)
220 // Keep going, we want to find all such errors in a single run
221 }
222 return nil
223 })
224 return productConfigMap
225}
226
227func getConfigVariables() {
228 path := filepath.Join(*rootDir, "build", "make", "core", "product.mk")
229 if err := mk2rbc.FindConfigVariables(path, mk2rbc.KnownVariables); err != nil {
230 quit(fmt.Errorf("%s\n(check --root[=%s], it should point to the source root)",
231 err, *rootDir))
232 }
233}
234
235// Implements mkparser.Scope, to be used by mkparser.Value.Value()
236type fileNameScope struct {
237 mk2rbc.ScopeBase
238}
239
240func (s fileNameScope) Get(name string) string {
241 if name != "BUILD_SYSTEM" {
242 return fmt.Sprintf("$(%s)", name)
243 }
244 return filepath.Join(*rootDir, "build", "make", "core")
245}
246
247func getSoongVariables() {
248 path := filepath.Join(*rootDir, "build", "make", "core", "soong_config.mk")
249 err := mk2rbc.FindSoongVariables(path, fileNameScope{}, mk2rbc.KnownVariables)
250 if err != nil {
251 quit(err)
252 }
253}
254
255var converted = make(map[string]*mk2rbc.StarlarkScript)
256
257//goland:noinspection RegExpRepeatedSpace
258var cpNormalizer = regexp.MustCompile(
259 "# Copyright \\(C\\) 20.. The Android Open Source Project")
260
261const cpNormalizedCopyright = "# Copyright (C) 20xx The Android Open Source Project"
262const copyright = `#
263# Copyright (C) 20xx The Android Open Source Project
264#
265# Licensed under the Apache License, Version 2.0 (the "License");
266# you may not use this file except in compliance with the License.
267# You may obtain a copy of the License at
268#
269# http://www.apache.org/licenses/LICENSE-2.0
270#
271# Unless required by applicable law or agreed to in writing, software
272# distributed under the License is distributed on an "AS IS" BASIS,
273# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
274# See the License for the specific language governing permissions and
275# limitations under the License.
276#
277`
278
279// Convert a single file.
280// Write the result either to the same directory, to the same place in
281// the output hierarchy, or to the stdout.
282// Optionally, recursively convert the files this one includes by
283// $(call inherit-product) or an include statement.
284func convertOne(mkFile string) (ok bool) {
285 if v, ok := converted[mkFile]; ok {
286 return v != nil
287 }
288 converted[mkFile] = nil
289 defer func() {
290 if r := recover(); r != nil {
291 ok = false
292 fmt.Fprintf(os.Stderr, "%s: panic while converting: %s\n%s\n", mkFile, r, debug.Stack())
293 }
294 }()
295
296 mk2starRequest := mk2rbc.Request{
297 MkFile: mkFile,
298 Reader: nil,
299 RootDir: *rootDir,
300 OutputDir: *outputTop,
301 OutputSuffix: *suffix,
302 TracedVariables: tracedVariables,
303 TraceCalls: *traceCalls,
304 WarnPartialSuccess: *warn,
305 }
306 if *errstat {
307 mk2starRequest.ErrorLogger = errorLogger
308 }
309 ss, err := mk2rbc.Convert(mk2starRequest)
310 if err != nil {
311 fmt.Fprintln(os.Stderr, mkFile, ": ", err)
312 return false
313 }
314 script := ss.String()
315 outputPath := outputFilePath(mkFile)
316
317 if *dryRun {
318 fmt.Printf("==== %s ====\n", outputPath)
319 // Print generated script after removing the copyright header
320 outText := cpNormalizer.ReplaceAllString(script, cpNormalizedCopyright)
321 fmt.Println(strings.TrimPrefix(outText, copyright))
322 } else {
323 if err := maybeBackup(outputPath); err != nil {
324 fmt.Fprintln(os.Stderr, err)
325 return false
326 }
327 if err := writeGenerated(outputPath, script); err != nil {
328 fmt.Fprintln(os.Stderr, err)
329 return false
330 }
331 }
332 ok = true
333 if *recurse {
334 for _, sub := range ss.SubConfigFiles() {
335 // File may be absent if it is a conditional load
336 if _, err := os.Stat(sub); os.IsNotExist(err) {
337 continue
338 }
339 ok = convertOne(sub) && ok
340 }
341 }
342 converted[mkFile] = ss
343 return ok
344}
345
346// Optionally saves the previous version of the generated file
347func maybeBackup(filename string) error {
348 stat, err := os.Stat(filename)
349 if os.IsNotExist(err) {
350 return nil
351 }
352 if !stat.Mode().IsRegular() {
353 return fmt.Errorf("%s exists and is not a regular file", filename)
354 }
355 switch *mode {
356 case "backup":
357 return os.Rename(filename, filename+backupSuffix)
358 case "write":
359 return os.Remove(filename)
360 default:
361 return fmt.Errorf("%s already exists, use --mode option", filename)
362 }
363}
364
365func outputFilePath(mkFile string) string {
366 path := strings.TrimSuffix(mkFile, filepath.Ext(mkFile)) + *suffix
367 if *outputTop != "" {
368 path = filepath.Join(*outputTop, path)
369 }
370 return path
371}
372
373func writeGenerated(path string, contents string) error {
374 if err := os.MkdirAll(filepath.Dir(path), os.ModeDir|os.ModePerm); err != nil {
375 return err
376 }
377 if err := ioutil.WriteFile(path, []byte(contents), 0644); err != nil {
378 return err
379 }
380 return nil
381}
382
383func printStats() {
384 var sortedFiles []string
385 if !*warn && !*verbose {
386 return
387 }
388 for p := range converted {
389 sortedFiles = append(sortedFiles, p)
390 }
391 sort.Strings(sortedFiles)
392
393 nOk, nPartial, nFailed := 0, 0, 0
394 for _, f := range sortedFiles {
395 if converted[f] == nil {
396 nFailed++
397 } else if converted[f].HasErrors() {
398 nPartial++
399 } else {
400 nOk++
401 }
402 }
403 if *warn {
404 if nPartial > 0 {
405 fmt.Fprintf(os.Stderr, "Conversion was partially successful for:\n")
406 for _, f := range sortedFiles {
407 if ss := converted[f]; ss != nil && ss.HasErrors() {
408 fmt.Fprintln(os.Stderr, " ", f)
409 }
410 }
411 }
412
413 if nFailed > 0 {
414 fmt.Fprintf(os.Stderr, "Conversion failed for files:\n")
415 for _, f := range sortedFiles {
416 if converted[f] == nil {
417 fmt.Fprintln(os.Stderr, " ", f)
418 }
419 }
420 }
421 }
422 if *verbose {
423 fmt.Fprintf(os.Stderr, "%-16s%5d\n", "Succeeded:", nOk)
424 fmt.Fprintf(os.Stderr, "%-16s%5d\n", "Partial:", nPartial)
425 fmt.Fprintf(os.Stderr, "%-16s%5d\n", "Failed:", nFailed)
426 }
427}
428
429type datum struct {
430 count int
431 formattingArgs []string
432}
433
434type errorsByType struct {
435 data map[string]datum
436}
437
438func (ebt errorsByType) NewError(message string, node parser.Node, args ...interface{}) {
439 v, exists := ebt.data[message]
440 if exists {
441 v.count++
442 } else {
443 v = datum{1, nil}
444 }
445 if strings.Contains(message, "%s") {
446 var newArg1 string
447 if len(args) == 0 {
448 panic(fmt.Errorf(`%s has %%s but args are missing`, message))
449 }
450 newArg1 = fmt.Sprint(args[0])
451 if message == "unsupported line" {
452 newArg1 = node.Dump()
453 } else if message == "unsupported directive %s" {
454 if newArg1 == "include" || newArg1 == "-include" {
455 newArg1 = node.Dump()
456 }
457 }
458 v.formattingArgs = append(v.formattingArgs, newArg1)
459 }
460 ebt.data[message] = v
461}
462
463func (ebt errorsByType) printStatistics() {
464 if len(ebt.data) > 0 {
465 fmt.Fprintln(os.Stderr, "Error counts:")
466 }
467 for message, data := range ebt.data {
468 if len(data.formattingArgs) == 0 {
469 fmt.Fprintf(os.Stderr, "%4d %s\n", data.count, message)
470 continue
471 }
472 itemsByFreq, count := stringsWithFreq(data.formattingArgs, 30)
473 fmt.Fprintf(os.Stderr, "%4d %s [%d unique items]:\n", data.count, message, count)
474 fmt.Fprintln(os.Stderr, " ", itemsByFreq)
475 }
476}
477
478func stringsWithFreq(items []string, topN int) (string, int) {
479 freq := make(map[string]int)
480 for _, item := range items {
481 freq[strings.TrimPrefix(strings.TrimSuffix(item, "]"), "[")]++
482 }
483 var sorted []string
484 for item := range freq {
485 sorted = append(sorted, item)
486 }
487 sort.Slice(sorted, func(i int, j int) bool {
488 return freq[sorted[i]] > freq[sorted[j]]
489 })
490 sep := ""
491 res := ""
492 for i, item := range sorted {
493 if i >= topN {
494 res += " ..."
495 break
496 }
497 count := freq[item]
498 if count > 1 {
499 res += fmt.Sprintf("%s%s(%d)", sep, item, count)
500 } else {
501 res += fmt.Sprintf("%s%s", sep, item)
502 }
503 sep = ", "
504 }
505 return res, len(sorted)
506}