# 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. import argparse import os import subprocess import traceback import util import logging from inferlib import utils MODULE_NAME = __name__ MODULE_DESCRIPTION = '''Run analysis of code built with a command like: buck [options] [target] Analysis examples: infer -- buck build HelloWorld''' def gen_instance(*args): return BuckAnalyzer(*args) # This creates an empty argparser for the module, which provides only # description/usage information and no arguments. def create_argparser(group_name=MODULE_NAME): """This defines the set of arguments that get added by this module to the set of global args defined in the infer top-level module Do not use this function directly, it should be invoked by the infer top-level module""" parser = argparse.ArgumentParser(add_help=False) group = parser.add_argument_group( "{grp} module".format(grp=MODULE_NAME), description=MODULE_DESCRIPTION, ) group.add_argument('--verbose', action='store_true', help='Print buck compilation steps') group.add_argument('--no-cache', action='store_true', help='Do not use buck distributed cache') group.add_argument('--print-harness', action='store_true', help='Print generated harness code (Android only)') group.add_argument('--use-flavors', action='store_true', help='Run Infer analysis through the use of flavors. ' 'Currently this is supported only for the cxx_* ' 'targets of Buck - e.g. cxx_library, cxx_binary - ' 'and not for Java. Note: this flag should be used ' 'in combination with passing the #infer flavor ' 'to the Buck target.') group.add_argument('--xcode-developer-dir', help='Specify the path to Xcode developer directory ' '(requires --use-flavors to work)') return parser class BuckAnalyzer: def __init__(self, args, cmd): self.args = args self.cmd = cmd util.log_java_version() logging.info(util.run_cmd_ignore_fail(['buck', '--version'])) def capture(self): try: if self.args.use_flavors: return self.capture_with_flavors() else: return self.capture_without_flavors() except subprocess.CalledProcessError as exc: if self.args.debug: traceback.print_exc() return exc.returncode def create_cxx_buck_configuration_args(self): # return a string that can be passed in input to buck # and configures the paths to infer/clang/plugin/xcode facebook_clang_plugins_root = utils.FCP_DIRECTORY clang_path = os.path.join( facebook_clang_plugins_root, 'clang', 'bin', 'clang', ) plugin_path = os.path.join( facebook_clang_plugins_root, 'libtooling', 'build', 'FacebookClangPlugin.dylib', ) args = [ '--config', 'infer.infer_bin={bin}'.format(bin=utils.get_infer_bin()), '--config', 'infer.clang_compiler={clang}'.format(clang=clang_path), '--config', 'infer.clang_plugin={plugin}'.format(plugin=plugin_path), ] if self.args.xcode_developer_dir is not None: args.append('--config') args.append('apple.xcode_developer_dir={devdir}'.format( devdir=self.args.xcode_developer_dir)) return args def _get_analysis_result_files(self): # TODO(8610738): Make targets extraction smarter buck_results_cmd = [ self.cmd[0], 'targets', '--show-output' ] + self.cmd[2:] + self.create_cxx_buck_configuration_args() proc = subprocess.Popen(buck_results_cmd, stdout=subprocess.PIPE) (buck_output, _) = proc.communicate() # remove target name prefixes from each line and split them into a list out = [x.split(None, 1)[1] for x in buck_output.strip().split('\n')] # from the resulting list, get only what ends in json return [x for x in out if x.endswith('.json')] def _run_buck_with_flavors(self): # TODO: Use buck to identify the project's root folder if not os.path.isfile('.buckconfig'): print('Please run this command from the folder where .buckconfig ' 'is located') return os.EX_USAGE subprocess.check_call( self.cmd + self.create_cxx_buck_configuration_args()) return os.EX_OK def capture_with_flavors(self): ret = self._run_buck_with_flavors() if not ret == os.EX_OK: return ret result_files = self._get_analysis_result_files() merged_results_path = \ os.path.join(self.args.infer_out, utils.JSON_REPORT_FILENAME) utils.merge_json_reports( result_files, merged_results_path) # TODO: adapt infer.print_errors to support json and print on screen print('Results saved in {results_path}'.format( results_path=merged_results_path)) return os.EX_OK def capture_without_flavors(self): # BuckAnalyze is a special case, and we run the analysis from here capture_cmd = [utils.get_cmd_in_bin_dir('BuckAnalyze')] if self.args.infer_out is not None: capture_cmd += ['--out', self.args.infer_out] if self.args.debug: capture_cmd.append('-g') if self.args.no_filtering: capture_cmd.append('--no-filtering') if self.args.verbose: capture_cmd.append('--verbose') if self.args.no_cache: capture_cmd.append('--no-cache') if self.args.print_harness: capture_cmd.append('--print-harness') capture_cmd += self.cmd[2:] # TODO: make extraction of targets smarter capture_cmd += ['--analyzer', self.args.analyzer] subprocess.check_call(capture_cmd) return os.EX_OK