1// Copyright 2017 Google Inc. All rights reserved.
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 androidmk
16
17import (
18	"bytes"
19	"fmt"
20	"strings"
21	"text/scanner"
22
23	"android/soong/bpfix/bpfix"
24
25	mkparser "android/soong/androidmk/parser"
26
27	bpparser "github.com/google/blueprint/parser"
28)
29
30// TODO: non-expanded variables with expressions
31
32type bpFile struct {
33	comments          []*bpparser.CommentGroup
34	defs              []bpparser.Definition
35	localAssignments  map[string]*bpparser.Property
36	globalAssignments map[string]*bpparser.Expression
37	variableRenames   map[string]string
38	scope             mkparser.Scope
39	module            *bpparser.Module
40
41	mkPos scanner.Position // Position of the last handled line in the makefile
42	bpPos scanner.Position // Position of the last emitted line to the blueprint file
43
44	inModule bool
45}
46
47var invalidVariableStringToReplacement = map[string]string{
48	"-": "_dash_",
49}
50
51// Fix steps that should only run in the androidmk tool, i.e. should only be applied to
52// newly-converted Android.bp files.
53var fixSteps = bpfix.FixStepsExtension{
54	Name: "androidmk",
55	Steps: []bpfix.FixStep{
56		{
57			Name: "RewriteRuntimeResourceOverlay",
58			Fix:  bpfix.RewriteRuntimeResourceOverlay,
59		},
60	},
61}
62
63func init() {
64	bpfix.RegisterFixStepExtension(&fixSteps)
65}
66
67func (f *bpFile) insertComment(s string) {
68	f.comments = append(f.comments, &bpparser.CommentGroup{
69		Comments: []*bpparser.Comment{
70			&bpparser.Comment{
71				Comment: []string{s},
72				Slash:   f.bpPos,
73			},
74		},
75	})
76	f.bpPos.Offset += len(s)
77}
78
79func (f *bpFile) insertExtraComment(s string) {
80	f.insertComment(s)
81	f.bpPos.Line++
82}
83
84// records that the given node failed to be converted and includes an explanatory message
85func (f *bpFile) errorf(failedNode mkparser.Node, message string, args ...interface{}) {
86	orig := failedNode.Dump()
87	message = fmt.Sprintf(message, args...)
88	f.addErrorText(fmt.Sprintf("// ANDROIDMK TRANSLATION ERROR: %s", message))
89
90	lines := strings.Split(orig, "\n")
91	for _, l := range lines {
92		f.insertExtraComment("// " + l)
93	}
94}
95
96// records that something unexpected occurred
97func (f *bpFile) warnf(message string, args ...interface{}) {
98	message = fmt.Sprintf(message, args...)
99	f.addErrorText(fmt.Sprintf("// ANDROIDMK TRANSLATION WARNING: %s", message))
100}
101
102// adds the given error message as-is to the bottom of the (in-progress) file
103func (f *bpFile) addErrorText(message string) {
104	f.insertExtraComment(message)
105}
106
107func (f *bpFile) setMkPos(pos, end scanner.Position) {
108	// It is unusual but not forbidden for pos.Line to be smaller than f.mkPos.Line
109	// For example:
110	//
111	// if true                       # this line is emitted 1st
112	// if true                       # this line is emitted 2nd
113	// some-target: some-file        # this line is emitted 3rd
114	//         echo doing something  # this recipe is emitted 6th
115	// endif #some comment           # this endif is emitted 4th; this comment is part of the recipe
116	//         echo doing more stuff # this is part of the recipe
117	// endif                         # this endif is emitted 5th
118	//
119	// However, if pos.Line < f.mkPos.Line, we treat it as though it were equal
120	if pos.Line >= f.mkPos.Line {
121		f.bpPos.Line += (pos.Line - f.mkPos.Line)
122		f.mkPos = end
123	}
124
125}
126
127type conditional struct {
128	cond string
129	eq   bool
130}
131
132func ConvertFile(filename string, buffer *bytes.Buffer) (string, []error) {
133	p := mkparser.NewParser(filename, buffer)
134
135	nodes, errs := p.Parse()
136	if len(errs) > 0 {
137		return "", errs
138	}
139
140	file := &bpFile{
141		scope:             androidScope(),
142		localAssignments:  make(map[string]*bpparser.Property),
143		globalAssignments: make(map[string]*bpparser.Expression),
144		variableRenames:   make(map[string]string),
145	}
146
147	var conds []*conditional
148	var assignmentCond *conditional
149	var tree *bpparser.File
150
151	for _, node := range nodes {
152		file.setMkPos(p.Unpack(node.Pos()), p.Unpack(node.End()))
153
154		switch x := node.(type) {
155		case *mkparser.Comment:
156			// Split the comment on escaped newlines and then
157			// add each chunk separately.
158			chunks := strings.Split(x.Comment, "\\\n")
159			file.insertComment("//" + chunks[0])
160			for i := 1; i < len(chunks); i++ {
161				file.bpPos.Line++
162				file.insertComment("//" + chunks[i])
163			}
164		case *mkparser.Assignment:
165			handleAssignment(file, x, assignmentCond)
166		case *mkparser.Directive:
167			switch x.Name {
168			case "include", "-include":
169				module, ok := mapIncludePath(x.Args.Value(file.scope))
170				if !ok {
171					file.errorf(x, "unsupported include")
172					continue
173				}
174				switch module {
175				case clearVarsPath:
176					resetModule(file)
177				case includeIgnoredPath:
178					// subdirs are already automatically included in Soong
179					continue
180				default:
181					handleModuleConditionals(file, x, conds)
182					makeModule(file, module)
183				}
184			case "ifeq", "ifneq", "ifdef", "ifndef":
185				args := x.Args.Dump()
186				eq := x.Name == "ifeq" || x.Name == "ifdef"
187				if _, ok := conditionalTranslations[args]; ok {
188					newCond := conditional{args, eq}
189					conds = append(conds, &newCond)
190					if file.inModule {
191						if assignmentCond == nil {
192							assignmentCond = &newCond
193						} else {
194							file.errorf(x, "unsupported nested conditional in module")
195						}
196					}
197				} else {
198					file.errorf(x, "unsupported conditional")
199					conds = append(conds, nil)
200					continue
201				}
202			case "else":
203				if len(conds) == 0 {
204					file.errorf(x, "missing if before else")
205					continue
206				} else if conds[len(conds)-1] == nil {
207					file.errorf(x, "else from unsupported conditional")
208					continue
209				}
210				conds[len(conds)-1].eq = !conds[len(conds)-1].eq
211			case "endif":
212				if len(conds) == 0 {
213					file.errorf(x, "missing if before endif")
214					continue
215				} else if conds[len(conds)-1] == nil {
216					file.errorf(x, "endif from unsupported conditional")
217					conds = conds[:len(conds)-1]
218				} else {
219					if assignmentCond == conds[len(conds)-1] {
220						assignmentCond = nil
221					}
222					conds = conds[:len(conds)-1]
223				}
224			default:
225				file.errorf(x, "unsupported directive")
226				continue
227			}
228		default:
229			file.errorf(x, "unsupported line")
230		}
231	}
232
233	tree = &bpparser.File{
234		Defs:     file.defs,
235		Comments: file.comments,
236	}
237
238	// check for common supported but undesirable structures and clean them up
239	fixer := bpfix.NewFixer(tree)
240	fixedTree, fixerErr := fixer.Fix(bpfix.NewFixRequest().AddAll())
241	if fixerErr != nil {
242		errs = append(errs, fixerErr)
243	} else {
244		tree = fixedTree
245	}
246
247	out, err := bpparser.Print(tree)
248	if err != nil {
249		errs = append(errs, err)
250		return "", errs
251	}
252
253	return string(out), errs
254}
255
256func renameVariableWithInvalidCharacters(name string) string {
257	renamed := ""
258	for invalid, replacement := range invalidVariableStringToReplacement {
259		if strings.Contains(name, invalid) {
260			renamed = strings.ReplaceAll(name, invalid, replacement)
261		}
262	}
263
264	return renamed
265}
266
267func invalidVariableStrings() string {
268	invalidStrings := make([]string, 0, len(invalidVariableStringToReplacement))
269	for s := range invalidVariableStringToReplacement {
270		invalidStrings = append(invalidStrings, "\""+s+"\"")
271	}
272	return strings.Join(invalidStrings, ", ")
273}
274
275func handleAssignment(file *bpFile, assignment *mkparser.Assignment, c *conditional) {
276	if !assignment.Name.Const() {
277		file.errorf(assignment, "unsupported non-const variable name")
278		return
279	}
280
281	if assignment.Target != nil {
282		file.errorf(assignment, "unsupported target assignment")
283		return
284	}
285
286	name := assignment.Name.Value(nil)
287	prefix := ""
288
289	if newName := renameVariableWithInvalidCharacters(name); newName != "" {
290		file.warnf("Variable names cannot contain: %s. Renamed \"%s\" to \"%s\"", invalidVariableStrings(), name, newName)
291		file.variableRenames[name] = newName
292		name = newName
293	}
294
295	if strings.HasPrefix(name, "LOCAL_") {
296		for _, x := range propertyPrefixes {
297			if strings.HasSuffix(name, "_"+x.mk) {
298				name = strings.TrimSuffix(name, "_"+x.mk)
299				prefix = x.bp
300				break
301			}
302		}
303
304		if c != nil {
305			if prefix != "" {
306				file.errorf(assignment, "prefix assignment inside conditional, skipping conditional")
307			} else {
308				var ok bool
309				if prefix, ok = conditionalTranslations[c.cond][c.eq]; !ok {
310					panic("unknown conditional")
311				}
312			}
313		}
314	} else {
315		if c != nil {
316			eq := "eq"
317			if !c.eq {
318				eq = "neq"
319			}
320			file.errorf(assignment, "conditional %s %s on global assignment", eq, c.cond)
321		}
322	}
323
324	appendVariable := assignment.Type == "+="
325
326	var err error
327	if prop, ok := rewriteProperties[name]; ok {
328		err = prop(variableAssignmentContext{file, prefix, assignment.Value, appendVariable})
329	} else {
330		switch {
331		case name == "LOCAL_ARM_MODE":
332			// This is a hack to get the LOCAL_ARM_MODE value inside
333			// of an arch: { arm: {} } block.
334			armModeAssign := assignment
335			armModeAssign.Name = mkparser.SimpleMakeString("LOCAL_ARM_MODE_HACK_arm", assignment.Name.Pos())
336			handleAssignment(file, armModeAssign, c)
337		case strings.HasPrefix(name, "LOCAL_"):
338			file.errorf(assignment, "unsupported assignment to %s", name)
339			return
340		default:
341			var val bpparser.Expression
342			val, err = makeVariableToBlueprint(file, assignment.Value, bpparser.ListType)
343			if err == nil {
344				err = setVariable(file, appendVariable, prefix, name, val, false)
345			}
346		}
347	}
348	if err != nil {
349		file.errorf(assignment, err.Error())
350	}
351}
352
353func handleModuleConditionals(file *bpFile, directive *mkparser.Directive, conds []*conditional) {
354	for _, c := range conds {
355		if c == nil {
356			continue
357		}
358
359		if _, ok := conditionalTranslations[c.cond]; !ok {
360			panic("unknown conditional " + c.cond)
361		}
362
363		disabledPrefix := conditionalTranslations[c.cond][!c.eq]
364
365		// Create a fake assignment with enabled = false
366		val, err := makeVariableToBlueprint(file, mkparser.SimpleMakeString("false", mkparser.NoPos), bpparser.BoolType)
367		if err == nil {
368			err = setVariable(file, false, disabledPrefix, "enabled", val, true)
369		}
370		if err != nil {
371			file.errorf(directive, err.Error())
372		}
373	}
374}
375
376func makeModule(file *bpFile, t string) {
377	file.module.Type = t
378	file.module.TypePos = file.module.LBracePos
379	file.module.RBracePos = file.bpPos
380	file.defs = append(file.defs, file.module)
381	file.inModule = false
382}
383
384func resetModule(file *bpFile) {
385	file.module = &bpparser.Module{}
386	file.module.LBracePos = file.bpPos
387	file.localAssignments = make(map[string]*bpparser.Property)
388	file.inModule = true
389}
390
391func makeVariableToBlueprint(file *bpFile, val *mkparser.MakeString,
392	typ bpparser.Type) (bpparser.Expression, error) {
393
394	var exp bpparser.Expression
395	var err error
396	switch typ {
397	case bpparser.ListType:
398		exp, err = makeToListExpression(val, file)
399	case bpparser.StringType:
400		exp, err = makeToStringExpression(val, file)
401	case bpparser.BoolType:
402		exp, err = makeToBoolExpression(val, file)
403	default:
404		panic("unknown type")
405	}
406
407	if err != nil {
408		return nil, err
409	}
410
411	return exp, nil
412}
413
414// If local is set to true, then the variable will be added as a part of the
415// variable at file.bpPos. For example, if file.bpPos references a module,
416// then calling this method will set a property on that module if local is set
417// to true. Otherwise, the Variable will be created at the root of the file.
418//
419// prefix should be populated with the top level value to be assigned, and
420// name with a sub-value. If prefix is empty, then name is the top level value.
421// For example, if prefix is "foo" and name is "bar" with a value of "baz", then
422// the following variable will be generated:
423//
424//	foo {
425//	  bar: "baz"
426//	}
427//
428// If prefix is the empty string and name is "foo" with a value of "bar", the
429// following variable will be generated (if it is a property):
430//
431// foo: "bar"
432func setVariable(file *bpFile, plusequals bool, prefix, name string, value bpparser.Expression, local bool) error {
433	if prefix != "" {
434		name = prefix + "." + name
435	}
436
437	pos := file.bpPos
438
439	var oldValue *bpparser.Expression
440	if local {
441		oldProp := file.localAssignments[name]
442		if oldProp != nil {
443			oldValue = &oldProp.Value
444		}
445	} else {
446		oldValue = file.globalAssignments[name]
447	}
448
449	if local {
450		if oldValue != nil && plusequals {
451			val, err := addValues(*oldValue, value)
452			if err != nil {
453				return fmt.Errorf("unsupported addition: %s", err.Error())
454			}
455			val.(*bpparser.Operator).OperatorPos = pos
456			*oldValue = val
457		} else {
458			names := strings.Split(name, ".")
459			if file.module == nil {
460				file.warnf("No 'include $(CLEAR_VARS)' detected before first assignment; clearing vars now")
461				resetModule(file)
462			}
463			container := &file.module.Properties
464
465			for i, n := range names[:len(names)-1] {
466				fqn := strings.Join(names[0:i+1], ".")
467				prop := file.localAssignments[fqn]
468				if prop == nil {
469					prop = &bpparser.Property{
470						Name:    n,
471						NamePos: pos,
472						Value: &bpparser.Map{
473							Properties: []*bpparser.Property{},
474						},
475					}
476					file.localAssignments[fqn] = prop
477					*container = append(*container, prop)
478				}
479				container = &prop.Value.(*bpparser.Map).Properties
480			}
481
482			prop := &bpparser.Property{
483				Name:    names[len(names)-1],
484				NamePos: pos,
485				Value:   value,
486			}
487			file.localAssignments[name] = prop
488			*container = append(*container, prop)
489		}
490	} else {
491		if oldValue != nil && plusequals {
492			a := &bpparser.Assignment{
493				Name:      name,
494				NamePos:   pos,
495				Value:     value,
496				OrigValue: value,
497				EqualsPos: pos,
498				Assigner:  "+=",
499			}
500			file.defs = append(file.defs, a)
501		} else {
502			if _, ok := file.globalAssignments[name]; ok {
503				return fmt.Errorf("cannot assign a variable multiple times: \"%s\"", name)
504			}
505			a := &bpparser.Assignment{
506				Name:      name,
507				NamePos:   pos,
508				Value:     value,
509				OrigValue: value,
510				EqualsPos: pos,
511				Assigner:  "=",
512			}
513			file.globalAssignments[name] = &a.Value
514			file.defs = append(file.defs, a)
515		}
516	}
517	return nil
518}
519