| // 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 |
| } |