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 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
|
#!/usr/bin/env python
from __future__ import print_function
import pywatchman
import argparse
import os
import sys
import subprocess
if pywatchman.compat.PYTHON3:
STRING_TYPES = (str, bytes)
else:
STRING_TYPES = (str, unicode) # noqa: F821
def patterns_to_terms(pats):
# convert a list of globs into the equivalent watchman expression term
if pats is None or len(pats) == 0:
return ['true']
terms = ['anyof']
for p in pats:
terms.append(['match', p, 'wholename', {'includedotfiles': True}])
return terms
class Target(object):
""" Base Class for a Target
We track the patterns that we consider to be the dependencies for
this target and establish a subscription for them.
When we receive notifications for that subscription, we know that
we should execute the command.
"""
def __init__(self, name, patterns, cmd):
self.name = name
self.patterns = patterns
self.cmd = cmd
self.triggered = False
def start(self, client, root):
query = {
'expression': patterns_to_terms(self.patterns),
'fields': ['name']
}
watch = client.query('watch-project', root)
if 'warning' in watch:
print('WARNING: ', watch['warning'], file=sys.stderr)
root_dir = watch['watch']
if 'relative_path' in watch:
query['relative_root'] = watch['relative_path']
# get the initial clock value so that we only get updates
query['since'] = client.query('clock', root_dir)['clock']
print('# Changes to files matching %s will execute `%s`' % (
' '.join(self.patterns), self.cmd),
file=sys.stderr)
sub = client.query('subscribe', root_dir, self.name, query)
def consumeEvents(self, client):
data = client.getSubscription(self.name)
if data is None:
return
self.triggered = True
def execute(self):
if not self.triggered:
return
self.triggered = False
print('# Execute: `%s`' % self.cmd, file=sys.stderr)
subprocess.call(self.cmd, shell=True)
class MakefileTarget(Target):
""" Represents a Makefile target that we'd like to build. """
def __init__(self, name, make, targets, patterns):
self.make = make
self.targets = targets
cmd = "%s %s" % (self.make, " ".join(self.targets))
super(MakefileTarget, self).__init__(name, patterns, cmd)
def __repr__(self):
return '{make=%r targets=%r pat=%r}' % (
self.make, self.targets, self.patterns)
class RunTarget(Target):
""" Represents a script that we'd like to run. """
def __init__(self, name, runfile, patterns):
self.runfile = runfile
super(RunTarget, self).__init__(name, patterns, self.runfile)
def __repr__(self):
return '{runfile=%r pat=%r}' % (
self.runfile, self.patterns)
class DefineTarget(argparse.Action):
""" argument parser helper to manage defining MakefileTarget instances. """
def __init__(self, option_strings, dest, **kwargs):
super(DefineTarget, self).__init__(option_strings, dest, **kwargs)
def __call__(self, parser, namespace, values, option_string=None):
targets = getattr(namespace, self.dest)
if targets is None:
targets = []
setattr(namespace, self.dest, targets)
if isinstance(values, STRING_TYPES):
values = [values]
if namespace.pattern is None or len(namespace.pattern) == 0:
print('no patterns were specified for target %s' %
values, file=sys.stderr)
sys.exit(1)
target = MakefileTarget('target_%d' % len(targets),
namespace.make, values, namespace.pattern)
targets.append(target)
# Clear out patterns between targets
namespace.pattern = None
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description="""
watchman-make waits for changes to files and then invokes a build tool
(by default, `make`) or provided script to process those changes.
It uses the watchman service to efficiently watch the appropriate files.
Events are consolidated and settled before they are dispatched to your build
tool, so that it won't start executing until after the files have stopped
changing.
You can tell watchman-make about one or more build targets and dependencies
for those targets or provide a script to run.
watchman-make will then trigger the build for the given targets or
run the provided script as changes are detected.
""")
parser.add_argument('-t', '--target', nargs='+', type=str, action=DefineTarget,
help="""
Specify a list of target(s) to pass to the make tool. The --make and
--pattern options that precede --target are used to define the trigger
condition.
""")
parser.add_argument('-s', '--settle', type=float, default=0.2,
help='How long to wait to allow changes to settle before invoking targets')
parser.add_argument('--make', type=str, default='make',
help="""
The name of the make tool to use for the next --target. The default is `make`.
You may include additional arguments; you are not limited to just the
path to a tool or script.
""")
parser.add_argument('-p', '--pattern', type=str, nargs='+',
help="""
Define filename matching patterns that will be used to trigger the next
--target definition.
The pattern syntax is wildmatch style; globbing with recursive matching
via '**'.
--pattern is reset to empty after each --target argument.
""")
parser.add_argument('--root', type=str, default='.', help="""
Define the root of the project. The default is to use the PWD.
All patterns are considered to be relative to this root, and the build
tool is executed with this location set as its PWD.
""")
parser.add_argument('-r', '--run', type=str, help="""
The script that should be run when changes are detected
""")
args = parser.parse_args()
if args.target is None and args.run is None:
print('# No run script or targets were specified, nothing to do.', file=sys.stderr)
sys.exit(1)
if args.target is None:
args.target = []
if args.run is not None:
args.target.append(RunTarget('RunTarget', args.run, args.pattern))
def check_root(desired_root):
try:
root = os.path.abspath(desired_root)
os.chdir(root)
return root
except Exception as ex:
print('--root=%s: specified path is invalid: %s' % (
desired_root, ex), file=sys.stderr)
sys.exit(1)
targets = {}
client = pywatchman.client(timeout=600)
try:
client.capabilityCheck(
required=['cmd-watch-project', 'wildmatch'])
root = check_root(args.root)
print('# Relative to %s' % root, file=sys.stderr)
for t in args.target:
t.start(client, root)
targets[t.name] = t
except pywatchman.CommandError as ex:
print('watchman:', str(ex), file=sys.stderr)
sys.exit(1)
print('# waiting for changes', file=sys.stderr)
while True:
try:
# Wait for changes to start to occur. We're happy to wait
# quite some time for this
client.setTimeout(600)
result = client.receive()
for _, t in targets.items():
t.consumeEvents(client)
# Now we wait for events to settle
client.setTimeout(args.settle)
settled = False
while not settled:
try:
result = client.receive()
for _, t in targets.items():
t.consumeEvents(client)
except pywatchman.SocketTimeout as ex:
# Our short settle timeout hit, so we're now settled
settled = True
break
# Now we can work on executing the targets
for _, t in targets.items():
t.execute()
# Print this at the bottom of the loop rather than the top
# because we may timeout every so often and it looks weird
# to keep printing 'waiting for changes' each time we do.
print('# waiting for changes', file=sys.stderr)
except pywatchman.SocketTimeout as ex:
# Let's check to see if we're still functional
try:
vers = client.query('version')
except Exception as ex:
print('watchman:', str(ex), file=sys.stderr)
sys.exit(1)
except pywatchman.WatchmanError as ex:
print('watchman:', str(ex), file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
# suppress ugly stack trace when they Ctrl-C
break
|