Reviewed By: jberdine Differential Revision: D3791413 fbshipit-source-id: 8e57aa7master
parent
49950afe26
commit
d11b2754a9
@ -0,0 +1,106 @@
|
|||||||
|
(*
|
||||||
|
* 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.
|
||||||
|
*)
|
||||||
|
|
||||||
|
open! Utils
|
||||||
|
|
||||||
|
(** Prints information about a unix error *)
|
||||||
|
let print_unix_error cmd e =
|
||||||
|
match e with
|
||||||
|
| Unix.Unix_error(err, _, _) ->
|
||||||
|
Logging.err "Cannot execute %s : %s\n%!"
|
||||||
|
cmd (Unix.error_message err)
|
||||||
|
| _ -> ()
|
||||||
|
|
||||||
|
(** Prints an error message to a log file, prints a message saying that the error can be
|
||||||
|
found in that file, and exits, with default code 1 or a given code. *)
|
||||||
|
let print_error_and_exit ?(exit_code=1) f el =
|
||||||
|
Logging.do_err f el;
|
||||||
|
let log_file = snd (Config.tmp_log_files_of_current_exe ()) in
|
||||||
|
Logging.stderr "\nAn error occured. Please, find details in %s\n\n%!" log_file;
|
||||||
|
exit exit_code
|
||||||
|
|
||||||
|
(** Executes a command and catches a potential exeption and prints it. *)
|
||||||
|
let exec_command cmd args env =
|
||||||
|
try Unix.execvpe cmd args env
|
||||||
|
with (Unix.Unix_error _ as e) ->
|
||||||
|
print_unix_error cmd e
|
||||||
|
|
||||||
|
(** Given an command to be executed, creates a process to execute this command,
|
||||||
|
and waits for it to terminate. The standard out and error are not redirected.
|
||||||
|
If the commands fails to execute, prints an error message and exits. *)
|
||||||
|
let create_process_and_wait cmd =
|
||||||
|
let pid = Unix.create_process cmd.(0) cmd Unix.stdin Unix.stdout Unix.stderr in
|
||||||
|
let _, status = Unix.waitpid [] pid in
|
||||||
|
let exit_code = match status with
|
||||||
|
| Unix.WEXITED i -> i
|
||||||
|
| _ -> 1 in
|
||||||
|
if exit_code <> 0 then
|
||||||
|
print_error_and_exit ~exit_code:exit_code
|
||||||
|
"Failed to execute: %s" (String.concat " " (Array.to_list cmd))
|
||||||
|
|
||||||
|
(** Given a process id and a function that describes the command that the process id
|
||||||
|
represents, prints a message explaining the command and its status, if in debug or stats mode.
|
||||||
|
It also prints a dot to show progress of jobs being finished. *)
|
||||||
|
let print_status f pid (status : Unix. process_status) =
|
||||||
|
if Config.debug_mode || Config.stats_mode then
|
||||||
|
(let program = f pid in
|
||||||
|
match status with
|
||||||
|
| WEXITED status ->
|
||||||
|
if status = 0 then
|
||||||
|
Logging.out "%s OK \n%!" program
|
||||||
|
else
|
||||||
|
Logging.err "%s exited with code %d\n%!" program status
|
||||||
|
| WSIGNALED signal ->
|
||||||
|
Logging.err "%s killed by signal %d\n%!" program signal
|
||||||
|
| WSTOPPED _ ->
|
||||||
|
Logging.err "%s stopped (???)\n%!" program);
|
||||||
|
Logging.stdout ".%!"
|
||||||
|
|
||||||
|
let start_current_jobs_count () = ref 0
|
||||||
|
|
||||||
|
let waited_for_jobs = ref 0
|
||||||
|
|
||||||
|
(** [wait_for_son pid_child f jobs_count] wait for pid_child
|
||||||
|
and all the other children and update the current jobs count.
|
||||||
|
Use f to print the job status *)
|
||||||
|
let rec wait_for_child pid_child f current_jobs_count =
|
||||||
|
let pid, status = Unix.wait () in
|
||||||
|
Pervasives.decr current_jobs_count;
|
||||||
|
Pervasives.incr waited_for_jobs;
|
||||||
|
print_status f pid status;
|
||||||
|
if pid <> pid_child then
|
||||||
|
wait_for_child pid_child f current_jobs_count
|
||||||
|
|
||||||
|
let pid_to_program jobsMap pid =
|
||||||
|
try
|
||||||
|
IntMap.find pid jobsMap
|
||||||
|
with Not_found -> ""
|
||||||
|
|
||||||
|
(** [run_jobs_in_parallel jobs_stack run_job cmd_to_string ] runs the jobs in
|
||||||
|
the given stack, by spawning the jobs in batches of n, where n is Config.jobs. It
|
||||||
|
then waits for all those jobs and starts a new batch and so on. cmd_to_string
|
||||||
|
is used for printing information about the job's status. *)
|
||||||
|
let run_jobs_in_parallel jobs_stack run_job cmd_to_string =
|
||||||
|
let run_job () =
|
||||||
|
let jobs_map = ref IntMap.empty in
|
||||||
|
let current_jobs_count = start_current_jobs_count () in
|
||||||
|
while not (Stack.is_empty jobs_stack) do
|
||||||
|
let job_cmd = Stack.pop jobs_stack in
|
||||||
|
Pervasives.incr current_jobs_count;
|
||||||
|
match Unix.fork () with
|
||||||
|
| 0 ->
|
||||||
|
run_job job_cmd
|
||||||
|
| pid_child ->
|
||||||
|
jobs_map := IntMap.add pid_child (cmd_to_string job_cmd) !jobs_map;
|
||||||
|
if Stack.length jobs_stack = 0 || !current_jobs_count >= Config.jobs then
|
||||||
|
wait_for_child pid_child (pid_to_program !jobs_map) current_jobs_count
|
||||||
|
done in
|
||||||
|
run_job ();
|
||||||
|
Logging.stdout ".\n%!";
|
||||||
|
Logging.out "Waited for %d jobs" !waited_for_jobs
|
@ -0,0 +1,34 @@
|
|||||||
|
(*
|
||||||
|
* 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.
|
||||||
|
*)
|
||||||
|
|
||||||
|
open! Utils
|
||||||
|
|
||||||
|
(** Given an command to be executed, creates a process to execute this command,
|
||||||
|
and waits for its execution. The standard out and error are not redirected.
|
||||||
|
If the commands fails to execute, prints an error message and exits. *)
|
||||||
|
val create_process_and_wait : string array -> unit
|
||||||
|
|
||||||
|
(** Given an command to be executed, creates a process to execute this command,
|
||||||
|
and waits for its execution. The standard out and error are not redirected.
|
||||||
|
If the commands fails to execute, prints an error message and exits. *)
|
||||||
|
val exec_command : string -> string array -> string array -> unit
|
||||||
|
|
||||||
|
(** Prints an error message to a log file, prints a message saying that the error can be
|
||||||
|
found in that file, and exist, with default code 1 or a given code. *)
|
||||||
|
val print_error_and_exit :
|
||||||
|
?exit_code:int -> ('a -> unit, Format.formatter, unit) format -> 'a -> 'b
|
||||||
|
|
||||||
|
(** Prints information about a unix error *)
|
||||||
|
val print_unix_error : string -> exn -> unit
|
||||||
|
|
||||||
|
(** [run_jobs_in_parallel jobs_stack run_job cmd_to_string ] runs the jobs in
|
||||||
|
the given stack, by spawning the jobs in batches of n, where n is Config.jobs. It
|
||||||
|
then waits for all those jobs and starts a new batch and so on. cmd_to_string
|
||||||
|
is used for printing information about the job's status. *)
|
||||||
|
val run_jobs_in_parallel : 'a Stack.t -> ('a -> unit) -> ('a -> string) -> unit
|
@ -0,0 +1,196 @@
|
|||||||
|
(*
|
||||||
|
* 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.
|
||||||
|
*)
|
||||||
|
|
||||||
|
open! Utils
|
||||||
|
module YBU = Yojson.Basic.Util
|
||||||
|
|
||||||
|
type compilation_data = {
|
||||||
|
dir : string;
|
||||||
|
command : string;
|
||||||
|
args : string list;
|
||||||
|
}
|
||||||
|
|
||||||
|
let capture_text =
|
||||||
|
if Config.analyzer = Some Config.Linters then "linting"
|
||||||
|
else "translating"
|
||||||
|
|
||||||
|
let swap_command cmd =
|
||||||
|
let clang = "clang" in
|
||||||
|
let clangplusplus = "clang++" in
|
||||||
|
if Utils.string_is_suffix clang cmd then
|
||||||
|
Config.wrappers_dir // clang
|
||||||
|
else if Utils.string_is_suffix clangplusplus cmd then
|
||||||
|
Config.wrappers_dir // clangplusplus
|
||||||
|
else assert false
|
||||||
|
(* The command in the compilation database json
|
||||||
|
emited by buck can only be clang or clang++ *)
|
||||||
|
|
||||||
|
(** Read the files to compile from the changed files index. *)
|
||||||
|
let read_files_to_compile () =
|
||||||
|
let changed_files = DB.SourceFileSet.empty in
|
||||||
|
match DB.read_changed_files_index with
|
||||||
|
| None ->
|
||||||
|
(match Config.changed_files_index with
|
||||||
|
| Some index ->
|
||||||
|
Process.print_error_and_exit "Error reading the changed files index %s.\n%!" index
|
||||||
|
| None -> changed_files)
|
||||||
|
| Some lines ->
|
||||||
|
IList.fold_left
|
||||||
|
(fun changed_files line ->
|
||||||
|
DB.SourceFileSet.add (DB.abs_source_file_from_path line) changed_files)
|
||||||
|
changed_files lines
|
||||||
|
|
||||||
|
(** Add file to compilation database, only if it is in changed_files. *)
|
||||||
|
let add_file_to_compilation_database file_path cmd_options changed_files compilation_database =
|
||||||
|
let add_file =
|
||||||
|
match Config.changed_files_index with
|
||||||
|
| Some _ -> DB.SourceFileSet.mem (DB.source_file_from_string file_path) changed_files
|
||||||
|
| None -> true in
|
||||||
|
if add_file then
|
||||||
|
compilation_database := StringMap.add file_path cmd_options !compilation_database
|
||||||
|
|
||||||
|
(** We have to replace the .o files because the path in buck-out doesn't exist at this point.
|
||||||
|
Moreover, in debug mode we create debug files in the place where the .o files are created,
|
||||||
|
so having all that in the results directory is convenient for finding the files and for
|
||||||
|
scanning the directory for running clang_frontend_stats. *)
|
||||||
|
let replace_clang_args arg =
|
||||||
|
if Filename.check_suffix arg ".o" then
|
||||||
|
let dir = Config.results_dir // Config.clang_build_output_dir_name in
|
||||||
|
let abbrev_source_file = DB.source_file_encoding (DB.source_file_from_string arg) in
|
||||||
|
dir // abbrev_source_file
|
||||||
|
else arg
|
||||||
|
|
||||||
|
(* Doing this argument manipulation here rather than in the wrappers because it seems to
|
||||||
|
be needed only with this integration.*)
|
||||||
|
let remove_clang_arg arg args =
|
||||||
|
if (arg = "-include-pch") || (Filename.check_suffix arg ".gch")
|
||||||
|
then args else arg :: args
|
||||||
|
|
||||||
|
(** Parse the compilation database json file into the compilationDatabase
|
||||||
|
map. The json file consists of an array of json objects that contain the file
|
||||||
|
to be compiled, the directory to be compiled in, and the compilation command as a list
|
||||||
|
and as a string. We pack this information into the compilationDatabase map, and remove the
|
||||||
|
clang invocation part, because we will use a clang wrapper. *)
|
||||||
|
let decode_compilation_database changed_files compilation_database _ path =
|
||||||
|
let collect_arguments compilation_argument args =
|
||||||
|
match compilation_argument with
|
||||||
|
| `String arg ->
|
||||||
|
let arg' = replace_clang_args arg in
|
||||||
|
remove_clang_arg arg' args
|
||||||
|
| _ -> failwith ("Json file doesn't have the expected format") in
|
||||||
|
let json = Yojson.Basic.from_file path in
|
||||||
|
let rec parse_json json =
|
||||||
|
match json with
|
||||||
|
| `List arguments ->
|
||||||
|
IList.iter parse_json arguments
|
||||||
|
| `Assoc [ ("directory", `String dir);
|
||||||
|
("file", `String file_path);
|
||||||
|
("arguments", `List compilation_arguments);
|
||||||
|
("command", `String _) ] ->
|
||||||
|
(match IList.fold_right collect_arguments compilation_arguments [] with
|
||||||
|
| [] -> failwith ("Command cannot be empty")
|
||||||
|
| cmd :: args ->
|
||||||
|
let wrapper_cmd = swap_command cmd in
|
||||||
|
let compilation_data = { dir; command = wrapper_cmd; args;} in
|
||||||
|
add_file_to_compilation_database file_path compilation_data changed_files
|
||||||
|
compilation_database)
|
||||||
|
| _ ->
|
||||||
|
failwith ("Json file doesn't have the expected format") in
|
||||||
|
parse_json json
|
||||||
|
|
||||||
|
(** The buck targets are assumed to start with //, aliases are not supported. *)
|
||||||
|
let check_args_for_targets args =
|
||||||
|
if not (IList.exists (Utils.string_is_prefix "//") args) then
|
||||||
|
let args_s = String.concat " " args in
|
||||||
|
Process.print_error_and_exit
|
||||||
|
"Error reading buck command %s. Please, pass buck targets, aliases are not allowed.\n%!"
|
||||||
|
args_s
|
||||||
|
|
||||||
|
let add_flavor_to_targets args =
|
||||||
|
let flavor =
|
||||||
|
match Config.use_compilation_database with
|
||||||
|
| Some `Deps -> "#uber-compilation-database"
|
||||||
|
| Some `NoDeps -> "#compilation-database"
|
||||||
|
| _ -> assert false (* cannot happen *) in
|
||||||
|
let process_arg arg =
|
||||||
|
(* Targets are assumed to start with //, aliases are not allowed *)
|
||||||
|
if Utils.string_is_prefix "//" arg then arg ^ flavor
|
||||||
|
else arg in
|
||||||
|
IList.map process_arg args
|
||||||
|
|
||||||
|
let create_files_stack compilation_database =
|
||||||
|
let stack = Stack.create () in
|
||||||
|
let add_to_stack file _ =
|
||||||
|
Stack.push file stack in
|
||||||
|
StringMap.iter add_to_stack !compilation_database;
|
||||||
|
stack
|
||||||
|
|
||||||
|
let run_compilation_file compilation_database file =
|
||||||
|
try
|
||||||
|
let compilation_data = StringMap.find file !compilation_database in
|
||||||
|
Unix.chdir compilation_data.dir;
|
||||||
|
let args = Array.of_list compilation_data.args in
|
||||||
|
let env = Array.append
|
||||||
|
(Unix.environment())
|
||||||
|
(Array.of_list [
|
||||||
|
"INFER_RESULTS_DIR="^Config.results_dir;
|
||||||
|
"FCP_RUN_SYNTAX_ONLY=1"]) in
|
||||||
|
Process.exec_command compilation_data.command args env
|
||||||
|
with Not_found ->
|
||||||
|
Process.print_error_and_exit "Failed to find compilation data for %s \n%!" file
|
||||||
|
|
||||||
|
let run_compilation_database compilation_database =
|
||||||
|
let number_of_files = StringMap.cardinal !compilation_database in
|
||||||
|
Logging.out "Starting %s %d files \n%!" capture_text number_of_files;
|
||||||
|
Logging.stdout "Starting %s %d files \n%!" capture_text number_of_files;
|
||||||
|
let jobsStack = create_files_stack compilation_database in
|
||||||
|
let capture_text_upper = String.capitalize capture_text in
|
||||||
|
let job_to_string = fun file -> capture_text_upper ^ " " ^ file in
|
||||||
|
Process.run_jobs_in_parallel jobsStack (run_compilation_file compilation_database) job_to_string
|
||||||
|
|
||||||
|
|
||||||
|
(** Computes the compilation database: a map from a file path to info to compile the file, i.e.
|
||||||
|
the dir where the compilation should be executed and the arguments to clang.*)
|
||||||
|
let get_compilation_database changed_files =
|
||||||
|
let cmd = IList.rev Config.buck_build_args in
|
||||||
|
match cmd with
|
||||||
|
| buck :: build :: args ->
|
||||||
|
(check_args_for_targets args;
|
||||||
|
let args_with_flavor = add_flavor_to_targets args in
|
||||||
|
let buck_build = Array.of_list (buck :: build :: args_with_flavor) in
|
||||||
|
Process.create_process_and_wait buck_build;
|
||||||
|
let buck_targets_list = buck :: "targets" :: "--show-output" :: args_with_flavor in
|
||||||
|
let buck_targets = String.concat " " buck_targets_list in
|
||||||
|
try
|
||||||
|
match Utils.with_process_in buck_targets Std.input_list with
|
||||||
|
| [] -> Logging.stdout "There are no files to process, exiting."; exit 0
|
||||||
|
| lines ->
|
||||||
|
let scan_output compilation_database_files chan =
|
||||||
|
Scanf.sscanf chan "%s %s"
|
||||||
|
(fun target file -> StringMap.add target file compilation_database_files) in
|
||||||
|
(* Map from targets to json output *)
|
||||||
|
let compilation_database_files = IList.fold_left scan_output StringMap.empty lines in
|
||||||
|
let compilation_database = ref StringMap.empty in
|
||||||
|
StringMap.iter
|
||||||
|
(decode_compilation_database changed_files compilation_database)
|
||||||
|
compilation_database_files;
|
||||||
|
compilation_database
|
||||||
|
with Unix.Unix_error (err, _, _) ->
|
||||||
|
Process.print_error_and_exit
|
||||||
|
"Cannot execute %s\n%!"
|
||||||
|
(buck_targets ^ " " ^ (Unix.error_message err)))
|
||||||
|
| _ ->
|
||||||
|
let cmd = String.concat " " cmd in
|
||||||
|
Process.print_error_and_exit "Incorrect buck command: %s. Please use buck build <targets>" cmd
|
||||||
|
|
||||||
|
let () =
|
||||||
|
let changed_files = read_files_to_compile () in
|
||||||
|
let compilation_database = get_compilation_database changed_files in
|
||||||
|
DB.create_dir (Config.results_dir // Config.clang_build_output_dir_name);
|
||||||
|
run_compilation_database compilation_database
|
Loading…
Reference in new issue