1616import os .path
1717import json
1818import logging
19- import tempfile
2019import multiprocessing
20+ import tempfile
21+ import functools
22+ import subprocess
2123import contextlib
2224import datetime
25+
2326from libscanbuild import command_entry_point , compiler_wrapper , \
24- wrapper_environment , run_build
27+ wrapper_environment , run_build , run_command
2528from libscanbuild .arguments import parse_args_for_scan_build , \
2629 parse_args_for_analyze_build
27- from libscanbuild .runner import run
2830from libscanbuild .intercept import capture
2931from libscanbuild .report import document
30- from libscanbuild .compilation import split_command
32+ from libscanbuild .compilation import split_command , classify_source , \
33+ compiler_language
34+ from libscanbuild .clang import get_version , get_arguments
35+ from libscanbuild .shell import decode
3136
3237__all__ = ['scan_build' , 'analyze_build' , 'analyze_compiler_wrapper' ]
3338
@@ -51,7 +56,7 @@ def scan_build():
5156 exit_code = capture (args )
5257 # Run the analyzer against the captured commands.
5358 if need_analyzer (args .build ):
54- run_analyzer (args )
59+ run_analyzer_parallel (args )
5560 else :
5661 # Run build command and analyzer with compiler wrappers.
5762 environment = setup_environment (args )
@@ -70,7 +75,7 @@ def analyze_build():
7075 # will re-assign the report directory as new output
7176 with report_directory (args .output , args .keep_empty ) as args .output :
7277 # Run the analyzer against a compilation db.
73- run_analyzer (args )
78+ run_analyzer_parallel (args )
7479 # Cover report generation and bug counting.
7580 number_of_bugs = document (args )
7681 # Set exit status as it was requested.
@@ -90,7 +95,7 @@ def need_analyzer(args):
9095 return len (args ) and not re .search ('configure|autogen' , args [0 ])
9196
9297
93- def run_analyzer (args ):
98+ def run_analyzer_parallel (args ):
9499 """ Runs the analyzer against the given compilation database. """
95100
96101 def exclude (filename ):
@@ -259,3 +264,277 @@ def prefix_with(constant, pieces):
259264 result .append ('-analyzer-viz-egraph-ubigraph' )
260265
261266 return prefix_with ('-Xclang' , result )
267+
268+
269+ def require (required ):
270+ """ Decorator for checking the required values in state.
271+
272+ It checks the required attributes in the passed state and stop when
273+ any of those is missing. """
274+
275+ def decorator (function ):
276+ @functools .wraps (function )
277+ def wrapper (* args , ** kwargs ):
278+ for key in required :
279+ if key not in args [0 ]:
280+ raise KeyError ('{0} not passed to {1}' .format (
281+ key , function .__name__ ))
282+
283+ return function (* args , ** kwargs )
284+
285+ return wrapper
286+
287+ return decorator
288+
289+
290+ @require (['command' , # entry from compilation database
291+ 'directory' , # entry from compilation database
292+ 'file' , # entry from compilation database
293+ 'clang' , # clang executable name (and path)
294+ 'direct_args' , # arguments from command line
295+ 'force_debug' , # kill non debug macros
296+ 'output_dir' , # where generated report files shall go
297+ 'output_format' , # it's 'plist' or 'html' or both
298+ 'output_failures' ]) # generate crash reports or not
299+ def run (opts ):
300+ """ Entry point to run (or not) static analyzer against a single entry
301+ of the compilation database.
302+
303+ This complex task is decomposed into smaller methods which are calling
304+ each other in chain. If the analyzis is not possibe the given method
305+ just return and break the chain.
306+
307+ The passed parameter is a python dictionary. Each method first check
308+ that the needed parameters received. (This is done by the 'require'
309+ decorator. It's like an 'assert' to check the contract between the
310+ caller and the called method.) """
311+
312+ try :
313+ command = opts .pop ('command' )
314+ command = command if isinstance (command , list ) else decode (command )
315+ logging .debug ("Run analyzer against '%s'" , command )
316+ opts .update (classify_parameters (command ))
317+
318+ return arch_check (opts )
319+ except Exception :
320+ logging .error ("Problem occured during analyzis." , exc_info = 1 )
321+ return None
322+
323+
324+ @require (['clang' , 'directory' , 'flags' , 'file' , 'output_dir' , 'language' ,
325+ 'error_output' , 'exit_code' ])
326+ def report_failure (opts ):
327+ """ Create report when analyzer failed.
328+
329+ The major report is the preprocessor output. The output filename generated
330+ randomly. The compiler output also captured into '.stderr.txt' file.
331+ And some more execution context also saved into '.info.txt' file. """
332+
333+ def extension ():
334+ """ Generate preprocessor file extension. """
335+
336+ mapping = {'objective-c++' : '.mii' , 'objective-c' : '.mi' , 'c++' : '.ii' }
337+ return mapping .get (opts ['language' ], '.i' )
338+
339+ def destination ():
340+ """ Creates failures directory if not exits yet. """
341+
342+ failures_dir = os .path .join (opts ['output_dir' ], 'failures' )
343+ if not os .path .isdir (failures_dir ):
344+ os .makedirs (failures_dir )
345+ return failures_dir
346+
347+ # Classify error type: when Clang terminated by a signal it's a 'Crash'.
348+ # (python subprocess Popen.returncode is negative when child terminated
349+ # by signal.) Everything else is 'Other Error'.
350+ error = 'crash' if opts ['exit_code' ] < 0 else 'other_error'
351+ # Create preprocessor output file name. (This is blindly following the
352+ # Perl implementation.)
353+ (handle , name ) = tempfile .mkstemp (suffix = extension (),
354+ prefix = 'clang_' + error + '_' ,
355+ dir = destination ())
356+ os .close (handle )
357+ # Execute Clang again, but run the syntax check only.
358+ cwd = opts ['directory' ]
359+ cmd = get_arguments (
360+ [opts ['clang' ], '-fsyntax-only' , '-E'
361+ ] + opts ['flags' ] + [opts ['file' ], '-o' , name ], cwd )
362+ run_command (cmd , cwd = cwd )
363+ # write general information about the crash
364+ with open (name + '.info.txt' , 'w' ) as handle :
365+ handle .write (opts ['file' ] + os .linesep )
366+ handle .write (error .title ().replace ('_' , ' ' ) + os .linesep )
367+ handle .write (' ' .join (cmd ) + os .linesep )
368+ handle .write (' ' .join (os .uname ()) + os .linesep )
369+ handle .write (get_version (opts ['clang' ]))
370+ handle .close ()
371+ # write the captured output too
372+ with open (name + '.stderr.txt' , 'w' ) as handle :
373+ handle .writelines (opts ['error_output' ])
374+ handle .close ()
375+
376+
377+ @require (['clang' , 'directory' , 'flags' , 'direct_args' , 'file' , 'output_dir' ,
378+ 'output_format' ])
379+ def run_analyzer (opts , continuation = report_failure ):
380+ """ It assembles the analysis command line and executes it. Capture the
381+ output of the analysis and returns with it. If failure reports are
382+ requested, it calls the continuation to generate it. """
383+
384+ def target ():
385+ """ Creates output file name for reports. """
386+ if opts ['output_format' ] in {'plist' , 'plist-html' }:
387+ (handle , name ) = tempfile .mkstemp (prefix = 'report-' ,
388+ suffix = '.plist' ,
389+ dir = opts ['output_dir' ])
390+ os .close (handle )
391+ return name
392+ return opts ['output_dir' ]
393+
394+ try :
395+ cwd = opts ['directory' ]
396+ cmd = get_arguments ([opts ['clang' ], '--analyze' ] +
397+ opts ['direct_args' ] + opts ['flags' ] +
398+ [opts ['file' ], '-o' , target ()],
399+ cwd )
400+ output = run_command (cmd , cwd = cwd )
401+ return {'error_output' : output , 'exit_code' : 0 }
402+ except subprocess .CalledProcessError as ex :
403+ result = {'error_output' : ex .output , 'exit_code' : ex .returncode }
404+ if opts .get ('output_failures' , False ):
405+ opts .update (result )
406+ continuation (opts )
407+ return result
408+
409+
410+ @require (['flags' , 'force_debug' ])
411+ def filter_debug_flags (opts , continuation = run_analyzer ):
412+ """ Filter out nondebug macros when requested. """
413+
414+ if opts .pop ('force_debug' ):
415+ # lazy implementation just append an undefine macro at the end
416+ opts .update ({'flags' : opts ['flags' ] + ['-UNDEBUG' ]})
417+
418+ return continuation (opts )
419+
420+
421+ @require (['language' , 'compiler' , 'file' , 'flags' ])
422+ def language_check (opts , continuation = filter_debug_flags ):
423+ """ Find out the language from command line parameters or file name
424+ extension. The decision also influenced by the compiler invocation. """
425+
426+ accepted = frozenset ({
427+ 'c' , 'c++' , 'objective-c' , 'objective-c++' , 'c-cpp-output' ,
428+ 'c++-cpp-output' , 'objective-c-cpp-output'
429+ })
430+
431+ # language can be given as a parameter...
432+ language = opts .pop ('language' )
433+ compiler = opts .pop ('compiler' )
434+ # ... or find out from source file extension
435+ if language is None and compiler is not None :
436+ language = classify_source (opts ['file' ], compiler == 'c' )
437+
438+ if language is None :
439+ logging .debug ('skip analysis, language not known' )
440+ return None
441+ elif language not in accepted :
442+ logging .debug ('skip analysis, language not supported' )
443+ return None
444+ else :
445+ logging .debug ('analysis, language: %s' , language )
446+ opts .update ({'language' : language ,
447+ 'flags' : ['-x' , language ] + opts ['flags' ]})
448+ return continuation (opts )
449+
450+
451+ @require (['arch_list' , 'flags' ])
452+ def arch_check (opts , continuation = language_check ):
453+ """ Do run analyzer through one of the given architectures. """
454+
455+ disabled = frozenset ({'ppc' , 'ppc64' })
456+
457+ received_list = opts .pop ('arch_list' )
458+ if received_list :
459+ # filter out disabled architectures and -arch switches
460+ filtered_list = [a for a in received_list if a not in disabled ]
461+ if filtered_list :
462+ # There should be only one arch given (or the same multiple
463+ # times). If there are multiple arch are given and are not
464+ # the same, those should not change the pre-processing step.
465+ # But that's the only pass we have before run the analyzer.
466+ current = filtered_list .pop ()
467+ logging .debug ('analysis, on arch: %s' , current )
468+
469+ opts .update ({'flags' : ['-arch' , current ] + opts ['flags' ]})
470+ return continuation (opts )
471+ else :
472+ logging .debug ('skip analysis, found not supported arch' )
473+ return None
474+ else :
475+ logging .debug ('analysis, on default arch' )
476+ return continuation (opts )
477+
478+ # To have good results from static analyzer certain compiler options shall be
479+ # omitted. The compiler flag filtering only affects the static analyzer run.
480+ #
481+ # Keys are the option name, value number of options to skip
482+ IGNORED_FLAGS = {
483+ '-c' : 0 , # compile option will be overwritten
484+ '-fsyntax-only' : 0 , # static analyzer option will be overwritten
485+ '-o' : 1 , # will set up own output file
486+ # flags below are inherited from the perl implementation.
487+ '-g' : 0 ,
488+ '-save-temps' : 0 ,
489+ '-install_name' : 1 ,
490+ '-exported_symbols_list' : 1 ,
491+ '-current_version' : 1 ,
492+ '-compatibility_version' : 1 ,
493+ '-init' : 1 ,
494+ '-e' : 1 ,
495+ '-seg1addr' : 1 ,
496+ '-bundle_loader' : 1 ,
497+ '-multiply_defined' : 1 ,
498+ '-sectorder' : 3 ,
499+ '--param' : 1 ,
500+ '--serialize-diagnostics' : 1
501+ }
502+
503+
504+ def classify_parameters (command ):
505+ """ Prepare compiler flags (filters some and add others) and take out
506+ language (-x) and architecture (-arch) flags for future processing. """
507+
508+ result = {
509+ 'flags' : [], # the filtered compiler flags
510+ 'arch_list' : [], # list of architecture flags
511+ 'language' : None , # compilation language, None, if not specified
512+ 'compiler' : compiler_language (command ) # 'c' or 'c++'
513+ }
514+
515+ # iterate on the compile options
516+ args = iter (command [1 :])
517+ for arg in args :
518+ # take arch flags into a separate basket
519+ if arg == '-arch' :
520+ result ['arch_list' ].append (next (args ))
521+ # take language
522+ elif arg == '-x' :
523+ result ['language' ] = next (args )
524+ # parameters which looks source file are not flags
525+ elif re .match (r'^[^-].+' , arg ) and classify_source (arg ):
526+ pass
527+ # ignore some flags
528+ elif arg in IGNORED_FLAGS :
529+ count = IGNORED_FLAGS [arg ]
530+ for _ in range (count ):
531+ next (args )
532+ # we don't care about extra warnings, but we should suppress ones
533+ # that we don't want to see.
534+ elif re .match (r'^-W.+' , arg ) and not re .match (r'^-Wno-.+' , arg ):
535+ pass
536+ # and consider everything else as compilation flag.
537+ else :
538+ result ['flags' ].append (arg )
539+
540+ return result
0 commit comments