-
Notifications
You must be signed in to change notification settings - Fork 205
/
watch.go
155 lines (138 loc) · 4.5 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
// +build !bootstrap
// Package watch provides a filesystem watcher that is used to rebuild affected targets.
package watch
import (
"fmt"
"path"
"time"
"github.com/fsnotify/fsnotify"
"github.com/streamrail/concurrent-map"
"gopkg.in/op/go-logging.v1"
"github.com/thought-machine/please/src/core"
"github.com/thought-machine/please/src/fs"
)
var log = logging.MustGetLogger("watch")
const debounceInterval = 50 * time.Millisecond
// A CallbackFunc is supplied to Watch in order to trigger a build.
type CallbackFunc func(*core.BuildState, []core.BuildLabel)
// 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.BuildLabels, callback CallbackFunc) {
// This hasn't been set before, do it now.
state.NeedTests = anyTests(state, labels)
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)
// The initial setup only builds targets, it doesn't test or run things.
// Do one of those now if requested.
if state.NeedTests || state.NeedRun {
build(state, labels, callback)
}
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
}
}
build(state, labels, callback)
case err := <-watcher.Errors:
log.Error("Error watching files:", 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)
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)
}
}
}
}
// anyTests returns true if any of the given labels refer to tests.
func anyTests(state *core.BuildState, labels []core.BuildLabel) bool {
for _, l := range labels {
if state.Graph.TargetOrDie(l).IsTest {
return true
}
}
return false
}
// build invokes a single build while watching.
func build(state *core.BuildState, labels []core.BuildLabel, callback CallbackFunc) {
// Set up a new state & copy relevant parts off the existing one.
ns := core.NewBuildState(state.Config.Please.NumThreads, state.Cache, state.Verbosity, state.Config)
ns.VerifyHashes = state.VerifyHashes
ns.NumTestRuns = state.NumTestRuns
ns.NeedTests = state.NeedTests
ns.NeedRun = state.NeedRun
ns.Watch = true
ns.CleanWorkdirs = state.CleanWorkdirs
ns.DebugTests = state.DebugTests
ns.ShowAllOutput = state.ShowAllOutput
callback(ns, labels)
}