• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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