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
414func setVariable(file *bpFile, plusequals bool, prefix, name string, value bpparser.Expression, local bool) error {
415	if prefix != "" {
416		name = prefix + "." + name
417	}
418
419	pos := file.bpPos
420
421	var oldValue *bpparser.Expression
422	if local {
423		oldProp := file.localAssignments[name]
424		if oldProp != nil {
425			oldValue = &oldProp.Value
426		}
427	} else {
428		oldValue = file.globalAssignments[name]
429	}
430
431	if local {
432		if oldValue != nil && plusequals {
433			val, err := addValues(*oldValue, value)
434			if err != nil {
435				return fmt.Errorf("unsupported addition: %s", err.Error())
436			}
437			val.(*bpparser.Operator).OperatorPos = pos
438			*oldValue = val
439		} else {
440			names := strings.Split(name, ".")
441			if file.module == nil {
442				file.warnf("No 'include $(CLEAR_VARS)' detected before first assignment; clearing vars now")
443				resetModule(file)
444			}
445			container := &file.module.Properties
446
447			for i, n := range names[:len(names)-1] {
448				fqn := strings.Join(names[0:i+1], ".")
449				prop := file.localAssignments[fqn]
450				if prop == nil {
451					prop = &bpparser.Property{
452						Name:    n,
453						NamePos: pos,
454						Value: &bpparser.Map{
455							Properties: []*bpparser.Property{},
456						},
457					}
458					file.localAssignments[fqn] = prop
459					*container = append(*container, prop)
460				}
461				container = &prop.Value.(*bpparser.Map).Properties
462			}
463
464			prop := &bpparser.Property{
465				Name:    names[len(names)-1],
466				NamePos: pos,
467				Value:   value,
468			}
469			file.localAssignments[name] = prop
470			*container = append(*container, prop)
471		}
472	} else {
473		if oldValue != nil && plusequals {
474			a := &bpparser.Assignment{
475				Name:      name,
476				NamePos:   pos,
477				Value:     value,
478				OrigValue: value,
479				EqualsPos: pos,
480				Assigner:  "+=",
481			}
482			file.defs = append(file.defs, a)
483		} else {
484			if _, ok := file.globalAssignments[name]; ok {
485				return fmt.Errorf("cannot assign a variable multiple times: \"%s\"", name)
486			}
487			a := &bpparser.Assignment{
488				Name:      name,
489				NamePos:   pos,
490				Value:     value,
491				OrigValue: value,
492				EqualsPos: pos,
493				Assigner:  "=",
494			}
495			file.globalAssignments[name] = &a.Value
496			file.defs = append(file.defs, a)
497		}
498	}
499	return nil
500}
501