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.

193 lines
8.4 KiB

(*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)
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 =
match (Config.buck_mode, clang_ignore_regex) with
| Some (ClangCompilationDB _), Some clang_ignore_regex ->
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
(* 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 *)
match List.hd rest with
| Some arg ->
if Str.string_match clang_ignore_regex arg 0 && not (ISys.file_exists arg) then (
Unix.mkdir_p (Filename.dirname arg) ;
let file = Unix.openfile ~mode:[Unix.O_CREAT; Unix.O_RDONLY] arg in
Unix.close file )
| None ->
()
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.
Clang adds "-faddrsig" by default on ELF targets. This is ok in itself, but for some
reason that flag is the only one to show up *after* the source file name in the -cc1
commands emitted by [clang -### ...]. Passing [-fno-addrsig] ensures that the source
path is always the last argument. *)
ClangCommand.append_args
[ "-fno-cxx-modules"
; "-Qunused-arguments"
; "-Wno-ignored-optimization-argument"
; "-fno-addrsig" ]
|> (* 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
( (* massage line to remove edge-cases for splitting *)
match
"\"" ^ 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
(not Config.skip_non_capture_clang_commands)
|| Option.exists Config.buck_mode ~f:BuckMode.is_clang_compilation_db
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 )