Index: tools/scan-build-py/bin/analyze-build =================================================================== --- /dev/null +++ tools/scan-build-py/bin/analyze-build @@ -0,0 +1,15 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# The LLVM Compiler Infrastructure +# +# This file is distributed under the University of Illinois Open Source +# License. See LICENSE.TXT for details. + +if __name__ == '__main__': + import sys + import os.path + this_dir = os.path.dirname(os.path.realpath(__file__)) + sys.path.append(os.path.dirname(this_dir)) + + from libscanbuild.interposition import main + sys.exit(main(this_dir)) Index: tools/scan-build-py/bin/analyze-c++ =================================================================== --- /dev/null +++ tools/scan-build-py/bin/analyze-c++ @@ -0,0 +1,15 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# The LLVM Compiler Infrastructure +# +# This file is distributed under the University of Illinois Open Source +# License. See LICENSE.TXT for details. + +if __name__ == '__main__': + import sys + import os.path + this_dir = os.path.dirname(os.path.realpath(__file__)) + sys.path.append(os.path.dirname(this_dir)) + + from libscanbuild.interposition import wrapper + sys.exit(wrapper(True)) Index: tools/scan-build-py/bin/analyze-cc =================================================================== --- /dev/null +++ tools/scan-build-py/bin/analyze-cc @@ -0,0 +1,15 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# The LLVM Compiler Infrastructure +# +# This file is distributed under the University of Illinois Open Source +# License. See LICENSE.TXT for details. + +if __name__ == '__main__': + import sys + import os.path + this_dir = os.path.dirname(os.path.realpath(__file__)) + sys.path.append(os.path.dirname(this_dir)) + + from libscanbuild.interposition import wrapper + sys.exit(wrapper(False)) Index: tools/scan-build-py/bin/intercept-build =================================================================== --- /dev/null +++ tools/scan-build-py/bin/intercept-build @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# The LLVM Compiler Infrastructure +# +# This file is distributed under the University of Illinois Open Source +# License. See LICENSE.TXT for details. + +if __name__ == '__main__': + import multiprocessing + multiprocessing.freeze_support() + + import sys + import os.path + this_dir = os.path.dirname(os.path.realpath(__file__)) + sys.path.append(os.path.dirname(this_dir)) + + from libscanbuild.driver import main + sys.exit(main(this_dir)) Index: tools/scan-build-py/bin/intercept-c++ =================================================================== --- /dev/null +++ tools/scan-build-py/bin/intercept-c++ @@ -0,0 +1,15 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# The LLVM Compiler Infrastructure +# +# This file is distributed under the University of Illinois Open Source +# License. See LICENSE.TXT for details. + +if __name__ == '__main__': + import sys + import os.path + this_dir = os.path.dirname(os.path.realpath(__file__)) + sys.path.append(os.path.dirname(this_dir)) + + from libscanbuild.intercept import wrapper + sys.exit(wrapper(True)) Index: tools/scan-build-py/bin/intercept-cc =================================================================== --- /dev/null +++ tools/scan-build-py/bin/intercept-cc @@ -0,0 +1,15 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# The LLVM Compiler Infrastructure +# +# This file is distributed under the University of Illinois Open Source +# License. See LICENSE.TXT for details. + +if __name__ == '__main__': + import sys + import os.path + this_dir = os.path.dirname(os.path.realpath(__file__)) + sys.path.append(os.path.dirname(this_dir)) + + from libscanbuild.intercept import wrapper + sys.exit(wrapper(False)) Index: tools/scan-build-py/bin/scan-build =================================================================== --- tools/scan-build-py/bin/scan-build +++ tools/scan-build-py/bin/scan-build @@ -1,15 +1,16 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# The LLVM Compiler Infrastructure -# -# This file is distributed under the University of Illinois Open Source -# License. See LICENSE.TXT for details. +#!/usr/bin/env bash + +set -o nounset +set -o errexit -import sys -import multiprocessing -from libscanbuild.driver import main +SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +# depend on which model you want the scan-build to work you need to choose +# from the two implementations. 'analyze-build' is working as the old Perl +# implementation was. while the 'intercept-build' is generate compilation +# database first and then run the analyzer against the entries. +# +#"$SCRIPT_DIR/analyze-build" $@ +#"$SCRIPT_DIR/intercept-build" all $@ -if __name__ == '__main__': - multiprocessing.freeze_support() - sys.exit(main()) +"$SCRIPT_DIR/intercept-build" all $@ Index: tools/scan-build-py/libear/config.h.in =================================================================== --- tools/scan-build-py/libear/config.h.in +++ tools/scan-build-py/libear/config.h.in @@ -21,7 +21,7 @@ #cmakedefine APPLE -#define ENV_OUTPUT "BEAR_OUTPUT" +#define ENV_OUTPUT "BUILD_INTERCEPT_TARGET_DIR" #ifdef APPLE # define ENV_FLAT "DYLD_FORCE_FLAT_NAMESPACE" Index: tools/scan-build-py/libear/ear.c =================================================================== --- tools/scan-build-py/libear/ear.c +++ tools/scan-build-py/libear/ear.c @@ -44,7 +44,6 @@ typedef char const * bear_env_t[ENV_SIZE]; static int bear_capture_env_t(bear_env_t *env); -static void bear_restore_env_t(bear_env_t *env); static void bear_release_env_t(bear_env_t *env); static char const **bear_update_environment(char *const envp[], bear_env_t *env); static char const **bear_update_environ(char const **in, char const *key, char const *value); @@ -293,12 +292,12 @@ DLSYM(func, fp, "execvp"); - bear_env_t current; - bear_capture_env_t(¤t); - bear_restore_env_t(&initial_env); + char **const original = environ; + char const **const modified = bear_update_environment(original, &initial_env); + environ = (char **)modified; int const result = (*fp)(file, argv); - bear_restore_env_t(¤t); - bear_release_env_t(¤t); + environ = original; + bear_strings_release(modified); return result; } @@ -311,12 +310,12 @@ DLSYM(func, fp, "execvP"); - bear_env_t current; - bear_capture_env_t(¤t); - bear_restore_env_t(&initial_env); + char **const original = environ; + char const **const modified = bear_update_environment(original, &initial_env); + environ = (char **)modified; int const result = (*fp)(file, search_path, argv); - bear_restore_env_t(¤t); - bear_release_env_t(¤t); + environ = original; + bear_strings_release(modified); return result; } @@ -421,16 +420,6 @@ return status; } -static void bear_restore_env_t(bear_env_t *env) { - for (size_t it = 0; it < ENV_SIZE; ++it) - if (((*env)[it]) - ? setenv(env_names[it], (*env)[it], 1) - : unsetenv(env_names[it])) { - perror("bear: setenv"); - exit(EXIT_FAILURE); - } -} - static void bear_release_env_t(bear_env_t *env) { for (size_t it = 0; it < ENV_SIZE; ++it) { free((void *)(*env)[it]); Index: tools/scan-build-py/libscanbuild/driver.py =================================================================== --- tools/scan-build-py/libscanbuild/driver.py +++ tools/scan-build-py/libscanbuild/driver.py @@ -30,7 +30,7 @@ __all__ = ['main'] -def main(): +def main(bin_dir): """ Entry point for 'scan-build'. """ try: @@ -42,7 +42,8 @@ logging.debug('Parsed arguments: %s', args) # run build command and capture compiler executions - exit_code = capture(args) if args.action in {'all', 'intercept'} else 0 + exit_code = capture(args, bin_dir) \ + if args.action in {'all', 'intercept'} else 0 # when we only do interception the job is done if args.action == 'intercept': return exit_code @@ -51,7 +52,7 @@ with ReportDirectory(args.output, args.keep_empty) as target_dir: run_analyzer(args, target_dir.name) # cover report generation and bug counting - number_of_bugs = document(args, target_dir.name) + number_of_bugs = document(args, target_dir.name, True) # remove the compilation database when it was not requested if args.action == 'all' and os.path.exists(args.cdb): os.unlink(args.cdb) @@ -107,8 +108,8 @@ def exclude(filename): """ Return true when any excluded directory prefix the filename. """ - return any(re.match(r'^' + directory, filename) for directory - in args.excludes) + return any(re.match(r'^' + directory, filename) + for directory in args.excludes) consts = { 'clang': args.clang, Index: tools/scan-build-py/libscanbuild/intercept.py =================================================================== --- tools/scan-build-py/libscanbuild/intercept.py +++ tools/scan-build-py/libscanbuild/intercept.py @@ -27,23 +27,18 @@ import os.path import re import shlex -import pkg_resources import itertools from libscanbuild import duplicate_check, tempdir from libscanbuild.command import Action, classify_parameters -__all__ = ['capture'] +__all__ = ['capture', 'wrapper'] -if 'darwin' == sys.platform: - ENVIRONMENTS = [("ENV_OUTPUT", "BEAR_OUTPUT"), - ("ENV_PRELOAD", "DYLD_INSERT_LIBRARIES"), - ("ENV_FLAT", "DYLD_FORCE_FLAT_NAMESPACE")] -else: - ENVIRONMENTS = [("ENV_OUTPUT", "BEAR_OUTPUT"), - ("ENV_PRELOAD", "LD_PRELOAD")] +GS = chr(0x1d) +RS = chr(0x1e) +US = chr(0x1f) -def capture(args): +def capture(args, wrappers_dir): """ The entry point of build command interception. """ def post_processing(commands): @@ -66,9 +61,11 @@ if os.path.exists(entry['file']) and not duplicate(entry)) return commands - with TemporaryDirectory(prefix='bear-', dir=tempdir()) as tmpdir: + with TemporaryDirectory(prefix='build-intercept', dir=tempdir()) as tmpdir: # run the build command - exit_code = run_build(args.build, tmpdir) + environment = setup_environment(args, tmpdir, wrappers_dir) + logging.debug('run build in environment: %s', environment) + exit_code = subprocess.call(args.build, env=environment) logging.debug('build finished with exit code: %d', exit_code) # read the intercepted exec calls commands = (parse_exec_trace(os.path.join(tmpdir, filename)) @@ -81,25 +78,71 @@ return exit_code -def run_build(command, destination): - """ Runs the original build command. +def setup_environment(args, destination, wrappers_dir): + """ Sets up the environment for the build command. It sets the required environment variables and execute the given command. - The exec calls will be logged by the 'libear' preloaded library. """ - - lib_name = 'libear.dylib' if 'darwin' == sys.platform else 'libear.so' - ear_so_file = pkg_resources.resource_filename('libscanbuild', lib_name) + The exec calls will be logged by the 'libear' preloaded library or by the + 'wrapper' programs. """ environment = dict(os.environ) - for alias, key in ENVIRONMENTS: - value = '1' - if alias == 'ENV_PRELOAD': - value = ear_so_file - elif alias == 'ENV_OUTPUT': - value = destination - environment.update({key: value}) - - return subprocess.call(command, env=environment) + environment.update({'BUILD_INTERCEPT_TARGET_DIR': destination}) + + if sys.platform in {'win32', 'cygwin'} or not ear_library_path(False): + environment.update({ + 'CC': os.path.join(wrappers_dir, 'intercept-cc'), + 'CXX': os.path.join(wrappers_dir, 'intercept-cxx'), + 'BUILD_INTERCEPT_CC': args.cc, + 'BUILD_INTERCEPT_CXX': args.cxx, + 'BUILD_INTERCEPT_VERBOSE': 'DEBUG' if args.verbose > 2 else 'INFO' + }) + elif 'darwin' == sys.platform: + environment.update({ + 'DYLD_INSERT_LIBRARIES': ear_library_path(True), + 'DYLD_FORCE_FLAT_NAMESPACE': '1' + }) + else: + environment.update({'LD_PRELOAD': ear_library_path(False)}) + + return environment + + +def wrapper(cplusplus): + """ This method implements basic compiler wrapper functionality. + + It does generate execution report into target directory. And execute + the wrapped compilation with the real compiler. The parameters for + report and execution are from environment variables. + + Those parameters which for 'libear' library can't have meaningful + values are faked. """ + + # initialize wrapper logging + logging.basicConfig(format='intercept: %(levelname)s: %(message)s', + level=os.getenv('BUILD_INTERCEPT_VERBOSE', 'INFO')) + # write report + try: + target_dir = os.getenv('BUILD_INTERCEPT_TARGET_DIR') + if not target_dir: + raise UserWarning('exec report target directory not found') + pid = str(os.getpid()) + target_file = os.path.join(target_dir, pid + '.cmd') + logging.debug('writing exec report to: %s', target_file) + with open(target_file, 'ab') as handler: + working_dir = os.getcwd() + command = US.join(sys.argv) + US + content = RS.join([pid, pid, 'wrapper', working_dir, command]) + GS + handler.write(content.encode('utf-8')) + except IOError: + logging.exception('writing exec report failed') + except UserWarning as warning: + logging.warning(warning) + # execute with real compiler + compiler = os.getenv('BUILD_INTERCEPT_CXX', 'c++') if cplusplus \ + else os.getenv('BUILD_INTERCEPT_CC', 'cc') + compilation = [compiler] + sys.argv[1:] + logging.debug('execute compiler: %s', compilation) + return subprocess.call(compilation) def parse_exec_trace(filename): @@ -109,9 +152,6 @@ generated by the interception library or wrapper command. A single report file _might_ contain multiple process creation info. """ - GS = chr(0x1d) - RS = chr(0x1e) - US = chr(0x1f) with open(filename, 'r') as handler: content = handler.read() for group in filter(bool, content.split(GS)): @@ -137,14 +177,12 @@ return os.path.normpath(fullname) atoms = classify_parameters(entry['command']) - if atoms['action'] <= Action.Compile: - for filename in atoms.get('files', []): - if is_source_file(filename): - yield { - 'directory': entry['directory'], - 'command': join_command(entry['command']), - 'file': abspath(entry['directory'], filename) - } + return ({ + 'directory': entry['directory'], + 'command': join_command(entry['command']), + 'file': abspath(entry['directory'], filename) + } for filename in atoms.get('files', []) + if is_source_file(filename) and atoms['action'] <= Action.Compile) def shell_escape(arg): @@ -175,6 +213,7 @@ """ A predicate to decide the entry is a compiler call or not. """ patterns = [ + re.compile(r'^([^/]*/)*intercept-c(c|\+\+)$'), re.compile(r'^([^/]*/)*c(c|\+\+)$'), re.compile(r'^([^/]*/)*([^-]*-)*g(cc|\+\+)(-\d+(\.\d+){0,2})?$'), re.compile(r'^([^/]*/)*([^-]*-)*clang(\+\+)?(-\d+(\.\d+){0,2})?$'), @@ -200,6 +239,18 @@ return '<>'.join([filename, directory, command]) +def ear_library_path(darwin): + """ Returns the full path to the 'libear' library. """ + + try: + import pkg_resources + lib_name = 'libear.dylib' if darwin else 'libear.so' + lib_file = pkg_resources.resource_filename('libscanbuild', lib_name) + return lib_file if os.path.exists(lib_file) else None + except ImportError: + return None + + if sys.version_info.major >= 3 and sys.version_info.minor >= 2: from tempfile import TemporaryDirectory else: Index: tools/scan-build-py/libscanbuild/interposition.py =================================================================== --- /dev/null +++ tools/scan-build-py/libscanbuild/interposition.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# The LLVM Compiler Infrastructure +# +# This file is distributed under the University of Illinois Open Source +# License. See LICENSE.TXT for details. + +import os +import sys +import argparse +import subprocess +import logging +from libscanbuild.options import (common_parameters, analyze_parameters, + build_command) +from libscanbuild.driver import (initialize_logging, ReportDirectory, + analyzer_params, print_checkers, + print_active_checkers) +from libscanbuild.report import document +from libscanbuild.clang import get_checkers +from libscanbuild.runner import action_check +from libscanbuild.intercept import is_source_file +from libscanbuild.command import classify_parameters + +__all__ = ['main', 'wrapper'] + + +def main(bin_dir): + """ Entry point for 'analyze-build'. """ + + try: + args = parse_and_validate_arguments() + # setup logging + initialize_logging(args) + logging.debug('Parsed arguments: %s', args) + # run the build + with ReportDirectory(args.output, args.keep_empty) as target_dir: + # run the build command + environment = setup_environment(args, target_dir.name, bin_dir) + logging.debug('run build in environment: %s', environment) + exit_code = subprocess.call(args.build, env=environment) + logging.debug('build finished with exit code: %d', exit_code) + # cover report generation and bug counting + number_of_bugs = document(args, target_dir.name, False) + # set exit status as it was requested + return number_of_bugs if args.status_bugs else exit_code + except KeyboardInterrupt: + return 1 + except Exception: + logging.exception("Something unexpected had happened.") + return 127 + + +def parse_and_validate_arguments(): + """ Parse and validate command line arguments. """ + + # create parser.. + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + common_parameters(parser, False) + analyze_parameters(parser) + build_command(parser) + # run it.. + args = parser.parse_args() + # validate.. + if args.help_checkers_verbose: + print_checkers(get_checkers(args.clang, args.plugins)) + parser.exit() + elif args.help_checkers: + print_active_checkers(get_checkers(args.clang, args.plugins)) + parser.exit() + if not args.build: + parser.error('missing build command') + # return it.. + return args + + +def setup_environment(args, destination, wrapper_dir): + """ Sets up the environment for the build command. """ + + environment = dict(os.environ) + environment.update({ + 'CC': os.path.join(wrapper_dir, 'analyze-cc'), + 'CXX': os.path.join(wrapper_dir, 'analyze-cxx'), + 'BUILD_ANALYZE_CC': args.cc, + 'BUILD_ANALYZE_CXX': args.cxx, + 'BUILD_ANALYZE_CLANG': args.clang, + 'BUILD_ANALYZE_VERBOSE': 'DEBUG' if args.verbose > 2 else 'WARNING', + 'BUILD_ANALYZE_REPORT_DIR': destination, + 'BUILD_ANALYZE_REPORT_FORMAT': args.output_format, + 'BUILD_ANALYZE_REPORT_FAILURES': 'yes' if args.report_failures else '', + 'BUILD_ANALYZE_PARAMETERS': ' '.join(analyzer_params(args)) + }) + return environment + + +def wrapper(cplusplus): + """ This method implements basic compiler wrapper functionality. """ + + # initialize wrapper logging + logging.basicConfig(format='analyze: %(levelname)s: %(message)s', + level=os.getenv('BUILD_ANALYZE_VERBOSE', 'INFO')) + # execute with real compiler + compiler = os.getenv('BUILD_ANALYZE_CXX', 'c++') if cplusplus \ + else os.getenv('BUILD_ANALYZE_CC', 'cc') + compilation = [compiler] + sys.argv[1:] + logging.info('execute compiler: %s', compilation) + result = subprocess.call(compilation) + try: + # collect the needed parameters from environment, crash when missing + consts = { + 'clang': os.getenv('BUILD_ANALYZE_CLANG'), + 'output_dir': os.getenv('BUILD_ANALYZE_REPORT_DIR'), + 'output_format': os.getenv('BUILD_ANALYZE_REPORT_FORMAT'), + 'report_failures': os.getenv('BUILD_ANALYZE_REPORT_FAILURES'), + 'direct_args': os.getenv('BUILD_ANALYZE_PARAMETERS', + '').split(' '), + 'directory': os.getcwd(), + } + # get relevant parameters from command line arguments + args = classify_parameters(sys.argv) + filenames = args.pop('files', []) + for filename in (name for name in filenames if is_source_file(name)): + parameters = dict(args, file=filename, **consts) + logging.debug('analyzer parameters %s', parameters) + current = action_check(parameters) + # display error message from the static analyzer + if current is not None: + for line in current['error_output']: + logging.info(line.rstrip()) + except Exception: + logging.exception("run analyzer inside compiler wrapper failed.") + # return compiler exit code + return result Index: tools/scan-build-py/libscanbuild/options.py =================================================================== --- tools/scan-build-py/libscanbuild/options.py +++ tools/scan-build-py/libscanbuild/options.py @@ -29,7 +29,7 @@ help="""Run the static analyzer against the given build command.""") - common_parameters(everything) + common_parameters(everything, True) analyze_parameters(everything) build_command(everything) @@ -38,7 +38,7 @@ formatter_class=argparse.ArgumentDefaultsHelpFormatter, help="""Only runs the build and write compilation database.""") - common_parameters(intercept) + common_parameters(intercept, True) intercept_parameters(intercept) build_command(intercept) @@ -48,23 +48,24 @@ help="""Only run the static analyzer against the given compilation database.""") - common_parameters(analyze) + common_parameters(analyze, True) analyze_parameters(analyze) return parser -def common_parameters(parser): +def common_parameters(parser, add_cdb): parser.add_argument( '--verbose', '-v', action='count', default=0, help="""Enable verbose output from '%(prog)s'. A second and third '-v' increases verbosity.""") - parser.add_argument('--cdb', - metavar='', - default="compile_commands.json", - help="""The JSON compilation database.""") + if add_cdb: + parser.add_argument('--cdb', + metavar='', + default="compile_commands.json", + help="""The JSON compilation database.""") def build_command(parser): @@ -112,14 +113,14 @@ within the main source file.""") format_group = parser.add_mutually_exclusive_group() format_group.add_argument( - '--plist', + '--plist', '-plist', dest='output_format', const='plist', default='html', action='store_const', help="""This option outputs the results as a set of .plist files.""") format_group.add_argument( - '--plist-html', + '--plist-html', '-plist-html', dest='output_format', const='plist-html', default='html', @@ -135,13 +136,13 @@ help="""Don't remove the build results directory even if no issues were reported.""") advanced.add_argument( - '--no-failure-reports', + '--no-failure-reports', '-no-failure-reports', dest='report_failures', action='store_false', help="""Do not create a 'failures' subdirectory that includes analyzer crash reports and preprocessed source files.""") advanced.add_argument( - '--stats', + '--stats', '-stats', action='store_true', help="""Generates visitation statistics for the project being analyzed. """) @@ -149,14 +150,14 @@ action='store_true', help="""Generate internal analyzer statistics.""") advanced.add_argument( - '--maxloop', + '--maxloop', '-maxloop', metavar='', type=int, default=4, help="""Specifiy the number of times a block can be visited before giving up. Increase for more comprehensive coverage at a cost of speed.""") - advanced.add_argument('--store', + advanced.add_argument('--store', '-store', metavar='', dest='store_model', default='region', @@ -167,7 +168,7 @@ analyze code. 'basic' was the default store model for checker-0.221 and earlier.""") advanced.add_argument( - '--constraints', + '--constraints', '-constraints', metavar='', dest='constraints_model', default='range', @@ -185,7 +186,29 @@ option by using the 'clang' packaged with Xcode (on OS X) or from the PATH.""") advanced.add_argument( - '--analyzer-config', + '--use-cc', + metavar='', + dest='cc', + default='cc', + help="""When '%(prog)s' analyzes a project by interposing a "fake + compiler", which executes a real compiler for compilation and + do other tasks (to run the static analyzer or just record the + compiler invocation). Because of this interposing, '%(prog)s' + does not know what compiler your project normally uses. + Instead, it simply overrides the CC environment variable, and + guesses your default compiler. + + If you need '%(prog)s' to use a specific compiler for + *compilation* then you can use this option to specify a path + to that compiler.""") + advanced.add_argument( + '--use-c++', + metavar='', + dest='cxx', + default='c++', + help="""This is the same as "--use-cc" but for C++ code.""") + advanced.add_argument( + '--analyzer-config', '-analyzer-config', metavar='', help="""Provide options to pass through to the analyzer's -analyzer-config flag. Several options are separated with @@ -211,16 +234,16 @@ plugins = parser.add_argument_group('checker options') plugins.add_argument( - '--load-plugin', + '--load-plugin', '-load-plugin', metavar='', dest='plugins', action='append', help="""Loading external checkers using the clang plugin interface.""") - plugins.add_argument('--enable-checker', + plugins.add_argument('--enable-checker', '-enable-checker', metavar='', action='append', help="""Enable specific checker.""") - plugins.add_argument('--disable-checker', + plugins.add_argument('--disable-checker', '-disable-checker', metavar='', action='append', help="""Disable specific checker.""") Index: tools/scan-build-py/libscanbuild/report.py =================================================================== --- tools/scan-build-py/libscanbuild/report.py +++ tools/scan-build-py/libscanbuild/report.py @@ -17,7 +17,6 @@ import json import shutil import glob -import pkg_resources import plistlib import itertools from libscanbuild import duplicate_check @@ -26,7 +25,7 @@ __all__ = ['document'] -def document(args, output_dir): +def document(args, output_dir, use_cdb): """ Generates cover report and returns the number of bugs/crashes. """ html_reports_available = args.output_format in {'html', 'plist-html'} @@ -38,9 +37,8 @@ result = crash_count + bug_counter.total # generate cover file when it's needed if html_reports_available and result: - # generate common prefix for source files to have sort filenames - with open(args.cdb, 'r') as handle: - prefix = commonprefix(item['file'] for item in json.load(handle)) + # common prefix for source files to have sort filenames + prefix = commonprefix_from(args.cdb) if use_cdb else os.getcwd() # assemble the cover from multiple fragments try: fragments = [] @@ -49,14 +47,14 @@ fragments.append(bug_report(output_dir, prefix)) if crash_count: fragments.append(crash_report(output_dir, prefix)) - assemble_cover(output_dir, prefix, args, fragments) + # copy additinal files to the report + copy_resource_files(output_dir) + if use_cdb: + shutil.copy(args.cdb, output_dir) finally: for fragment in fragments: os.remove(fragment) - # copy additinal files to the report - copy_resource_files(output_dir) - shutil.copy(args.cdb, output_dir) return result @@ -269,8 +267,8 @@ bugs = itertools.chain.from_iterable( # parser creates a bug generator not the bug itself - parser(filename) for filename - in glob.iglob(os.path.join(output_dir, pattern))) + parser(filename) + for filename in glob.iglob(os.path.join(output_dir, pattern))) return (bug for bug in bugs if not duplicate(bug)) @@ -422,10 +420,17 @@ def copy_resource_files(output_dir): """ Copy the javascript and css files to the report directory. """ - this_package = 'libscanbuild' - resources_dir = pkg_resources.resource_filename(this_package, 'resources') - for resource in pkg_resources.resource_listdir(this_package, 'resources'): - shutil.copy(os.path.join(resources_dir, resource), output_dir) + try: + import pkg_resources + package = 'libscanbuild' + resources_dir = pkg_resources.resource_filename(package, 'resources') + for resource in pkg_resources.resource_listdir(package, 'resources'): + shutil.copy(os.path.join(resources_dir, resource), output_dir) + except ImportError: + resources_dir = os.path.join( + os.path.dirname(os.path.realpath(__file__)), 'resources') + for resource in os.listdir(resources_dir): + shutil.copy(os.path.join(resources_dir, resource), output_dir) def encode_value(container, key, encode): @@ -439,12 +444,7 @@ def chop(prefix, filename): """ Create 'filename' from '/prefix/filename' """ - if not len(prefix): - return filename - if prefix[-1] != os.path.sep: - prefix += os.path.sep - split = filename.split(prefix, 1) - return split[1] if len(split) == 2 else split[0] + return filename if not len(prefix) else os.path.relpath(filename, prefix) def escape(text): @@ -480,6 +480,13 @@ return '{2}'.format(name, attributes, os.linesep) +def commonprefix_from(filename): + """ Create file prefix from a compilation database entries. """ + + with open(filename, 'r') as handle: + return commonprefix(item['file'] for item in json.load(handle)) + + def commonprefix(files): """ Fixed version of os.path.commonprefix. Return the longest path prefix that is a prefix of all paths in filenames. """ Index: tools/scan-build-py/libscanbuild/runner.py =================================================================== --- tools/scan-build-py/libscanbuild/runner.py +++ tools/scan-build-py/libscanbuild/runner.py @@ -27,9 +27,9 @@ just return and break the chain. """ try: - logging.debug("Run analyzer against '%s'", opts['command']) - opts.update(classify_parameters(shlex.split(opts['command']))) - del opts['command'] + command = opts.pop('command') + logging.debug("Run analyzer against '%s'", command) + opts.update(classify_parameters(shlex.split(command))) return action_check(opts) except Exception: @@ -167,16 +167,15 @@ common = [] if 'arch' in opts: - common.extend(['-arch', opts['arch']]) - del opts['arch'] - if 'compile_options' in opts: - common.extend(opts['compile_options']) - del opts['compile_options'] + common.extend(['-arch', opts.pop('arch')]) + common.extend(opts.pop('compile_options', [])) common.extend(['-x', opts['language']]) common.append(opts['file']) - opts.update({'analyze': ['--analyze'] + opts['direct_args'] + common, - 'report': ['-fsyntax-only', '-E'] + common}) + opts.update({ + 'analyze': ['--analyze'] + opts['direct_args'] + common, + 'report': ['-fsyntax-only', '-E'] + common + }) return continuation(opts) @@ -237,8 +236,7 @@ key = 'archs_seen' if key in opts: # filter out disabled architectures and -arch switches - archs = [a for a in opts[key] - if '-arch' != a and a not in disableds] + archs = [a for a in opts[key] if '-arch' != a and a not in disableds] if not archs: logging.debug('skip analysis, found not supported arch') @@ -263,8 +261,7 @@ def action_check(opts, continuation=arch_check): """ Continue analysis only if it compilation or link. """ - if opts['action'] <= Action.Compile: - del opts['action'] + if opts.pop('action') <= Action.Compile: return continuation(opts) else: logging.debug('skip analysis, not compilation nor link') Index: tools/scan-build-py/setup.py =================================================================== --- tools/scan-build-py/setup.py +++ tools/scan-build-py/setup.py @@ -52,7 +52,9 @@ description='static code analyzer wrapper for Clang.', long_description=open('README.md').read(), zip_safe=False, - scripts=['bin/scan-build'], + scripts=['bin/scan-build', + 'bin/intercept-build', 'bin/intercept-cc', 'bin/intercept-c++', + 'bin/analyze-build', 'bin/analyze-cc', 'bin/analyze-c++'], packages=['libscanbuild'], package_data={'libscanbuild': ['resources/*']}, cmdclass={'buildear': BuildEAR, 'install': Install, 'build': Build}, Index: tools/scan-build-py/tests/functional/cases/__init__.py =================================================================== --- tools/scan-build-py/tests/functional/cases/__init__.py +++ tools/scan-build-py/tests/functional/cases/__init__.py @@ -23,10 +23,8 @@ def make_args(target): this_dir, _ = os.path.split(__file__) path = os.path.normpath(os.path.join(this_dir, '..', 'src')) - return ['make', - 'SRCDIR={}'.format(path), - 'OBJDIR={}'.format(target), - '-f', os.path.join(path, 'build', 'Makefile')] + return ['make', 'SRCDIR={}'.format(path), 'OBJDIR={}'.format(target), '-f', + os.path.join(path, 'build', 'Makefile')] def silent_call(cmd): Index: tools/scan-build-py/tests/functional/cases/test_create_cdb.py =================================================================== --- tools/scan-build-py/tests/functional/cases/test_create_cdb.py +++ tools/scan-build-py/tests/functional/cases/test_create_cdb.py @@ -13,14 +13,13 @@ class CompilationDatabaseTest(unittest.TestCase): - @staticmethod def run_intercept(tmpdir, args): - result = os.path.join(tmpdir, 'cdb.json') - make = make_args(tmpdir) + args - silent_check_call(['scan-build', 'intercept', '--cdb', result] + - make) - return result + result = os.path.join(tmpdir, 'cdb.json') + make = make_args(tmpdir) + args + silent_check_call( + ['intercept-build', 'intercept', '--cdb', result] + make) + return result def test_successful_build(self): with fixtures.TempDir() as tmpdir: @@ -44,7 +43,7 @@ with fixtures.TempDir() as tmpdir: result = os.path.join(tmpdir, 'cdb.json') make = make_args(tmpdir) + ['build_regular'] - silent_check_call(['scan-build', 'intercept', '--cdb', result, + silent_check_call(['intercept-build', 'intercept', '--cdb', result, 'env', '-'] + make) self.assertTrue(os.path.isfile(result)) with open(result, 'r') as handler: @@ -63,7 +62,8 @@ with fixtures.TempDir() as tmpdir: result = os.path.join(tmpdir, 'cdb.json') make = make_args(tmpdir) + ['build_broken'] - silent_call(['scan-build', 'intercept', '--cdb', result] + make) + silent_call( + ['intercept-build', 'intercept', '--cdb', result] + make) self.assertTrue(os.path.isfile(result)) with open(result, 'r') as handler: content = json.load(handler) @@ -71,13 +71,12 @@ class ExitCodeTest(unittest.TestCase): - @staticmethod def run_intercept(tmpdir, target): - result = os.path.join(tmpdir, 'cdb.json') - make = make_args(tmpdir) + [target] - return silent_call(['scan-build', 'intercept', '--cdb', result] + - make) + result = os.path.join(tmpdir, 'cdb.json') + make = make_args(tmpdir) + [target] + return silent_call( + ['intercept-build', 'intercept', '--cdb', result] + make) def test_successful_build(self): with fixtures.TempDir() as tmpdir: @@ -91,14 +90,13 @@ class ResumeFeatureTest(unittest.TestCase): - @staticmethod def run_intercept(tmpdir, target, args): - result = os.path.join(tmpdir, 'cdb.json') - make = make_args(tmpdir) + [target] - silent_check_call(['scan-build', 'intercept', '--cdb', result] + - args + make) - return result + result = os.path.join(tmpdir, 'cdb.json') + make = make_args(tmpdir) + [target] + silent_check_call( + ['intercept-build', 'intercept', '--cdb', result] + args + make) + return result def test_overwrite_existing_cdb(self): with fixtures.TempDir() as tmpdir: Index: tools/scan-build-py/tests/functional/cases/test_exec_anatomy.py =================================================================== --- tools/scan-build-py/tests/functional/cases/test_exec_anatomy.py +++ tools/scan-build-py/tests/functional/cases/test_exec_anatomy.py @@ -14,7 +14,8 @@ def run(source_dir, target_dir): def execute(cmd): - return subprocess.check_call(cmd, cwd=target_dir, + return subprocess.check_call(cmd, + cwd=target_dir, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) @@ -23,13 +24,12 @@ result_file = os.path.join(target_dir, 'result.json') expected_file = os.path.join(target_dir, 'expected.json') - execute(['scan-build', 'intercept', '--cdb', result_file, - './exec', expected_file]) + execute(['intercept-build', 'intercept', '--cdb', result_file, './exec', + expected_file]) return (expected_file, result_file) class ExecAnatomyTest(unittest.TestCase): - def assertEqualJson(self, expected, result): def read_json(filename): with open(filename) as handler: Index: tools/scan-build-py/tests/functional/cases/test_from_cdb.py =================================================================== --- tools/scan-build-py/tests/functional/cases/test_from_cdb.py +++ tools/scan-build-py/tests/functional/cases/test_from_cdb.py @@ -29,7 +29,8 @@ def run_driver(directory, cdb, args): - cmd = ['scan-build', 'analyze', '--cdb', cdb, '--output', directory] + args + cmd = ['intercept-build', 'analyze', '--cdb', cdb, '--output', directory] \ + + args child = subprocess.Popen(cmd, universal_newlines=True, stdout=subprocess.PIPE, @@ -41,7 +42,6 @@ class OutputDirectoryTest(unittest.TestCase): - def test_regular_keeps_report_dir(self): with fixtures.TempDir() as tmpdir: cdb = prepare_cdb('regular', tmpdir) @@ -65,7 +65,6 @@ class ExitCodeTest(unittest.TestCase): - def test_regular_does_not_set_exit_code(self): with fixtures.TempDir() as tmpdir: cdb = prepare_cdb('regular', tmpdir) @@ -112,7 +111,6 @@ class OutputFormatTest(unittest.TestCase): - @staticmethod def get_html_count(directory): return len(glob.glob(os.path.join(directory, 'report-*.html'))) @@ -151,7 +149,6 @@ class FailureReportTest(unittest.TestCase): - def test_broken_creates_failure_reports(self): with fixtures.TempDir() as tmpdir: cdb = prepare_cdb('broken', tmpdir) @@ -169,12 +166,12 @@ class TitleTest(unittest.TestCase): - def assertTitleEqual(self, directory, expected): import re patterns = [ re.compile(r'(?P<page>.*)'), - re.compile(r'

(?P.*)

')] + re.compile(r'

(?P.*)

') + ] result = dict() index = os.path.join(directory, 'result', 'index.html') Index: tools/scan-build-py/tests/functional/cases/test_from_cmd.py =================================================================== --- tools/scan-build-py/tests/functional/cases/test_from_cmd.py +++ tools/scan-build-py/tests/functional/cases/test_from_cmd.py @@ -12,10 +12,10 @@ class OutputDirectoryTest(unittest.TestCase): - @staticmethod def run_sb(outdir, args): - return silent_check_call(['scan-build', 'all', '-o', outdir] + args) + return silent_check_call( + ['intercept-build', 'all', '-o', outdir] + args) def test_regular_keeps_report_dir(self): with fixtures.TempDir() as tmpdir: Index: tools/scan-build-py/tests/unit/test_report.py =================================================================== --- tools/scan-build-py/tests/unit/test_report.py +++ tools/scan-build-py/tests/unit/test_report.py @@ -116,7 +116,11 @@ self.assertEqual('file', sut.chop('/prefix/', '/prefix/file')) self.assertEqual('lib/file', sut.chop('/prefix/', '/prefix/lib/file')) self.assertEqual('/prefix/file', sut.chop('', '/prefix/file')) - self.assertEqual('/prefix/file', sut.chop('apple', '/prefix/file')) + + def test_chop_when_cwd(self): + self.assertEqual('../src/file', sut.chop('/cwd', '/src/file')) + self.assertEqual('../src/file', sut.chop('/prefix/cwd', + '/prefix/src/file')) class GetPrefixFromCompilationDatabaseTest(fixtures.TestCase):