You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

190 lines
8.4 KiB

(*
* 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 =
| CanonicalCommand of ClangCommand.t (** commands output by [clang -###] *)
| DriverCommand of ClangCommand.t (** commands prior to [clang -###] treatment *)
| 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 clang command, return a list of new commands to run according to the results of `clang
-### [args]`. *)
let clang_driver_action_items : ClangCommand.t -> action_item list =
fun cmd ->
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
CanonicalCommand
( match
(* massage line to remove edge-cases for splitting *)
"\"" ^ line ^ " \"" |> (* split by whitespace *)
Str.split (Str.regexp_string "\" \"")
with
| prog :: args ->
ClangCommand.mk ~is_driver:false 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 ignored_errors =
Str.regexp "clang[^ :]*: \\(error\\|warning\\): unsupported argument .* to option 'fsanitize='"
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
&& not (Str.string_match ignored_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
(** Given a list of arguments for clang [args], return a list of new commands to run according to
the results of `clang -### [args]` if the command can be analysed. *)
let normalize ~prog ~args : action_item list =
let cmd = ClangCommand.mk ~is_driver:true ClangQuotes.SingleQuotes ~prog ~args in
if ClangCommand.may_capture cmd then clang_driver_action_items cmd else [DriverCommand cmd]
let exec_action_item ~prog ~args = 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:@\n\
'%s' %a@\n\
@\n\
Error message:@\n\
%s@\n\
@\n\
*** Infer needs a working compilation command to run."
prog Pp.cli_args args error
| ClangWarning warning ->
L.external_warning "%s@\n" warning
| CanonicalCommand clang_cmd ->
Capture.capture clang_cmd
| DriverCommand clang_cmd ->
if
Option.is_none Config.buck_compilation_database
|| Config.skip_analysis_in_path_skips_compilation
then Capture.run_clang clang_cmd Utils.echo_in
else
L.debug Capture Quiet "Skipping seemingly uninteresting clang driver command %s@\n"
(ClangCommand.command_to_run 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 ~prog ~args) 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 )