all repos

viye @ 66ef839

my shot at reimplementing xiki

viye/internal/plumbing/makefile/makefile.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
some refactoring again, 1 month ago
1
package makefile
2
3
import (
4
	"bufio"
5
	"bytes"
6
	"context"
7
	"errors"
8
	"os"
9
	"os/exec"
10
	"path/filepath"
11
	"slices"
12
	"strings"
13
14
	"github.com/olexsmir/viye/internal/viye"
15
)
16
17
type Tool struct{}
18
19
func (Tool) Name() string               { return "make" }
20
func (Tool) Match(c *viye.Context) bool { return c.Cmd == "make" }
21
func (Tool) Execute(c *viye.Context) (string, error) {
22
	fpath, found := findMakefile(c)
23
	if !found {
24
		return "", errors.New("makefile not found")
25
	}
26
27
	contents, err := os.ReadFile(fpath)
28
	if err != nil {
29
		return "", err
30
	}
31
32
	tasks, err := listTasksFromMakefile(contents)
33
	if err != nil {
34
		return "", err
35
	}
36
37
	switch len(c.Args) {
38
	case 0: // list make file tasks
39
		return viye.FormatBulletList(tasks), nil
40
41
	case 1: // run specified task
42
		if !slices.Contains(tasks, c.Args[0]) {
43
			return "", errors.New("task not found")
44
		}
45
46
		ctx, cancel := context.WithTimeout(context.Background(), viye.Timeout)
47
		defer cancel()
48
49
		cmd := exec.CommandContext(ctx, "make", c.Args[0])
50
		cmd.Dir = c.Dir
51
		out, err := cmd.Output()
52
		if ctx.Err() == context.DeadlineExceeded {
53
			return "", viye.ErrTimeout
54
		}
55
		if err != nil {
56
			return "", err
57
		}
58
		return viye.FormatOutput(out), nil
59
60
	default:
61
		return "", errors.New("invalid command, make usage: make [task]")
62
	}
63
}
64
65
func findMakefile(c *viye.Context) (name string, found bool) {
66
	for _, mfile := range []string{"makefile", "Makefile", "GNUMakefile"} {
67
		path := filepath.Join(c.Dir, mfile)
68
		if _, err := os.Stat(path); err == nil {
69
			return path, true
70
		}
71
	}
72
	return "", false
73
}
74
75
func listTasksFromMakefile(contents []byte) ([]string, error) {
76
	var tasks []string
77
	scanner := bufio.NewScanner(bytes.NewReader(contents))
78
	for scanner.Scan() {
79
		line := scanner.Text()
80
81
		if len(line) == 0 || line[0] == '\t' || line[0] == ' ' {
82
			continue
83
		}
84
		if strings.HasPrefix(line, "#") { // comments
85
			continue
86
		}
87
88
		colon := strings.IndexByte(line, ':')
89
		if colon <= 0 {
90
			continue
91
		}
92
93
		name := strings.TrimSpace(line[:colon])
94
		if name == "" {
95
			continue
96
		}
97
98
		rest := line[colon+1:]
99
		if strings.HasPrefix(rest, "=") { // variable assigments
100
			continue
101
		}
102
		if strings.HasPrefix(rest, ":") { // ::= or double-colon rule
103
			rest = rest[1:]
104
			if strings.HasPrefix(rest, "=") {
105
				continue
106
			}
107
		}
108
109
		if strings.Contains(name, "%") || strings.HasPrefix(name, ".") { // pattern rules and special targets
110
			continue
111
		}
112
113
		for n := range strings.FieldsSeq(name) {
114
			tasks = append(tasks, n)
115
		}
116
	}
117
	return tasks, nil
118
}