// Copyright 2017 Google Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package androidmk import ( "bytes" "fmt" "strings" "text/scanner" "android/soong/bpfix/bpfix" mkparser "android/soong/androidmk/parser" bpparser "github.com/google/blueprint/parser" ) // TODO: non-expanded variables with expressions type bpFile struct { comments []*bpparser.CommentGroup defs []bpparser.Definition localAssignments map[string]*bpparser.Property globalAssignments map[string]*bpparser.Expression variableRenames map[string]string scope mkparser.Scope module *bpparser.Module mkPos scanner.Position // Position of the last handled line in the makefile bpPos scanner.Position // Position of the last emitted line to the blueprint file inModule bool } var invalidVariableStringToReplacement = map[string]string{ "-": "_dash_", } // Fix steps that should only run in the androidmk tool, i.e. should only be applied to // newly-converted Android.bp files. var fixSteps = bpfix.FixStepsExtension{ Name: "androidmk", Steps: []bpfix.FixStep{ { Name: "RewriteRuntimeResourceOverlay", Fix: bpfix.RewriteRuntimeResourceOverlay, }, }, } func init() { bpfix.RegisterFixStepExtension(&fixSteps) } func (f *bpFile) insertComment(s string) { f.comments = append(f.comments, &bpparser.CommentGroup{ Comments: []*bpparser.Comment{ &bpparser.Comment{ Comment: []string{s}, Slash: f.bpPos, }, }, }) f.bpPos.Offset += len(s) } func (f *bpFile) insertExtraComment(s string) { f.insertComment(s) f.bpPos.Line++ } // records that the given node failed to be converted and includes an explanatory message func (f *bpFile) errorf(failedNode mkparser.Node, message string, args ...interface{}) { orig := failedNode.Dump() message = fmt.Sprintf(message, args...) f.addErrorText(fmt.Sprintf("// ANDROIDMK TRANSLATION ERROR: %s", message)) lines := strings.Split(orig, "\n") for _, l := range lines { f.insertExtraComment("// " + l) } } // records that something unexpected occurred func (f *bpFile) warnf(message string, args ...interface{}) { message = fmt.Sprintf(message, args...) f.addErrorText(fmt.Sprintf("// ANDROIDMK TRANSLATION WARNING: %s", message)) } // adds the given error message as-is to the bottom of the (in-progress) file func (f *bpFile) addErrorText(message string) { f.insertExtraComment(message) } func (f *bpFile) setMkPos(pos, end scanner.Position) { // It is unusual but not forbidden for pos.Line to be smaller than f.mkPos.Line // For example: // // if true # this line is emitted 1st // if true # this line is emitted 2nd // some-target: some-file # this line is emitted 3rd // echo doing something # this recipe is emitted 6th // endif #some comment # this endif is emitted 4th; this comment is part of the recipe // echo doing more stuff # this is part of the recipe // endif # this endif is emitted 5th // // However, if pos.Line < f.mkPos.Line, we treat it as though it were equal if pos.Line >= f.mkPos.Line { f.bpPos.Line += (pos.Line - f.mkPos.Line) f.mkPos = end } } type conditional struct { cond string eq bool } func ConvertFile(filename string, buffer *bytes.Buffer) (string, []error) { p := mkparser.NewParser(filename, buffer) nodes, errs := p.Parse() if len(errs) > 0 { return "", errs } file := &bpFile{ scope: androidScope(), localAssignments: make(map[string]*bpparser.Property), globalAssignments: make(map[string]*bpparser.Expression), variableRenames: make(map[string]string), } var conds []*conditional var assignmentCond *conditional var tree *bpparser.File for _, node := range nodes { file.setMkPos(p.Unpack(node.Pos()), p.Unpack(node.End())) switch x := node.(type) { case *mkparser.Comment: // Split the comment on escaped newlines and then // add each chunk separately. chunks := strings.Split(x.Comment, "\\\n") file.insertComment("//" + chunks[0]) for i := 1; i < len(chunks); i++ { file.bpPos.Line++ file.insertComment("//" + chunks[i]) } case *mkparser.Assignment: handleAssignment(file, x, assignmentCond) case *mkparser.Directive: switch x.Name { case "include", "-include": module, ok := mapIncludePath(x.Args.Value(file.scope)) if !ok { file.errorf(x, "unsupported include") continue } switch module { case clearVarsPath: resetModule(file) case includeIgnoredPath: // subdirs are already automatically included in Soong continue default: handleModuleConditionals(file, x, conds) makeModule(file, module) } case "ifeq", "ifneq", "ifdef", "ifndef": args := x.Args.Dump() eq := x.Name == "ifeq" || x.Name == "ifdef" if _, ok := conditionalTranslations[args]; ok { newCond := conditional{args, eq} conds = append(conds, &newCond) if file.inModule { if assignmentCond == nil { assignmentCond = &newCond } else { file.errorf(x, "unsupported nested conditional in module") } } } else { file.errorf(x, "unsupported conditional") conds = append(conds, nil) continue } case "else": if len(conds) == 0 { file.errorf(x, "missing if before else") continue } else if conds[len(conds)-1] == nil { file.errorf(x, "else from unsupported conditional") continue } conds[len(conds)-1].eq = !conds[len(conds)-1].eq case "endif": if len(conds) == 0 { file.errorf(x, "missing if before endif") continue } else if conds[len(conds)-1] == nil { file.errorf(x, "endif from unsupported conditional") conds = conds[:len(conds)-1] } else { if assignmentCond == conds[len(conds)-1] { assignmentCond = nil } conds = conds[:len(conds)-1] } default: file.errorf(x, "unsupported directive") continue } default: file.errorf(x, "unsupported line") } } tree = &bpparser.File{ Defs: file.defs, Comments: file.comments, } // check for common supported but undesirable structures and clean them up fixer := bpfix.NewFixer(tree) fixedTree, fixerErr := fixer.Fix(bpfix.NewFixRequest().AddAll()) if fixerErr != nil { errs = append(errs, fixerErr) } else { tree = fixedTree } out, err := bpparser.Print(tree) if err != nil { errs = append(errs, err) return "", errs } return string(out), errs } func renameVariableWithInvalidCharacters(name string) string { renamed := "" for invalid, replacement := range invalidVariableStringToReplacement { if strings.Contains(name, invalid) { renamed = strings.ReplaceAll(name, invalid, replacement) } } return renamed } func invalidVariableStrings() string { invalidStrings := make([]string, 0, len(invalidVariableStringToReplacement)) for s := range invalidVariableStringToReplacement { invalidStrings = append(invalidStrings, "\""+s+"\"") } return strings.Join(invalidStrings, ", ") } func handleAssignment(file *bpFile, assignment *mkparser.Assignment, c *conditional) { if !assignment.Name.Const() { file.errorf(assignment, "unsupported non-const variable name") return } if assignment.Target != nil { file.errorf(assignment, "unsupported target assignment") return } name := assignment.Name.Value(nil) prefix := "" if newName := renameVariableWithInvalidCharacters(name); newName != "" { file.warnf("Variable names cannot contain: %s. Renamed \"%s\" to \"%s\"", invalidVariableStrings(), name, newName) file.variableRenames[name] = newName name = newName } if strings.HasPrefix(name, "LOCAL_") { for _, x := range propertyPrefixes { if strings.HasSuffix(name, "_"+x.mk) { name = strings.TrimSuffix(name, "_"+x.mk) prefix = x.bp break } } if c != nil { if prefix != "" { file.errorf(assignment, "prefix assignment inside conditional, skipping conditional") } else { var ok bool if prefix, ok = conditionalTranslations[c.cond][c.eq]; !ok { panic("unknown conditional") } } } } else { if c != nil { eq := "eq" if !c.eq { eq = "neq" } file.errorf(assignment, "conditional %s %s on global assignment", eq, c.cond) } } appendVariable := assignment.Type == "+=" var err error if prop, ok := rewriteProperties[name]; ok { err = prop(variableAssignmentContext{file, prefix, assignment.Value, appendVariable}) } else { switch { case name == "LOCAL_ARM_MODE": // This is a hack to get the LOCAL_ARM_MODE value inside // of an arch: { arm: {} } block. armModeAssign := assignment armModeAssign.Name = mkparser.SimpleMakeString("LOCAL_ARM_MODE_HACK_arm", assignment.Name.Pos()) handleAssignment(file, armModeAssign, c) case strings.HasPrefix(name, "LOCAL_"): file.errorf(assignment, "unsupported assignment to %s", name) return default: var val bpparser.Expression val, err = makeVariableToBlueprint(file, assignment.Value, bpparser.ListType) if err == nil { err = setVariable(file, appendVariable, prefix, name, val, false) } } } if err != nil { file.errorf(assignment, err.Error()) } } func handleModuleConditionals(file *bpFile, directive *mkparser.Directive, conds []*conditional) { for _, c := range conds { if c == nil { continue } if _, ok := conditionalTranslations[c.cond]; !ok { panic("unknown conditional " + c.cond) } disabledPrefix := conditionalTranslations[c.cond][!c.eq] // Create a fake assignment with enabled = false val, err := makeVariableToBlueprint(file, mkparser.SimpleMakeString("false", mkparser.NoPos), bpparser.BoolType) if err == nil { err = setVariable(file, false, disabledPrefix, "enabled", val, true) } if err != nil { file.errorf(directive, err.Error()) } } } func makeModule(file *bpFile, t string) { file.module.Type = t file.module.TypePos = file.module.LBracePos file.module.RBracePos = file.bpPos file.defs = append(file.defs, file.module) file.inModule = false } func resetModule(file *bpFile) { file.module = &bpparser.Module{} file.module.LBracePos = file.bpPos file.localAssignments = make(map[string]*bpparser.Property) file.inModule = true } func makeVariableToBlueprint(file *bpFile, val *mkparser.MakeString, typ bpparser.Type) (bpparser.Expression, error) { var exp bpparser.Expression var err error switch typ { case bpparser.ListType: exp, err = makeToListExpression(val, file) case bpparser.StringType: exp, err = makeToStringExpression(val, file) case bpparser.BoolType: exp, err = makeToBoolExpression(val, file) default: panic("unknown type") } if err != nil { return nil, err } return exp, nil } func setVariable(file *bpFile, plusequals bool, prefix, name string, value bpparser.Expression, local bool) error { if prefix != "" { name = prefix + "." + name } pos := file.bpPos var oldValue *bpparser.Expression if local { oldProp := file.localAssignments[name] if oldProp != nil { oldValue = &oldProp.Value } } else { oldValue = file.globalAssignments[name] } if local { if oldValue != nil && plusequals { val, err := addValues(*oldValue, value) if err != nil { return fmt.Errorf("unsupported addition: %s", err.Error()) } val.(*bpparser.Operator).OperatorPos = pos *oldValue = val } else { names := strings.Split(name, ".") if file.module == nil { file.warnf("No 'include $(CLEAR_VARS)' detected before first assignment; clearing vars now") resetModule(file) } container := &file.module.Properties for i, n := range names[:len(names)-1] { fqn := strings.Join(names[0:i+1], ".") prop := file.localAssignments[fqn] if prop == nil { prop = &bpparser.Property{ Name: n, NamePos: pos, Value: &bpparser.Map{ Properties: []*bpparser.Property{}, }, } file.localAssignments[fqn] = prop *container = append(*container, prop) } container = &prop.Value.(*bpparser.Map).Properties } prop := &bpparser.Property{ Name: names[len(names)-1], NamePos: pos, Value: value, } file.localAssignments[name] = prop *container = append(*container, prop) } } else { if oldValue != nil && plusequals { a := &bpparser.Assignment{ Name: name, NamePos: pos, Value: value, OrigValue: value, EqualsPos: pos, Assigner: "+=", } file.defs = append(file.defs, a) } else { if _, ok := file.globalAssignments[name]; ok { return fmt.Errorf("cannot assign a variable multiple times: \"%s\"", name) } a := &bpparser.Assignment{ Name: name, NamePos: pos, Value: value, OrigValue: value, EqualsPos: pos, Assigner: "=", } file.globalAssignments[name] = &a.Value file.defs = append(file.defs, a) } } return nil }