From 513eee305998b111caaab597edbce3ac003f19d8 Mon Sep 17 00:00:00 2001 From: martinoluca Date: Mon, 2 Nov 2015 08:21:05 -0800 Subject: [PATCH] Adding initial support for Buck for Cxx with flavored targets Summary: public Adding initial support for Buck for Cxx on the toplevel infer command. If a `cxx_binary`/`cxx_library` builds with the command `buck build //targetName:target` then the analysis of Infer can be triggered through the `#infer` flavor. This change configures buck to run Infer, whenever the `#infer` flavor gets passed: just run `infer --use-flavors -- buck build //targetName:target#infer` Please note that when using this feature (`--use-flavors`), the toplevel `infer` command has to be launched from the root of the buck project (which is where the `.buckconfig` file is located) and the `#infer` flavor must be passed on the target. Reviewed By: jvillard Differential Revision: D2508296 fb-gh-sync-id: 63c0dea --- infer/bin/utils.py | 35 +++++++--- infer/lib/capture/buck.py | 109 ++++++++++++++++++++++++++++---- infer/lib/capture/xcodebuild.py | 12 +--- 3 files changed, 127 insertions(+), 29 deletions(-) diff --git a/infer/bin/utils.py b/infer/bin/utils.py index 82f47b930..90233f439 100644 --- a/infer/bin/utils.py +++ b/infer/bin/utils.py @@ -109,11 +109,17 @@ def error(msg): print(msg, file=sys.stderr) -def get_cmd_in_bin_dir(binary_name): +def get_infer_bin(): # this relies on the fact that utils.py is located in infer/bin - return os.path.join( - os.path.dirname(os.path.realpath(__file__)), - binary_name) + return BIN_DIRECTORY + + +def get_cmd_in_bin_dir(binary_name): + return os.path.join(get_infer_bin(), binary_name) + + +def get_infer_root(): + return os.path.join(get_infer_bin(), '..', '..') def write_cmd_streams_to_file(logfile, cmd=None, out=None, err=None): @@ -344,6 +350,20 @@ def invoke_function_with_callbacks( raise +def save_as_json(data, filename): + with open(filename, 'w') as file_out: + json.dump(data, file_out, indent=2) + + +def merge_json_reports(report_paths, merged_report_path): + # TODO: use streams instead of loading the entire json in memory + json_data = [] + for json_path in report_paths: + with open(json_path, 'r') as fd: + json_data = json_data + json.loads(fd.read()) + save_as_json(json_data, merged_report_path) + + def create_json_report(out_dir): csv_report_filename = os.path.join(out_dir, CSV_REPORT_FILENAME) json_report_filename = os.path.join(out_dir, JSON_REPORT_FILENAME) @@ -351,10 +371,9 @@ def create_json_report(out_dir): with open(csv_report_filename, 'r') as file_in: reader = csv.reader(file_in) rows = [row for row in reader] - with open(json_report_filename, 'w') as file_out: - headers = rows[0] - issues = [dict(zip(headers, row)) for row in rows[1:]] - json.dump(issues, file_out, indent=2) + headers = rows[0] + issues = [dict(zip(headers, row)) for row in rows[1:]] + save_as_json(issues, json_report_filename) def get_plural(_str, count): diff --git a/infer/lib/capture/buck.py b/infer/lib/capture/buck.py index f100b795e..efd671a55 100644 --- a/infer/lib/capture/buck.py +++ b/infer/lib/capture/buck.py @@ -44,19 +44,110 @@ def create_argparser(group_name=MODULE_NAME): 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'])) - self.cmd = cmd[2:] # TODO: make the extraction of targets smarter - 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 = os.path.join( + utils.get_infer_root(), + 'facebook-clang-plugins', + ) + 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 inferlib.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: @@ -71,13 +162,7 @@ class BuckAnalyzer: capture_cmd.append('--no-cache') if self.args.print_harness: capture_cmd.append('--print-harness') - - capture_cmd += self.cmd + capture_cmd += self.cmd[2:] # TODO: make extraction of targets smarter capture_cmd += ['--analyzer', self.args.analyzer] - try: - subprocess.check_call(capture_cmd) - return os.EX_OK - except subprocess.CalledProcessError as exc: - if self.args.debug: - traceback.print_exc() - return exc.returncode + subprocess.check_call(capture_cmd) + return os.EX_OK diff --git a/infer/lib/capture/xcodebuild.py b/infer/lib/capture/xcodebuild.py index 3d7fe3f18..b1c8e5549 100644 --- a/infer/lib/capture/xcodebuild.py +++ b/infer/lib/capture/xcodebuild.py @@ -19,15 +19,9 @@ Analysis examples: infer -- xcodebuild -target HelloWorldApp -sdk iphonesimulator infer -- xcodebuild -workspace HelloWorld.xcworkspace -scheme HelloWorld''' -SCRIPT_DIR = os.path.dirname(__file__) -INFER_ROOT = os.path.join(SCRIPT_DIR, '..', '..', '..') -FCP_ROOT = os.path.join(INFER_ROOT, 'facebook-clang-plugins') -CLANG_WRAPPER = os.path.join( - SCRIPT_DIR, 'clang', -) -CLANGPLUSPLUS_WRAPPER = os.path.join( - SCRIPT_DIR, 'clang++', -) +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) +CLANG_WRAPPER = os.path.join(SCRIPT_DIR, 'clang') +CLANGPLUSPLUS_WRAPPER = os.path.join(SCRIPT_DIR, 'clang++') def gen_instance(*args):