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
15package mk2rbc
16
17import (
18	"bytes"
19	"fmt"
20	"io/ioutil"
21	"os"
22	"regexp"
23	"strings"
24
25	mkparser "android/soong/androidmk/parser"
26)
27
28type context struct {
29	includeFileScope mkparser.Scope
30	registrar        variableRegistrar
31}
32
33// Scans the makefile Soong uses to generate soong.variables file,
34// collecting variable names and types from the lines that look like this:
35//
36//	$(call add_json_XXX,  <...>,             $(VAR))
37func FindSoongVariables(mkFile string, includeFileScope mkparser.Scope, registrar variableRegistrar) error {
38	ctx := context{includeFileScope, registrar}
39	return ctx.doFind(mkFile)
40}
41
42func (ctx *context) doFind(mkFile string) error {
43	mkContents, err := ioutil.ReadFile(mkFile)
44	if err != nil {
45		return err
46	}
47	parser := mkparser.NewParser(mkFile, bytes.NewBuffer(mkContents))
48	nodes, errs := parser.Parse()
49	if len(errs) > 0 {
50		for _, e := range errs {
51			fmt.Fprintln(os.Stderr, "ERROR:", e)
52		}
53		return fmt.Errorf("cannot parse %s", mkFile)
54	}
55	for _, node := range nodes {
56		switch t := node.(type) {
57		case *mkparser.Variable:
58			ctx.handleVariable(t)
59		case *mkparser.Directive:
60			ctx.handleInclude(t)
61		}
62	}
63	return nil
64}
65
66func (ctx context) NewSoongVariable(name, typeString string) {
67	var valueType starlarkType
68	switch typeString {
69	case "bool":
70		// TODO: We run into several issues later on if we type this as a bool:
71		//    - We still assign bool-typed variables to strings
72		//    - When emitting the final results as make code, some bool's false values have to
73		//      be an empty string, and some have to be false in order to match the make variables.
74		valueType = starlarkTypeString
75	case "csv":
76		// Only PLATFORM_VERSION_ALL_CODENAMES, and it's a list
77		valueType = starlarkTypeList
78	case "list":
79		valueType = starlarkTypeList
80	case "str":
81		valueType = starlarkTypeString
82	case "val":
83		// Only PLATFORM_SDK_VERSION uses this, and it's integer
84		valueType = starlarkTypeInt
85	default:
86		panic(fmt.Errorf("unknown Soong variable type %s", typeString))
87	}
88
89	ctx.registrar.NewVariable(name, VarClassSoong, valueType)
90}
91
92func (ctx context) handleInclude(t *mkparser.Directive) {
93	if t.Name != "include" && t.Name != "-include" {
94		return
95	}
96	includedPath := t.Args.Value(ctx.includeFileScope)
97	err := ctx.doFind(includedPath)
98	if err != nil && t.Name == "include" {
99		fmt.Fprintf(os.Stderr, "cannot include %s: %s", includedPath, err)
100	}
101}
102
103var callFuncRex = regexp.MustCompile("^call +add_json_(str|val|bool|csv|list) *,")
104
105func (ctx context) handleVariable(t *mkparser.Variable) {
106	// From the variable reference looking as follows:
107	//  $(call json_add_TYPE,arg1,$(VAR))
108	// we infer that the type of $(VAR) is TYPE
109	// VAR can be a simple variable name, or another call
110	// (e.g., $(call invert_bool, $(X)), from which we can infer
111	// that the type of X is bool
112	if prefix, v, ok := prefixedVariable(t.Name); ok && strings.HasPrefix(prefix, "call add_json") {
113		if match := callFuncRex.FindStringSubmatch(prefix); match != nil {
114			ctx.inferSoongVariableType(match[1], v)
115			// NOTE(asmundak): sometimes arg1 (the name of the Soong variable defined
116			// in this statement) may indicate that there is a Make counterpart. E.g, from
117			//     $(call add_json_bool, DisablePreopt, $(call invert_bool,$(ENABLE_PREOPT)))
118			// it may be inferred that there is a Make boolean variable DISABLE_PREOPT.
119			// Unfortunately, Soong variable names have no 1:1 correspondence to Make variables,
120			// for instance,
121			//       $(call add_json_list, PatternsOnSystemOther, $(SYSTEM_OTHER_ODEX_FILTER))
122			// does not mean that there is PATTERNS_ON_SYSTEM_OTHER
123			// Our main interest lies in finding the variables whose values are lists, and
124			// so far there are none that can be found this way, so it is not important.
125		} else {
126			panic(fmt.Errorf("cannot match the call: %s", prefix))
127		}
128	}
129}
130
131var (
132	callInvertBoolRex = regexp.MustCompile("^call +invert_bool *, *$")
133	callFilterBoolRex = regexp.MustCompile("^(filter|filter-out) +(true|false), *$")
134)
135
136func (ctx context) inferSoongVariableType(vType string, n *mkparser.MakeString) {
137	if n.Const() {
138		ctx.NewSoongVariable(n.Strings[0], vType)
139		return
140	}
141	if prefix, v, ok := prefixedVariable(n); ok {
142		if callInvertBoolRex.MatchString(prefix) || callFilterBoolRex.MatchString(prefix) {
143			// It is $(call invert_bool, $(VAR)) or $(filter[-out] [false|true],$(VAR))
144			ctx.inferSoongVariableType("bool", v)
145		}
146	}
147}
148
149// If MakeString is foo$(BAR), returns 'foo', BAR(as *MakeString) and true
150func prefixedVariable(s *mkparser.MakeString) (string, *mkparser.MakeString, bool) {
151	if len(s.Strings) != 2 || s.Strings[1] != "" {
152		return "", nil, false
153	}
154	return s.Strings[0], s.Variables[0].Name, true
155}
156