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
|
#!/usr/bin/env python3
# encoding: utf-8
# Thomas Nagy, 2015 (ita)
"""
Execute tasks through strace to obtain dependencies after the process is run. This
scheme is similar to that of the Fabricate script.
To use::
def configure(conf):
conf.load('strace')
WARNING:
* This will not work when advanced scanners are needed (qt4/qt5)
* The overhead of running 'strace' is significant (56s -> 1m29s)
* It will not work on Windows :-)
"""
import os, re, threading
from waflib import Task, Logs, Utils
#TRACECALLS = 'trace=access,chdir,clone,creat,execve,exit_group,fork,lstat,lstat64,mkdir,open,rename,stat,stat64,symlink,vfork'
TRACECALLS = 'trace=process,file'
BANNED = ('/tmp', '/proc', '/sys', '/dev')
s_process = r'(?:clone|fork|vfork)\(.*?(?P<npid>\d+)'
s_file = r'(?P<call>\w+)\("(?P<path>([^"\\]|\\.)*)"(.*)'
re_lines = re.compile(r'^(?P<pid>\d+)\s+(?:(?:%s)|(?:%s))\r*$' % (s_file, s_process), re.IGNORECASE | re.MULTILINE)
strace_lock = threading.Lock()
def configure(conf):
conf.find_program('strace')
def task_method(func):
# Decorator function to bind/replace methods on the base Task class
#
# The methods Task.exec_command and Task.sig_implicit_deps already exists and are rarely overridden
# we thus expect that we are the only ones doing this
try:
setattr(Task.Task, 'nostrace_%s' % func.__name__, getattr(Task.Task, func.__name__))
except AttributeError:
pass
setattr(Task.Task, func.__name__, func)
return func
@task_method
def get_strace_file(self):
try:
return self.strace_file
except AttributeError:
pass
if self.outputs:
ret = self.outputs[0].abspath() + '.strace'
else:
ret = '%s%s%d%s' % (self.generator.bld.bldnode.abspath(), os.sep, id(self), '.strace')
self.strace_file = ret
return ret
@task_method
def get_strace_args(self):
return (self.env.STRACE or ['strace']) + ['-e', TRACECALLS, '-f', '-o', self.get_strace_file()]
@task_method
def exec_command(self, cmd, **kw):
bld = self.generator.bld
if not 'cwd' in kw:
kw['cwd'] = self.get_cwd()
args = self.get_strace_args()
fname = self.get_strace_file()
if isinstance(cmd, list):
cmd = args + cmd
else:
cmd = '%s %s' % (' '.join(args), cmd)
try:
ret = bld.exec_command(cmd, **kw)
finally:
if not ret:
self.parse_strace_deps(fname, kw['cwd'])
return ret
@task_method
def sig_implicit_deps(self):
# bypass the scanner functions
return
@task_method
def parse_strace_deps(self, path, cwd):
# uncomment the following line to disable the dependencies and force a file scan
# return
try:
cnt = Utils.readf(path)
finally:
try:
os.remove(path)
except OSError:
pass
if not isinstance(cwd, str):
cwd = cwd.abspath()
nodes = []
bld = self.generator.bld
try:
cache = bld.strace_cache
except AttributeError:
cache = bld.strace_cache = {}
# chdir and relative paths
pid_to_cwd = {}
global BANNED
done = set()
for m in re.finditer(re_lines, cnt):
# scraping the output of strace
pid = m.group('pid')
if m.group('npid'):
npid = m.group('npid')
pid_to_cwd[npid] = pid_to_cwd.get(pid, cwd)
continue
p = m.group('path').replace('\\"', '"')
if p == '.' or m.group().find('= -1 ENOENT') > -1:
# just to speed it up a bit
continue
if not os.path.isabs(p):
p = os.path.join(pid_to_cwd.get(pid, cwd), p)
call = m.group('call')
if call == 'chdir':
pid_to_cwd[pid] = p
continue
if p in done:
continue
done.add(p)
for x in BANNED:
if p.startswith(x):
break
else:
if p.endswith('/') or os.path.isdir(p):
continue
try:
node = cache[p]
except KeyError:
strace_lock.acquire()
try:
cache[p] = node = bld.root.find_node(p)
if not node:
continue
finally:
strace_lock.release()
nodes.append(node)
# record the dependencies then force the task signature recalculation for next time
if Logs.verbose:
Logs.debug('deps: real scanner for %r returned %r', self, nodes)
bld = self.generator.bld
bld.node_deps[self.uid()] = nodes
bld.raw_deps[self.uid()] = []
try:
del self.cache_sig
except AttributeError:
pass
self.signature()
|