PageRenderTime 51ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/fabricate.py

https://bitbucket.org/grantj/remotecircle
Python | 1060 lines | 1030 code | 8 blank | 22 comment | 13 complexity | b2d3df9091bb2267c44027f5765db732 MD5 | raw file
  1. #!/usr/bin/env python
  2. """Build tool that finds dependencies automatically for any language.
  3. fabricate is a build tool that finds dependencies automatically for any
  4. language. It's small and just works. No hidden stuff behind your back. It was
  5. inspired by Bill McCloskey's make replacement, memoize, but fabricate works on
  6. Windows as well as Linux.
  7. Read more about how to use it and how it works on the project page:
  8. http://code.google.com/p/fabricate/
  9. Like memoize, fabricate is released under a "New BSD license". fabricate is
  10. copyright (c) 2009 Brush Technology. Full text of the license is here:
  11. http://code.google.com/p/fabricate/wiki/License
  12. To get help on fabricate functions:
  13. from fabricate import *
  14. help(function)
  15. """
  16. # fabricate version number
  17. __version__ = '1.14'
  18. # if version of .deps file has changed, we know to not use it
  19. deps_version = 2
  20. import atexit
  21. import optparse
  22. import os
  23. import platform
  24. import re
  25. import shlex
  26. import stat
  27. import subprocess
  28. import sys
  29. import tempfile
  30. import time
  31. # so you can do "from fabricate import *" to simplify your build script
  32. __all__ = ['setup', 'run', 'autoclean', 'main', 'shell', 'fabricate_version',
  33. 'memoize', 'outofdate',
  34. 'ExecutionError', 'md5_hasher', 'mtime_hasher',
  35. 'Runner', 'AtimesRunner', 'StraceRunner', 'AlwaysRunner',
  36. 'SmartRunner', 'Builder']
  37. import textwrap
  38. __doc__ += "Exported functions are:\n" + ' ' + '\n '.join(textwrap.wrap(', '.join(__all__), 80))
  39. FAT_atime_resolution = 24*60*60 # resolution on FAT filesystems (seconds)
  40. FAT_mtime_resolution = 2
  41. # NTFS resolution is < 1 ms
  42. # We assume this is considerably more than time to run a new process
  43. NTFS_atime_resolution = 0.0002048 # resolution on NTFS filesystems (seconds)
  44. NTFS_mtime_resolution = 0.0002048 # is actually 0.1us but python's can be
  45. # as low as 204.8us due to poor
  46. # float precision when storing numbers
  47. # as big as NTFS file times can be
  48. # (float has 52-bit precision and NTFS
  49. # FILETIME has 63-bit precision, so
  50. # we've lost 11 bits = 2048)
  51. # So we can use md5func in old and new versions of Python without warnings
  52. try:
  53. import hashlib
  54. md5func = hashlib.md5
  55. except ImportError:
  56. import md5
  57. md5func = md5.new
  58. # Use json, or pickle on older Python versions if simplejson not installed
  59. try:
  60. import json
  61. except ImportError:
  62. try:
  63. import simplejson as json
  64. except ImportError:
  65. import cPickle
  66. # needed to ignore the indent= argument for pickle's dump()
  67. class PickleJson:
  68. def load(self, f):
  69. return cPickle.load(f)
  70. def dump(self, obj, f, indent=None, sort_keys=None):
  71. return cPickle.dump(obj, f)
  72. json = PickleJson()
  73. def printerr(message):
  74. """ Print given message to stderr with a line feed. """
  75. print >>sys.stderr, message
  76. class PathError(Exception): pass
  77. class ExecutionError(Exception):
  78. """ Raised by shell() and run() if command returns non-zero exit code. """
  79. pass
  80. def args_to_list(args):
  81. """ Return a flat list of the given arguments for shell(). """
  82. arglist = []
  83. for arg in args:
  84. if arg is None:
  85. continue
  86. if hasattr(arg, '__iter__'):
  87. arglist.extend(args_to_list(arg))
  88. else:
  89. if not isinstance(arg, basestring):
  90. arg = str(arg)
  91. arglist.append(arg)
  92. return arglist
  93. def shell(*args, **kwargs):
  94. r""" Run a command: program name is given in first arg and command line
  95. arguments in the rest of the args. Iterables (lists and tuples) in args
  96. are recursively converted to separate arguments, non-string types are
  97. converted with str(arg), and None is ignored. For example:
  98. >>> def tail(input, n=3, flags=None):
  99. >>> args = ['-n', n]
  100. >>> return shell('tail', args, flags, input=input)
  101. >>> tail('a\nb\nc\nd\ne\n')
  102. 'c\nd\ne\n'
  103. >>> tail('a\nb\nc\nd\ne\n', 2, ['-v'])
  104. '==> standard input <==\nd\ne\n'
  105. Keyword arguments kwargs are interpreted as follows:
  106. "input" is a string to pass standard input into the process (or the
  107. default of None to use parent's stdin, eg: the keyboard)
  108. "silent" is True (default) to return process's standard output as a
  109. string, or False to print it as it comes out
  110. "shell" set to True will run the command via the shell (/bin/sh or
  111. COMSPEC) instead of running the command directly (the default)
  112. Raises ExecutionError(message, output, status) if the command returns
  113. a non-zero status code. """
  114. return _shell(args, **kwargs)
  115. def _shell(args, input=None, silent=True, shell=False):
  116. if input:
  117. stdin = subprocess.PIPE
  118. else:
  119. stdin = None
  120. if silent:
  121. stdout = subprocess.PIPE
  122. else:
  123. stdout = None
  124. arglist = args_to_list(args)
  125. if not arglist:
  126. raise TypeError('shell() takes at least 1 argument (0 given)')
  127. if shell:
  128. # handle subprocess.Popen quirk where subsequent args are passed
  129. # to bash instead of to our command
  130. command = subprocess.list2cmdline(arglist)
  131. else:
  132. command = arglist
  133. proc = subprocess.Popen(command, stdin=stdin, stdout=stdout,
  134. stderr=subprocess.STDOUT, shell=shell)
  135. output, stderr = proc.communicate(input)
  136. status = proc.wait()
  137. if status:
  138. raise ExecutionError('%r exited with status %d'
  139. % (os.path.basename(arglist[0]), status),
  140. output, status)
  141. if silent:
  142. return output
  143. def md5_hasher(filename):
  144. """ Return MD5 hash of given filename, or None if file doesn't exist. """
  145. try:
  146. f = open(filename, 'rb')
  147. try:
  148. return md5func(f.read()).hexdigest()
  149. finally:
  150. f.close()
  151. except IOError:
  152. return None
  153. def mtime_hasher(filename):
  154. """ Return modification time of file, or None if file doesn't exist. """
  155. try:
  156. st = os.stat(filename)
  157. return repr(st.st_mtime)
  158. except (IOError, OSError):
  159. return None
  160. class RunnerUnsupportedException(Exception):
  161. """ Exception raise by Runner constructor if it is not supported
  162. on the current platform."""
  163. pass
  164. class Runner(object):
  165. def __call__(self, *args):
  166. """ Run command and return (dependencies, outputs), where
  167. dependencies is a list of the filenames of files that the
  168. command depended on, and output is a list of the filenames
  169. of files that the command modified."""
  170. raise NotImplementedError()
  171. def ignore(self, name):
  172. return self._builder.ignore.search(name)
  173. class AtimesRunner(Runner):
  174. def __init__(self, builder):
  175. self._builder = builder
  176. self.atimes = AtimesRunner.has_atimes(self._builder.dirs)
  177. if self.atimes == 0:
  178. raise RunnerUnsupportedException(
  179. 'atimes are not supported on this platform')
  180. @staticmethod
  181. def file_has_atimes(filename):
  182. """ Return whether the given filesystem supports access time updates for
  183. this file. Return:
  184. - 0 if no a/mtimes not updated
  185. - 1 if the atime resolution is at least one day and
  186. the mtime resolution at least 2 seconds (as on FAT filesystems)
  187. - 2 if the atime and mtime resolutions are both < ms
  188. (NTFS filesystem has 100 ns resolution). """
  189. def access_file(filename):
  190. """ Access (read a byte from) file to try to update its access time. """
  191. f = open(filename)
  192. f.read(1)
  193. f.close()
  194. initial = os.stat(filename)
  195. os.utime(filename, (
  196. initial.st_atime-FAT_atime_resolution,
  197. initial.st_mtime-FAT_mtime_resolution))
  198. adjusted = os.stat(filename)
  199. access_file(filename)
  200. after = os.stat(filename)
  201. # Check that a/mtimes actually moved back by at least resolution and
  202. # updated by a file access.
  203. # add NTFS_atime_resolution to account for float resolution factors
  204. # Comment on resolution/2 in atimes_runner()
  205. if initial.st_atime-adjusted.st_atime > FAT_atime_resolution+NTFS_atime_resolution or \
  206. initial.st_mtime-adjusted.st_mtime > FAT_mtime_resolution+NTFS_atime_resolution or \
  207. initial.st_atime==adjusted.st_atime or \
  208. initial.st_mtime==adjusted.st_mtime or \
  209. not after.st_atime-FAT_atime_resolution/2 > adjusted.st_atime:
  210. return 0
  211. os.utime(filename, (
  212. initial.st_atime-NTFS_atime_resolution,
  213. initial.st_mtime-NTFS_mtime_resolution))
  214. adjusted = os.stat(filename)
  215. # Check that a/mtimes actually moved back by at least resolution
  216. # Note: != comparison here fails due to float rounding error
  217. # double NTFS_atime_resolution to account for float resolution factors
  218. if initial.st_atime-adjusted.st_atime > NTFS_atime_resolution*2 or \
  219. initial.st_mtime-adjusted.st_mtime > NTFS_mtime_resolution*2 or \
  220. initial.st_atime==adjusted.st_atime or \
  221. initial.st_mtime==adjusted.st_mtime:
  222. return 1
  223. return 2
  224. @staticmethod
  225. def exists(path):
  226. if not os.path.exists(path):
  227. # Note: in linux, error may not occur: strace runner doesn't check
  228. raise PathError("build dirs specified a non-existant path '%s'" % path)
  229. @staticmethod
  230. def has_atimes(paths):
  231. """ Return whether a file created in each path supports atimes and mtimes.
  232. Return value is the same as used by file_has_atimes
  233. Note: for speed, this only tests files created at the top directory
  234. of each path. A safe assumption in most build environments.
  235. In the unusual case that any sub-directories are mounted
  236. on alternate file systems that don't support atimes, the build may
  237. fail to identify a dependency """
  238. atimes = 2 # start by assuming we have best atimes
  239. for path in paths:
  240. AtimesRunner.exists(path)
  241. handle, filename = tempfile.mkstemp(dir=path)
  242. try:
  243. try:
  244. f = os.fdopen(handle, 'wb')
  245. except:
  246. os.close(handle)
  247. raise
  248. try:
  249. f.write('x') # need a byte in the file for access test
  250. finally:
  251. f.close()
  252. atimes = min(atimes, AtimesRunner.file_has_atimes(filename))
  253. finally:
  254. os.remove(filename)
  255. return atimes
  256. def _file_times(self, path, depth):
  257. """ Helper function for file_times().
  258. Return a dict of file times, recursing directories that don't
  259. start with self._builder.ignoreprefix """
  260. AtimesRunner.exists(path)
  261. names = os.listdir(path)
  262. times = {}
  263. ignoreprefix = self._builder.ignoreprefix
  264. for name in names:
  265. if ignoreprefix and name.startswith(ignoreprefix):
  266. continue
  267. if path == '.':
  268. fullname = name
  269. else:
  270. fullname = os.path.join(path, name)
  271. st = os.stat(fullname)
  272. if stat.S_ISDIR(st.st_mode):
  273. if depth > 1:
  274. times.update(self._file_times(fullname, depth-1))
  275. elif stat.S_ISREG(st.st_mode):
  276. times[fullname] = st.st_atime, st.st_mtime
  277. return times
  278. def file_times(self):
  279. """ Return a dict of "filepath: (atime, mtime)" entries for each file
  280. in self._builder.dirs. "filepath" is the absolute path, "atime" is
  281. the access time, "mtime" the modification time.
  282. Recurse directories that don't start with
  283. self._builder.ignoreprefix and have depth less than
  284. self._builder.dirdepth. """
  285. times = {}
  286. for path in self._builder.dirs:
  287. times.update(self._file_times(path, self._builder.dirdepth))
  288. return times
  289. def _utime(self, filename, atime, mtime):
  290. """ Call os.utime but ignore permission errors """
  291. try:
  292. os.utime(filename, (atime, mtime))
  293. except OSError, e:
  294. # ignore permission errors -- we can't build with files
  295. # that we can't access anyway
  296. if e.errno != 1:
  297. raise
  298. def _age_atimes(self, filetimes):
  299. """ Age files' atimes and mtimes to be at least FAT_xx_resolution old.
  300. Only adjust if the given filetimes dict says it isn't that old,
  301. and return a new dict of filetimes with the ages adjusted. """
  302. adjusted = {}
  303. now = time.time()
  304. for filename, entry in filetimes.iteritems():
  305. if now-entry[0] < FAT_atime_resolution or now-entry[1] < FAT_mtime_resolution:
  306. entry = entry[0] - FAT_atime_resolution, entry[1] - FAT_mtime_resolution
  307. self._utime(filename, entry[0], entry[1])
  308. adjusted[filename] = entry
  309. return adjusted
  310. def __call__(self, *args):
  311. """ Run command and return its dependencies and outputs, using before
  312. and after access times to determine dependencies. """
  313. # For Python pre-2.5, ensure os.stat() returns float atimes
  314. old_stat_float = os.stat_float_times()
  315. os.stat_float_times(True)
  316. originals = self.file_times()
  317. if self.atimes == 2:
  318. befores = originals
  319. atime_resolution = 0
  320. mtime_resolution = 0
  321. else:
  322. befores = self._age_atimes(originals)
  323. atime_resolution = FAT_atime_resolution
  324. mtime_resolution = FAT_mtime_resolution
  325. shell(*args, **dict(silent=False))
  326. afters = self.file_times()
  327. deps = []
  328. outputs = []
  329. for name in afters:
  330. if name in befores:
  331. # if file exists before+after && mtime changed, add to outputs
  332. # Note: Can't just check that atimes > than we think they were
  333. # before because os might have rounded them to a later
  334. # date than what we think we set them to in befores.
  335. # So we make sure they're > by at least 1/2 the
  336. # resolution. This will work for anything with a
  337. # resolution better than FAT.
  338. if afters[name][1]-mtime_resolution/2 > befores[name][1]:
  339. outputs.append(name)
  340. elif afters[name][0]-atime_resolution/2 > befores[name][0]:
  341. # otherwise add to deps if atime changed
  342. if not self.ignore(name):
  343. deps.append(name)
  344. else:
  345. # file created (in afters but not befores), add as output
  346. if not self.ignore(name):
  347. outputs.append(name)
  348. if self.atimes < 2:
  349. # Restore atimes of files we didn't access: not for any functional
  350. # reason -- it's just to preserve the access time for the user's info
  351. for name in deps:
  352. originals.pop(name)
  353. for name in originals:
  354. original = originals[name]
  355. if original != afters.get(name, None):
  356. self._utime(name, original[0], original[1])
  357. os.stat_float_times(old_stat_float) # restore stat_float_times value
  358. return deps, outputs
  359. class StraceProcess(object):
  360. def __init__(self, cwd='.'):
  361. self.cwd = cwd
  362. self.deps = set()
  363. self.outputs = set()
  364. def add_dep(self, dep):
  365. self.deps.add(dep)
  366. def add_output(self, output):
  367. self.outputs.add(output)
  368. def __str__(self):
  369. return '<StraceProcess cwd=%s deps=%s outputs=%s>' % \
  370. (self.cwd, self.deps, self.outputs)
  371. class StraceRunner(Runner):
  372. keep_temps = False
  373. def __init__(self, builder):
  374. self.strace_version = StraceRunner.get_strace_version()
  375. if self.strace_version == 0:
  376. raise RunnerUnsupportedException('strace is not available')
  377. if self.strace_version == 32:
  378. self._stat_re = self._stat32_re
  379. self._stat_func = 'stat'
  380. else:
  381. self._stat_re = self._stat64_re
  382. self._stat_func = 'stat64'
  383. self._builder = builder
  384. self.temp_count = 0
  385. @staticmethod
  386. def get_strace_version():
  387. """ Return 0 if this system doesn't have strace, nonzero otherwise
  388. (64 if strace supports stat64, 32 otherwise). """
  389. if platform.system() == 'Windows':
  390. # even if windows has strace, it's probably a dodgy cygwin one
  391. return 0
  392. try:
  393. proc = subprocess.Popen(['strace', '-e', 'trace=stat64'], stderr=subprocess.PIPE)
  394. stdout, stderr = proc.communicate()
  395. proc.wait()
  396. if 'invalid system call' in stderr:
  397. return 32
  398. else:
  399. return 64
  400. except OSError:
  401. return 0
  402. # Regular expressions for parsing of strace log
  403. _open_re = re.compile(r'(?P<pid>\d+)\s+open\("(?P<name>[^"]*)", (?P<mode>[^,)]*)')
  404. _stat32_re = re.compile(r'(?P<pid>\d+)\s+stat\("(?P<name>[^"]*)", .*')
  405. _stat64_re = re.compile(r'(?P<pid>\d+)\s+stat64\("(?P<name>[^"]*)", .*')
  406. _execve_re = re.compile(r'(?P<pid>\d+)\s+execve\("(?P<name>[^"]*)", .*')
  407. _mkdir_re = re.compile(r'(?P<pid>\d+)\s+mkdir\("(?P<name>[^"]*)", .*')
  408. _rename_re = re.compile(r'(?P<pid>\d+)\s+rename\("[^"]*", "(?P<name>[^"]*)"\)')
  409. _kill_re = re.compile(r'(?P<pid>\d+)\s+killed by.*')
  410. _chdir_re = re.compile(r'(?P<pid>\d+)\s+chdir\("(?P<cwd>[^"]*)"\)')
  411. _exit_group_re = re.compile(r'(?P<pid>\d+)\s+exit_group\((?P<status>.*)\).*')
  412. _clone_re = re.compile(r'(?P<pid_clone>\d+)\s+(clone|fork|vfork)\(.*\)\s*=\s*(?P<pid>\d*)')
  413. # Regular expressions for detecting interrupted lines in strace log
  414. # 3618 clone( <unfinished ...>
  415. # 3618 <... clone resumed> child_stack=0, flags=CLONE, child_tidptr=0x7f83deffa780) = 3622
  416. _unfinished_start_re = re.compile(r'(?P<pid>\d+)(?P<body>.*)<unfinished ...>$')
  417. _unfinished_end_re = re.compile(r'(?P<pid>\d+)\s+\<\.\.\..*\>(?P<body>.*)')
  418. def _do_strace(self, args, outfile, outname):
  419. """ Run strace on given command args, sending output to file.
  420. Return (status code, list of dependencies, list of outputs). """
  421. shell('strace', '-fo', outname, '-e',
  422. 'trace=open,%s,execve,exit_group,chdir,mkdir,rename,clone,vfork,fork' % self._stat_func,
  423. args, silent=False)
  424. cwd = '.'
  425. status = 0
  426. processes = {} # dictionary of processes (key = pid)
  427. unfinished = {} # list of interrupted entries in strace log
  428. for line in outfile:
  429. # look for split lines
  430. unfinished_start_match = self._unfinished_start_re.match(line)
  431. unfinished_end_match = self._unfinished_end_re.match(line)
  432. if unfinished_start_match:
  433. pid = unfinished_start_match.group('pid')
  434. body = unfinished_start_match.group('body')
  435. unfinished[pid] = pid + ' ' + body
  436. continue
  437. elif unfinished_end_match:
  438. pid = unfinished_end_match.group('pid')
  439. body = unfinished_end_match.group('body')
  440. line = unfinished[pid] + body
  441. del unfinished[pid]
  442. is_output = False
  443. open_match = self._open_re.match(line)
  444. stat_match = self._stat_re.match(line)
  445. execve_match = self._execve_re.match(line)
  446. mkdir_match = self._mkdir_re.match(line)
  447. rename_match = self._rename_re.match(line)
  448. clone_match = self._clone_re.match(line)
  449. kill_match = self._kill_re.match(line)
  450. if kill_match:
  451. return None, None, None
  452. match = None
  453. if execve_match:
  454. pid = execve_match.group('pid')
  455. if pid not in processes:
  456. processes[pid] = StraceProcess()
  457. match = execve_match
  458. elif clone_match:
  459. pid = clone_match.group('pid')
  460. pid_clone = clone_match.group('pid_clone')
  461. processes[pid] = StraceProcess(processes[pid_clone].cwd)
  462. elif open_match:
  463. match = open_match
  464. mode = match.group('mode')
  465. if 'O_WRONLY' in mode or 'O_RDWR' in mode:
  466. # it's an output file if opened for writing
  467. is_output = True
  468. elif stat_match:
  469. match = stat_match
  470. elif mkdir_match:
  471. match = mkdir_match
  472. elif rename_match:
  473. match = rename_match
  474. # the destination of a rename is an output file
  475. is_output = True
  476. if match:
  477. name = match.group('name')
  478. pid = match.group('pid')
  479. cwd = processes[pid].cwd
  480. if cwd != '.':
  481. name = os.path.join(cwd, name)
  482. if (self._builder._is_relevant(name)
  483. and not self.ignore(name)
  484. and (os.path.isfile(name)
  485. or os.path.isdir(name)
  486. or not os.path.lexists(name))):
  487. if is_output:
  488. processes[pid].add_output(name)
  489. else:
  490. processes[pid].add_dep(name)
  491. match = self._chdir_re.match(line)
  492. if match:
  493. processes[pid].cwd = os.path.join(processes[pid].cwd, match.group('cwd'))
  494. match = self._exit_group_re.match(line)
  495. if match:
  496. status = int(match.group('status'))
  497. # collect outputs and dependencies from all processes
  498. deps = set()
  499. outputs = set()
  500. for pid, process in processes.items():
  501. deps = deps.union(process.deps)
  502. outputs = outputs.union(process.outputs)
  503. return status, list(deps), list(outputs)
  504. def __call__(self, *args):
  505. """ Run command and return its dependencies and outputs, using strace
  506. to determine dependencies (by looking at what files are opened or
  507. modified). """
  508. if self.keep_temps:
  509. outname = 'strace%03d.txt' % self.temp_count
  510. self.temp_count += 1
  511. handle = os.open(outname, os.O_CREAT)
  512. else:
  513. handle, outname = tempfile.mkstemp()
  514. try:
  515. try:
  516. outfile = os.fdopen(handle, 'r')
  517. except:
  518. os.close(handle)
  519. raise
  520. try:
  521. status, deps, outputs = self._do_strace(args, outfile, outname)
  522. if status is None:
  523. raise ExecutionError(
  524. '%r was killed unexpectedly' % args[0], '', -1)
  525. finally:
  526. outfile.close()
  527. finally:
  528. if not self.keep_temps:
  529. os.remove(outname)
  530. if status:
  531. raise ExecutionError('%r exited with status %d'
  532. % (os.path.basename(args[0]), status),
  533. '', status)
  534. return list(deps), list(outputs)
  535. class AlwaysRunner(Runner):
  536. def __init__(self, builder):
  537. pass
  538. def __call__(self, *args):
  539. """ Runner that always runs given command, used as a backup in case
  540. a system doesn't have strace or atimes. """
  541. shell(*args, **dict(silent=False))
  542. return None, None
  543. class SmartRunner(Runner):
  544. def __init__(self, builder):
  545. self._builder = builder
  546. self._runner = None
  547. def __call__(self, *args):
  548. """ Smart command runner that uses StraceRunner if it can,
  549. otherwise AtimesRunner if available, otherwise AlwaysRunner.
  550. When first called, it caches which runner it used for next time."""
  551. if self._runner is None:
  552. try:
  553. self._runner = StraceRunner(self._builder)
  554. except RunnerUnsupportedException:
  555. try:
  556. self._runner = AtimesRunner(self._builder)
  557. except RunnerUnsupportedException:
  558. self._runner = AlwaysRunner(self._builder)
  559. print(self._runner)
  560. return self._runner(*args)
  561. class Builder(object):
  562. """ The Builder.
  563. You may supply a "runner" class to change the way commands are run
  564. or dependencies are determined. For an example, see:
  565. http://code.google.com/p/fabricate/wiki/HowtoMakeYourOwnRunner
  566. A "runner" must be a subclass of Runner and must have a __call__()
  567. function that takes a command as a list of args and returns a tuple of
  568. (deps, outputs), where deps is a list of rel-path'd dependency files
  569. and outputs is a list of rel-path'd output files. The default runner
  570. is SmartRunner, which automatically picks one of StraceRunner,
  571. AtimesRunner, or AlwaysRunner depending on your system.
  572. A "runner" class may have an __init__() function that takes the
  573. builder as a parameter.
  574. """
  575. def __init__(self, runner=None, dirs=None, dirdepth=100, ignoreprefix='.',
  576. ignore=None, hasher=md5_hasher, depsname='.deps',
  577. quiet=False):
  578. """ Initialise a Builder with the given options.
  579. "runner" specifies how programs should be run. It is either a
  580. callable compatible with the Runner class, or a string selecting
  581. one of the standard runners ("atimes_runner", "strace_runner",
  582. "always_runner", or "smart_runner").
  583. "dirs" is a list of paths to look for dependencies (or outputs) in
  584. if using the strace or atimes runners.
  585. "dirdepth" is the depth to recurse into the paths in "dirs" (default
  586. essentially means infinitely). Set to 1 to just look at the
  587. immediate paths in "dirs" and not recurse at all. This can be
  588. useful to speed up the AtimesRunner if you're building in a large
  589. tree and you don't care about all of the subdirectories.
  590. "ignoreprefix" prevents recursion into directories that start with
  591. prefix. It defaults to '.' to ignore svn directories.
  592. Change it to '_svn' if you use _svn hidden directories.
  593. "ignore" is a regular expression. Any dependency that contains a
  594. regex match is ignored and not put into the dependency list.
  595. Note that the regex may be VERBOSE (spaces are ignored and # line
  596. comments allowed -- use \ prefix to insert these characters)
  597. "hasher" is a function which returns a string which changes when
  598. the contents of its filename argument changes, or None on error.
  599. Default is md5_hasher, but can also be mtime_hasher.
  600. "depsname" is the name of the JSON dependency file to load/save.
  601. "quiet" set to True tells the builder to not display the commands being
  602. executed (or other non-error output).
  603. """
  604. if runner is not None:
  605. self.set_runner(runner)
  606. elif hasattr(self, 'runner'):
  607. # For backwards compatibility, if a derived class has
  608. # defined a "runner" method then use it:
  609. pass
  610. else:
  611. self.runner = SmartRunner(self)
  612. if dirs is None:
  613. dirs = ['.']
  614. self.dirs = dirs
  615. self.dirdepth = dirdepth
  616. self.ignoreprefix = ignoreprefix
  617. if ignore is None:
  618. ignore = r'$x^' # something that can't match
  619. self.ignore = re.compile(ignore, re.VERBOSE)
  620. self.depsname = depsname
  621. self.hasher = hasher
  622. self.quiet = quiet
  623. self.checking = False
  624. def echo(self, message):
  625. """ Print message, but only if builder is not in quiet mode. """
  626. if not self.quiet:
  627. print message
  628. def echo_command(self, command):
  629. """ Show a command being executed. """
  630. self.echo(command)
  631. def echo_delete(self, filename, error=None):
  632. """ Show a file being deleted. For subclassing Builder and overriding
  633. this function, the exception is passed in if an OSError occurs
  634. while deleting a file. """
  635. if error is None:
  636. self.echo('deleting %s' % filename)
  637. def run(self, *args):
  638. """ Run command given in args as per shell(), but only if its
  639. dependencies or outputs have changed or don't exist. """
  640. arglist = args_to_list(args)
  641. if not arglist:
  642. raise TypeError('run() takes at least 1 argument (0 given)')
  643. # we want a command line string for the .deps file key and for display
  644. command = subprocess.list2cmdline(arglist)
  645. if not self.cmdline_outofdate(command):
  646. return
  647. # if just checking up-to-date-ness, set flag and do nothing more
  648. self.outofdate_flag = True
  649. if self.checking:
  650. return
  651. # use runner to run command and collect dependencies
  652. self.echo_command(command)
  653. deps, outputs = self.runner(*arglist)
  654. if deps is not None or outputs is not None:
  655. deps_dict = {}
  656. # hash the dependency inputs and outputs
  657. for dep in deps:
  658. hashed = self.hasher(dep)
  659. if hashed is not None:
  660. deps_dict[dep] = "input-" + hashed
  661. for output in outputs:
  662. hashed = self.hasher(output)
  663. if hashed is not None:
  664. deps_dict[output] = "output-" + hashed
  665. self.deps[command] = deps_dict
  666. def memoize(self, command):
  667. """ Run the given command, but only if its dependencies have changed --
  668. like run(), but returns the status code instead of raising an
  669. exception on error. If "command" is a string (as per memoize.py)
  670. it's split into args using shlex.split() in a POSIX/bash style,
  671. otherwise it's a list of args as per run().
  672. This function is for compatiblity with memoize.py and is
  673. deprecated. Use run() instead. """
  674. if isinstance(command, basestring):
  675. args = shlex.split(command)
  676. else:
  677. args = args_to_list(command)
  678. try:
  679. self.run(args)
  680. return 0
  681. except ExecutionError, exc:
  682. message, data, status = exc
  683. return status
  684. def outofdate(self, func):
  685. """ Return True if given build function is out of date. """
  686. self.checking = True
  687. self.outofdate_flag = False
  688. func()
  689. self.checking = False
  690. return self.outofdate_flag
  691. def cmdline_outofdate(self, command):
  692. """ Return True if given command line is out of date. """
  693. if command in self.deps:
  694. # command has been run before, see if deps have changed
  695. for dep, oldhash in self.deps[command].items():
  696. assert oldhash.startswith('input-') or \
  697. oldhash.startswith('output-'), \
  698. "%s file corrupt, do a clean!" % self.depsname
  699. oldhash = oldhash.split('-', 1)[1]
  700. # make sure this dependency or output hasn't changed
  701. newhash = self.hasher(dep)
  702. if newhash is None or newhash != oldhash:
  703. break
  704. else:
  705. # all dependencies are unchanged
  706. return False
  707. # command has never been run, or one of the dependencies didn't
  708. # exist or had changed
  709. return True
  710. def autoclean(self):
  711. """ Automatically delete all outputs of this build as well as the .deps
  712. file. """
  713. # first build a list of all the outputs from the .deps file
  714. outputs = []
  715. for command, deps in self.deps.items():
  716. outputs.extend(dep for dep, hashed in deps.items()
  717. if hashed.startswith('output-'))
  718. outputs.append(self.depsname)
  719. self._deps = None
  720. for output in outputs:
  721. try:
  722. os.remove(output)
  723. except OSError, e:
  724. self.echo_delete(output, e)
  725. else:
  726. self.echo_delete(output)
  727. @property
  728. def deps(self):
  729. """ Lazy load .deps file so that instantiating a Builder is "safe". """
  730. if not hasattr(self, '_deps') or self._deps is None:
  731. self.read_deps()
  732. atexit.register(self.write_deps, depsname=os.path.abspath(self.depsname))
  733. return self._deps
  734. def read_deps(self):
  735. """ Read dependency JSON file into deps object. """
  736. try:
  737. f = open(self.depsname)
  738. try:
  739. self._deps = json.load(f)
  740. # make sure the version is correct
  741. if self._deps.get('.deps_version', 0) != deps_version:
  742. printerr('Bad %s dependency file version! Rebuilding.'
  743. % self.depsname)
  744. self._deps = {}
  745. self._deps.pop('.deps_version', None)
  746. finally:
  747. f.close()
  748. except IOError:
  749. self._deps = {}
  750. def write_deps(self, depsname=None):
  751. """ Write out deps object into JSON dependency file. """
  752. if self._deps is None:
  753. return # we've cleaned so nothing to save
  754. self.deps['.deps_version'] = deps_version
  755. if depsname is None:
  756. depsname = self.depsname
  757. f = open(depsname, 'w')
  758. try:
  759. json.dump(self.deps, f, indent=4, sort_keys=True)
  760. finally:
  761. f.close()
  762. self._deps.pop('.deps_version', None)
  763. _runner_map = {
  764. 'atimes_runner' : AtimesRunner,
  765. 'strace_runner' : StraceRunner,
  766. 'always_runner' : AlwaysRunner,
  767. 'smart_runner' : SmartRunner,
  768. }
  769. def set_runner(self, runner):
  770. """Set the runner for this builder. "runner" is either a Runner
  771. subclass (e.g. SmartRunner), or a string selecting one of the
  772. standard runners ("atimes_runner", "strace_runner",
  773. "always_runner", or "smart_runner")."""
  774. try:
  775. self.runner = self._runner_map[runner](self)
  776. except KeyError:
  777. if isinstance(runner, basestring):
  778. # For backwards compatibility, allow runner to be the
  779. # name of a method in a derived class:
  780. self.runner = getattr(self, runner)
  781. else:
  782. # pass builder to runner class to get a runner instance
  783. self.runner = runner(self)
  784. def _is_relevant(self, fullname):
  785. """ Return True if file is in the dependency search directories. """
  786. # need to abspath to compare rel paths with abs
  787. fullname = os.path.abspath(fullname)
  788. for path in self.dirs:
  789. path = os.path.abspath(path)
  790. if fullname.startswith(path):
  791. rest = fullname[len(path):]
  792. # files in dirs starting with ignoreprefix are not relevant
  793. if os.sep+self.ignoreprefix in os.sep+os.path.dirname(rest):
  794. continue
  795. # files deeper than dirdepth are not relevant
  796. if rest.count(os.sep) > self.dirdepth:
  797. continue
  798. return True
  799. return False
  800. # default Builder instance, used by helper run() and main() helper functions
  801. default_builder = Builder()
  802. default_command = 'build'
  803. def setup(builder=None, default=None, **kwargs):
  804. """ Setup the default Builder (or an instance of given builder if "builder"
  805. is not None) with the same keyword arguments as for Builder().
  806. "default" is the name of the default function to run when the build
  807. script is run with no command line arguments. """
  808. global default_builder, default_command
  809. if builder is not None:
  810. default_builder = builder()
  811. if default is not None:
  812. default_command = default
  813. default_builder.__init__(**kwargs)
  814. setup.__doc__ += '\n\n' + Builder.__init__.__doc__
  815. def run(*args):
  816. """ Run the given command, but only if its dependencies have changed. Uses
  817. the default Builder. """
  818. default_builder.run(*args)
  819. def autoclean():
  820. """ Automatically delete all outputs of the default build. """
  821. default_builder.autoclean()
  822. def memoize(command):
  823. return default_builder.memoize(command)
  824. memoize.__doc__ = Builder.memoize.__doc__
  825. def outofdate(command):
  826. """ Return True if given command is out of date and needs to be run. """
  827. return default_builder.outofdate(command)
  828. def parse_options(usage, extra_options=None):
  829. """ Parse command line options and return (parser, options, args). """
  830. parser = optparse.OptionParser(usage='Usage: %prog '+usage,
  831. version='%prog '+__version__)
  832. parser.disable_interspersed_args()
  833. parser.add_option('-t', '--time', action='store_true',
  834. help='use file modification times instead of MD5 sums')
  835. parser.add_option('-d', '--dir', action='append',
  836. help='add DIR to list of relevant directories')
  837. parser.add_option('-c', '--clean', action='store_true',
  838. help='autoclean build outputs before running')
  839. parser.add_option('-q', '--quiet', action='store_true',
  840. help="don't echo commands, only print errors")
  841. parser.add_option('-k', '--keep', action='store_true',
  842. help='keep temporary strace output files')
  843. if extra_options:
  844. # add any user-specified options passed in via main()
  845. for option in extra_options:
  846. parser.add_option(option)
  847. options, args = parser.parse_args()
  848. default_builder.quiet = options.quiet
  849. if options.time:
  850. default_builder.hasher = mtime_hasher
  851. if options.dir:
  852. default_builder.dirs += options.dir
  853. if options.clean:
  854. default_builder.autoclean()
  855. if options.keep:
  856. StraceRunner.keep_temps = options.keep
  857. return parser, options, args
  858. def fabricate_version(min=None, max=None):
  859. """ If min is given, assert that the running fabricate is at least that
  860. version or exit with an error message. If max is given, assert that
  861. the running fabricate is at most that version. Return the current
  862. fabricate version string. This function was introduced in v1.14;
  863. for prior versions, the version string is available only as module
  864. local string fabricate.__version__ """
  865. if min is not None and float(__version__) < min:
  866. sys.stderr.write(("fabricate is version %s. This build script "
  867. "requires at least version %.2f") % (__version__, min))
  868. sys.exit()
  869. if max is not None and float(__version__) > max:
  870. sys.stderr.write(("fabricate is version %s. This build script "
  871. "requires at most version %.2f") % (__version__, max))
  872. sys.exit()
  873. return __version__
  874. def main(globals_dict=None, build_dir=None, extra_options=None):
  875. """ Run the default function or the function(s) named in the command line
  876. arguments. Call this at the end of your build script. If one of the
  877. functions returns nonzero, main will exit with the last nonzero return
  878. value as its status code.
  879. extra_options is an optional list of options created with
  880. optparse.make_option(). The pseudo-global variable main.options
  881. is set to the parsed options list.
  882. """
  883. usage = '[options] build script functions to run'
  884. parser, options, actions = parse_options(usage, extra_options)
  885. main.options = options
  886. if not actions:
  887. actions = [default_command]
  888. original_path = os.getcwd()
  889. if None in [globals_dict, build_dir]:
  890. try:
  891. frame = sys._getframe(1)
  892. except:
  893. printerr("Your Python version doesn't support sys._getframe(1),")
  894. printerr("call main(globals(), build_dir) explicitly")
  895. sys.exit(1)
  896. if globals_dict is None:
  897. globals_dict = frame.f_globals
  898. if build_dir is None:
  899. build_file = frame.f_globals.get('__file__', None)
  900. if build_file:
  901. build_dir = os.path.dirname(build_file)
  902. if build_dir:
  903. if not options.quiet and os.path.abspath(build_dir) != original_path:
  904. print "Entering directory '%s'" % build_dir
  905. os.chdir(build_dir)
  906. status = 0
  907. try:
  908. for action in actions:
  909. if '(' not in action:
  910. action = action.strip() + '()'
  911. name = action.split('(')[0].split('.')[0]
  912. if name in globals_dict:
  913. this_status = eval(action, globals_dict)
  914. if this_status:
  915. status = int(this_status)
  916. else:
  917. printerr('%r command not defined!' % action)
  918. sys.exit(1)
  919. except ExecutionError, exc:
  920. message, data, status = exc
  921. printerr('fabricate: ' + message)
  922. finally:
  923. if not options.quiet and os.path.abspath(build_dir) != original_path:
  924. print "Leaving directory '%s' back to '%s'" % (build_dir, original_path)
  925. os.chdir(original_path)
  926. sys.exit(status)
  927. if __name__ == '__main__':
  928. # if called as a script, emulate memoize.py -- run() command line
  929. parser, options, args = parse_options('[options] command line to run')
  930. status = 0
  931. if args:
  932. status = memoize(args)
  933. elif not options.clean:
  934. parser.print_help()
  935. status = 1
  936. # autoclean may have been used
  937. sys.exit(status)