1# -*- coding: utf-8 -*- 2# The LLVM Compiler Infrastructure 3# 4# This file is distributed under the University of Illinois Open Source 5# License. See LICENSE.TXT for details. 6""" This module is responsible to run the analyzer commands. """ 7 8import re 9import os 10import os.path 11import tempfile 12import functools 13import subprocess 14import logging 15from libscanbuild.compilation import classify_source, compiler_language 16from libscanbuild.clang import get_version, get_arguments 17from libscanbuild.shell import decode 18 19__all__ = ['run'] 20 21# To have good results from static analyzer certain compiler options shall be 22# omitted. The compiler flag filtering only affects the static analyzer run. 23# 24# Keys are the option name, value number of options to skip 25IGNORED_FLAGS = { 26 '-c': 0, # compile option will be overwritten 27 '-fsyntax-only': 0, # static analyzer option will be overwritten 28 '-o': 1, # will set up own output file 29 # flags below are inherited from the perl implementation. 30 '-g': 0, 31 '-save-temps': 0, 32 '-install_name': 1, 33 '-exported_symbols_list': 1, 34 '-current_version': 1, 35 '-compatibility_version': 1, 36 '-init': 1, 37 '-e': 1, 38 '-seg1addr': 1, 39 '-bundle_loader': 1, 40 '-multiply_defined': 1, 41 '-sectorder': 3, 42 '--param': 1, 43 '--serialize-diagnostics': 1 44} 45 46 47def require(required): 48 """ Decorator for checking the required values in state. 49 50 It checks the required attributes in the passed state and stop when 51 any of those is missing. """ 52 53 def decorator(function): 54 @functools.wraps(function) 55 def wrapper(*args, **kwargs): 56 for key in required: 57 if key not in args[0]: 58 raise KeyError('{0} not passed to {1}'.format( 59 key, function.__name__)) 60 61 return function(*args, **kwargs) 62 63 return wrapper 64 65 return decorator 66 67 68@require(['command', # entry from compilation database 69 'directory', # entry from compilation database 70 'file', # entry from compilation database 71 'clang', # clang executable name (and path) 72 'direct_args', # arguments from command line 73 'force_debug', # kill non debug macros 74 'output_dir', # where generated report files shall go 75 'output_format', # it's 'plist' or 'html' or both 76 'output_failures']) # generate crash reports or not 77def run(opts): 78 """ Entry point to run (or not) static analyzer against a single entry 79 of the compilation database. 80 81 This complex task is decomposed into smaller methods which are calling 82 each other in chain. If the analyzis is not possibe the given method 83 just return and break the chain. 84 85 The passed parameter is a python dictionary. Each method first check 86 that the needed parameters received. (This is done by the 'require' 87 decorator. It's like an 'assert' to check the contract between the 88 caller and the called method.) """ 89 90 try: 91 command = opts.pop('command') 92 command = command if isinstance(command, list) else decode(command) 93 logging.debug("Run analyzer against '%s'", command) 94 opts.update(classify_parameters(command)) 95 96 return arch_check(opts) 97 except Exception: 98 logging.error("Problem occured during analyzis.", exc_info=1) 99 return None 100 101 102@require(['clang', 'directory', 'flags', 'file', 'output_dir', 'language', 103 'error_type', 'error_output', 'exit_code']) 104def report_failure(opts): 105 """ Create report when analyzer failed. 106 107 The major report is the preprocessor output. The output filename generated 108 randomly. The compiler output also captured into '.stderr.txt' file. 109 And some more execution context also saved into '.info.txt' file. """ 110 111 def extension(opts): 112 """ Generate preprocessor file extension. """ 113 114 mapping = {'objective-c++': '.mii', 'objective-c': '.mi', 'c++': '.ii'} 115 return mapping.get(opts['language'], '.i') 116 117 def destination(opts): 118 """ Creates failures directory if not exits yet. """ 119 120 name = os.path.join(opts['output_dir'], 'failures') 121 if not os.path.isdir(name): 122 os.makedirs(name) 123 return name 124 125 error = opts['error_type'] 126 (handle, name) = tempfile.mkstemp(suffix=extension(opts), 127 prefix='clang_' + error + '_', 128 dir=destination(opts)) 129 os.close(handle) 130 cwd = opts['directory'] 131 cmd = get_arguments([opts['clang'], '-fsyntax-only', '-E'] + 132 opts['flags'] + [opts['file'], '-o', name], cwd) 133 logging.debug('exec command in %s: %s', cwd, ' '.join(cmd)) 134 subprocess.call(cmd, cwd=cwd) 135 # write general information about the crash 136 with open(name + '.info.txt', 'w') as handle: 137 handle.write(opts['file'] + os.linesep) 138 handle.write(error.title().replace('_', ' ') + os.linesep) 139 handle.write(' '.join(cmd) + os.linesep) 140 handle.write(' '.join(os.uname()) + os.linesep) 141 handle.write(get_version(opts['clang'])) 142 handle.close() 143 # write the captured output too 144 with open(name + '.stderr.txt', 'w') as handle: 145 handle.writelines(opts['error_output']) 146 handle.close() 147 # return with the previous step exit code and output 148 return { 149 'error_output': opts['error_output'], 150 'exit_code': opts['exit_code'] 151 } 152 153 154@require(['clang', 'directory', 'flags', 'direct_args', 'file', 'output_dir', 155 'output_format']) 156def run_analyzer(opts, continuation=report_failure): 157 """ It assembles the analysis command line and executes it. Capture the 158 output of the analysis and returns with it. If failure reports are 159 requested, it calls the continuation to generate it. """ 160 161 def output(): 162 """ Creates output file name for reports. """ 163 if opts['output_format'] in {'plist', 'plist-html'}: 164 (handle, name) = tempfile.mkstemp(prefix='report-', 165 suffix='.plist', 166 dir=opts['output_dir']) 167 os.close(handle) 168 return name 169 return opts['output_dir'] 170 171 cwd = opts['directory'] 172 cmd = get_arguments([opts['clang'], '--analyze'] + opts['direct_args'] + 173 opts['flags'] + [opts['file'], '-o', output()], 174 cwd) 175 logging.debug('exec command in %s: %s', cwd, ' '.join(cmd)) 176 child = subprocess.Popen(cmd, 177 cwd=cwd, 178 universal_newlines=True, 179 stdout=subprocess.PIPE, 180 stderr=subprocess.STDOUT) 181 output = child.stdout.readlines() 182 child.stdout.close() 183 # do report details if it were asked 184 child.wait() 185 if opts.get('output_failures', False) and child.returncode: 186 error_type = 'crash' if child.returncode & 127 else 'other_error' 187 opts.update({ 188 'error_type': error_type, 189 'error_output': output, 190 'exit_code': child.returncode 191 }) 192 return continuation(opts) 193 # return the output for logging and exit code for testing 194 return {'error_output': output, 'exit_code': child.returncode} 195 196 197@require(['flags', 'force_debug']) 198def filter_debug_flags(opts, continuation=run_analyzer): 199 """ Filter out nondebug macros when requested. """ 200 201 if opts.pop('force_debug'): 202 # lazy implementation just append an undefine macro at the end 203 opts.update({'flags': opts['flags'] + ['-UNDEBUG']}) 204 205 return continuation(opts) 206 207 208@require(['file', 'directory']) 209def set_file_path_relative(opts, continuation=filter_debug_flags): 210 """ Set source file path to relative to the working directory. 211 212 The only purpose of this function is to pass the SATestBuild.py tests. """ 213 214 opts.update({'file': os.path.relpath(opts['file'], opts['directory'])}) 215 216 return continuation(opts) 217 218 219@require(['language', 'compiler', 'file', 'flags']) 220def language_check(opts, continuation=set_file_path_relative): 221 """ Find out the language from command line parameters or file name 222 extension. The decision also influenced by the compiler invocation. """ 223 224 accepted = frozenset({ 225 'c', 'c++', 'objective-c', 'objective-c++', 'c-cpp-output', 226 'c++-cpp-output', 'objective-c-cpp-output' 227 }) 228 229 # language can be given as a parameter... 230 language = opts.pop('language') 231 compiler = opts.pop('compiler') 232 # ... or find out from source file extension 233 if language is None and compiler is not None: 234 language = classify_source(opts['file'], compiler == 'c') 235 236 if language is None: 237 logging.debug('skip analysis, language not known') 238 return None 239 elif language not in accepted: 240 logging.debug('skip analysis, language not supported') 241 return None 242 else: 243 logging.debug('analysis, language: %s', language) 244 opts.update({'language': language, 245 'flags': ['-x', language] + opts['flags']}) 246 return continuation(opts) 247 248 249@require(['arch_list', 'flags']) 250def arch_check(opts, continuation=language_check): 251 """ Do run analyzer through one of the given architectures. """ 252 253 disabled = frozenset({'ppc', 'ppc64'}) 254 255 received_list = opts.pop('arch_list') 256 if received_list: 257 # filter out disabled architectures and -arch switches 258 filtered_list = [a for a in received_list if a not in disabled] 259 if filtered_list: 260 # There should be only one arch given (or the same multiple 261 # times). If there are multiple arch are given and are not 262 # the same, those should not change the pre-processing step. 263 # But that's the only pass we have before run the analyzer. 264 current = filtered_list.pop() 265 logging.debug('analysis, on arch: %s', current) 266 267 opts.update({'flags': ['-arch', current] + opts['flags']}) 268 return continuation(opts) 269 else: 270 logging.debug('skip analysis, found not supported arch') 271 return None 272 else: 273 logging.debug('analysis, on default arch') 274 return continuation(opts) 275 276 277def classify_parameters(command): 278 """ Prepare compiler flags (filters some and add others) and take out 279 language (-x) and architecture (-arch) flags for future processing. """ 280 281 result = { 282 'flags': [], # the filtered compiler flags 283 'arch_list': [], # list of architecture flags 284 'language': None, # compilation language, None, if not specified 285 'compiler': compiler_language(command) # 'c' or 'c++' 286 } 287 288 # iterate on the compile options 289 args = iter(command[1:]) 290 for arg in args: 291 # take arch flags into a separate basket 292 if arg == '-arch': 293 result['arch_list'].append(next(args)) 294 # take language 295 elif arg == '-x': 296 result['language'] = next(args) 297 # parameters which looks source file are not flags 298 elif re.match(r'^[^-].+', arg) and classify_source(arg): 299 pass 300 # ignore some flags 301 elif arg in IGNORED_FLAGS: 302 count = IGNORED_FLAGS[arg] 303 for _ in range(count): 304 next(args) 305 # we don't care about extra warnings, but we should suppress ones 306 # that we don't want to see. 307 elif re.match(r'^-W.+', arg) and not re.match(r'^-Wno-.+', arg): 308 pass 309 # and consider everything else as compilation flag. 310 else: 311 result['flags'].append(arg) 312 313 return result 314