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  }