aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile11
-rw-r--r--ast.go47
-rw-r--r--eval.go167
-rw-r--r--exec.go104
-rw-r--r--log.go31
-rw-r--r--main.go15
-rw-r--r--parser.go210
-rwxr-xr-xruntest.rb58
-rw-r--r--test/basic_dep.mk6
-rw-r--r--test/basic_rule.mk4
-rw-r--r--test/basic_var.mk4
-rw-r--r--test/var_target.mk5
-rw-r--r--test/warning.mk3
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
diff --git a/ast.go b/ast.go
new file mode 100644
index 0000000..07b01da
--- /dev/null
+++ b/ast.go
@@ -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)
+ }
+}
diff --git a/eval.go b/eval.go
new file mode 100644
index 0000000..a676171
--- /dev/null
+++ b/eval.go
@@ -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,
+ }
+}
diff --git a/exec.go b/exec.go
new file mode 100644
index 0000000..11b3653
--- /dev/null
+++ b/exec.go
@@ -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)
+}
diff --git a/log.go b/log.go
new file mode 100644
index 0000000..be5b681
--- /dev/null
+++ b/log.go
@@ -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("")
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..9f82548
--- /dev/null
+++ b/main.go
@@ -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: