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.

367 lines
12 KiB

(*
* Copyright (c) 2009 - 2013 Monoidics ltd.
* Copyright (c) 2013 - 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
open! PVariant
module F = Format
module Hashtbl = Caml.Hashtbl
module L = Die
(** initial process times *)
let initial_times = Unix.times ()
(** recursively traverse a path for files ending with a given extension *)
let find_files ~path ~extension =
let rec traverse_dir_aux init dir_path =
let aux base_path files rel_path =
let full_path = base_path ^/ rel_path in
match (Unix.stat full_path).Unix.st_kind with
| Unix.S_REG when String.is_suffix ~suffix:extension full_path ->
full_path :: files
| Unix.S_DIR ->
traverse_dir_aux files full_path
| _ ->
files
in
Sys.fold_dir ~init ~f:(aux dir_path) dir_path
in
traverse_dir_aux [] path
(** read a source file and return a list of lines, or None in case of error *)
let read_file fname =
let res = ref [] in
let cin_ref = ref None in
let cleanup () = match !cin_ref with None -> () | Some cin -> In_channel.close cin in
try
let cin = In_channel.create fname in
cin_ref := Some cin ;
while true do
let line = In_channel.input_line_exn cin in
res := line :: !res
done ;
assert false
with
| End_of_file ->
cleanup () ;
Ok (List.rev !res)
| Sys_error error ->
cleanup () ; Error error
(** type for files used for printing *)
type outfile =
{ fname: string (** name of the file *)
; out_c: Out_channel.t (** output channel *)
; fmt: F.formatter (** formatter for printing *) }
(** create an outfile for the command line *)
let create_outfile fname =
try
let out_c = Out_channel.create fname in
let fmt = F.formatter_of_out_channel out_c in
Some {fname; out_c; fmt}
with Sys_error _ ->
F.fprintf F.err_formatter "error: cannot create file %s@." fname ;
None
(** close an outfile *)
let close_outf outf = Out_channel.close outf.out_c
(** Convert a filename to an absolute one if it is relative, and normalize "." and ".." *)
let filename_to_absolute ~root fname =
let add_entry rev_done entry =
match (entry, rev_done) with
| ".", [] ->
entry :: rev_done (* id on . *)
| ".", _ ->
rev_done (* path/. --> path *)
| "..", ("." | "..") :: _ ->
entry :: rev_done (* id on {.,..}/.. *)
| "..", ["/"] ->
rev_done (* /.. -> / *)
| "..", _ :: rev_done_parent ->
rev_done_parent (* path/dir/.. --> path *)
| _ ->
entry :: rev_done
in
let abs_fname = if Filename.is_absolute fname then fname else root ^/ fname in
Filename.of_parts (List.rev (List.fold ~f:add_entry ~init:[] (Filename.parts abs_fname)))
(** Convert an absolute filename to one relative to the given directory. *)
let filename_to_relative ~root fname =
let rec relativize_if_under origin target =
match (origin, target) with
| x :: xs, y :: ys when String.equal x y ->
relativize_if_under xs ys
| [], [] ->
Some "."
| [], ys ->
Some (Filename.of_parts ys)
| _ ->
None
in
relativize_if_under (Filename.parts root) (Filename.parts fname)
let directory_fold f init path =
let collect current_dir (accu, dirs) path =
let full_path = current_dir ^/ path in
try
if Sys.is_directory full_path = `Yes then (accu, full_path :: dirs)
else (f accu full_path, dirs)
with Sys_error _ -> (accu, dirs)
in
let rec loop accu dirs =
match dirs with
| [] ->
accu
| d :: tl ->
let new_accu, new_dirs = Array.fold ~f:(collect d) ~init:(accu, tl) (Sys.readdir d) in
loop new_accu new_dirs
in
if Sys.is_directory path = `Yes then loop init [path] else f init path
let directory_iter f path =
let apply current_dir dirs path =
let full_path = current_dir ^/ path in
try
if Sys.is_directory full_path = `Yes then full_path :: dirs
else
let () = f full_path in
dirs
with Sys_error _ -> dirs
in
let rec loop dirs =
match dirs with
| [] ->
()
| d :: tl ->
let new_dirs = Array.fold ~f:(apply d) ~init:tl (Sys.readdir d) in
loop new_dirs
in
if Sys.is_directory path = `Yes then loop [path] else f path
let directory_is_empty path = Sys.readdir path |> Array.is_empty
let string_crc_hex32 s = Caml.Digest.to_hex (Caml.Digest.string s)
let read_json_file path =
try Ok (Yojson.Basic.from_file path) with Sys_error msg | Yojson.Json_error msg -> Error msg
let do_finally_swallow_timeout ~f ~finally =
let res =
try f () with exc ->
reraise_after exc ~f:(fun () ->
try finally () |> ignore with _ -> (* swallow in favor of the original exception *) () )
in
let res' = finally () in
(res, res')
let try_finally_swallow_timeout ~f ~finally =
let res, () = do_finally_swallow_timeout ~f ~finally in
res
let with_file_in file ~f =
let ic = In_channel.create file in
let f () = f ic in
let finally () = In_channel.close ic in
try_finally_swallow_timeout ~f ~finally
let with_file_out file ~f =
let oc = Out_channel.create file in
let f () = f oc in
let finally () = Out_channel.close oc in
try_finally_swallow_timeout ~f ~finally
let write_json_to_file destfile json =
with_file_out destfile ~f:(fun oc -> Yojson.Basic.pretty_to_channel oc json)
let consume_in chan_in =
try while true do In_channel.input_line_exn chan_in |> ignore done with End_of_file -> ()
let with_process_in command read =
let chan = Unix.open_process_in command in
let f () = read chan in
let finally () = consume_in chan ; Unix.close_process_in chan in
do_finally_swallow_timeout ~f ~finally
let shell_escape_command =
let no_quote_needed = Str.regexp "^[A-Za-z0-9-_%/:,.]+$" in
let easy_single_quotable = Str.regexp "^[^']+$" in
let easy_double_quotable = Str.regexp "^[^$`\\!]+$" in
let escape = function
| "" ->
"''"
| arg ->
if Str.string_match no_quote_needed arg 0 then arg
else if Str.string_match easy_single_quotable arg 0 then F.sprintf "'%s'" arg
else if Str.string_match easy_double_quotable arg 0 then
arg |> Escape.escape_double_quotes |> F.sprintf "\"%s\""
else
(* ends on-going single quote, output single quote inside double quotes, then open a new single
quote *)
arg |> Escape.escape_map (function '\'' -> Some "'\"'\"'" | _ -> None)
|> F.sprintf "'%s'"
in
fun cmd -> List.map ~f:escape cmd |> String.concat ~sep:" "
let with_process_lines ~(debug: ('a, F.formatter, unit) format -> 'a) ~cmd ~tmp_prefix ~f =
let shell_cmd = shell_escape_command cmd in
let verbose_err_file = Filename.temp_file tmp_prefix ".err" in
let shell_cmd_redirected = Printf.sprintf "%s 2>'%s'" shell_cmd verbose_err_file in
debug "Trying to execute: %s@\n%!" shell_cmd_redirected ;
let input_lines chan = In_channel.input_lines ~fix_win_eol:true chan in
let res = with_process_in shell_cmd_redirected input_lines in
let verbose_errlog = with_file_in verbose_err_file ~f:In_channel.input_all in
if not (String.equal verbose_errlog "") then
debug "@\nlog:@\n<<<<<<@\n%s@\n>>>>>>@\n%!" verbose_errlog ;
match res with
| lines, Ok () ->
f lines
| lines, (Error _ as err) ->
let output = String.concat ~sep:"\n" lines in
L.(die ExternalError)
"*** Failed to execute: %s@\n*** Command: %s@\n*** Output:@\n%s@."
(Unix.Exit_or_signal.to_string_hum err)
shell_cmd output
(** Create a directory if it does not exist already. *)
let create_dir dir =
try
if (Unix.stat dir).Unix.st_kind <> Unix.S_DIR then
L.(die ExternalError) "file '%s' already exists and is not a directory" dir
with Unix.Unix_error _ ->
try Unix.mkdir dir ~perm:0o700 with Unix.Unix_error _ ->
let created_concurrently =
(* check if another process created it meanwhile *)
try Polymorphic_compare.( = ) (Unix.stat dir).Unix.st_kind Unix.S_DIR
with Unix.Unix_error _ -> false
in
if not created_concurrently then L.(die ExternalError) "cannot create directory '%s'" dir
let realpath_cache = Hashtbl.create 1023
let realpath ?(warn_on_error= true) path =
match Hashtbl.find realpath_cache path with
| exception Not_found -> (
match Filename.realpath path with
| realpath ->
Hashtbl.add realpath_cache path (Ok realpath) ;
realpath
| exception (Unix.Unix_error (code, _, arg) as exn) ->
reraise_after exn ~f:(fun () ->
if warn_on_error then
F.eprintf "WARNING: Failed to resolve file %s with \"%s\" @\n@." arg
(Unix.Error.message code) ;
(* cache failures as well *)
Hashtbl.add realpath_cache path (Error exn) ) )
| Ok path ->
path
| Error exn ->
raise exn
(* never closed *)
let devnull = lazy (Unix.openfile "/dev/null" ~mode:[Unix.O_WRONLY])
let suppress_stderr2 f2 x1 x2 =
let restore_stderr src = Unix.dup2 ~src ~dst:Unix.stderr ; Unix.close src in
let orig_stderr = Unix.dup Unix.stderr in
Unix.dup2 ~src:(Lazy.force devnull) ~dst:Unix.stderr ;
let f () = f2 x1 x2 in
let finally () = restore_stderr orig_stderr in
protect ~f ~finally
let compare_versions v1 v2 =
let int_list_of_version v =
let lv = String.split ~on:'.' v in
let int_of_string_or_zero v = try int_of_string v with Failure _ -> 0 in
List.map ~f:int_of_string_or_zero lv
in
let lv1 = int_list_of_version v1 in
let lv2 = int_list_of_version v2 in
[%compare : int list] lv1 lv2
let write_file_with_locking ?(delete= false) ~f:do_write fname =
Unix.with_file
~mode:Unix.([O_WRONLY; O_CREAT])
fname
~f:(fun file_descr ->
if Unix.flock file_descr Unix.Flock_command.lock_exclusive then (
(* make sure we're not writing over some existing, possibly longer content: some other
process may have snagged the file from under us between open(2) and flock(2) so passing
O_TRUNC to open(2) above would not be a good substitute for calling ftruncate(2)
below. *)
Unix.ftruncate file_descr ~len:Int64.zero ;
let outc = Unix.out_channel_of_descr file_descr in
do_write outc ;
Out_channel.flush outc ;
ignore (Unix.flock file_descr Unix.Flock_command.unlock) ) ) ;
if delete then try Unix.unlink fname with Unix.Unix_error _ -> ()
let rec rmtree name =
match Unix.((lstat name).st_kind) with
| S_DIR ->
let dir = Unix.opendir name in
let rec rmdir dir =
match Unix.readdir_opt dir with
| Some entry ->
if not
( String.equal entry Filename.current_dir_name
|| String.equal entry Filename.parent_dir_name )
then rmtree (name ^/ entry) ;
rmdir dir
| None ->
Unix.closedir dir ; Unix.rmdir name
in
rmdir dir
| _ ->
Unix.unlink name
| exception Unix.Unix_error (Unix.ENOENT, _, _) ->
()
let without_gc ~f =
let stat = Gc.get () in
let space_oh = stat.space_overhead in
Gc.set {stat with space_overhead= 10000} ;
let res = f () in
Gc.set {stat with space_overhead= space_oh} ;
res
let yield () =
Unix.select ~read:[] ~write:[] ~except:[] ~timeout:(`After Time_ns.Span.min_value) |> ignore
let better_hash x = Marshal.to_string x [Marshal.No_sharing] |> Caml.Digest.string
let unlink_file_on_exit temp_file =
"Cleaning temporary file " ^ temp_file
|> Epilogues.register ~f:(fun () -> try Unix.unlink temp_file with _ -> ())