[driver] decouple logic for choosing capture mode from the capture

Summary:
Previously, we would first compute which build command is at hand, based on the
first argument after "infer --", then do everything depending on that piece of
information. However, the build command alone is not enough to know in which
"build mode" we are operating. For instance, there are several build modes
corresponding to "buck" build commands.

This led to duplication of the logic (to retrieve which build mode we are in in
the various phases of an infer run), and some invariants that had to be
re-asserted at various points in the code, eg that the arguments are not empty.

This diff adds a `build_mode` type (renaming the previous `build_mode` to
`build_system`) that identifies the various integrations we support. We compute
the build mode at the start of infer, then pass the build mode around.

Also, move `run_javac` to a new `integration/Javac.ml` file given that it's a
bit large.

Reviewed By: jberdine

Differential Revision: D4415074

fbshipit-source-id: db854a0
master
Jules Villard 8 years ago committed by Facebook Github Bot
parent f065f7653a
commit 4028e7ca78

@ -252,7 +252,7 @@ rei:
%.rei : %.mli %.rei : %.mli
refmt -assume-explicit-arity -heuristics-file unary.txt -parse ml -print re $< > $*.rei refmt -assume-explicit-arity -heuristics-file unary.txt -parse ml -print re $< > $*.rei
roots:=Infer InferAnalyzeExe InferClang JMain InferPrintExe StatsAggregator roots:=Infer InferAnalyzeExe InferClang InferPrintExe StatsAggregator
clusters:=base clang java IR clusters:=base clang java IR
ml_src_files:=$(shell find $(DEPENDENCIES) -regex '.*\.ml\(i\)*' -not -path facebook/scripts/eradicate_stats.ml) ml_src_files:=$(shell find $(DEPENDENCIES) -regex '.*\.ml\(i\)*' -not -path facebook/scripts/eradicate_stats.ml)

@ -38,36 +38,66 @@ let rec rmtree name =
() ()
type build_mode = type build_system =
| Analyze | Ant | Buck | ClangCompilationDB | Gradle | Java | Javac | Make | Mvn | Ndk | Xcode | BAnalyze | BAnt | BBuck | BGradle | BJava | BJavac | BMake | BMvn | BNdk | BXcode
let build_mode_of_string path =
match Filename.basename path with
| "analyze" -> Analyze
| "ant" -> Ant
| "buck" -> Buck
| "gradle" | "gradlew" -> Gradle
| "java" -> Java
| "javac" -> Javac
| "cc" | "clang" | "clang++" | "cmake" | "configure" | "g++" | "gcc" | "make" | "waf" -> Make
| "mvn" -> Mvn
| "ndk-build" -> Ndk
| "xcodebuild" -> Xcode
| cmd -> failwithf "Unsupported build command %s" cmd
let string_of_build_mode = function
| Analyze -> "analyze"
| Ant -> "ant"
| Buck -> "buck"
| ClangCompilationDB -> "clang compilation database"
| Gradle -> "gradle"
| Java -> "java"
| Javac -> "javac"
| Make -> "make/cc"
| Mvn -> "maven"
| Ndk -> "ndk-build"
| Xcode -> "xcodebuild"
(* List of ([build system], [executable name]). Several executables may map to the same build
system. In that case, the first one in the list will be used for printing, eg, in which mode
infer is running. *)
let build_system_exe_assoc = [
BAnalyze, "analyze"; BAnt, "ant"; BBuck, "buck"; BGradle, "gradle"; BGradle, "gradlew";
BJava, "java"; BJavac, "javac";
(* NOTE: "make/cc" is not a valid exe name and thus will never be matched, we only use it for
printing *)
BMake, "make/cc"; BMake, "cc"; BMake, "clang"; BMake, "clang++"; BMake, "cmake";
BMake, "configure"; BMake, "g++"; BMake, "gcc"; BMake, "make"; BMake, "waf";
BMvn, "mvn"; BNdk, "ndk-build"; BXcode, "xcodebuild";
]
let build_system_of_exe_name name =
try
List.Assoc.find_exn (List.Assoc.inverse build_system_exe_assoc) name
with Not_found ->
invalid_argf "Unsupported build command %s" name
let string_of_build_system build_system =
List.Assoc.find_exn build_system_exe_assoc build_system
(* based on the build_system and options passed to infer, we run in different driver modes *)
type driver_mode =
| Analyze
| BuckGenrule of string
| BuckCompilationDB
| ClangCompilationDB of string list
| Javac of Javac.compiler * string * string list
| PythonCapture of build_system * string list
| XcodeXcpretty
let pp_driver_mode fmt driver_mode =
let log_argfile_arg fname =
try
F.fprintf fmt "-- Contents of '%s'@\n" fname;
In_channel.iter_lines ~f:(F.fprintf fmt "%s@\n") (In_channel.create fname);
F.fprintf fmt "-- /Contents of '%s'@." fname;
with exn ->
F.fprintf fmt " Error reading file '%s':@\n %a@." fname Exn.pp exn in
match driver_mode with
| Analyze | BuckGenrule _ | BuckCompilationDB | ClangCompilationDB _ | PythonCapture (_,_)
| XcodeXcpretty ->
(* these are pretty boring, do not log anything *)
()
| Javac (_, prog, args) ->
F.fprintf fmt "Javac driver mode:@\nprog = %s@\n" prog;
let log_arg arg =
F.fprintf fmt "Arg: %s@\n" arg;
(* "@fname" means that fname is an arg file containing additional arguments to pass to
javac. *)
String.chop_prefix ~prefix:"@" arg
|>
(* Sometimes these argfiles go away at the end of the build and we cannot inspect them after
the fact, so log them now. *)
Option.iter ~f:log_argfile_arg in
List.iter ~f:log_arg args
let remove_results_dir () = let remove_results_dir () =
rmtree Config.results_dir rmtree Config.results_dir
@ -128,74 +158,6 @@ let run_command ~prog ~args cleanup =
-> cleanup status -> cleanup status
; ok_exn (Unix.Exit_or_signal.or_error status) ; ok_exn (Unix.Exit_or_signal.or_error status)
let run_javac build_mode build_cmd =
let build_prog, build_args =
match build_cmd with
| prog :: args -> (prog, args)
| [] -> invalid_arg "run_java: build command cannot be empty" in
let prog, prog_args =
match build_mode, Config.java_jar_compiler with
| _, None -> (build_prog, ["-J-Duser.language=en"])
| Java, Some jar -> (build_prog, ["-jar"; jar])
| _, Some jar -> (* fall back to java in PATH to avoid passing -jar to javac *)
("java", ["-jar"; jar]) in
let cli_args, file_args =
let rec has_classes_out = function
| [] -> false
| ("-d" | "-classes_out")::_ -> true
| file_arg::tl when String.is_prefix file_arg ~prefix:"@" -> (
let fname = String.slice file_arg 1 (String.length file_arg) in
match In_channel.read_lines fname with
| lines ->
(* crude but we only care about simple cases that will not involve trickiness, eg
unbalanced or escaped quotes such as "ending in\"" *)
let lines_without_quotes =
List.map ~f:(String.strip ~drop:(function '"' | '\'' -> true | _ -> false)) lines in
has_classes_out lines_without_quotes || has_classes_out tl
| exception _ ->
has_classes_out tl)
| _::tl ->
has_classes_out tl in
let args =
"-verbose" :: "-g" ::
(* Ensure that some form of "-d ..." is passed to javac. It's unclear whether this is strictly
needed but the tests break without this for now. See discussion in D4397716. *)
if has_classes_out build_args then
build_args
else
"-d" :: Config.javac_classes_out :: build_args in
List.partition_tf args ~f:(fun arg ->
(* As mandated by javac, argument files must not contain certain arguments. *)
String.is_prefix ~prefix:"-J" arg || String.is_prefix ~prefix:"@" arg) in
(* Pass non-special args via a file to avoid exceeding the command line size limit. *)
let args_file =
let file = Filename.temp_file "args_" "" in
let quoted_file_args =
List.map file_args ~f:(fun arg ->
if String.contains arg '\'' then arg else F.sprintf "'%s'" arg) in
Out_channel.with_file file ~f:(fun oc -> Out_channel.output_lines oc quoted_file_args) ;
file in
let cli_file_args = cli_args @ ["@" ^ args_file] in
let args = prog_args @ cli_file_args in
let verbose_out_file = Filename.temp_file "javac_" ".out" in
Unix.with_file verbose_out_file ~mode:[Unix.O_WRONLY] ~f:(
fun verbose_out_fd ->
L.out "Logging into %s@\n" verbose_out_file;
L.out "Current working directory: '%s'@." (Sys.getcwd ());
try
L.out "Trying to execute: '%s' '%s'@." prog (String.concat ~sep:"' '" args);
Unix_.fork_redirect_exec_wait ~prog ~args ~stderr:verbose_out_fd ()
with exn ->
try
L.out "*** Failed!@\nTrying to execute javac instead: '%s' '%s'@\nLogging into %s@."
"javac" (String.concat ~sep:"' '" cli_file_args) verbose_out_file;
Unix_.fork_redirect_exec_wait ~prog:"javac" ~args:cli_file_args ~stderr:verbose_out_fd ()
with _ ->
L.stderr "Failed to execute: %s %s@." prog (String.concat ~sep:" " args);
raise exn
);
verbose_out_file
let check_xcpretty () = let check_xcpretty () =
match Unix.system "xcpretty --version" with match Unix.system "xcpretty --version" with
| Ok () -> () | Ok () -> ()
@ -212,34 +174,25 @@ let capture_with_compilation_database db_files =
let compilation_database = CompilationDatabase.from_json_files db_files in let compilation_database = CompilationDatabase.from_json_files db_files in
CaptureCompilationDatabase.capture_files_in_database compilation_database CaptureCompilationDatabase.capture_files_in_database compilation_database
let capture build_cmd build_mode = let capture = function
match build_mode, Config.generated_classes with | Analyze->
| _, Some path ->
L.stdout "Capturing for Buck genrule compatibility...@\n";
JMain.main (lazy (JClasspath.load_from_arguments path))
| Analyze, _ ->
() ()
| Buck, _ when Config.use_compilation_database <> None -> | BuckCompilationDB ->
L.stdout "Capturing using Buck's compilation database...@\n"; L.stdout "Capturing using Buck's compilation database...@\n";
let json_cdb = CaptureCompilationDatabase.get_compilation_database_files_buck () in let json_cdb = CaptureCompilationDatabase.get_compilation_database_files_buck () in
capture_with_compilation_database json_cdb capture_with_compilation_database json_cdb
| ClangCompilationDB, _ -> | BuckGenrule path ->
L.stdout "Capturing for Buck genrule compatibility...@\n";
JMain.main (lazy (JClasspath.load_from_arguments path))
| ClangCompilationDB db_files ->
L.stdout "Capturing using compilation database...@\n"; L.stdout "Capturing using compilation database...@\n";
capture_with_compilation_database !Config.clang_compilation_db_files capture_with_compilation_database db_files
| (Java | Javac), _ -> | Javac (compiler, prog, args) ->
L.stdout "Capturing in javac mode...@."; L.stdout "Capturing in javac mode...@.";
let verbose_out_file = run_javac build_mode build_cmd in Javac.capture compiler ~prog ~args
if Config.analyzer <> Config.Compile then | PythonCapture (build_system, build_cmd) ->
JMain.main (lazy (JClasspath.load_from_verbose_output verbose_out_file)) ; L.stdout "Capturing in %s mode...@." (string_of_build_system build_system);
if not (Config.debug_mode || Config.stats_mode) then Unix.unlink verbose_out_file; let in_buck_mode = build_system = BBuck in
| Xcode, _ when Config.xcpretty ->
L.stdout "Capturing using xcpretty...@\n";
check_xcpretty ();
let json_cdb = CaptureCompilationDatabase.get_compilation_database_files_xcodebuild () in
capture_with_compilation_database json_cdb
| build_mode, _ ->
L.stdout "Capturing in %s mode...@." (string_of_build_mode build_mode);
let in_buck_mode = build_mode = Buck in
let infer_py = Config.lib_dir ^/ "python" ^/ "infer.py" in let infer_py = Config.lib_dir ^/ "python" ^/ "infer.py" in
let args = let args =
List.rev_append Config.anon_args ( List.rev_append Config.anon_args (
@ -289,6 +242,11 @@ let capture build_cmd build_mode =
(* swallow infer.py argument parsing error *) (* swallow infer.py argument parsing error *)
Config.print_usage_exit () Config.print_usage_exit ()
) )
| XcodeXcpretty ->
L.stdout "Capturing using xcpretty...@\n";
check_xcpretty ();
let json_cdb = CaptureCompilationDatabase.get_compilation_database_files_xcodebuild () in
capture_with_compilation_database json_cdb
let run_parallel_analysis () = let run_parallel_analysis () =
let multicore_dir = Config.results_dir ^/ Config.multicore_dir_name in let multicore_dir = Config.results_dir ^/ Config.multicore_dir_name in
@ -332,21 +290,20 @@ let report () =
L.stderr "** Error running the reporting script:@\n** %s %s@\n** See error above@." L.stderr "** Error running the reporting script:@\n** %s %s@\n** See error above@."
prog (String.concat ~sep:" " args) prog (String.concat ~sep:" " args)
let analyze = function let analyze driver_mode =
| Buck when Config.use_compilation_database = None -> let should_analyze, should_report = match driver_mode, Config.analyzer with
(* In Buck mode when compilation db is not used, analysis is invoked either from capture or a | PythonCapture (BBuck, _), _ ->
separate Analyze invocation is necessary, depending on the buck flavor used. *) (* In Buck mode when compilation db is not used, analysis is invoked either from capture or
() a separate Analyze invocation is necessary, depending on the buck flavor used. *)
| _ -> false, false
let should_analyze, should_report = match Config.analyzer with | _, (Capture | Compile) ->
| Infer | Eradicate | Checkers | Tracing | Crashcontext | Quandary | Threadsafety -> false, false
| _, (Infer | Eradicate | Checkers | Tracing | Crashcontext | Quandary | Threadsafety) ->
true, true true, true
| Linters -> | _, Linters ->
false, true false, true in
| Capture | Compile -> if (should_analyze || should_report) &&
false, false in (Sys.file_exists Config.(results_dir ^/ captured_dir_name)) <> `Yes then (
if (should_analyze || should_report)
&& (Sys.file_exists Config.(results_dir ^/ captured_dir_name)) <> `Yes then (
L.stderr "There was nothing to analyze, exiting@." ; L.stderr "There was nothing to analyze, exiting@." ;
exit 1 exit 1
); );
@ -362,41 +319,53 @@ let fail_on_issue_epilogue () =
if issues <> [] then exit Config.fail_on_issue_exit_code if issues <> [] then exit Config.fail_on_issue_exit_code
| None -> () | None -> ()
let log_build_cmd build_mode build_cmd = let log_infer_args driver_mode =
L.out "INFER_ARGS=%s@." (Option.value (Sys.getenv CLOpt.args_env_var) ~default:"<not found>"); L.out "INFER_ARGS = %s@\n" (Option.value (Sys.getenv CLOpt.args_env_var) ~default:"<not found>");
L.out "Project root = %s@." Config.project_root; List.iter ~f:(L.out "anon arg: %s@\n") Config.anon_args;
let log_arg arg = List.iter ~f:(L.out "rest arg: %s@\n") Config.rest;
L.out "Arg: %s@\n" arg; L.out "Project root = %s@\n" Config.project_root;
if (build_mode = Java || build_mode = Javac) && (String.is_prefix arg ~prefix:"@") then ( L.out "CWD = %s@\n" (Sys.getcwd ());
let fname = String.slice arg 1 (String.length arg) in L.out "Driver mode:@\n%a@." pp_driver_mode driver_mode
match In_channel.input_lines (In_channel.create fname) with
| lines ->
L.out "-- Contents of '%s'@\n" fname;
L.out "%s@\n" (String.concat ~sep:"\n" lines);
L.out "-- /Contents of '%s'@\n" fname;
| exception exn ->
L.out " Error reading file '%s':@\n %a@." fname Exn.pp exn
) in
List.iter ~f:log_arg build_cmd
let () = let driver_mode_of_build_cmd build_cmd =
let build_cmd = IList.rev Config.rest in match build_cmd with
let build_mode = match build_cmd with
| path :: _ ->
build_mode_of_string path
| [] -> | [] ->
if not (List.is_empty !Config.clang_compilation_db_files) then if not (List.is_empty !Config.clang_compilation_db_files) then
ClangCompilationDB ClangCompilationDB !Config.clang_compilation_db_files
else else
Analyze in Analyze
if not (build_mode = Analyze || Config.(buck || continue_capture || reactive_mode)) then | prog :: args ->
match build_system_of_exe_name (Filename.basename prog) with
| BAnalyze ->
Analyze
| BBuck when Config.use_compilation_database <> None ->
BuckCompilationDB
| BJava ->
Javac (Javac.Java, prog, args)
| BJavac ->
Javac (Javac.Javac, prog, args)
| BXcode when Config.xcpretty ->
XcodeXcpretty
| BAnt | BBuck | BGradle | BMake | BMvn | BNdk | BXcode as build_system ->
PythonCapture (build_system, build_cmd)
let get_driver_mode () =
match Config.generated_classes with
| Some path ->
BuckGenrule path
| None ->
driver_mode_of_build_cmd (IList.rev Config.rest)
let () =
let driver_mode = get_driver_mode () in
if not (driver_mode = Analyze || Config.(buck || continue_capture || reactive_mode)) then
remove_results_dir () ; remove_results_dir () ;
create_results_dir () ; create_results_dir () ;
(* re-set log files, as default files were in results_dir removed above *) (* re-set log files, as default files were in results_dir removed above *)
L.set_log_file_identifier Config.current_exe None ; L.set_log_file_identifier Config.current_exe None ;
if Config.print_builtins then Builtin.print_and_exit () ; if Config.print_builtins then Builtin.print_and_exit () ;
if Config.is_originator then L.do_out "%s@\n" Config.version_string ; if Config.is_originator then L.do_out "%s@\n" Config.version_string ;
if Config.debug_mode || Config.stats_mode then log_build_cmd build_mode build_cmd; if Config.debug_mode || Config.stats_mode then log_infer_args driver_mode ;
(* infer might be called from a Makefile and itself uses `make` to run the analysis in parallel, (* infer might be called from a Makefile and itself uses `make` to run the analysis in parallel,
but cannot communicate with the parent make command. Since infer won't interfere with them but cannot communicate with the parent make command. Since infer won't interfere with them
anyway, pretend that we are not called from another make to prevent make falling back to a anyway, pretend that we are not called from another make to prevent make falling back to a
@ -404,13 +373,14 @@ let () =
Unix.unsetenv "MAKEFLAGS"; Unix.unsetenv "MAKEFLAGS";
register_perf_stats_report () ; register_perf_stats_report () ;
touch_start_file () ; touch_start_file () ;
capture build_cmd build_mode ; capture driver_mode ;
analyze build_mode ; analyze driver_mode ;
if Config.is_originator then ( if Config.is_originator then (
StatsAggregator.generate_files () ; StatsAggregator.generate_files () ;
let in_buck_mode = match driver_mode with | PythonCapture (BBuck, _) -> true | _ -> false in
if Config.analyzer = Config.Crashcontext then if Config.analyzer = Config.Crashcontext then
Crashcontext.crashcontext_epilogue ~in_buck_mode:(build_mode = Buck); Crashcontext.crashcontext_epilogue ~in_buck_mode;
if build_mode = Buck then if in_buck_mode then
clean_results_dir () ; clean_results_dir () ;
if Config.fail_on_bug then if Config.fail_on_bug then
fail_on_issue_epilogue () ; fail_on_issue_epilogue () ;

@ -0,0 +1,86 @@
(*
* Copyright (c) 2017 - 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.
*)
open! IStd
module L = Logging
module F = Format
type compiler = Java | Javac
let compile compiler build_prog build_args =
let prog, prog_args =
match compiler, Config.java_jar_compiler with
| _, None -> (build_prog, ["-J-Duser.language=en"])
| Java, Some jar -> (build_prog, ["-jar"; jar])
| _, Some jar -> (* fall back to java in PATH to avoid passing -jar to javac *)
("java", ["-jar"; jar]) in
let cli_args, file_args =
let rec has_classes_out = function
| [] -> false
| ("-d" | "-classes_out")::_ -> true
| file_arg::tl when String.is_prefix file_arg ~prefix:"@" -> (
let fname = String.slice file_arg 1 (String.length file_arg) in
match In_channel.read_lines fname with
| lines ->
(* crude but we only care about simple cases that will not involve trickiness, eg
unbalanced or escaped quotes such as "ending in\"" *)
let lines_without_quotes =
List.map ~f:(String.strip ~drop:(function '"' | '\'' -> true | _ -> false)) lines in
has_classes_out lines_without_quotes || has_classes_out tl
| exception _ ->
has_classes_out tl)
| _::tl ->
has_classes_out tl in
let args =
"-verbose" :: "-g" ::
(* Ensure that some form of "-d ..." is passed to javac. It's unclear whether this is strictly
needed but the tests break without this for now. See discussion in D4397716. *)
if has_classes_out build_args then
build_args
else
"-d" :: Config.javac_classes_out :: build_args in
List.partition_tf args ~f:(fun arg ->
(* As mandated by javac, argument files must not contain certain arguments. *)
String.is_prefix ~prefix:"-J" arg || String.is_prefix ~prefix:"@" arg) in
(* Pass non-special args via a file to avoid exceeding the command line size limit. *)
let args_file =
let file = Filename.temp_file "args_" "" in
let quoted_file_args =
List.map file_args ~f:(fun arg ->
if String.contains arg '\'' then arg else F.sprintf "'%s'" arg) in
Out_channel.with_file file ~f:(fun oc -> Out_channel.output_lines oc quoted_file_args) ;
file in
let cli_file_args = cli_args @ ["@" ^ args_file] in
let args = prog_args @ cli_file_args in
let verbose_out_file = Filename.temp_file "javac_" ".out" in
Unix.with_file verbose_out_file ~mode:[Unix.O_WRONLY] ~f:(
fun verbose_out_fd ->
L.out "Logging into %s@\n" verbose_out_file;
L.out "Current working directory: '%s'@." (Sys.getcwd ());
try
L.out "Trying to execute: '%s' '%s'@." prog (String.concat ~sep:"' '" args);
Unix_.fork_redirect_exec_wait ~prog ~args ~stderr:verbose_out_fd ()
with exn ->
try
L.out "*** Failed!@\nTrying to execute javac instead: '%s' '%s'@\nLogging into %s@."
"javac" (String.concat ~sep:"' '" cli_file_args) verbose_out_file;
Unix_.fork_redirect_exec_wait ~prog:"javac" ~args:cli_file_args ~stderr:verbose_out_fd ()
with _ ->
L.stderr "Failed to execute: %s %s@." prog (String.concat ~sep:" " args);
raise exn
);
verbose_out_file
let capture compiler ~prog ~args =
let verbose_out_file = compile compiler prog args in
if Config.analyzer <> Config.Compile then
JMain.main (lazy (JClasspath.load_from_verbose_output verbose_out_file)) ;
if not (Config.debug_mode || Config.stats_mode) then Unix.unlink verbose_out_file

@ -0,0 +1,14 @@
(*
* Copyright (c) 2017 - 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.
*)
open! IStd
type compiler = Java | Javac
val capture : compiler -> prog:string -> args:string list -> unit
Loading…
Cancel
Save