#!/usr/bin/env python2.7 # Copyright (c) 2015 - present Facebook, Inc. # All rights reserved. # # This source code is licensed under the BSD style license found in the # LICENSE file in the root directory of this source tree. An additional grant # of patent rights can be found in the PATENTS file in the same directory. from __future__ import absolute_import from __future__ import division from __future__ import print_function from __future__ import unicode_literals import argparse import imp import locale import logging import os import platform import sys import inferlib from inferlib import analyze, config, utils from inferlib.capture import make CAPTURE_PACKAGE = 'capture' TOP_LEVEL_ENVVAR = 'TOP_LEVEL_INFER_INSTANCE_ALREADY_RUNNING' # token that identifies the end of the options for infer and the beginning # of the compilation command CMD_MARKER = '--' # insert here the correspondence between module name and the list of # compiler/build-systems it handles. # All supported commands should be listed here MODULE_TO_COMMAND = { 'ant': ['ant'], 'analyze': ['analyze'], 'buck': ['buck'], 'gradle': ['gradle', 'gradlew'], 'javac': ['javac'], 'java': ['java'], 'make': make.SUPPORTED_COMMANDS, 'xcodebuild': ['xcodebuild'], 'mvn': ['mvn'], 'ndk-build': ['ndk-build'], 'clang-compilation-database': ['clang-compilation-database'], } def get_commands(): """Return all commands that are supported.""" # flatten and dedup the list of commands return set(sum(MODULE_TO_COMMAND.values(), [])) def get_module_name(command): """ Return module that is able to handle the command. None if there is no such module.""" for module, commands in MODULE_TO_COMMAND.iteritems(): if command in commands: return module return None def load_module(mod_name): pkg_info = imp.find_module(CAPTURE_PACKAGE, inferlib.__path__) imported_pkg = imp.load_module(CAPTURE_PACKAGE, *pkg_info) # load the requested module (e.g. make) mod_file, mod_path, mod_descr = \ imp.find_module(mod_name, imported_pkg.__path__) try: return imp.load_module( '{pkg}.{mod}'.format(pkg=imported_pkg.__name__, mod=mod_name), mod_file, mod_path, mod_descr) finally: if mod_file: mod_file.close() def split_args_to_parse(): sys_argv = map(utils.decode, sys.argv) dd_index = \ sys_argv.index(CMD_MARKER) if CMD_MARKER in sys_argv else len(sys_argv) cmd_raw = sys_argv[dd_index + 1:] return (sys_argv[1:dd_index], cmd_raw) class FailSilentlyArgumentParser(argparse.ArgumentParser): '''We want to leave the handling of printing usage messages to the OCaml code. To do so, swallow error messages from ArgumentParser and exit with a special error code (101) that infer.ml looks for. ''' def error(self, message): utils.stderr(message) utils.stderr('') exit(22) # in sync with infer.ml def print_help(self, file=None): exit(22) # in sync with infer.ml def create_argparser(parents=[]): parser = FailSilentlyArgumentParser( parents=[analyze.infer_parser] + parents, add_help=False, formatter_class=argparse.RawDescriptionHelpFormatter, ) group = parser.add_argument_group( 'supported compiler/build-system commands') supported_commands = ', '.join(get_commands()) group.add_argument( CMD_MARKER, metavar='', dest='nullarg', default=None, help=('Command to run the compiler/build-system. ' 'Supported build commands (run `infer --help -- ` for ' 'extra help, e.g. `infer --help -- javac`): {}'.format( supported_commands)), ) return parser def main(): toplevel_envvar_value = os.environ.get(TOP_LEVEL_ENVVAR, None) is_toplevel_instance = False if toplevel_envvar_value is None: os.environ[TOP_LEVEL_ENVVAR] = '1' is_toplevel_instance = True to_parse, cmd = split_args_to_parse() # get the module name (if any), then load it capture_module_name = os.path.basename(cmd[0]) if len(cmd) > 0 else None mod_name = get_module_name(capture_module_name) imported_module = None if mod_name: # There is module that supports the command imported_module = load_module(mod_name) # get the module's argparser and merge it with the global argparser module_argparser = [] if imported_module: module_argparser.append( imported_module.create_argparser(capture_module_name) ) global_argparser = create_argparser(module_argparser) args = global_argparser.parse_args(to_parse) remove_infer_out = (imported_module is not None and not args.reactive and capture_module_name != 'analyze' and not args.buck) if remove_infer_out: analyze.remove_infer_out(args.infer_out) if imported_module is not None: analyze.create_results_dir(args.infer_out) analyze.reset_start_file(args.infer_out, touch_if_present=not args.continue_capture) utils.configure_logging(args) logging.info('output of locale.getdefaultlocale(): %s', str(locale.getdefaultlocale())) logging.info('encoding we chose in the end: %s', config.CODESET) logging.info('Running command %s', ' '.join(map(utils.decode, sys.argv))) logging.info('Path to infer script %s (%s)', utils.decode(__file__), os.path.realpath(utils.decode(__file__))) if not args.buck: # Should not print the Infer version when Infer is run by Buck. # Buck reads the whole stderr output to compute the target key logging.info(analyze.get_infer_version()) logging.info('Platform: %s', utils.decode(platform.platform())) def log_getenv(k): v = os.getenv(k) if v is not None: v = utils.decode(v) else: v = '' logging.info('%s=%s', k, v) log_getenv('PATH') log_getenv('SHELL') log_getenv('PWD') capture_exitcode = imported_module.gen_instance(args, cmd).capture() if capture_exitcode != os.EX_OK: logging.error('Error during capture phase, exiting') exit(capture_exitcode) logging.info('Capture phase was successful') elif capture_module_name is not None: # There was a command, but it's not supported utils.stdout('Command "{cmd}" not recognised' .format(cmd='' if capture_module_name is None else capture_module_name)) global_argparser.print_help() sys.exit(1) else: global_argparser.print_help() sys.exit(os.EX_OK) buck_not_in_compilation_database_mode = \ mod_name == 'buck' and not args.use_compilation_database if not (buck_not_in_compilation_database_mode or mod_name == 'javac' or mod_name == 'java'): analysis = analyze.AnalyzerWrapper(args) analysis.analyze_and_report() analysis.save_stats() if is_toplevel_instance is True: buck_out_for_stats_aggregator = None if (mod_name == 'buck' and os.path.isfile( os.path.join(args.infer_out, config.INFER_BUCK_DEPS_FILENAME))): buck_out_for_stats_aggregator = 'buck-out' logging.info('Aggregating stats') output = utils.run_infer_stats_aggregator( args.infer_out, buck_out_for_stats_aggregator) logging.info(output) if __name__ == '__main__': main()