• 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, []string{}) && 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, []string{}) {
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, []string{}) {
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, loadStack []string) (ok bool) {
314	if v, ok := converted[mkFile]; ok {
315		if v == nil {
316			fmt.Fprintf(os.Stderr, "Cycle in load graph:\n%s\n%s\n\n", strings.Join(loadStack, "\n"), mkFile)
317			return false
318		}
319		return true
320	}
321	converted[mkFile] = nil
322	defer func() {
323		if r := recover(); r != nil {
324			ok = false
325			fmt.Fprintf(os.Stderr, "%s: panic while converting: %s\n%s\n", mkFile, r, debug.Stack())
326		}
327	}()
328
329	mk2starRequest := mk2rbc.Request{
330		MkFile:          mkFile,
331		Reader:          nil,
332		OutputDir:       *outputTop,
333		OutputSuffix:    *suffix,
334		TracedVariables: tracedVariables,
335		TraceCalls:      *traceCalls,
336		SourceFS:        os.DirFS("."),
337		MakefileFinder:  makefileFinder,
338		ErrorLogger:     errorLogger,
339	}
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	loadStack = append(loadStack, mkFile)
364	ok = true
365	if *recurse {
366		for _, sub := range ss.SubConfigFiles() {
367			// File may be absent if it is a conditional load
368			if _, err := os.Stat(sub); os.IsNotExist(err) {
369				continue
370			}
371			ok = convertOne(sub, loadStack) && ok
372		}
373	}
374	converted[mkFile] = ss
375	return ok
376}
377
378// Optionally saves the previous version of the generated file
379func maybeBackup(filename string) error {
380	stat, err := os.Stat(filename)
381	if os.IsNotExist(err) {
382		return nil
383	}
384	if !stat.Mode().IsRegular() {
385		return fmt.Errorf("%s exists and is not a regular file", filename)
386	}
387	switch *mode {
388	case "backup":
389		return os.Rename(filename, filename+backupSuffix)
390	case "write":
391		return os.Remove(filename)
392	default:
393		return fmt.Errorf("%s already exists, use --mode option", filename)
394	}
395}
396
397func outputFilePath(mkFile string) string {
398	path := strings.TrimSuffix(mkFile, filepath.Ext(mkFile)) + *suffix
399	if *outputTop != "" {
400		path = filepath.Join(*outputTop, path)
401	}
402	return path
403}
404
405func writeGenerated(path string, contents string) error {
406	if err := os.MkdirAll(filepath.Dir(path), os.ModeDir|os.ModePerm); err != nil {
407		return err
408	}
409	if err := ioutil.WriteFile(path, []byte(contents), 0644); err != nil {
410		return err
411	}
412	return nil
413}
414
415func printStats() {
416	var sortedFiles []string
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 nPartial > 0 {
433		fmt.Fprintf(os.Stderr, "Conversion was partially successful for:\n")
434		for _, f := range sortedFiles {
435			if ss := converted[f]; ss != nil && ss.HasErrors() {
436				fmt.Fprintln(os.Stderr, "  ", f)
437			}
438		}
439	}
440
441	if nFailed > 0 {
442		fmt.Fprintf(os.Stderr, "Conversion failed for files:\n")
443		for _, f := range sortedFiles {
444			if converted[f] == nil {
445				fmt.Fprintln(os.Stderr, "  ", f)
446			}
447		}
448	}
449}
450
451type datum struct {
452	count          int
453	formattingArgs []string
454}
455
456type errorSink struct {
457	data map[string]datum
458}
459
460func (ebt errorSink) NewError(el mk2rbc.ErrorLocation, node parser.Node, message string, args ...interface{}) {
461	fmt.Fprint(os.Stderr, el, ": ")
462	fmt.Fprintf(os.Stderr, message, args...)
463	fmt.Fprintln(os.Stderr)
464	if !*errstat {
465		return
466	}
467
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 errorSink) 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}
536
537// FindCommandMakefileFinder is an implementation of mk2rbc.MakefileFinder that
538// runs the unix find command to find all the makefiles in the source tree.
539type FindCommandMakefileFinder struct {
540	cachedRoot      string
541	cachedMakefiles []string
542}
543
544func (l *FindCommandMakefileFinder) Find(root string) []string {
545	if l.cachedMakefiles != nil && l.cachedRoot == root {
546		return l.cachedMakefiles
547	}
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	result := make([]string, 0)
567	for scanner.Scan() {
568		result = append(result, strings.TrimPrefix(scanner.Text(), "./"))
569	}
570	stdout.Close()
571	err = scanner.Err()
572	if err != nil {
573		panic(fmt.Errorf("cannot get the output from %s: %s", cmd, err))
574	}
575	l.cachedRoot = root
576	l.cachedMakefiles = result
577	return l.cachedMakefiles
578}
579
580// FileListMakefileFinder is an implementation of mk2rbc.MakefileFinder that
581// reads a file containing the list of makefiles in the android source tree.
582// This file is generated by soong's finder, so that it can be computed while
583// soong is already walking the source tree looking for other files. If the root
584// to find makefiles under is not the root of the android source tree, it will
585// fall back to using FindCommandMakefileFinder.
586type FileListMakefileFinder struct {
587	FindCommandMakefileFinder
588	cachedMakefiles []string
589	filePath        string
590}
591
592func (l *FileListMakefileFinder) Find(root string) []string {
593	root, err1 := filepath.Abs(root)
594	wd, err2 := os.Getwd()
595	if root != wd || err1 != nil || err2 != nil {
596		return l.FindCommandMakefileFinder.Find(root)
597	}
598	if l.cachedMakefiles != nil {
599		return l.cachedMakefiles
600	}
601
602	file, err := os.Open(l.filePath)
603	if err != nil {
604		panic(fmt.Errorf("Cannot read makefile list: %s\n", err))
605	}
606	defer file.Close()
607
608	result := make([]string, 0)
609	scanner := bufio.NewScanner(file)
610	for scanner.Scan() {
611		line := scanner.Text()
612		if len(line) > 0 {
613			result = append(result, line)
614		}
615	}
616
617	if err = scanner.Err(); err != nil {
618		panic(fmt.Errorf("Cannot read makefile list: %s\n", err))
619	}
620	l.cachedMakefiles = result
621	return l.cachedMakefiles
622}
623