diff --git a/infer/src/Makefile b/infer/src/Makefile index 2f1a131a0..1f6d89745 100644 --- a/infer/src/Makefile +++ b/infer/src/Makefile @@ -252,7 +252,7 @@ rei: %.rei : %.mli 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 ml_src_files:=$(shell find $(DEPENDENCIES) -regex '.*\.ml\(i\)*' -not -path facebook/scripts/eradicate_stats.ml) diff --git a/infer/src/backend/infer.ml b/infer/src/backend/infer.ml index 94fa27287..bce13ccb4 100644 --- a/infer/src/backend/infer.ml +++ b/infer/src/backend/infer.ml @@ -38,36 +38,66 @@ let rec rmtree name = () -type build_mode = - | Analyze | Ant | Buck | ClangCompilationDB | Gradle | Java | Javac | Make | Mvn | Ndk | Xcode - -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" +type build_system = + | BAnalyze | BAnt | BBuck | BGradle | BJava | BJavac | BMake | BMvn | BNdk | BXcode +(* 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 () = rmtree Config.results_dir @@ -128,74 +158,6 @@ let run_command ~prog ~args cleanup = -> cleanup 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 () = match Unix.system "xcpretty --version" with | Ok () -> () @@ -212,34 +174,25 @@ let capture_with_compilation_database db_files = let compilation_database = CompilationDatabase.from_json_files db_files in CaptureCompilationDatabase.capture_files_in_database compilation_database -let capture build_cmd build_mode = - match build_mode, Config.generated_classes with - | _, Some path -> - L.stdout "Capturing for Buck genrule compatibility...@\n"; - JMain.main (lazy (JClasspath.load_from_arguments path)) - | Analyze, _ -> +let capture = function + | Analyze-> () - | Buck, _ when Config.use_compilation_database <> None -> + | BuckCompilationDB -> L.stdout "Capturing using Buck's compilation database...@\n"; let json_cdb = CaptureCompilationDatabase.get_compilation_database_files_buck () in 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"; - capture_with_compilation_database !Config.clang_compilation_db_files - | (Java | Javac), _ -> + capture_with_compilation_database db_files + | Javac (compiler, prog, args) -> L.stdout "Capturing in javac mode...@."; - let verbose_out_file = run_javac build_mode build_cmd 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; - | 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 + Javac.capture compiler ~prog ~args + | PythonCapture (build_system, build_cmd) -> + L.stdout "Capturing in %s mode...@." (string_of_build_system build_system); + let in_buck_mode = build_system = BBuck in let infer_py = Config.lib_dir ^/ "python" ^/ "infer.py" in let args = List.rev_append Config.anon_args ( @@ -289,6 +242,11 @@ let capture build_cmd build_mode = (* swallow infer.py argument parsing error *) 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 multicore_dir = Config.results_dir ^/ Config.multicore_dir_name in @@ -332,26 +290,25 @@ let report () = L.stderr "** Error running the reporting script:@\n** %s %s@\n** See error above@." prog (String.concat ~sep:" " args) -let analyze = function - | Buck when Config.use_compilation_database = None -> - (* 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. *) - () - | _ -> - let should_analyze, should_report = match Config.analyzer with - | Infer | Eradicate | Checkers | Tracing | Crashcontext | Quandary | Threadsafety -> - true, true - | Linters -> - false, true - | Capture | Compile -> - false, false in - if (should_analyze || should_report) - && (Sys.file_exists Config.(results_dir ^/ captured_dir_name)) <> `Yes then ( - L.stderr "There was nothing to analyze, exiting@." ; - exit 1 - ); - if should_analyze then execute_analyze (); - if should_report then report () +let analyze driver_mode = + let should_analyze, should_report = match driver_mode, Config.analyzer with + | PythonCapture (BBuck, _), _ -> + (* 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 + | _, (Capture | Compile) -> + false, false + | _, (Infer | Eradicate | Checkers | Tracing | Crashcontext | Quandary | Threadsafety) -> + true, true + | _, Linters -> + false, true in + if (should_analyze || should_report) && + (Sys.file_exists Config.(results_dir ^/ captured_dir_name)) <> `Yes then ( + L.stderr "There was nothing to analyze, exiting@." ; + exit 1 + ); + if should_analyze then execute_analyze (); + if should_report then report () (** as the Config.fail_on_bug flag mandates, exit with error when an issue is reported *) let fail_on_issue_epilogue () = @@ -362,41 +319,53 @@ let fail_on_issue_epilogue () = if issues <> [] then exit Config.fail_on_issue_exit_code | None -> () -let log_build_cmd build_mode build_cmd = - L.out "INFER_ARGS=%s@." (Option.value (Sys.getenv CLOpt.args_env_var) ~default:""); - L.out "Project root = %s@." Config.project_root; - let log_arg arg = - L.out "Arg: %s@\n" arg; - if (build_mode = Java || build_mode = Javac) && (String.is_prefix arg ~prefix:"@") then ( - let fname = String.slice arg 1 (String.length arg) in - 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 log_infer_args driver_mode = + L.out "INFER_ARGS = %s@\n" (Option.value (Sys.getenv CLOpt.args_env_var) ~default:""); + List.iter ~f:(L.out "anon arg: %s@\n") Config.anon_args; + List.iter ~f:(L.out "rest arg: %s@\n") Config.rest; + L.out "Project root = %s@\n" Config.project_root; + L.out "CWD = %s@\n" (Sys.getcwd ()); + L.out "Driver mode:@\n%a@." pp_driver_mode driver_mode + +let driver_mode_of_build_cmd build_cmd = + match build_cmd with + | [] -> + if not (List.is_empty !Config.clang_compilation_db_files) then + ClangCompilationDB !Config.clang_compilation_db_files + else + Analyze + | 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 build_cmd = IList.rev Config.rest in - let build_mode = match build_cmd with - | path :: _ -> - build_mode_of_string path - | [] -> - if not (List.is_empty !Config.clang_compilation_db_files) then - ClangCompilationDB - else - Analyze in - if not (build_mode = Analyze || Config.(buck || continue_capture || reactive_mode)) then + let driver_mode = get_driver_mode () in + if not (driver_mode = Analyze || Config.(buck || continue_capture || reactive_mode)) then remove_results_dir () ; create_results_dir () ; (* re-set log files, as default files were in results_dir removed above *) L.set_log_file_identifier Config.current_exe None ; if Config.print_builtins then Builtin.print_and_exit () ; 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, 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 @@ -404,13 +373,14 @@ let () = Unix.unsetenv "MAKEFLAGS"; register_perf_stats_report () ; touch_start_file () ; - capture build_cmd build_mode ; - analyze build_mode ; + capture driver_mode ; + analyze driver_mode ; if Config.is_originator then ( StatsAggregator.generate_files () ; + let in_buck_mode = match driver_mode with | PythonCapture (BBuck, _) -> true | _ -> false in if Config.analyzer = Config.Crashcontext then - Crashcontext.crashcontext_epilogue ~in_buck_mode:(build_mode = Buck); - if build_mode = Buck then + Crashcontext.crashcontext_epilogue ~in_buck_mode; + if in_buck_mode then clean_results_dir () ; if Config.fail_on_bug then fail_on_issue_epilogue () ; diff --git a/infer/src/integration/Javac.ml b/infer/src/integration/Javac.ml new file mode 100644 index 000000000..0d934641a --- /dev/null +++ b/infer/src/integration/Javac.ml @@ -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 diff --git a/infer/src/integration/Javac.mli b/infer/src/integration/Javac.mli new file mode 100644 index 000000000..39f16b18a --- /dev/null +++ b/infer/src/integration/Javac.mli @@ -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