diff options
author | Fumitoshi Ukai <ukai@chromium.org> | 2015-03-30 16:52:41 +0900 |
---|---|---|
committer | Fumitoshi Ukai <ukai@chromium.org> | 2015-03-30 16:52:41 +0900 |
commit | 119dc912cfe6fceec9992fd04b5bb046af60129e (patch) | |
tree | b431718bd98e588f8c18cef3e33552b294b392ca | |
download | kati-119dc912cfe6fceec9992fd04b5bb046af60129e.tar.gz |
kati: initial commit (from hamaji)
-rw-r--r-- | Makefile | 11 | ||||
-rw-r--r-- | ast.go | 47 | ||||
-rw-r--r-- | eval.go | 167 | ||||
-rw-r--r-- | exec.go | 104 | ||||
-rw-r--r-- | log.go | 31 | ||||
-rw-r--r-- | main.go | 15 | ||||
-rw-r--r-- | parser.go | 210 | ||||
-rwxr-xr-x | runtest.rb | 58 | ||||
-rw-r--r-- | test/basic_dep.mk | 6 | ||||
-rw-r--r-- | test/basic_rule.mk | 4 | ||||
-rw-r--r-- | test/basic_var.mk | 4 | ||||
-rw-r--r-- | test/var_target.mk | 5 | ||||
-rw-r--r-- | test/warning.mk | 3 |
13 files changed, 665 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..23de2e6 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +GOSRC = $(wildcard *.go) + +all: kati + +kati: $(GOSRC) + env $(shell go env) go build -o $@ . + +test: + ruby runtest.rb + +.PHONY: test @@ -0,0 +1,47 @@ +package main + +const ( + AST_ASSIGN = iota + AST_RULE +) + +type AST interface { + typ() int + show() +} + +type ASTBase struct { + lineno int +} + +type AssignAST struct { + ASTBase + lhs string + rhs string +} + +func (ast *AssignAST) typ() int { + return AST_ASSIGN +} + +func (ast *AssignAST) show() { + Log("%s=%s", ast.lhs, ast.rhs) +} + +type RuleAST struct { + ASTBase + lhs string + rhs string + cmds []string +} + +func (ast *RuleAST) typ() int { + return AST_RULE +} + +func (ast *RuleAST) show() { + Log("%s: %s", ast.lhs, ast.rhs) + for _, cmd := range ast.cmds { + Log("\t%s", cmd) + } +} @@ -0,0 +1,167 @@ +package main + +import ( + "bytes" + "os/exec" + "path/filepath" + "regexp" + "strings" +) + +type Rule struct { + output string + inputs []string + cmds []string +} + +type EvalResult struct { + vars map[string]string + rules []*Rule + refs map[string]bool +} + +type Evaluator struct { + out_vars map[string]string + out_rules []*Rule + refs map[string]bool + vars map[string]string + cur_rule *Rule +} + +func newEvaluator() *Evaluator { + return &Evaluator{ + out_vars: make(map[string]string), + refs: make(map[string]bool), + vars: make(map[string]string), + } +} + +func (ev *Evaluator) evalFunction(ex string) (string, bool) { + if strings.HasPrefix(ex, "wildcard ") { + arg := ex[len("wildcard "):] + + files, err := filepath.Glob(arg) + if err != nil { + panic(err) + } + return strings.Join(files, " "), true + } else if strings.HasPrefix(ex, "shell ") { + arg := ex[len("shell "):] + + args := []string{"/bin/sh", "-c", arg} + cmd := exec.Cmd{ + Path: args[0], + Args: args, + } + out, err := cmd.CombinedOutput() + if err != nil { + panic(err) + } + re, err := regexp.Compile("\\s") + if err != nil { + panic(err) + } + return string(re.ReplaceAllString(string(out), " ")), true + } + return "", false +} + +func (ev *Evaluator) evalExprSlice(ex string, term byte) (string, int) { + var buf bytes.Buffer + i := 0 + for i < len(ex) && ex[i] != term { + ch := ex[i] + i++ + switch ch { + case '$': + if i >= len(ex) || ex[i] == term { + continue + } + + var varname string + switch ex[i] { + case '@': + buf.WriteString(ev.cur_rule.output) + i++ + continue + case '(': + v, j := ev.evalExprSlice(ex[i+1:], ')') + i += j + 2 + if r, done := ev.evalFunction(v); done { + buf.WriteString(r) + continue + } + + varname = v + default: + varname = string(ex[i]) + i++ + } + + value, present := ev.vars[varname] + if !present { + ev.refs[varname] = true + value = ev.out_vars[varname] + } + buf.WriteString(value) + + default: + buf.WriteByte(ch) + } + } + return buf.String(), i +} + +func (ev *Evaluator) evalExpr(ex string) string { + r, i := ev.evalExprSlice(ex, 0) + if len(ex) != i { + panic("Had a null character?") + } + return r +} + +func (ev *Evaluator) evalAssign(ast *AssignAST) { + lhs := ev.evalExpr(ast.lhs) + rhs := ev.evalExpr(ast.rhs) + Log("ASSIGN: %s=%s", lhs, rhs) + ev.out_vars[lhs] = rhs +} + +func (ev *Evaluator) evalRule(ast *RuleAST) { + ev.cur_rule = &Rule{} + lhs := ev.evalExpr(ast.lhs) + ev.cur_rule.output = lhs + rhs := ev.evalExpr(ast.rhs) + if rhs != "" { + ev.cur_rule.inputs = strings.Split(rhs, " ") + } + var cmds []string + for _, cmd := range ast.cmds { + cmds = append(cmds, ev.evalExpr(cmd)) + } + Log("RULE: %s=%s", lhs, rhs) + ev.cur_rule.cmds = cmds + ev.out_rules = append(ev.out_rules, ev.cur_rule) + ev.cur_rule = nil +} + +func (ev *Evaluator) eval(ast AST) { + switch ast.typ() { + case AST_ASSIGN: + ev.evalAssign(ast.(*AssignAST)) + case AST_RULE: + ev.evalRule(ast.(*RuleAST)) + } +} + +func Eval(mk Makefile) *EvalResult { + ev := newEvaluator() + for _, stmt := range mk.stmts { + ev.eval(stmt) + } + return &EvalResult{ + vars: ev.out_vars, + rules: ev.out_rules, + refs: ev.refs, + } +} @@ -0,0 +1,104 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "time" +) + +type Executor struct { + rules map[string]*Rule +} + +func newExecutor() *Executor { + return &Executor{ + rules: make(map[string]*Rule), + } +} + +func getTimestamp(filename string) int64 { + st, err := os.Stat(filename) + if err != nil { + return -2 + } + return st.ModTime().Unix() +} + +func (ex *Executor) runCommands(cmds []string) { + for _, cmd := range cmds { + fmt.Printf("%s\n", cmd) + + args := []string{"/bin/sh", "-c", cmd} + cmd := exec.Cmd{ + Path: args[0], + Args: args, + } + out, err := cmd.CombinedOutput() + if err != nil { + panic(err) + } + success := false + if cmd.ProcessState != nil { + success = cmd.ProcessState.Success() + } + + fmt.Printf("%s", out) + if !success { + panic("Command failed") + } + } +} + +func (ex *Executor) build(output string) int64 { + Log("Building: %s", output) + output_ts := getTimestamp(output) + + rule, present := ex.rules[output] + if !present { + if output_ts >= 0 { + return output_ts + } + Error("No rule to make target '%s'", output) + } + + latest := int64(-1) + for _, input := range rule.inputs { + ts := ex.build(input) + if latest < ts { + latest = ts + } + } + + if output_ts >= latest { + return output_ts + } + + ex.runCommands(rule.cmds) + + output_ts = getTimestamp(output) + if output_ts < 0 { + output_ts = time.Now().Unix() + } + return output_ts +} + +func (ex *Executor) exec(er *EvalResult) { + if len(er.rules) == 0 { + panic("No targets.") + } + + for _, rule := range er.rules { + if _, present := ex.rules[rule.output]; present { + Warn("overiding recipie for target '%s'", rule.output) + } + ex.rules[rule.output] = rule + } + + ex.build(er.rules[0].output) +} + +func Exec(er *EvalResult) { + ex := newExecutor() + ex.exec(er) +} @@ -0,0 +1,31 @@ +package main + +import ( + "bytes" + "fmt" +) + +func Log(f string, a ...interface{}) { + var buf bytes.Buffer + buf.WriteString("*kati*: ") + buf.WriteString(f) + buf.WriteByte('\n') + fmt.Printf(buf.String(), a...) +} + +func Warn(f string, a ...interface{}) { + var buf bytes.Buffer + buf.WriteString("warning: ") + buf.WriteString(f) + buf.WriteByte('\n') + fmt.Printf(buf.String(), a...) +} + +func Error(f string, a ...interface{}) { + var buf bytes.Buffer + buf.WriteString("error: ") + buf.WriteString(f) + buf.WriteByte('\n') + fmt.Printf(buf.String(), a...) + panic("") +} @@ -0,0 +1,15 @@ +package main + +func main() { + mk, err := ParseDefaultMakefile() + if err != nil { + panic(err) + } + + for _, stmt := range mk.stmts { + stmt.show() + } + + er := Eval(mk) + Exec(er) +} diff --git a/parser.go b/parser.go new file mode 100644 index 0000000..12dffe3 --- /dev/null +++ b/parser.go @@ -0,0 +1,210 @@ +package main + +import ( + "bufio" + "errors" + "io" + "os" + "strings" +) + +type Makefile struct { + stmts []AST +} + +type parser struct { + rd *bufio.Reader + mk Makefile + lineno int + done bool +} + +func exists(filename string) bool { + f, err := os.Open(filename) + if err != nil { + return false + } + f.Close() + return true +} + +func isdigit(ch byte) bool { + return ch >= '0' && ch <= '9' +} + +func isident(ch byte) bool { + return (ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z' || ch == '_' || ch == '.') +} + +func newParser(rd io.Reader) *parser { + return &parser{ + rd: bufio.NewReader(rd), + } +} + +func (p *parser) readByte() (byte, error) { + ch, err := p.rd.ReadByte() + if err != nil { + p.done = true + } + return ch, err +} + +func (p *parser) unreadByte() { + p.rd.UnreadByte() +} + +func (p *parser) skipWhiteSpaces() error { + for { + ch, err := p.readByte() + if err != nil { + return err + } + switch ch { + case '\n': + p.lineno++ + fallthrough + case '\r', ' ': + continue + default: + p.unreadByte() + return nil + } + } +} + +func (p *parser) getNextToken() (string, error) { + if err := p.skipWhiteSpaces(); err != nil { + return "", err + } + ch, err := p.readByte() + if err != nil { + return "", errors.New("TODO") + } + switch ch { + case '$', '=': + return string(ch), nil + case ':': + var s []byte + s = append(s, ch) + ch, err := p.readByte() + if ch == ':' { + ch, err = p.readByte() + } + if err != nil { + return string(s), err + } + if ch == '=' { + s = append(s, ch) + } else { + p.unreadByte() + } + return string(s), nil + default: + if isident(ch) { + var s []byte + s = append(s, ch) + for { + ch, err := p.readByte() + if err != nil { + return string(s), err + } + if isident(ch) || isdigit(ch) { + s = append(s, ch) + } else { + p.unreadByte() + return string(s), nil + } + } + } + } + + return "", errors.New("foobar") +} + +func (p *parser) readUntilEol() string { + var r []byte + for { + ch, err := p.readByte() + if err != nil || ch == '\n' { + return string(r) + } + r = append(r, ch) + } +} + +func (p *parser) parseAssign(lhs string) AST { + ast := &AssignAST{lhs: lhs} + ast.lineno = p.lineno + ast.rhs = strings.TrimSpace(p.readUntilEol()) + return ast +} + +func (p *parser) parseRule(lhs string) AST { + ast := &RuleAST{lhs: lhs} + ast.lineno = p.lineno + ast.rhs = strings.TrimSpace(p.readUntilEol()) + for { + ch, err := p.readByte() + if err != nil { + return ast + } + switch ch { + case '\n': + continue + case '\t': + ast.cmds = append(ast.cmds, strings.TrimSpace(p.readUntilEol())) + continue + default: + p.unreadByte() + return ast + } + } +} + +func (p *parser) parse() (Makefile, error) { + for { + tok, err := p.getNextToken() + Log("tok=%s", tok) + if err == io.EOF { + return p.mk, nil + } else if err != nil { + return p.mk, err + } + switch tok { + default: + ntok, err := p.getNextToken() + if err != nil { + return p.mk, err + } + switch ntok { + case "=": + ast := p.parseAssign(tok) + p.mk.stmts = append(p.mk.stmts, ast) + case ":": + ast := p.parseRule(tok) + p.mk.stmts = append(p.mk.stmts, ast) + } + } + } + return p.mk, nil +} + +func ParseMakefile(filename string) (Makefile, error) { + f, err := os.Open(filename) + if err != nil { + return Makefile{}, err + } + parser := newParser(f) + return parser.parse() +} + +func ParseDefaultMakefile() (Makefile, error) { + candidates := []string{"GNUmakefile", "makefile", "Makefile"} + for _, filename := range candidates { + if exists(filename) { + return ParseMakefile(filename) + } + } + return Makefile{}, errors.New("No targets specified and no makefile found.") +} diff --git a/runtest.rb b/runtest.rb new file mode 100755 index 0000000..72a9f84 --- /dev/null +++ b/runtest.rb @@ -0,0 +1,58 @@ +#!/usr/bin/env ruby + +require 'fileutils' + +def get_output_filenames + files = Dir.glob('*') + files.delete('Makefile') + files +end + +def cleanup + get_output_filenames.each do |fname| + FileUtils.rm fname + end +end + +Dir.glob('test/*.mk').sort.each do |mk| + c = File.read(mk) + + name = mk[/([^\/]+)\.mk$/, 1] + dir = "out/#{name}" + FileUtils.rm_rf(dir) + FileUtils.mkdir_p(dir) + + Dir.chdir(dir) do + File.open("Makefile", 'w') do |ofile| + ofile.print(c) + end + + expected = '' + output = '' + + c.scan(/^test\d*/).sort.each do |tc| + cleanup + expected += "=== #{tc} ===\n" + `make 2>&1` + expected_files = get_output_filenames + cleanup + output += "=== #{tc} ===\n" + `../../kati 2>&1` + output_files = get_output_filenames + + expected.gsub!(/^make\[.*\n/, '') + output.gsub!(/^\*kati\*.*\n/, '') + + expected += "\n=== FILES ===\n#{expected_files * "\n"}\n" + output += "\n=== FILES ===\n#{output_files * "\n"}\n" + end + + File.open('out.make', 'w'){|ofile|ofile.print(expected)} + File.open('out.kati', 'w'){|ofile|ofile.print(output)} + + if expected != output + puts "#{name}: FAIL" + puts `diff -u out.make out.kati` + else + puts "#{name}: OK" + end + end +end diff --git a/test/basic_dep.mk b/test/basic_dep.mk new file mode 100644 index 0000000..de51a7f --- /dev/null +++ b/test/basic_dep.mk @@ -0,0 +1,6 @@ +test1: foo + +test2: foo + +foo: + echo foo > $@ diff --git a/test/basic_rule.mk b/test/basic_rule.mk new file mode 100644 index 0000000..eb26971 --- /dev/null +++ b/test/basic_rule.mk @@ -0,0 +1,4 @@ +test: foo + +foo: + echo foo diff --git a/test/basic_var.mk b/test/basic_var.mk new file mode 100644 index 0000000..f684c09 --- /dev/null +++ b/test/basic_var.mk @@ -0,0 +1,4 @@ +VAR=var + +test: + echo $(VAR) diff --git a/test/var_target.mk b/test/var_target.mk new file mode 100644 index 0000000..bd9e4e0 --- /dev/null +++ b/test/var_target.mk @@ -0,0 +1,5 @@ +FOO=BAR +$(FOO)=BAZ + +test: + echo $(BAR) diff --git a/test/warning.mk b/test/warning.mk new file mode 100644 index 0000000..ac61f15 --- /dev/null +++ b/test/warning.mk @@ -0,0 +1,3 @@ +$(warning foo) + +test: |