|
|
|
(*
|
|
|
|
* Copyright (c) 2016 - 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.
|
|
|
|
*)
|
|
|
|
|
|
|
|
(** Given a clang command, normalize it via `clang -###` if needed to get a clear view of what work
|
|
|
|
is being done and which source files are being compiled, if any, then replace compilation
|
|
|
|
commands by our own clang with our plugin attached for each source file. *)
|
|
|
|
open! IStd
|
|
|
|
module L = Logging
|
|
|
|
|
|
|
|
type action_item = Command of ClangCommand.t | ClangError of string | ClangWarning of string
|
|
|
|
|
|
|
|
let clang_ignore_regex = Option.map ~f:Str.regexp Config.clang_ignore_regex
|
|
|
|
|
|
|
|
let check_for_existing_file args =
|
|
|
|
if Option.is_some clang_ignore_regex && Option.is_none Config.buck_compilation_database then
|
|
|
|
let arg_files, args_list = List.partition_tf ~f:(String.is_prefix ~prefix:"@") args in
|
|
|
|
let read_arg_files args_list arg_file_at =
|
|
|
|
let file = String.slice arg_file_at 1 (String.length arg_file_at) in
|
|
|
|
let args_list_file = In_channel.read_lines file in
|
|
|
|
List.append args_list args_list_file
|
|
|
|
in
|
|
|
|
let all_args_ = List.fold_left ~f:read_arg_files ~init:args_list arg_files in
|
|
|
|
let all_args = List.map ~f:String.strip all_args_ in
|
|
|
|
let rec check_for_existing_file_arg args =
|
|
|
|
match args with
|
|
|
|
| [] ->
|
|
|
|
()
|
|
|
|
| option :: rest ->
|
|
|
|
if String.equal option "-c" then
|
|
|
|
match
|
|
|
|
(* infer-capture-all flavour of buck produces path to generated file that doesn't exist.
|
|
|
|
Create empty file empty file and pass that to clang. This is to enable compilation to continue *)
|
|
|
|
(clang_ignore_regex, List.hd rest)
|
|
|
|
with
|
|
|
|
| Some regexp, Some arg ->
|
|
|
|
if Str.string_match regexp arg 0 && Sys.file_exists arg <> `Yes then (
|
|
|
|
Unix.mkdir_p (Filename.dirname arg) ;
|
|
|
|
let file = Unix.openfile ~mode:[Unix.O_CREAT; Unix.O_RDONLY] arg in
|
|
|
|
Unix.close file )
|
|
|
|
| _ ->
|
|
|
|
()
|
|
|
|
else check_for_existing_file_arg rest
|
|
|
|
in
|
|
|
|
check_for_existing_file_arg all_args
|
|
|
|
|
|
|
|
|
|
|
|
(** Given a list of arguments for clang [args], return a list of new commands to run according to
|
|
|
|
the results of `clang -### [args]`. *)
|
|
|
|
let normalize ~prog ~args : action_item list =
|
|
|
|
let cmd = ClangCommand.mk ClangQuotes.SingleQuotes ~prog ~args in
|
|
|
|
let clang_hashhashhash =
|
|
|
|
Printf.sprintf "%s 2>&1"
|
|
|
|
( ClangCommand.prepend_arg "-###" cmd
|
|
|
|
|> (* c++ modules are not supported, so let clang know in case it was passed "-fmodules".
|
|
|
|
Unfortunately we cannot know accurately if "-fmodules" was passed because we don't go
|
|
|
|
into argument files at this point ("clang -### ..." will do that for us), so we also pass
|
|
|
|
"-Qunused-arguments" to silence the potential warning that "-fno-cxx-modules" was
|
|
|
|
ignored. Moreover, "-fno-cxx-modules" is only accepted by the clang driver so we have to
|
|
|
|
pass it now.
|
|
|
|
|
|
|
|
Using clang instead of gcc may trigger warnings about unsupported optimization flags;
|
|
|
|
passing -Wno-ignored-optimization-argument prevents that. *)
|
|
|
|
ClangCommand.append_args
|
|
|
|
["-fno-cxx-modules"; "-Qunused-arguments"; "-Wno-ignored-optimization-argument"]
|
|
|
|
|> (* If -fembed-bitcode is passed, it leads to multiple cc1 commands, which try to read .bc
|
|
|
|
files that don't get generated, and fail. So pass -fembed-bitcode=off to disable. *)
|
|
|
|
ClangCommand.append_args ["-fembed-bitcode=off"]
|
|
|
|
|> ClangCommand.command_to_run )
|
|
|
|
in
|
|
|
|
L.(debug Capture Medium) "clang -### invocation: %s@\n" clang_hashhashhash ;
|
|
|
|
let normalized_commands = ref [] in
|
|
|
|
let one_line line =
|
|
|
|
if String.is_prefix ~prefix:" \"" line then
|
|
|
|
Command
|
|
|
|
( match
|
|
|
|
(* massage line to remove edge-cases for splitting *)
|
|
|
|
"\"" ^ line ^ " \"" |> (* split by whitespace *)
|
|
|
|
Str.split (Str.regexp_string "\" \"")
|
|
|
|
with
|
|
|
|
| prog :: args ->
|
|
|
|
ClangCommand.mk ClangQuotes.EscapedDoubleQuotes ~prog ~args
|
|
|
|
| [] ->
|
|
|
|
L.(die InternalError) "ClangWrapper: argv cannot be empty" )
|
|
|
|
else if Str.string_match (Str.regexp "clang[^ :]*: warning: ") line 0 then ClangWarning line
|
|
|
|
else ClangError line
|
|
|
|
in
|
|
|
|
let commands_or_errors =
|
|
|
|
(* commands generated by `clang -### ...` start with ' "/absolute/path/to/binary"' *)
|
|
|
|
Str.regexp " \"/\\|clang[^ :]*: \\(error\\|warning\\): "
|
|
|
|
in
|
|
|
|
let consume_input i =
|
|
|
|
try
|
|
|
|
while true do
|
|
|
|
let line = In_channel.input_line_exn i in
|
|
|
|
(* keep only commands and errors *)
|
|
|
|
if Str.string_match commands_or_errors line 0 then normalized_commands
|
|
|
|
:= one_line line :: !normalized_commands
|
|
|
|
done
|
|
|
|
with End_of_file -> ()
|
|
|
|
in
|
|
|
|
(* collect stdout and stderr output together (in reverse order) *)
|
|
|
|
Utils.with_process_in clang_hashhashhash consume_input |> ignore ;
|
|
|
|
normalized_commands := List.rev !normalized_commands ;
|
|
|
|
!normalized_commands
|
|
|
|
|
|
|
|
|
|
|
|
let exec_action_item = function
|
|
|
|
| ClangError error ->
|
|
|
|
(* An error in the output of `clang -### ...`. Outputs the error and fail. This is because
|
|
|
|
`clang -###` pretty much never fails, but warns of failures on stderr instead. *)
|
|
|
|
L.(die UserError)
|
|
|
|
"Failed to execute compilation command. Output:@\n%s@\n*** Infer needs a working compilation command to run."
|
|
|
|
error
|
|
|
|
| ClangWarning warning ->
|
|
|
|
L.external_warning "%s@\n" warning
|
|
|
|
| Command clang_cmd ->
|
|
|
|
Capture.capture clang_cmd
|
|
|
|
|
|
|
|
|
|
|
|
let exe ~prog ~args =
|
|
|
|
let xx_suffix = match String.is_suffix ~suffix:"++" prog with true -> "++" | false -> "" in
|
|
|
|
(* use clang in facebook-clang-plugins *)
|
|
|
|
let clang_xx = CFrontend_config.clang_bin xx_suffix in
|
|
|
|
check_for_existing_file args ;
|
|
|
|
let commands = normalize ~prog:clang_xx ~args in
|
|
|
|
(* xcodebuild projects may require the object files to be generated by the Apple compiler, eg to
|
|
|
|
generate precompiled headers compatible with Apple's clang. *)
|
|
|
|
let prog, should_run_original_command =
|
|
|
|
match Config.fcp_apple_clang with
|
|
|
|
| Some bin ->
|
|
|
|
let bin_xx = bin ^ xx_suffix in
|
|
|
|
L.(debug Capture Medium) "Will run Apple clang %s" bin_xx ;
|
|
|
|
(bin_xx, true)
|
|
|
|
| None ->
|
|
|
|
(clang_xx, false)
|
|
|
|
in
|
|
|
|
List.iter ~f:exec_action_item commands ;
|
|
|
|
if List.is_empty commands || should_run_original_command then (
|
|
|
|
if List.is_empty commands then
|
|
|
|
(* No command to execute after -###, let's execute the original command
|
|
|
|
instead.
|
|
|
|
|
|
|
|
In particular, this can happen when
|
|
|
|
- there are only assembly commands to execute, which we skip, or
|
|
|
|
- the user tries to run `infer -- clang -c file_that_does_not_exist.c`. In this case, this
|
|
|
|
will fail with the appropriate error message from clang instead of silently analyzing 0
|
|
|
|
files. *)
|
|
|
|
L.(debug Capture Quiet)
|
|
|
|
"WARNING: `clang -### <args>` returned an empty set of commands to run and no error. Will run the original command directly:@\n %s@\n"
|
|
|
|
(String.concat ~sep:" " @@ prog :: args) ;
|
|
|
|
Process.create_process_and_wait ~prog ~args )
|
|
|
|
|