You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
727 lines
16 KiB
727 lines
16 KiB
// Copyright 2015 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 kati |
|
|
|
import ( |
|
"bytes" |
|
"crypto/sha1" |
|
"fmt" |
|
"os" |
|
"path/filepath" |
|
"strconv" |
|
"strings" |
|
"sync" |
|
|
|
"github.com/golang/glog" |
|
) |
|
|
|
type fileState int |
|
|
|
const ( |
|
fileExists fileState = iota |
|
fileNotExists |
|
fileInconsistent // Modified during kati is running. |
|
) |
|
|
|
type accessedMakefile struct { |
|
Filename string |
|
Hash [sha1.Size]byte |
|
State fileState |
|
} |
|
|
|
type accessCache struct { |
|
mu sync.Mutex |
|
m map[string]*accessedMakefile |
|
} |
|
|
|
func newAccessCache() *accessCache { |
|
return &accessCache{ |
|
m: make(map[string]*accessedMakefile), |
|
} |
|
} |
|
|
|
func (ac *accessCache) update(fn string, hash [sha1.Size]byte, st fileState) string { |
|
if ac == nil { |
|
return "" |
|
} |
|
ac.mu.Lock() |
|
defer ac.mu.Unlock() |
|
rm, present := ac.m[fn] |
|
if present { |
|
switch rm.State { |
|
case fileExists: |
|
if st != fileExists { |
|
return fmt.Sprintf("%s was removed after the previous read", fn) |
|
} else if !bytes.Equal(hash[:], rm.Hash[:]) { |
|
ac.m[fn].State = fileInconsistent |
|
return fmt.Sprintf("%s was modified after the previous read", fn) |
|
} |
|
return "" |
|
case fileNotExists: |
|
if st != fileNotExists { |
|
ac.m[fn].State = fileInconsistent |
|
return fmt.Sprintf("%s was created after the previous read", fn) |
|
} |
|
case fileInconsistent: |
|
return "" |
|
} |
|
return "" |
|
} |
|
ac.m[fn] = &accessedMakefile{ |
|
Filename: fn, |
|
Hash: hash, |
|
State: st, |
|
} |
|
return "" |
|
} |
|
|
|
func (ac *accessCache) Slice() []*accessedMakefile { |
|
if ac == nil { |
|
return nil |
|
} |
|
ac.mu.Lock() |
|
defer ac.mu.Unlock() |
|
r := []*accessedMakefile{} |
|
for _, v := range ac.m { |
|
r = append(r, v) |
|
} |
|
return r |
|
} |
|
|
|
type evalResult struct { |
|
vars Vars |
|
rules []*rule |
|
ruleVars map[string]Vars |
|
accessedMks []*accessedMakefile |
|
exports map[string]bool |
|
vpaths searchPaths |
|
} |
|
|
|
type srcpos struct { |
|
filename string |
|
lineno int |
|
} |
|
|
|
func (p srcpos) String() string { |
|
return fmt.Sprintf("%s:%d", p.filename, p.lineno) |
|
} |
|
|
|
// EvalError is an error in kati evaluation. |
|
type EvalError struct { |
|
Filename string |
|
Lineno int |
|
Err error |
|
} |
|
|
|
func (e EvalError) Error() string { |
|
return fmt.Sprintf("%s:%d: %v", e.Filename, e.Lineno, e.Err) |
|
} |
|
|
|
func (p srcpos) errorf(f string, args ...interface{}) error { |
|
return EvalError{ |
|
Filename: p.filename, |
|
Lineno: p.lineno, |
|
Err: fmt.Errorf(f, args...), |
|
} |
|
} |
|
|
|
func (p srcpos) error(err error) error { |
|
if _, ok := err.(EvalError); ok { |
|
return err |
|
} |
|
return EvalError{ |
|
Filename: p.filename, |
|
Lineno: p.lineno, |
|
Err: err, |
|
} |
|
} |
|
|
|
// Evaluator manages makefile evaluation. |
|
type Evaluator struct { |
|
paramVars []tmpval // $1 => paramVars[1] |
|
outVars Vars |
|
outRules []*rule |
|
outRuleVars map[string]Vars |
|
vars Vars |
|
lastRule *rule |
|
currentScope Vars |
|
cache *accessCache |
|
exports map[string]bool |
|
vpaths []vpath |
|
|
|
avoidIO bool |
|
hasIO bool |
|
// delayedOutputs are commands which should run at ninja-time |
|
// (i.e., info, warning, and error). |
|
delayedOutputs []string |
|
|
|
srcpos |
|
} |
|
|
|
// NewEvaluator creates new Evaluator. |
|
func NewEvaluator(vars map[string]Var) *Evaluator { |
|
return &Evaluator{ |
|
outVars: make(Vars), |
|
vars: vars, |
|
outRuleVars: make(map[string]Vars), |
|
exports: make(map[string]bool), |
|
} |
|
} |
|
|
|
func (ev *Evaluator) args(buf *evalBuffer, args ...Value) ([][]byte, error) { |
|
pos := make([]int, 0, len(args)) |
|
for _, arg := range args { |
|
buf.resetSep() |
|
err := arg.Eval(buf, ev) |
|
if err != nil { |
|
return nil, err |
|
} |
|
pos = append(pos, buf.Len()) |
|
} |
|
v := buf.Bytes() |
|
buf.args = buf.args[:0] |
|
s := 0 |
|
for _, p := range pos { |
|
buf.args = append(buf.args, v[s:p]) |
|
s = p |
|
} |
|
return buf.args, nil |
|
} |
|
|
|
func (ev *Evaluator) evalAssign(ast *assignAST) error { |
|
ev.lastRule = nil |
|
lhs, rhs, err := ev.evalAssignAST(ast) |
|
if err != nil { |
|
return err |
|
} |
|
if glog.V(1) { |
|
glog.Infof("ASSIGN: %s=%q (flavor:%q)", lhs, rhs, rhs.Flavor()) |
|
} |
|
if lhs == "" { |
|
return ast.errorf("*** empty variable name.") |
|
} |
|
ev.outVars.Assign(lhs, rhs) |
|
return nil |
|
} |
|
|
|
func (ev *Evaluator) evalAssignAST(ast *assignAST) (string, Var, error) { |
|
ev.srcpos = ast.srcpos |
|
|
|
var lhs string |
|
switch v := ast.lhs.(type) { |
|
case literal: |
|
lhs = string(v) |
|
case tmpval: |
|
lhs = string(v) |
|
default: |
|
buf := newEbuf() |
|
err := v.Eval(buf, ev) |
|
if err != nil { |
|
return "", nil, err |
|
} |
|
lhs = string(trimSpaceBytes(buf.Bytes())) |
|
buf.release() |
|
} |
|
rhs, err := ast.evalRHS(ev, lhs) |
|
if err != nil { |
|
return "", nil, err |
|
} |
|
return lhs, rhs, nil |
|
} |
|
|
|
func (ev *Evaluator) setTargetSpecificVar(assign *assignAST, output string) error { |
|
vars, present := ev.outRuleVars[output] |
|
if !present { |
|
vars = make(Vars) |
|
ev.outRuleVars[output] = vars |
|
} |
|
ev.currentScope = vars |
|
lhs, rhs, err := ev.evalAssignAST(assign) |
|
if err != nil { |
|
return err |
|
} |
|
if glog.V(1) { |
|
glog.Infof("rule outputs:%q assign:%q%s%q (flavor:%q)", output, lhs, assign.op, rhs, rhs.Flavor()) |
|
} |
|
vars.Assign(lhs, &targetSpecificVar{v: rhs, op: assign.op}) |
|
ev.currentScope = nil |
|
return nil |
|
} |
|
|
|
func (ev *Evaluator) evalMaybeRule(ast *maybeRuleAST) error { |
|
ev.lastRule = nil |
|
ev.srcpos = ast.srcpos |
|
|
|
if glog.V(1) { |
|
glog.Infof("maybe rule %s: %q assign:%v", ev.srcpos, ast.expr, ast.assign) |
|
} |
|
|
|
abuf := newEbuf() |
|
aexpr := toExpr(ast.expr) |
|
var rhs expr |
|
semi := ast.semi |
|
for i, v := range aexpr { |
|
var hashFound bool |
|
var buf evalBuffer |
|
buf.resetSep() |
|
switch v.(type) { |
|
case literal, tmpval: |
|
s := v.String() |
|
i := strings.Index(s, "#") |
|
if i >= 0 { |
|
hashFound = true |
|
v = tmpval(trimRightSpaceBytes([]byte(s[:i]))) |
|
} |
|
} |
|
err := v.Eval(&buf, ev) |
|
if err != nil { |
|
return err |
|
} |
|
b := buf.Bytes() |
|
if ast.isRule { |
|
abuf.Write(b) |
|
continue |
|
} |
|
eq := findLiteralChar(b, '=', 0, skipVar) |
|
if eq >= 0 { |
|
abuf.Write(b[:eq+1]) |
|
if eq+1 < len(b) { |
|
rhs = append(rhs, tmpval(trimLeftSpaceBytes(b[eq+1:]))) |
|
} |
|
if i+1 < len(aexpr) { |
|
rhs = append(rhs, aexpr[i+1:]...) |
|
} |
|
if ast.semi != nil { |
|
rhs = append(rhs, literal(';')) |
|
sexpr, _, err := parseExpr(ast.semi, nil, parseOp{}) |
|
if err != nil { |
|
return err |
|
} |
|
rhs = append(rhs, toExpr(sexpr)...) |
|
semi = nil |
|
} |
|
break |
|
} |
|
abuf.Write(b) |
|
if hashFound { |
|
break |
|
} |
|
} |
|
|
|
line := abuf.Bytes() |
|
r := &rule{srcpos: ast.srcpos} |
|
if glog.V(1) { |
|
glog.Infof("rule? %s: %q assign:%v rhs:%s", r.srcpos, line, ast.assign, rhs) |
|
} |
|
assign, err := r.parse(line, ast.assign, rhs) |
|
if err != nil { |
|
ws := newWordScanner(line) |
|
if ws.Scan() { |
|
if string(ws.Bytes()) == "override" { |
|
warnNoPrefix(ast.srcpos, "invalid `override' directive") |
|
return nil |
|
} |
|
} |
|
return ast.error(err) |
|
} |
|
abuf.release() |
|
if glog.V(1) { |
|
glog.Infof("rule %q assign:%v rhs:%v=> outputs:%q, inputs:%q", ast.expr, ast.assign, rhs, r.outputs, r.inputs) |
|
} |
|
|
|
// TODO: Pretty print. |
|
// glog.V(1).Infof("RULE: %s=%s (%d commands)", lhs, rhs, len(cmds)) |
|
|
|
if assign != nil { |
|
glog.V(1).Infof("target specific var: %#v", assign) |
|
for _, output := range r.outputs { |
|
ev.setTargetSpecificVar(assign, output) |
|
} |
|
for _, output := range r.outputPatterns { |
|
ev.setTargetSpecificVar(assign, output.String()) |
|
} |
|
return nil |
|
} |
|
|
|
if semi != nil { |
|
r.cmds = append(r.cmds, string(semi)) |
|
} |
|
if glog.V(1) { |
|
glog.Infof("rule outputs:%q cmds:%q", r.outputs, r.cmds) |
|
} |
|
ev.lastRule = r |
|
ev.outRules = append(ev.outRules, r) |
|
return nil |
|
} |
|
|
|
func (ev *Evaluator) evalCommand(ast *commandAST) error { |
|
ev.srcpos = ast.srcpos |
|
if ev.lastRule == nil || ev.lastRule.outputs == nil { |
|
// This could still be an assignment statement. See |
|
// assign_after_tab.mk. |
|
if strings.IndexByte(ast.cmd, '=') >= 0 { |
|
line := trimLeftSpace(ast.cmd) |
|
mk, err := parseMakefileString(line, ast.srcpos) |
|
if err != nil { |
|
return ast.errorf("parse failed: %q: %v", line, err) |
|
} |
|
if len(mk.stmts) >= 1 && mk.stmts[len(mk.stmts)-1].(*assignAST) != nil { |
|
for _, stmt := range mk.stmts { |
|
err = ev.eval(stmt) |
|
if err != nil { |
|
return err |
|
} |
|
} |
|
} |
|
return nil |
|
} |
|
// Or, a comment is OK. |
|
if strings.TrimSpace(ast.cmd)[0] == '#' { |
|
return nil |
|
} |
|
return ast.errorf("*** commands commence before first target.") |
|
} |
|
ev.lastRule.cmds = append(ev.lastRule.cmds, ast.cmd) |
|
if ev.lastRule.cmdLineno == 0 { |
|
ev.lastRule.cmdLineno = ast.lineno |
|
} |
|
return nil |
|
} |
|
|
|
func (ev *Evaluator) paramVar(name string) (Var, error) { |
|
idx, err := strconv.ParseInt(name, 10, 32) |
|
if err != nil { |
|
return nil, fmt.Errorf("param: %s: %v", name, err) |
|
} |
|
i := int(idx) |
|
if i < 0 || i >= len(ev.paramVars) { |
|
return nil, fmt.Errorf("param: %s out of %d", name, len(ev.paramVars)) |
|
} |
|
return &automaticVar{value: []byte(ev.paramVars[i])}, nil |
|
} |
|
|
|
// LookupVar looks up named variable. |
|
func (ev *Evaluator) LookupVar(name string) Var { |
|
if ev.currentScope != nil { |
|
v := ev.currentScope.Lookup(name) |
|
if v.IsDefined() { |
|
return v |
|
} |
|
} |
|
v := ev.outVars.Lookup(name) |
|
if v.IsDefined() { |
|
return v |
|
} |
|
v, err := ev.paramVar(name) |
|
if err == nil { |
|
return v |
|
} |
|
return ev.vars.Lookup(name) |
|
} |
|
|
|
func (ev *Evaluator) lookupVarInCurrentScope(name string) Var { |
|
if ev.currentScope != nil { |
|
v := ev.currentScope.Lookup(name) |
|
return v |
|
} |
|
v := ev.outVars.Lookup(name) |
|
if v.IsDefined() { |
|
return v |
|
} |
|
v, err := ev.paramVar(name) |
|
if err == nil { |
|
return v |
|
} |
|
return ev.vars.Lookup(name) |
|
} |
|
|
|
// EvaluateVar evaluates variable named name. |
|
// Only for a few special uses such as getting SHELL and handling |
|
// export/unexport. |
|
func (ev *Evaluator) EvaluateVar(name string) (string, error) { |
|
var buf evalBuffer |
|
buf.resetSep() |
|
err := ev.LookupVar(name).Eval(&buf, ev) |
|
if err != nil { |
|
return "", err |
|
} |
|
return buf.String(), nil |
|
} |
|
|
|
func (ev *Evaluator) evalIncludeFile(fname string, mk makefile) error { |
|
te := traceEvent.begin("include", literal(fname), traceEventMain) |
|
defer func() { |
|
traceEvent.end(te) |
|
}() |
|
var err error |
|
makefileList := ev.outVars.Lookup("MAKEFILE_LIST") |
|
makefileList, err = makefileList.Append(ev, mk.filename) |
|
if err != nil { |
|
return err |
|
} |
|
ev.outVars.Assign("MAKEFILE_LIST", makefileList) |
|
|
|
for _, stmt := range mk.stmts { |
|
err = ev.eval(stmt) |
|
if err != nil { |
|
return err |
|
} |
|
} |
|
return nil |
|
} |
|
|
|
func (ev *Evaluator) evalInclude(ast *includeAST) error { |
|
ev.lastRule = nil |
|
ev.srcpos = ast.srcpos |
|
|
|
glog.Infof("%s include %q", ev.srcpos, ast.expr) |
|
v, _, err := parseExpr([]byte(ast.expr), nil, parseOp{}) |
|
if err != nil { |
|
return ast.errorf("parse failed: %q: %v", ast.expr, err) |
|
} |
|
var buf evalBuffer |
|
buf.resetSep() |
|
err = v.Eval(&buf, ev) |
|
if err != nil { |
|
return ast.errorf("%v", err) |
|
} |
|
pats := splitSpaces(buf.String()) |
|
buf.Reset() |
|
|
|
var files []string |
|
for _, pat := range pats { |
|
if strings.Contains(pat, "*") || strings.Contains(pat, "?") { |
|
matched, err := filepath.Glob(pat) |
|
if err != nil { |
|
return ast.errorf("glob error: %s: %v", pat, err) |
|
} |
|
files = append(files, matched...) |
|
} else { |
|
files = append(files, pat) |
|
} |
|
} |
|
|
|
for _, fn := range files { |
|
fn = trimLeadingCurdir(fn) |
|
if IgnoreOptionalInclude != "" && ast.op == "-include" && matchPattern(fn, IgnoreOptionalInclude) { |
|
continue |
|
} |
|
mk, hash, err := makefileCache.parse(fn) |
|
if os.IsNotExist(err) { |
|
if ast.op == "include" { |
|
return ev.errorf("%v\nNOTE: kati does not support generating missing makefiles", err) |
|
} |
|
msg := ev.cache.update(fn, hash, fileNotExists) |
|
if msg != "" { |
|
warn(ev.srcpos, "%s", msg) |
|
} |
|
continue |
|
} |
|
msg := ev.cache.update(fn, hash, fileExists) |
|
if msg != "" { |
|
warn(ev.srcpos, "%s", msg) |
|
} |
|
err = ev.evalIncludeFile(fn, mk) |
|
if err != nil { |
|
return err |
|
} |
|
} |
|
return nil |
|
} |
|
|
|
func (ev *Evaluator) evalIf(iast *ifAST) error { |
|
var isTrue bool |
|
switch iast.op { |
|
case "ifdef", "ifndef": |
|
expr := iast.lhs |
|
buf := newEbuf() |
|
err := expr.Eval(buf, ev) |
|
if err != nil { |
|
return iast.errorf("%v\n expr:%s", err, expr) |
|
} |
|
v := ev.LookupVar(buf.String()) |
|
buf.Reset() |
|
err = v.Eval(buf, ev) |
|
if err != nil { |
|
return iast.errorf("%v\n expr:%s=>%s", err, expr, v) |
|
} |
|
value := buf.String() |
|
val := buf.Len() |
|
buf.release() |
|
isTrue = (val > 0) == (iast.op == "ifdef") |
|
if glog.V(1) { |
|
glog.Infof("%s lhs=%q value=%q => %t", iast.op, iast.lhs, value, isTrue) |
|
} |
|
case "ifeq", "ifneq": |
|
lexpr := iast.lhs |
|
rexpr := iast.rhs |
|
buf := newEbuf() |
|
params, err := ev.args(buf, lexpr, rexpr) |
|
if err != nil { |
|
return iast.errorf("%v\n (%s,%s)", err, lexpr, rexpr) |
|
} |
|
lhs := string(params[0]) |
|
rhs := string(params[1]) |
|
buf.release() |
|
isTrue = (lhs == rhs) == (iast.op == "ifeq") |
|
if glog.V(1) { |
|
glog.Infof("%s lhs=%q %q rhs=%q %q => %t", iast.op, iast.lhs, lhs, iast.rhs, rhs, isTrue) |
|
} |
|
default: |
|
return iast.errorf("unknown if statement: %q", iast.op) |
|
} |
|
|
|
var stmts []ast |
|
if isTrue { |
|
stmts = iast.trueStmts |
|
} else { |
|
stmts = iast.falseStmts |
|
} |
|
for _, stmt := range stmts { |
|
err := ev.eval(stmt) |
|
if err != nil { |
|
return err |
|
} |
|
} |
|
return nil |
|
} |
|
|
|
func (ev *Evaluator) evalExport(ast *exportAST) error { |
|
ev.lastRule = nil |
|
ev.srcpos = ast.srcpos |
|
|
|
v, _, err := parseExpr(ast.expr, nil, parseOp{}) |
|
if err != nil { |
|
return ast.errorf("failed to parse: %q: %v", string(ast.expr), err) |
|
} |
|
var buf evalBuffer |
|
buf.resetSep() |
|
err = v.Eval(&buf, ev) |
|
if err != nil { |
|
return ast.errorf("%v\n expr:%s", err, v) |
|
} |
|
if ast.hasEqual { |
|
ev.exports[string(trimSpaceBytes(buf.Bytes()))] = ast.export |
|
} else { |
|
for _, n := range splitSpacesBytes(buf.Bytes()) { |
|
ev.exports[string(n)] = ast.export |
|
} |
|
} |
|
return nil |
|
} |
|
|
|
func (ev *Evaluator) evalVpath(ast *vpathAST) error { |
|
ev.lastRule = nil |
|
ev.srcpos = ast.srcpos |
|
|
|
var ebuf evalBuffer |
|
ebuf.resetSep() |
|
err := ast.expr.Eval(&ebuf, ev) |
|
if err != nil { |
|
return ast.errorf("%v\n expr:%s", err, ast.expr) |
|
} |
|
ws := newWordScanner(ebuf.Bytes()) |
|
if !ws.Scan() { |
|
ev.vpaths = nil |
|
return nil |
|
} |
|
pat := string(ws.Bytes()) |
|
if !ws.Scan() { |
|
vpaths := ev.vpaths |
|
ev.vpaths = nil |
|
for _, v := range vpaths { |
|
if v.pattern == pat { |
|
continue |
|
} |
|
ev.vpaths = append(ev.vpaths, v) |
|
} |
|
return nil |
|
} |
|
// The search path, DIRECTORIES, is a list of directories to be |
|
// searched, separated by colons (semi-colons on MS-DOS and |
|
// MS-Windows) or blanks, just like the search path used in the |
|
// `VPATH' variable. |
|
var dirs []string |
|
for { |
|
for _, dir := range bytes.Split(ws.Bytes(), []byte{':'}) { |
|
dirs = append(dirs, string(dir)) |
|
} |
|
if !ws.Scan() { |
|
break |
|
} |
|
} |
|
ev.vpaths = append(ev.vpaths, vpath{ |
|
pattern: pat, |
|
dirs: dirs, |
|
}) |
|
return nil |
|
} |
|
|
|
func (ev *Evaluator) eval(stmt ast) error { |
|
return stmt.eval(ev) |
|
} |
|
|
|
func eval(mk makefile, vars Vars, useCache bool) (er *evalResult, err error) { |
|
ev := NewEvaluator(vars) |
|
if useCache { |
|
ev.cache = newAccessCache() |
|
} |
|
|
|
makefileList := vars.Lookup("MAKEFILE_LIST") |
|
if !makefileList.IsDefined() { |
|
makefileList = &simpleVar{value: []string{""}, origin: "file"} |
|
} |
|
makefileList, err = makefileList.Append(ev, mk.filename) |
|
if err != nil { |
|
return nil, err |
|
} |
|
ev.outVars.Assign("MAKEFILE_LIST", makefileList) |
|
|
|
for _, stmt := range mk.stmts { |
|
err = ev.eval(stmt) |
|
if err != nil { |
|
return nil, err |
|
} |
|
} |
|
|
|
vpaths := searchPaths{ |
|
vpaths: ev.vpaths, |
|
} |
|
v, found := ev.outVars["VPATH"] |
|
if found { |
|
wb := newWbuf() |
|
err := v.Eval(wb, ev) |
|
if err != nil { |
|
return nil, err |
|
} |
|
// In the 'VPATH' variable, directory names are separated |
|
// by colons or blanks. (on windows, semi-colons) |
|
for _, word := range wb.words { |
|
for _, dir := range bytes.Split(word, []byte{':'}) { |
|
vpaths.dirs = append(vpaths.dirs, string(dir)) |
|
} |
|
} |
|
} |
|
glog.Infof("vpaths: %#v", vpaths) |
|
|
|
return &evalResult{ |
|
vars: ev.outVars, |
|
rules: ev.outRules, |
|
ruleVars: ev.outRuleVars, |
|
accessedMks: ev.cache.Slice(), |
|
exports: ev.exports, |
|
vpaths: vpaths, |
|
}, nil |
|
}
|
|
|