Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 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 implements the 'scan-build' command API. |
| 7 | |
| 8 | To run the static analyzer against a build is done in multiple steps: |
| 9 | |
| 10 | -- Intercept: capture the compilation command during the build, |
| 11 | -- Analyze: run the analyzer against the captured commands, |
| 12 | -- Report: create a cover report from the analyzer outputs. """ |
| 13 | |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 14 | import re |
| 15 | import os |
| 16 | import os.path |
| 17 | import json |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 18 | import logging |
Laszlo Nagy | 258ff25 | 2017-02-14 10:43:38 +0000 | [diff] [blame] | 19 | import tempfile |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 20 | import multiprocessing |
Laszlo Nagy | 258ff25 | 2017-02-14 10:43:38 +0000 | [diff] [blame] | 21 | import contextlib |
| 22 | import datetime |
Laszlo Nagy | 2e9c922 | 2017-03-04 01:08:05 +0000 | [diff] [blame] | 23 | from libscanbuild import command_entry_point, compiler_wrapper, \ |
Laszlo Nagy | 5270bb9 | 2017-03-08 21:18:51 +0000 | [diff] [blame] | 24 | wrapper_environment, run_build |
| 25 | from libscanbuild.arguments import parse_args_for_scan_build, \ |
| 26 | parse_args_for_analyze_build |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 27 | from libscanbuild.runner import run |
| 28 | from libscanbuild.intercept import capture |
Laszlo Nagy | 258ff25 | 2017-02-14 10:43:38 +0000 | [diff] [blame] | 29 | from libscanbuild.report import document |
Laszlo Nagy | 8bd63e5 | 2016-04-19 12:03:03 +0000 | [diff] [blame] | 30 | from libscanbuild.compilation import split_command |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 31 | |
Laszlo Nagy | 5270bb9 | 2017-03-08 21:18:51 +0000 | [diff] [blame] | 32 | __all__ = ['scan_build', 'analyze_build', 'analyze_compiler_wrapper'] |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 33 | |
| 34 | COMPILER_WRAPPER_CC = 'analyze-cc' |
| 35 | COMPILER_WRAPPER_CXX = 'analyze-c++' |
| 36 | |
| 37 | |
| 38 | @command_entry_point |
Laszlo Nagy | 5270bb9 | 2017-03-08 21:18:51 +0000 | [diff] [blame] | 39 | def scan_build(): |
| 40 | """ Entry point for scan-build command. """ |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 41 | |
Laszlo Nagy | 5270bb9 | 2017-03-08 21:18:51 +0000 | [diff] [blame] | 42 | args = parse_args_for_scan_build() |
Laszlo Nagy | 57db7c6 | 2017-03-21 10:15:18 +0000 | [diff] [blame^] | 43 | # will re-assign the report directory as new output |
| 44 | with report_directory(args.output, args.keep_empty) as args.output: |
Laszlo Nagy | 5270bb9 | 2017-03-08 21:18:51 +0000 | [diff] [blame] | 45 | # Run against a build command. there are cases, when analyzer run |
| 46 | # is not required. But we need to set up everything for the |
| 47 | # wrappers, because 'configure' needs to capture the CC/CXX values |
| 48 | # for the Makefile. |
| 49 | if args.intercept_first: |
| 50 | # Run build command with intercept module. |
| 51 | exit_code = capture(args) |
| 52 | # Run the analyzer against the captured commands. |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 53 | if need_analyzer(args.build): |
Laszlo Nagy | 57db7c6 | 2017-03-21 10:15:18 +0000 | [diff] [blame^] | 54 | run_analyzer(args) |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 55 | else: |
Laszlo Nagy | 5270bb9 | 2017-03-08 21:18:51 +0000 | [diff] [blame] | 56 | # Run build command and analyzer with compiler wrappers. |
Laszlo Nagy | 57db7c6 | 2017-03-21 10:15:18 +0000 | [diff] [blame^] | 57 | environment = setup_environment(args) |
Laszlo Nagy | 52c1d7e | 2017-02-14 10:30:50 +0000 | [diff] [blame] | 58 | exit_code = run_build(args.build, env=environment) |
Laszlo Nagy | 5270bb9 | 2017-03-08 21:18:51 +0000 | [diff] [blame] | 59 | # Cover report generation and bug counting. |
Laszlo Nagy | 57db7c6 | 2017-03-21 10:15:18 +0000 | [diff] [blame^] | 60 | number_of_bugs = document(args) |
Laszlo Nagy | 5270bb9 | 2017-03-08 21:18:51 +0000 | [diff] [blame] | 61 | # Set exit status as it was requested. |
| 62 | return number_of_bugs if args.status_bugs else exit_code |
| 63 | |
| 64 | |
| 65 | @command_entry_point |
| 66 | def analyze_build(): |
| 67 | """ Entry point for analyze-build command. """ |
| 68 | |
| 69 | args = parse_args_for_analyze_build() |
Laszlo Nagy | 57db7c6 | 2017-03-21 10:15:18 +0000 | [diff] [blame^] | 70 | # will re-assign the report directory as new output |
| 71 | with report_directory(args.output, args.keep_empty) as args.output: |
Laszlo Nagy | 5270bb9 | 2017-03-08 21:18:51 +0000 | [diff] [blame] | 72 | # Run the analyzer against a compilation db. |
Laszlo Nagy | 57db7c6 | 2017-03-21 10:15:18 +0000 | [diff] [blame^] | 73 | run_analyzer(args) |
Laszlo Nagy | 5270bb9 | 2017-03-08 21:18:51 +0000 | [diff] [blame] | 74 | # Cover report generation and bug counting. |
Laszlo Nagy | 57db7c6 | 2017-03-21 10:15:18 +0000 | [diff] [blame^] | 75 | number_of_bugs = document(args) |
Laszlo Nagy | 5270bb9 | 2017-03-08 21:18:51 +0000 | [diff] [blame] | 76 | # Set exit status as it was requested. |
| 77 | return number_of_bugs if args.status_bugs else 0 |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 78 | |
| 79 | |
| 80 | def need_analyzer(args): |
| 81 | """ Check the intent of the build command. |
| 82 | |
| 83 | When static analyzer run against project configure step, it should be |
| 84 | silent and no need to run the analyzer or generate report. |
| 85 | |
| 86 | To run `scan-build` against the configure step might be neccessary, |
| 87 | when compiler wrappers are used. That's the moment when build setup |
| 88 | check the compiler and capture the location for the build process. """ |
| 89 | |
| 90 | return len(args) and not re.search('configure|autogen', args[0]) |
| 91 | |
| 92 | |
Laszlo Nagy | 57db7c6 | 2017-03-21 10:15:18 +0000 | [diff] [blame^] | 93 | def run_analyzer(args): |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 94 | """ Runs the analyzer against the given compilation database. """ |
| 95 | |
| 96 | def exclude(filename): |
| 97 | """ Return true when any excluded directory prefix the filename. """ |
| 98 | return any(re.match(r'^' + directory, filename) |
| 99 | for directory in args.excludes) |
| 100 | |
| 101 | consts = { |
| 102 | 'clang': args.clang, |
Laszlo Nagy | 57db7c6 | 2017-03-21 10:15:18 +0000 | [diff] [blame^] | 103 | 'output_dir': args.output, |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 104 | 'output_format': args.output_format, |
| 105 | 'output_failures': args.output_failures, |
Yury Gribov | a6560eb | 2016-02-18 11:08:46 +0000 | [diff] [blame] | 106 | 'direct_args': analyzer_params(args), |
Laszlo Nagy | 8bd63e5 | 2016-04-19 12:03:03 +0000 | [diff] [blame] | 107 | 'force_debug': args.force_debug |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 108 | } |
| 109 | |
| 110 | logging.debug('run analyzer against compilation database') |
| 111 | with open(args.cdb, 'r') as handle: |
| 112 | generator = (dict(cmd, **consts) |
| 113 | for cmd in json.load(handle) if not exclude(cmd['file'])) |
| 114 | # when verbose output requested execute sequentially |
| 115 | pool = multiprocessing.Pool(1 if args.verbose > 2 else None) |
| 116 | for current in pool.imap_unordered(run, generator): |
| 117 | if current is not None: |
| 118 | # display error message from the static analyzer |
| 119 | for line in current['error_output']: |
| 120 | logging.info(line.rstrip()) |
| 121 | pool.close() |
| 122 | pool.join() |
| 123 | |
| 124 | |
Laszlo Nagy | 57db7c6 | 2017-03-21 10:15:18 +0000 | [diff] [blame^] | 125 | def setup_environment(args): |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 126 | """ Set up environment for build command to interpose compiler wrapper. """ |
| 127 | |
| 128 | environment = dict(os.environ) |
Laszlo Nagy | 2e9c922 | 2017-03-04 01:08:05 +0000 | [diff] [blame] | 129 | environment.update(wrapper_environment(args)) |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 130 | environment.update({ |
Laszlo Nagy | 5270bb9 | 2017-03-08 21:18:51 +0000 | [diff] [blame] | 131 | 'CC': COMPILER_WRAPPER_CC, |
| 132 | 'CXX': COMPILER_WRAPPER_CXX, |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 133 | 'ANALYZE_BUILD_CLANG': args.clang if need_analyzer(args.build) else '', |
Laszlo Nagy | 57db7c6 | 2017-03-21 10:15:18 +0000 | [diff] [blame^] | 134 | 'ANALYZE_BUILD_REPORT_DIR': args.output, |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 135 | 'ANALYZE_BUILD_REPORT_FORMAT': args.output_format, |
| 136 | 'ANALYZE_BUILD_REPORT_FAILURES': 'yes' if args.output_failures else '', |
Yury Gribov | a6560eb | 2016-02-18 11:08:46 +0000 | [diff] [blame] | 137 | 'ANALYZE_BUILD_PARAMETERS': ' '.join(analyzer_params(args)), |
Laszlo Nagy | 8bd63e5 | 2016-04-19 12:03:03 +0000 | [diff] [blame] | 138 | 'ANALYZE_BUILD_FORCE_DEBUG': 'yes' if args.force_debug else '' |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 139 | }) |
| 140 | return environment |
| 141 | |
| 142 | |
Laszlo Nagy | 2e9c922 | 2017-03-04 01:08:05 +0000 | [diff] [blame] | 143 | @command_entry_point |
| 144 | def analyze_compiler_wrapper(): |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 145 | """ Entry point for `analyze-cc` and `analyze-c++` compiler wrappers. """ |
| 146 | |
Laszlo Nagy | 2e9c922 | 2017-03-04 01:08:05 +0000 | [diff] [blame] | 147 | return compiler_wrapper(analyze_compiler_wrapper_impl) |
| 148 | |
| 149 | |
| 150 | def analyze_compiler_wrapper_impl(result, execution): |
| 151 | """ Implements analyzer compiler wrapper functionality. """ |
| 152 | |
| 153 | # don't run analyzer when compilation fails. or when it's not requested. |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 154 | if result or not os.getenv('ANALYZE_BUILD_CLANG'): |
Laszlo Nagy | 2e9c922 | 2017-03-04 01:08:05 +0000 | [diff] [blame] | 155 | return |
| 156 | |
| 157 | # check is it a compilation? |
| 158 | compilation = split_command(execution.cmd) |
| 159 | if compilation is None: |
| 160 | return |
| 161 | # collect the needed parameters from environment, crash when missing |
| 162 | parameters = { |
| 163 | 'clang': os.getenv('ANALYZE_BUILD_CLANG'), |
| 164 | 'output_dir': os.getenv('ANALYZE_BUILD_REPORT_DIR'), |
| 165 | 'output_format': os.getenv('ANALYZE_BUILD_REPORT_FORMAT'), |
| 166 | 'output_failures': os.getenv('ANALYZE_BUILD_REPORT_FAILURES'), |
| 167 | 'direct_args': os.getenv('ANALYZE_BUILD_PARAMETERS', |
| 168 | '').split(' '), |
| 169 | 'force_debug': os.getenv('ANALYZE_BUILD_FORCE_DEBUG'), |
| 170 | 'directory': execution.cwd, |
| 171 | 'command': [execution.cmd[0], '-c'] + compilation.flags |
| 172 | } |
| 173 | # call static analyzer against the compilation |
| 174 | for source in compilation.files: |
| 175 | parameters.update({'file': source}) |
| 176 | logging.debug('analyzer parameters %s', parameters) |
| 177 | current = run(parameters) |
| 178 | # display error message from the static analyzer |
| 179 | if current is not None: |
| 180 | for line in current['error_output']: |
| 181 | logging.info(line.rstrip()) |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 182 | |
| 183 | |
Laszlo Nagy | 258ff25 | 2017-02-14 10:43:38 +0000 | [diff] [blame] | 184 | @contextlib.contextmanager |
| 185 | def report_directory(hint, keep): |
| 186 | """ Responsible for the report directory. |
| 187 | |
| 188 | hint -- could specify the parent directory of the output directory. |
| 189 | keep -- a boolean value to keep or delete the empty report directory. """ |
| 190 | |
| 191 | stamp_format = 'scan-build-%Y-%m-%d-%H-%M-%S-%f-' |
| 192 | stamp = datetime.datetime.now().strftime(stamp_format) |
| 193 | parent_dir = os.path.abspath(hint) |
| 194 | if not os.path.exists(parent_dir): |
| 195 | os.makedirs(parent_dir) |
| 196 | name = tempfile.mkdtemp(prefix=stamp, dir=parent_dir) |
| 197 | |
| 198 | logging.info('Report directory created: %s', name) |
| 199 | |
| 200 | try: |
| 201 | yield name |
| 202 | finally: |
| 203 | if os.listdir(name): |
| 204 | msg = "Run 'scan-view %s' to examine bug reports." |
| 205 | keep = True |
| 206 | else: |
| 207 | if keep: |
| 208 | msg = "Report directory '%s' contains no report, but kept." |
| 209 | else: |
| 210 | msg = "Removing directory '%s' because it contains no report." |
| 211 | logging.warning(msg, name) |
| 212 | |
| 213 | if not keep: |
| 214 | os.rmdir(name) |
| 215 | |
| 216 | |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 217 | def analyzer_params(args): |
| 218 | """ A group of command line arguments can mapped to command |
| 219 | line arguments of the analyzer. This method generates those. """ |
| 220 | |
| 221 | def prefix_with(constant, pieces): |
| 222 | """ From a sequence create another sequence where every second element |
| 223 | is from the original sequence and the odd elements are the prefix. |
| 224 | |
| 225 | eg.: prefix_with(0, [1,2,3]) creates [0, 1, 0, 2, 0, 3] """ |
| 226 | |
| 227 | return [elem for piece in pieces for elem in [constant, piece]] |
| 228 | |
| 229 | result = [] |
| 230 | |
| 231 | if args.store_model: |
| 232 | result.append('-analyzer-store={0}'.format(args.store_model)) |
| 233 | if args.constraints_model: |
Laszlo Nagy | 8bd63e5 | 2016-04-19 12:03:03 +0000 | [diff] [blame] | 234 | result.append('-analyzer-constraints={0}'.format( |
| 235 | args.constraints_model)) |
Laszlo Nagy | bc68758 | 2016-01-12 22:38:41 +0000 | [diff] [blame] | 236 | if args.internal_stats: |
| 237 | result.append('-analyzer-stats') |
| 238 | if args.analyze_headers: |
| 239 | result.append('-analyzer-opt-analyze-headers') |
| 240 | if args.stats: |
| 241 | result.append('-analyzer-checker=debug.Stats') |
| 242 | if args.maxloop: |
| 243 | result.extend(['-analyzer-max-loop', str(args.maxloop)]) |
| 244 | if args.output_format: |
| 245 | result.append('-analyzer-output={0}'.format(args.output_format)) |
| 246 | if args.analyzer_config: |
| 247 | result.append(args.analyzer_config) |
| 248 | if args.verbose >= 4: |
| 249 | result.append('-analyzer-display-progress') |
| 250 | if args.plugins: |
| 251 | result.extend(prefix_with('-load', args.plugins)) |
| 252 | if args.enable_checker: |
| 253 | checkers = ','.join(args.enable_checker) |
| 254 | result.extend(['-analyzer-checker', checkers]) |
| 255 | if args.disable_checker: |
| 256 | checkers = ','.join(args.disable_checker) |
| 257 | result.extend(['-analyzer-disable-checker', checkers]) |
| 258 | if os.getenv('UBIVIZ'): |
| 259 | result.append('-analyzer-viz-egraph-ubigraph') |
| 260 | |
| 261 | return prefix_with('-Xclang', result) |