/
watch.go
158 lines (144 loc) · 4.4 KB
/
watch.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// +build !bootstrap
// Package watch provides a filesystem watcher that is used to rebuild affected targets.
package watch
import (
"fmt"
"os"
"os/exec"
"path"
"strings"
"time"
"github.com/fsnotify/fsnotify"
"github.com/streamrail/concurrent-map"
"gopkg.in/op/go-logging.v1"
"core"
"fs"
)
var log = logging.MustGetLogger("watch")
const debounceInterval = 50 * time.Millisecond
// Watch starts watching the sources of the given labels for changes and triggers
// rebuilds whenever they change.
// It never returns successfully, it will either watch forever or die.
func Watch(state *core.BuildState, labels []core.BuildLabel, run bool) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatalf("Error setting up watcher: %s", err)
}
// This sets up the actual watches. It must be done in a separate goroutine.
files := cmap.New()
go startWatching(watcher, state, labels, files)
cmds := commands(state, labels, run)
for {
select {
case event := <-watcher.Events:
log.Info("Event: %s", event)
if !files.Has(event.Name) {
log.Notice("Skipping notification for %s", event.Name)
continue
}
// Quick debounce; poll and discard all events for the next brief period.
outer:
for {
select {
case <-watcher.Events:
case <-time.After(debounceInterval):
break outer
}
}
runBuild(state, cmds, labels)
case err := <-watcher.Errors:
log.Error("Error watching files:", err)
}
}
}
func runBuild(state *core.BuildState, commands []string, labels []core.BuildLabel) {
binary, err := os.Executable()
if err != nil {
log.Warning("Can't determine current executable, will assume 'plz'")
binary = "plz"
}
cmd := core.ExecCommand(binary, commands...)
cmd.Args = append(cmd.Args, "-c", state.Config.Build.Config)
for _, label := range labels {
cmd.Args = append(cmd.Args, label.String())
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
log.Notice("Running %s %s...", binary, strings.Join(commands, " "))
if err := cmd.Run(); err != nil {
// Only log the error if it's not a straightforward non-zero exit; the user will presumably
// already have been pestered about that.
if _, ok := err.(*exec.ExitError); !ok {
log.Error("Failed to run %s: %s", binary, err)
}
}
}
func startWatching(watcher *fsnotify.Watcher, state *core.BuildState, labels []core.BuildLabel, files cmap.ConcurrentMap) {
// Deduplicate seen targets & sources.
targets := map[*core.BuildTarget]struct{}{}
dirs := map[string]struct{}{}
var startWatch func(*core.BuildTarget)
startWatch = func(target *core.BuildTarget) {
if _, present := targets[target]; present {
return
}
targets[target] = struct{}{}
for _, source := range target.AllSources() {
addSource(watcher, state, source, dirs, files)
}
for _, datum := range target.Data {
addSource(watcher, state, datum, dirs, files)
}
for _, dep := range target.Dependencies() {
startWatch(dep)
}
pkg := state.Graph.PackageOrDie(target.Label.PackageName)
if !files.Has(pkg.Filename) {
log.Notice("Adding watch on %s", pkg.Filename)
files.Set(pkg.Filename, struct{}{})
}
for _, subinclude := range pkg.Subincludes {
startWatch(state.Graph.TargetOrDie(subinclude))
}
}
for _, label := range labels {
startWatch(state.Graph.TargetOrDie(label))
}
// Drop a message here so they know when it's actually ready to go.
fmt.Println("And now my watch begins...")
}
func addSource(watcher *fsnotify.Watcher, state *core.BuildState, source core.BuildInput, dirs map[string]struct{}, files cmap.ConcurrentMap) {
if source.Label() == nil {
for _, src := range source.Paths(state.Graph) {
if err := fs.Walk(src, func(src string, isDir bool) error {
files.Set(src, struct{}{})
dir := src
if !isDir {
dir = path.Dir(src)
}
if _, present := dirs[dir]; !present {
log.Notice("Adding watch on %s", dir)
dirs[dir] = struct{}{}
if err := watcher.Add(dir); err != nil {
log.Error("Failed to add watch on %s: %s", src, err)
}
}
return nil
}); err != nil {
log.Error("Failed to add watch on %s: %s", src, err)
}
}
}
}
// commands returns the plz commands that should be used for the given labels.
func commands(state *core.BuildState, labels []core.BuildLabel, run bool) []string {
if run {
return []string{"run", "parallel"}
}
for _, label := range labels {
if state.Graph.TargetOrDie(label).IsTest {
return []string{"test"}
}
}
return []string{"build"}
}