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(outputModulePath(files[0]), outputModulePath(*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			outputModulePath(files[0]), outputModulePath(*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 outputModulePath(mkFile string) string {
406	path := outputFilePath(mkFile)
407	path, err := mk2rbc.RelativeToCwd(path)
408	if err != nil {
409		panic(err)
410	}
411	return "//" + path
412}
413
414func writeGenerated(path string, contents string) error {
415	if err := os.MkdirAll(filepath.Dir(path), os.ModeDir|os.ModePerm); err != nil {
416		return err
417	}
418	if err := ioutil.WriteFile(path, []byte(contents), 0644); err != nil {
419		return err
420	}
421	return nil
422}
423
424func printStats() {
425	var sortedFiles []string
426	for p := range converted {
427		sortedFiles = append(sortedFiles, p)
428	}
429	sort.Strings(sortedFiles)
430
431	nOk, nPartial, nFailed := 0, 0, 0
432	for _, f := range sortedFiles {
433		if converted[f] == nil {
434			nFailed++
435		} else if converted[f].HasErrors() {
436			nPartial++
437		} else {
438			nOk++
439		}
440	}
441	if nPartial > 0 {
442		fmt.Fprintf(os.Stderr, "Conversion was partially successful for:\n")
443		for _, f := range sortedFiles {
444			if ss := converted[f]; ss != nil && ss.HasErrors() {
445				fmt.Fprintln(os.Stderr, "  ", f)
446			}
447		}
448	}
449
450	if nFailed > 0 {
451		fmt.Fprintf(os.Stderr, "Conversion failed for files:\n")
452		for _, f := range sortedFiles {
453			if converted[f] == nil {
454				fmt.Fprintln(os.Stderr, "  ", f)
455			}
456		}
457	}
458}
459
460type datum struct {
461	count          int
462	formattingArgs []string
463}
464
465type errorSink struct {
466	data map[string]datum
467}
468
469func (ebt errorSink) NewError(el mk2rbc.ErrorLocation, node parser.Node, message string, args ...interface{}) {
470	fmt.Fprint(os.Stderr, el, ": ")
471	fmt.Fprintf(os.Stderr, message, args...)
472	fmt.Fprintln(os.Stderr)
473	if !*errstat {
474		return
475	}
476
477	v, exists := ebt.data[message]
478	if exists {
479		v.count++
480	} else {
481		v = datum{1, nil}
482	}
483	if strings.Contains(message, "%s") {
484		var newArg1 string
485		if len(args) == 0 {
486			panic(fmt.Errorf(`%s has %%s but args are missing`, message))
487		}
488		newArg1 = fmt.Sprint(args[0])
489		if message == "unsupported line" {
490			newArg1 = node.Dump()
491		} else if message == "unsupported directive %s" {
492			if newArg1 == "include" || newArg1 == "-include" {
493				newArg1 = node.Dump()
494			}
495		}
496		v.formattingArgs = append(v.formattingArgs, newArg1)
497	}
498	ebt.data[message] = v
499}
500
501func (ebt errorSink) printStatistics() {
502	if len(ebt.data) > 0 {
503		fmt.Fprintln(os.Stderr, "Error counts:")
504	}
505	for message, data := range ebt.data {
506		if len(data.formattingArgs) == 0 {
507			fmt.Fprintf(os.Stderr, "%4d %s\n", data.count, message)
508			continue
509		}
510		itemsByFreq, count := stringsWithFreq(data.formattingArgs, 30)
511		fmt.Fprintf(os.Stderr, "%4d %s [%d unique items]:\n", data.count, message, count)
512		fmt.Fprintln(os.Stderr, "      ", itemsByFreq)
513	}
514}
515
516func stringsWithFreq(items []string, topN int) (string, int) {
517	freq := make(map[string]int)
518	for _, item := range items {
519		freq[strings.TrimPrefix(strings.TrimSuffix(item, "]"), "[")]++
520	}
521	var sorted []string
522	for item := range freq {
523		sorted = append(sorted, item)
524	}
525	sort.Slice(sorted, func(i int, j int) bool {
526		return freq[sorted[i]] > freq[sorted[j]]
527	})
528	sep := ""
529	res := ""
530	for i, item := range sorted {
531		if i >= topN {
532			res += " ..."
533			break
534		}
535		count := freq[item]
536		if count > 1 {
537			res += fmt.Sprintf("%s%s(%d)", sep, item, count)
538		} else {
539			res += fmt.Sprintf("%s%s", sep, item)
540		}
541		sep = ", "
542	}
543	return res, len(sorted)
544}
545
546// FindCommandMakefileFinder is an implementation of mk2rbc.MakefileFinder that
547// runs the unix find command to find all the makefiles in the source tree.
548type FindCommandMakefileFinder struct {
549	cachedRoot      string
550	cachedMakefiles []string
551}
552
553func (l *FindCommandMakefileFinder) Find(root string) []string {
554	if l.cachedMakefiles != nil && l.cachedRoot == root {
555		return l.cachedMakefiles
556	}
557
558	// Return all *.mk files but not in hidden directories.
559
560	// NOTE(asmundak): as it turns out, even the WalkDir (which is an _optimized_ directory tree walker)
561	// is about twice slower than running `find` command (14s vs 6s on the internal Android source tree).
562	common_args := []string{"!", "-type", "d", "-name", "*.mk", "!", "-path", "*/.*/*"}
563	if root != "" {
564		common_args = append([]string{root}, common_args...)
565	}
566	cmd := exec.Command("/usr/bin/find", common_args...)
567	stdout, err := cmd.StdoutPipe()
568	if err == nil {
569		err = cmd.Start()
570	}
571	if err != nil {
572		panic(fmt.Errorf("cannot get the output from %s: %s", cmd, err))
573	}
574	scanner := bufio.NewScanner(stdout)
575	result := make([]string, 0)
576	for scanner.Scan() {
577		result = append(result, strings.TrimPrefix(scanner.Text(), "./"))
578	}
579	stdout.Close()
580	err = scanner.Err()
581	if err != nil {
582		panic(fmt.Errorf("cannot get the output from %s: %s", cmd, err))
583	}
584	l.cachedRoot = root
585	l.cachedMakefiles = result
586	return l.cachedMakefiles
587}
588
589// FileListMakefileFinder is an implementation of mk2rbc.MakefileFinder that
590// reads a file containing the list of makefiles in the android source tree.
591// This file is generated by soong's finder, so that it can be computed while
592// soong is already walking the source tree looking for other files. If the root
593// to find makefiles under is not the root of the android source tree, it will
594// fall back to using FindCommandMakefileFinder.
595type FileListMakefileFinder struct {
596	FindCommandMakefileFinder
597	cachedMakefiles []string
598	filePath        string
599}
600
601func (l *FileListMakefileFinder) Find(root string) []string {
602	root, err1 := filepath.Abs(root)
603	wd, err2 := os.Getwd()
604	if root != wd || err1 != nil || err2 != nil {
605		return l.FindCommandMakefileFinder.Find(root)
606	}
607	if l.cachedMakefiles != nil {
608		return l.cachedMakefiles
609	}
610
611	file, err := os.Open(l.filePath)
612	if err != nil {
613		panic(fmt.Errorf("Cannot read makefile list: %s\n", err))
614	}
615	defer file.Close()
616
617	result := make([]string, 0)
618	scanner := bufio.NewScanner(file)
619	for scanner.Scan() {
620		line := scanner.Text()
621		if len(line) > 0 {
622			result = append(result, line)
623		}
624	}
625
626	if err = scanner.Err(); err != nil {
627		panic(fmt.Errorf("Cannot read makefile list: %s\n", err))
628	}
629	l.cachedMakefiles = result
630	return l.cachedMakefiles
631}
632