github.com/sercand/please@v13.4.0+incompatible/src/watch/watch.go (about) 1 // +build !bootstrap 2 3 // Package watch provides a filesystem watcher that is used to rebuild affected targets. 4 package watch 5 6 import ( 7 "fmt" 8 "path" 9 "time" 10 11 "github.com/fsnotify/fsnotify" 12 "github.com/streamrail/concurrent-map" 13 "gopkg.in/op/go-logging.v1" 14 15 "github.com/thought-machine/please/src/core" 16 "github.com/thought-machine/please/src/fs" 17 ) 18 19 var log = logging.MustGetLogger("watch") 20 21 const debounceInterval = 50 * time.Millisecond 22 23 // A CallbackFunc is supplied to Watch in order to trigger a build. 24 type CallbackFunc func(*core.BuildState, []core.BuildLabel) 25 26 // Watch starts watching the sources of the given labels for changes and triggers 27 // rebuilds whenever they change. 28 // It never returns successfully, it will either watch forever or die. 29 func Watch(state *core.BuildState, labels core.BuildLabels, callback CallbackFunc) { 30 // This hasn't been set before, do it now. 31 state.NeedTests = anyTests(state, labels) 32 watcher, err := fsnotify.NewWatcher() 33 if err != nil { 34 log.Fatalf("Error setting up watcher: %s", err) 35 } 36 // This sets up the actual watches. It must be done in a separate goroutine. 37 files := cmap.New() 38 go startWatching(watcher, state, labels, files) 39 40 // The initial setup only builds targets, it doesn't test or run things. 41 // Do one of those now if requested. 42 if state.NeedTests || state.NeedRun { 43 build(state, labels, callback) 44 } 45 46 for { 47 select { 48 case event := <-watcher.Events: 49 log.Info("Event: %s", event) 50 if !files.Has(event.Name) { 51 log.Notice("Skipping notification for %s", event.Name) 52 continue 53 } 54 55 // Quick debounce; poll and discard all events for the next brief period. 56 outer: 57 for { 58 select { 59 case <-watcher.Events: 60 case <-time.After(debounceInterval): 61 break outer 62 } 63 } 64 build(state, labels, callback) 65 case err := <-watcher.Errors: 66 log.Error("Error watching files:", err) 67 } 68 } 69 } 70 71 func startWatching(watcher *fsnotify.Watcher, state *core.BuildState, labels []core.BuildLabel, files cmap.ConcurrentMap) { 72 // Deduplicate seen targets & sources. 73 targets := map[*core.BuildTarget]struct{}{} 74 dirs := map[string]struct{}{} 75 76 var startWatch func(*core.BuildTarget) 77 startWatch = func(target *core.BuildTarget) { 78 if _, present := targets[target]; present { 79 return 80 } 81 targets[target] = struct{}{} 82 for _, source := range target.AllSources() { 83 addSource(watcher, state, source, dirs, files) 84 } 85 for _, datum := range target.Data { 86 addSource(watcher, state, datum, dirs, files) 87 } 88 for _, dep := range target.Dependencies() { 89 startWatch(dep) 90 } 91 pkg := state.Graph.PackageOrDie(target.Label) 92 if !files.Has(pkg.Filename) { 93 log.Notice("Adding watch on %s", pkg.Filename) 94 files.Set(pkg.Filename, struct{}{}) 95 } 96 for _, subinclude := range pkg.Subincludes { 97 startWatch(state.Graph.TargetOrDie(subinclude)) 98 } 99 } 100 101 for _, label := range labels { 102 startWatch(state.Graph.TargetOrDie(label)) 103 } 104 // Drop a message here so they know when it's actually ready to go. 105 fmt.Println("And now my watch begins...") 106 } 107 108 func addSource(watcher *fsnotify.Watcher, state *core.BuildState, source core.BuildInput, dirs map[string]struct{}, files cmap.ConcurrentMap) { 109 if source.Label() == nil { 110 for _, src := range source.Paths(state.Graph) { 111 if err := fs.Walk(src, func(src string, isDir bool) error { 112 files.Set(src, struct{}{}) 113 dir := src 114 if !isDir { 115 dir = path.Dir(src) 116 } 117 if _, present := dirs[dir]; !present { 118 log.Notice("Adding watch on %s", dir) 119 dirs[dir] = struct{}{} 120 if err := watcher.Add(dir); err != nil { 121 log.Error("Failed to add watch on %s: %s", src, err) 122 } 123 } 124 return nil 125 }); err != nil { 126 log.Error("Failed to add watch on %s: %s", src, err) 127 } 128 } 129 } 130 } 131 132 // anyTests returns true if any of the given labels refer to tests. 133 func anyTests(state *core.BuildState, labels []core.BuildLabel) bool { 134 for _, l := range labels { 135 if state.Graph.TargetOrDie(l).IsTest { 136 return true 137 } 138 } 139 return false 140 } 141 142 // build invokes a single build while watching. 143 func build(state *core.BuildState, labels []core.BuildLabel, callback CallbackFunc) { 144 // Set up a new state & copy relevant parts off the existing one. 145 ns := core.NewBuildState(state.Config.Please.NumThreads, state.Cache, state.Verbosity, state.Config) 146 ns.VerifyHashes = state.VerifyHashes 147 ns.NumTestRuns = state.NumTestRuns 148 ns.NeedTests = state.NeedTests 149 ns.NeedRun = state.NeedRun 150 ns.Watch = true 151 ns.CleanWorkdirs = state.CleanWorkdirs 152 ns.DebugTests = state.DebugTests 153 ns.ShowAllOutput = state.ShowAllOutput 154 callback(ns, labels) 155 }