#!/usr/bin/env python # 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()