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