653 lines
21 KiB
OCaml
653 lines
21 KiB
OCaml
|
|
(*
|
||
|
|
* Copyright (c) 2013-2020 Thomas Gazagnaire <thomas@gazagnaire.org>
|
||
|
|
* Copyright (c) 2013-2020 Anil Madhavapeddy <anil@recoil.org>
|
||
|
|
* Copyright (c) 2015-2020 Gabriel Radanne <drupyog@zoho.com>
|
||
|
|
* Copyright (c) 2019-2020 Etienne Millon <etienne@tarides.com>
|
||
|
|
*
|
||
|
|
* Permission to use, copy, modify, and distribute this software for any
|
||
|
|
* purpose with or without fee is hereby granted, provided that the above
|
||
|
|
* copyright notice and this permission notice appear in all copies.
|
||
|
|
*
|
||
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||
|
|
*)
|
||
|
|
|
||
|
|
let src = Logs.Src.create "functoria.action" ~doc:"functoria library"
|
||
|
|
|
||
|
|
module Log = (val Logs.src_log src : Logs.LOG)
|
||
|
|
open Astring
|
||
|
|
|
||
|
|
type 'a or_err = ('a, Rresult.R.msg) result
|
||
|
|
type tmp_name_pat = Bos.OS.File.tmp_name_pat
|
||
|
|
|
||
|
|
type 'a with_output = {
|
||
|
|
mode : int option;
|
||
|
|
path : Fpath.t;
|
||
|
|
purpose : string;
|
||
|
|
contents : Format.formatter -> 'a;
|
||
|
|
append : bool;
|
||
|
|
}
|
||
|
|
|
||
|
|
type channel = [ `Null | `Fmt of Format.formatter ]
|
||
|
|
type cmd = { cmd : Bos.Cmd.t; err : channel; out : channel; trim : bool }
|
||
|
|
type ls = { root : Fpath.t; filter : Fpath.t -> bool }
|
||
|
|
|
||
|
|
type _ command =
|
||
|
|
| Rmdir : Fpath.t -> unit command
|
||
|
|
| Mkdir : Fpath.t -> bool command
|
||
|
|
| Ls : ls -> Fpath.t list command
|
||
|
|
| Rm : Fpath.t -> unit command
|
||
|
|
| Is_file : Fpath.t -> bool command
|
||
|
|
| Is_dir : Fpath.t -> bool command
|
||
|
|
| Size_of : Fpath.t -> int option command
|
||
|
|
| Run_cmd : cmd -> unit command
|
||
|
|
| Run_cmd_out : cmd -> string command
|
||
|
|
| Run_cmd_cli : Bos.Cmd.t -> unit command
|
||
|
|
| Get_var : string -> string option command
|
||
|
|
| Set_var : string * string option -> unit command
|
||
|
|
| With_dir : Fpath.t * (unit -> 'a t) -> 'a command
|
||
|
|
| Pwd : Fpath.t command
|
||
|
|
| Tmp_file : int option * tmp_name_pat -> Fpath.t command
|
||
|
|
| Write_file : Fpath.t * string -> unit command
|
||
|
|
| Read_file : Fpath.t -> string command
|
||
|
|
| With_output : 'a with_output -> 'a command
|
||
|
|
|
||
|
|
and _ t =
|
||
|
|
| Done : 'a -> 'a t
|
||
|
|
| Fail : string -> 'a t
|
||
|
|
| Run : 'r command * ('r -> 'a t) -> 'a t
|
||
|
|
|
||
|
|
let ok x = Done x
|
||
|
|
let error e = Fail e
|
||
|
|
let errorf fmt = Fmt.kstr error fmt
|
||
|
|
|
||
|
|
let rec bind ~f = function
|
||
|
|
| Done r -> f r
|
||
|
|
| Fail s -> Fail s
|
||
|
|
| Run (c, k) ->
|
||
|
|
let k2 r = bind ~f (k r) in
|
||
|
|
Run (c, k2)
|
||
|
|
|
||
|
|
let map ~f x = bind x ~f:(fun y -> ok (f y))
|
||
|
|
let rec seq = function [] -> ok () | h :: t -> bind ~f:(fun () -> seq t) h
|
||
|
|
let wrap x = Run (x, ok)
|
||
|
|
let ( ! ) = Fpath.normalize
|
||
|
|
let rm path = wrap @@ Rm !path
|
||
|
|
let rmdir path = wrap @@ Rmdir !path
|
||
|
|
let mkdir path = wrap @@ Mkdir !path
|
||
|
|
let ls path filter = wrap @@ Ls { root = !path; filter }
|
||
|
|
let with_dir path f = wrap @@ With_dir (!path, f)
|
||
|
|
let pwd () = wrap @@ Pwd
|
||
|
|
let is_file path = wrap @@ Is_file !path
|
||
|
|
let is_dir path = wrap @@ Is_dir !path
|
||
|
|
let size_of path = wrap @@ Size_of !path
|
||
|
|
let set_var c v = wrap @@ Set_var (c, v)
|
||
|
|
let get_var c = wrap @@ Get_var c
|
||
|
|
|
||
|
|
let run_cmd ?(err = `Fmt Fmt.stderr) ?(out = `Fmt Fmt.stdout) cmd =
|
||
|
|
wrap @@ Run_cmd { cmd; out; err; trim = false }
|
||
|
|
|
||
|
|
let run_cmd_out ?(err = `Fmt Fmt.stderr) cmd =
|
||
|
|
wrap @@ Run_cmd_out { cmd; out = `Null; err; trim = true }
|
||
|
|
|
||
|
|
let run_cmd_cli cmd = wrap @@ Run_cmd_cli cmd
|
||
|
|
let write_file path contents = wrap @@ Write_file (!path, contents)
|
||
|
|
let read_file path = wrap @@ Read_file !path
|
||
|
|
let tmp_file ?mode pat = wrap @@ Tmp_file (mode, pat)
|
||
|
|
|
||
|
|
let with_output ?mode ?(append = false) ~path ~purpose contents =
|
||
|
|
wrap @@ With_output { append; mode; path; purpose; contents }
|
||
|
|
|
||
|
|
let pfo ppf s = match ppf with `Null -> () | `Fmt ppf -> Fmt.pf ppf "%s%!" s
|
||
|
|
|
||
|
|
let interpret_cmd { cmd; err; out; trim } =
|
||
|
|
Log.debug (fun l -> l "RUN: %a" Bos.Cmd.pp cmd);
|
||
|
|
let open Rresult in
|
||
|
|
let err =
|
||
|
|
match err with
|
||
|
|
| `Null -> Ok (Bos.OS.Cmd.err_null, fun () -> Ok ())
|
||
|
|
| `Fmt ppf ->
|
||
|
|
Bos.OS.File.tmp "cmd-err-%s" >>| fun path ->
|
||
|
|
let flush () = Bos.OS.File.read path >>| fun s -> Fmt.pf ppf "%s%!" s in
|
||
|
|
(Bos.OS.Cmd.err_file path, flush)
|
||
|
|
in
|
||
|
|
err >>= fun (err, flush_err) ->
|
||
|
|
let res = Bos.OS.Cmd.run_out ~err cmd in
|
||
|
|
let res = Bos.OS.Cmd.out_string ~trim res in
|
||
|
|
res >>= fun (str_out, _) ->
|
||
|
|
pfo out str_out;
|
||
|
|
flush_err () >>= fun () -> Bos.OS.Cmd.success res
|
||
|
|
|
||
|
|
let interpret_cmd_cli cmd =
|
||
|
|
Log.debug (fun l -> l "RUN-CLI: %a" Bos.Cmd.pp cmd);
|
||
|
|
let res = Bos.OS.Cmd.run_out cmd in
|
||
|
|
match Bos.OS.Cmd.out_stdout res with
|
||
|
|
| Ok ((), (_, `Exited 0)) -> Ok ()
|
||
|
|
| Ok ((), (_, `Exited _)) -> Error (`Msg "")
|
||
|
|
| failure -> Bos.OS.Cmd.success failure
|
||
|
|
|
||
|
|
let rec interpret_command : type r. r command -> r or_err = function
|
||
|
|
| Rmdir path ->
|
||
|
|
Log.debug (fun l -> l "rmdir %a" Fpath.pp path);
|
||
|
|
Bos.OS.Dir.delete ~recurse:true path
|
||
|
|
| Mkdir path ->
|
||
|
|
Log.debug (fun l -> l "mkdir %a" Fpath.pp path);
|
||
|
|
Bos.OS.Dir.create ~path:true path
|
||
|
|
| Ls { root; filter } ->
|
||
|
|
let open Rresult in
|
||
|
|
Log.debug (fun l -> l "ls %a" Fpath.pp root);
|
||
|
|
Bos.OS.Path.matches ~dotfiles:true Fpath.(root / "$(file)")
|
||
|
|
>>| fun files -> List.filter filter files
|
||
|
|
| Rm path ->
|
||
|
|
Log.debug (fun l -> l "rm %a" Fpath.pp path);
|
||
|
|
Bos.OS.File.delete ~must_exist:false path
|
||
|
|
| Is_file path ->
|
||
|
|
Log.debug (fun l -> l "is-file %a" Fpath.pp path);
|
||
|
|
Bos.OS.File.exists path
|
||
|
|
| Is_dir path ->
|
||
|
|
Log.debug (fun l -> l "is-dir %a" Fpath.pp path);
|
||
|
|
Bos.OS.Dir.exists path
|
||
|
|
| Size_of path -> (
|
||
|
|
Log.debug (fun l -> l "size-of %a" Fpath.pp path);
|
||
|
|
match Bos.OS.Path.stat path with
|
||
|
|
| Ok s -> Ok (Some s.Unix.st_size)
|
||
|
|
| _ -> Ok None)
|
||
|
|
| Run_cmd cmd -> Rresult.(interpret_cmd cmd >>| fun _ -> ())
|
||
|
|
| Run_cmd_out cmd -> interpret_cmd cmd
|
||
|
|
| Run_cmd_cli cmd -> interpret_cmd_cli cmd
|
||
|
|
| Set_var (c, v) ->
|
||
|
|
Log.debug (fun l ->
|
||
|
|
l "set_var %s %a" c Fmt.(option ~none:(any "<unset>") string) v);
|
||
|
|
Bos.OS.Env.set_var c v
|
||
|
|
| Get_var c ->
|
||
|
|
Log.debug (fun l -> l "get_var %s" c);
|
||
|
|
Ok (Bos.OS.Env.var c)
|
||
|
|
| With_dir (dir, f) ->
|
||
|
|
let f () = run (f ()) in
|
||
|
|
let open Rresult in
|
||
|
|
Bos.OS.Dir.current () >>= fun old ->
|
||
|
|
Log.debug (fun l -> l "entering %a" Fpath.pp dir);
|
||
|
|
Rresult.R.join @@ Bos.OS.Dir.with_current dir f () >>| fun r ->
|
||
|
|
Log.debug (fun l -> l "entering %a" Fpath.pp old);
|
||
|
|
r
|
||
|
|
| Pwd ->
|
||
|
|
Log.debug (fun l -> l "pwd");
|
||
|
|
Bos.OS.Dir.current ()
|
||
|
|
| Write_file (path, contents) ->
|
||
|
|
Log.debug (fun l -> l "write %a" Fpath.pp path);
|
||
|
|
Bos.OS.File.write path contents
|
||
|
|
| Read_file path ->
|
||
|
|
Log.debug (fun l -> l "read-file %a" Fpath.pp path);
|
||
|
|
Bos.OS.File.read path
|
||
|
|
| Tmp_file (mode, pat) ->
|
||
|
|
Log.debug (fun l -> l "tmp-file %s" Fmt.(str pat "*"));
|
||
|
|
Bos.OS.File.tmp ?mode pat
|
||
|
|
| With_output { mode; path; purpose; contents; append } -> (
|
||
|
|
try
|
||
|
|
let oc =
|
||
|
|
let path = Fpath.to_string path in
|
||
|
|
let mode = match mode with None -> 0o666 | Some m -> m in
|
||
|
|
if append then
|
||
|
|
open_out_gen [ Open_wronly; Open_append; Open_text ] mode path
|
||
|
|
else open_out path
|
||
|
|
in
|
||
|
|
let ppf = Format.formatter_of_out_channel oc in
|
||
|
|
let r = contents ppf in
|
||
|
|
Fmt.pf ppf "%!";
|
||
|
|
flush oc;
|
||
|
|
close_out oc;
|
||
|
|
Ok r
|
||
|
|
with e ->
|
||
|
|
Rresult.R.error_msgf "couldn't open output channel for %s: %a" purpose
|
||
|
|
Fmt.exn e)
|
||
|
|
|
||
|
|
and run : type r. r t -> r or_err = function
|
||
|
|
| Done r -> Ok r
|
||
|
|
| Fail f -> Error (`Msg f)
|
||
|
|
| Run (cmd, k) -> Rresult.R.bind (interpret_command cmd) (fun x -> run @@ k x)
|
||
|
|
|
||
|
|
type files = [ `Passtrough of Fpath.t | `Files of (Fpath.t * string) list ]
|
||
|
|
|
||
|
|
let default_exec cmd =
|
||
|
|
let cmd =
|
||
|
|
Fmt.str "$(%a)\n" Fmt.(list ~sep:(any " ") string) (Bos.Cmd.to_list cmd)
|
||
|
|
in
|
||
|
|
Some (cmd, "")
|
||
|
|
|
||
|
|
(* (simple) virtual environment *)
|
||
|
|
module Env : sig
|
||
|
|
type t
|
||
|
|
|
||
|
|
val eq : t -> t -> bool
|
||
|
|
val pp : t Fmt.t
|
||
|
|
val diff_files : old:t -> t -> Fpath.Set.t
|
||
|
|
val pwd : t -> Fpath.t
|
||
|
|
val chdir : t -> Fpath.t -> t
|
||
|
|
val ls : t -> Fpath.t -> Fpath.t list option
|
||
|
|
|
||
|
|
val v :
|
||
|
|
?exec:(Bos.Cmd.t -> (string * string) option) ->
|
||
|
|
?env:(string * string) list ->
|
||
|
|
?pwd:Fpath.t ->
|
||
|
|
?files:files ->
|
||
|
|
unit ->
|
||
|
|
t
|
||
|
|
|
||
|
|
val exec : t -> Bos.Cmd.t -> (string * string) option
|
||
|
|
val is_file : t -> Fpath.t -> bool
|
||
|
|
val is_dir : t -> Fpath.t -> bool
|
||
|
|
val mkdir : t -> Fpath.t -> (t * bool) option
|
||
|
|
val rm : t -> Fpath.t -> (t * bool) option
|
||
|
|
val rmdir : t -> Fpath.t -> t
|
||
|
|
val size_of : t -> Fpath.t -> int option
|
||
|
|
val write : t -> Fpath.t -> string -> t
|
||
|
|
val read : t -> Fpath.t -> string option
|
||
|
|
val tmp_file : t -> tmp_name_pat -> Fpath.t
|
||
|
|
val set_var : t -> string -> string option -> t
|
||
|
|
val get_var : t -> string -> string option
|
||
|
|
end = struct
|
||
|
|
type t = {
|
||
|
|
files : string Fpath.Map.t;
|
||
|
|
pwd : Fpath.t;
|
||
|
|
env : string String.Map.t;
|
||
|
|
exec : Bos.Cmd.t -> (string * string) option;
|
||
|
|
}
|
||
|
|
|
||
|
|
let diff_files ~old t =
|
||
|
|
let to_set t =
|
||
|
|
Fpath.Map.fold
|
||
|
|
(fun f _ acc ->
|
||
|
|
match Fpath.rem_prefix t.pwd f with
|
||
|
|
| None -> acc
|
||
|
|
| Some f -> Fpath.Set.add f acc)
|
||
|
|
t.files Fpath.Set.empty
|
||
|
|
in
|
||
|
|
Fpath.Set.diff (to_set t) (to_set old)
|
||
|
|
|
||
|
|
let scan dir =
|
||
|
|
(let open Rresult in
|
||
|
|
Bos.OS.Path.fold ~dotfiles:true ~elements:`Files ~traverse:`Any
|
||
|
|
(fun file files ->
|
||
|
|
files >>= fun files ->
|
||
|
|
Bos.OS.File.read file >>| fun c -> (file, c) :: files)
|
||
|
|
(Ok []) [ dir ])
|
||
|
|
|> Rresult.R.join
|
||
|
|
|> Rresult.R.error_msg_to_invalid_arg
|
||
|
|
|
||
|
|
let v ?(exec = default_exec) ?env ?pwd ?(files = `Files []) () =
|
||
|
|
let env =
|
||
|
|
match env with Some e -> String.Map.of_list e | None -> String.Map.empty
|
||
|
|
in
|
||
|
|
let pwd = match pwd with None -> Fpath.v "/" | Some p -> p in
|
||
|
|
let files =
|
||
|
|
let files =
|
||
|
|
match files with `Passtrough dir -> scan dir | `Files files -> files
|
||
|
|
in
|
||
|
|
let files =
|
||
|
|
List.map
|
||
|
|
(fun (f, c) ->
|
||
|
|
match Fpath.is_rel f with
|
||
|
|
| false -> (f, c)
|
||
|
|
| true -> (Fpath.(pwd // f), c))
|
||
|
|
files
|
||
|
|
in
|
||
|
|
List.map (fun (f, c) -> (Fpath.normalize f, c)) files
|
||
|
|
in
|
||
|
|
{ files = Fpath.Map.of_list files; pwd; env; exec }
|
||
|
|
|
||
|
|
let eq x y =
|
||
|
|
Fpath.Map.equal ( = ) x.files y.files
|
||
|
|
&& Fpath.equal x.pwd y.pwd
|
||
|
|
&& String.Map.equal ( = ) x.env y.env
|
||
|
|
|
||
|
|
let pp =
|
||
|
|
let open Fmt.Dump in
|
||
|
|
record
|
||
|
|
[
|
||
|
|
field "files" (fun t -> t.files) (Fpath.Map.dump string);
|
||
|
|
field "pwd" (fun t -> t.pwd) Fpath.dump;
|
||
|
|
field "env" (fun t -> t.env) (String.Map.dump string);
|
||
|
|
]
|
||
|
|
|
||
|
|
let pwd t = t.pwd
|
||
|
|
let exec t cmd = t.exec cmd
|
||
|
|
|
||
|
|
let mk_path t path =
|
||
|
|
match (Fpath.to_string t.pwd, Fpath.is_rel path) with
|
||
|
|
| _, true -> Fpath.(normalize @@ (t.pwd // path))
|
||
|
|
| _, false -> Fpath.normalize path
|
||
|
|
|
||
|
|
let chdir t path =
|
||
|
|
let pwd = mk_path t path in
|
||
|
|
{ t with pwd }
|
||
|
|
|
||
|
|
let is_root path = Fpath.to_string path = "/"
|
||
|
|
|
||
|
|
let mkdir t path =
|
||
|
|
let path = mk_path t path in
|
||
|
|
if is_root path then Some (t, false)
|
||
|
|
else
|
||
|
|
match Fpath.Map.find path t.files with
|
||
|
|
| Some f when f <> "<DIR>" -> None
|
||
|
|
| r ->
|
||
|
|
let t = { t with files = Fpath.Map.add path "<DIR>" t.files } in
|
||
|
|
Some (t, r = None)
|
||
|
|
|
||
|
|
let rmdir t path =
|
||
|
|
let path = mk_path t path in
|
||
|
|
let files =
|
||
|
|
Fpath.Map.filter
|
||
|
|
(fun f _ ->
|
||
|
|
let f = mk_path t f in
|
||
|
|
let b = not (Fpath.is_prefix path f) in
|
||
|
|
b)
|
||
|
|
t.files
|
||
|
|
in
|
||
|
|
{ t with files }
|
||
|
|
|
||
|
|
let ls t path =
|
||
|
|
let root = mk_path t path in
|
||
|
|
match Fpath.Map.find root t.files with
|
||
|
|
| Some "<DIR>" -> Some []
|
||
|
|
| Some _ -> Some [ path ]
|
||
|
|
| None -> (
|
||
|
|
Fpath.Map.fold
|
||
|
|
(fun file _ acc ->
|
||
|
|
let file = mk_path t file in
|
||
|
|
match Fpath.relativize ~root file with
|
||
|
|
| None -> acc
|
||
|
|
| Some f -> f :: acc)
|
||
|
|
t.files []
|
||
|
|
|> function
|
||
|
|
| [] -> None
|
||
|
|
| x -> Some (List.rev x))
|
||
|
|
|
||
|
|
let write t path f =
|
||
|
|
let path = mk_path t path in
|
||
|
|
{ t with files = Fpath.Map.add path f t.files }
|
||
|
|
|
||
|
|
let read t path =
|
||
|
|
let path = mk_path t path in
|
||
|
|
Fpath.Map.find path t.files
|
||
|
|
|
||
|
|
let tmp_file t pat =
|
||
|
|
let rec aux n =
|
||
|
|
let dir = Fpath.v "/tmp" in
|
||
|
|
let file = Fpath.(dir / Fmt.str pat (string_of_int n)) in
|
||
|
|
if Fpath.Map.mem file t.files then aux (n + 1) else file
|
||
|
|
in
|
||
|
|
aux 0
|
||
|
|
|
||
|
|
let is_dir t path =
|
||
|
|
let path = mk_path t path in
|
||
|
|
match Fpath.Map.find path t.files with
|
||
|
|
| Some "<DIR>" -> true
|
||
|
|
| Some _ -> false
|
||
|
|
| None ->
|
||
|
|
Fpath.Map.exists
|
||
|
|
(fun f _ ->
|
||
|
|
let f = mk_path t f in
|
||
|
|
Fpath.is_prefix path f)
|
||
|
|
t.files
|
||
|
|
|
||
|
|
let is_file t path =
|
||
|
|
let path = mk_path t path in
|
||
|
|
match Fpath.Map.find path t.files with
|
||
|
|
| Some "<DIR>" | None -> false
|
||
|
|
| Some _ -> true
|
||
|
|
|
||
|
|
let rm t path =
|
||
|
|
let path = mk_path t path in
|
||
|
|
match Fpath.Map.find path t.files with
|
||
|
|
| Some "<DIR>" -> None
|
||
|
|
| Some _ -> Some ({ t with files = Fpath.Map.remove path t.files }, true)
|
||
|
|
| None -> if is_dir t path then None else Some (t, false)
|
||
|
|
|
||
|
|
let size_of t path =
|
||
|
|
let path = mk_path t path in
|
||
|
|
match Fpath.Map.find path t.files with
|
||
|
|
| None -> None
|
||
|
|
| Some "<DIR>" -> Some 0
|
||
|
|
| Some f -> Some (String.length f)
|
||
|
|
|
||
|
|
let set_var t c = function
|
||
|
|
| None -> { t with env = String.Map.remove c t.env }
|
||
|
|
| Some v -> { t with env = String.Map.add c v t.env }
|
||
|
|
|
||
|
|
let get_var t c = String.Map.find c t.env
|
||
|
|
end
|
||
|
|
|
||
|
|
let error_msg = Rresult.R.error_msgf
|
||
|
|
|
||
|
|
type env = Env.t
|
||
|
|
|
||
|
|
let env = Env.v
|
||
|
|
|
||
|
|
type 'a domain = { result : 'a or_err; env : Env.t; logs : string list }
|
||
|
|
|
||
|
|
let pp_or_err pp_a = Rresult.R.pp ~error:Rresult.R.pp_msg ~ok:pp_a
|
||
|
|
let eq_or_err eq_a = Rresult.R.equal ~error:( = ) ~ok:eq_a
|
||
|
|
|
||
|
|
let pp_domain pp_a =
|
||
|
|
let open Fmt.Dump in
|
||
|
|
record
|
||
|
|
[
|
||
|
|
field "result" (fun t -> t.result) (pp_or_err pp_a);
|
||
|
|
field "env" (fun t -> t.env) Env.pp;
|
||
|
|
field "logs" (fun t -> t.logs) Fmt.Dump.(list string);
|
||
|
|
]
|
||
|
|
|
||
|
|
let eq_domain eq a b =
|
||
|
|
eq_or_err eq a.result b.result && Env.eq a.env b.env && a.logs = b.logs
|
||
|
|
|
||
|
|
let dom result env logs = { result; env; logs }
|
||
|
|
|
||
|
|
let interpret_dry_cmd env { cmd; err; out; _ } : string domain =
|
||
|
|
Log.debug (fun l -> l "Run_cmd '%a'" Bos.Cmd.pp cmd);
|
||
|
|
let log x = Fmt.str "Run_cmd '%a' (%s)" Bos.Cmd.pp cmd x in
|
||
|
|
match Env.exec env cmd with
|
||
|
|
| None -> dom (error_msg "'%a' not found" Bos.Cmd.pp cmd) env [ log "error" ]
|
||
|
|
| Some (o, e) ->
|
||
|
|
pfo out o;
|
||
|
|
pfo err e;
|
||
|
|
dom (Ok o) env [ log "ok" ]
|
||
|
|
|
||
|
|
let interpret_dry_cmd_cli env cmd : unit domain =
|
||
|
|
Log.debug (fun l -> l "Run_cmd_cli '%a'" Bos.Cmd.pp cmd);
|
||
|
|
let log x = Fmt.str "Run_cmd_cli '%a' (%s)" Bos.Cmd.pp cmd x in
|
||
|
|
match Env.exec env cmd with
|
||
|
|
| None -> dom (error_msg "'%a' not found" Bos.Cmd.pp cmd) env [ log "error" ]
|
||
|
|
| Some _ -> dom (Ok ()) env [ log "ok" ]
|
||
|
|
|
||
|
|
let rec interpret_dry : type r. env:Env.t -> r command -> r domain =
|
||
|
|
fun ~env -> function
|
||
|
|
| Mkdir path -> (
|
||
|
|
Log.debug (fun l -> l "Mkdir %a" Fpath.pp path);
|
||
|
|
let log s = Fmt.str "Mkdir %a (%s)" Fpath.pp path s in
|
||
|
|
match Env.mkdir env path with
|
||
|
|
| Some (env, true) -> dom (Ok true) env [ log "created" ]
|
||
|
|
| Some (env, false) -> dom (Ok false) env [ log "already exists" ]
|
||
|
|
| None ->
|
||
|
|
dom
|
||
|
|
(error_msg "a file named '%a' already exists" Fpath.pp path)
|
||
|
|
env
|
||
|
|
[ log "error" ])
|
||
|
|
| Rmdir path ->
|
||
|
|
Log.debug (fun l -> l "Rmdir %a" Fpath.pp path);
|
||
|
|
let log s = Fmt.str "Rmdir %a (%s)" Fpath.pp path s in
|
||
|
|
if Env.is_dir env path || Env.is_file env path then
|
||
|
|
dom (Ok ()) (Env.rmdir env path) [ log "removed" ]
|
||
|
|
else dom (Ok ()) env [ log "no-op" ]
|
||
|
|
| Ls { root; filter } -> (
|
||
|
|
Log.debug (fun l -> l "Ls %a" Fpath.pp root);
|
||
|
|
let logs fmt = Fmt.kstr (Fmt.str "Ls %a (%s)" Fpath.pp root) fmt in
|
||
|
|
match Env.ls env root with
|
||
|
|
| None ->
|
||
|
|
dom
|
||
|
|
(error_msg "%a: no such file or directory" Fpath.pp root)
|
||
|
|
env
|
||
|
|
[ logs "error" ]
|
||
|
|
| Some es -> (
|
||
|
|
match List.filter filter es with
|
||
|
|
| ([] | [ _ ]) as e ->
|
||
|
|
dom (Ok e) env [ logs "%d entry" (List.length e) ]
|
||
|
|
| es -> dom (Ok es) env [ logs "%d entries" (List.length es) ]))
|
||
|
|
| Rm path -> (
|
||
|
|
Log.debug (fun l -> l "Rm %a" Fpath.pp path);
|
||
|
|
let log s = Fmt.str "Rm %a (%s)" Fpath.pp path s in
|
||
|
|
match Env.rm env path with
|
||
|
|
| Some (env, b) ->
|
||
|
|
dom (Ok ()) env [ log (if b then "removed" else "no-op") ]
|
||
|
|
| None ->
|
||
|
|
dom (error_msg "%a is a directory" Fpath.pp path) env [ log "error" ])
|
||
|
|
| Is_file path ->
|
||
|
|
Log.debug (fun l -> l "Is_file %a" Fpath.pp path);
|
||
|
|
let r = Env.is_file env path in
|
||
|
|
dom (Ok r) env [ Fmt.str "Is_file? %a -> %b" Fpath.pp path r ]
|
||
|
|
| Is_dir path ->
|
||
|
|
Log.debug (fun l -> l "Is_dir %a" Fpath.pp path);
|
||
|
|
let r = Env.is_dir env path in
|
||
|
|
dom (Ok r) env [ Fmt.str "Is_dir? %a -> %b" Fpath.pp path r ]
|
||
|
|
| Size_of path ->
|
||
|
|
Log.debug (fun l -> l "Size_of %a" Fpath.pp path);
|
||
|
|
let r = Env.size_of env path in
|
||
|
|
dom (Ok r) env
|
||
|
|
[
|
||
|
|
Fmt.str "Size_of %a -> %a" Fpath.pp path
|
||
|
|
Fmt.(option ~none:(any "error") int)
|
||
|
|
r;
|
||
|
|
]
|
||
|
|
| Run_cmd cmd -> (
|
||
|
|
let domain = interpret_dry_cmd env cmd in
|
||
|
|
match domain.result with
|
||
|
|
| Ok _ -> { domain with result = Ok () }
|
||
|
|
| Error _ as r -> { domain with result = r })
|
||
|
|
| Run_cmd_out cmd -> interpret_dry_cmd env cmd
|
||
|
|
| Run_cmd_cli cmd -> interpret_dry_cmd_cli env cmd
|
||
|
|
| Write_file (path, s) ->
|
||
|
|
Log.debug (fun l -> l "Write_file %a" Fpath.pp path);
|
||
|
|
dom (Ok ()) (Env.write env path s)
|
||
|
|
[ Fmt.str "Write to %a (%d bytes)" Fpath.pp path (String.length s) ]
|
||
|
|
| Read_file path -> (
|
||
|
|
Log.debug (fun l -> l "Read_file %a" Fpath.pp path);
|
||
|
|
match Env.read env path with
|
||
|
|
| None ->
|
||
|
|
let log = Fmt.str "Read: %a" Fpath.pp path in
|
||
|
|
dom (error_msg "read_file: file does not exist") env [ log ]
|
||
|
|
| Some r ->
|
||
|
|
let log =
|
||
|
|
Fmt.str "Read %a (%d bytes)" Fpath.pp path (String.length r)
|
||
|
|
in
|
||
|
|
dom (Ok r) env [ log ])
|
||
|
|
| Tmp_file (_, pat) ->
|
||
|
|
Log.debug (fun l -> l "Tmp_file %s" Fmt.(str pat "*"));
|
||
|
|
let r = Env.tmp_file env pat in
|
||
|
|
dom (Ok r) env [ Fmt.str "Tmp_file -> %a" Fpath.pp r ]
|
||
|
|
| Set_var (c, v) ->
|
||
|
|
Log.debug (fun l ->
|
||
|
|
l "Set_var %s %a" c Fmt.(option ~none:(any "<none>") string) v);
|
||
|
|
let env = Env.set_var env c v in
|
||
|
|
let log =
|
||
|
|
Fmt.str "Set_var %s %a" c Fmt.(option ~none:(any "<unset>") string) v
|
||
|
|
in
|
||
|
|
dom (Ok ()) env [ log ]
|
||
|
|
| Get_var c ->
|
||
|
|
Log.debug (fun l -> l "Get_var %s" c);
|
||
|
|
let v = Env.get_var env c in
|
||
|
|
let log =
|
||
|
|
Fmt.str "Get_var %s -> %a" c
|
||
|
|
Fmt.(option ~none:(any "<not set>") string)
|
||
|
|
v
|
||
|
|
in
|
||
|
|
dom (Ok v) env [ log ]
|
||
|
|
| With_dir (dir, f) ->
|
||
|
|
Log.debug (fun l -> l "With_dir %a" Fpath.pp dir);
|
||
|
|
let old = Env.pwd env in
|
||
|
|
let env = Env.chdir env dir in
|
||
|
|
let domain = dry_run ~env (f ()) in
|
||
|
|
let env = Env.chdir domain.env old in
|
||
|
|
let log =
|
||
|
|
Fmt.str "With_dir %a [%a]" Fpath.pp dir
|
||
|
|
Fmt.(vbox ~indent:2 (list ~sep:(any "@,") string))
|
||
|
|
domain.logs
|
||
|
|
in
|
||
|
|
{ domain with env; logs = [ log ] }
|
||
|
|
| Pwd ->
|
||
|
|
Log.debug (fun l -> l "Pwd");
|
||
|
|
let r = Env.pwd env in
|
||
|
|
dom (Ok r) env [ Fmt.str "Pwd -> %a" Fpath.pp r ]
|
||
|
|
| With_output { mode; path; purpose; contents; append } ->
|
||
|
|
let pp_append ppf () = if append then Fmt.string ppf "[append]" else () in
|
||
|
|
Log.debug (fun l ->
|
||
|
|
l "With_output%a %a (%s)" pp_append () Fpath.pp path purpose);
|
||
|
|
let buf = Buffer.create 0 in
|
||
|
|
let fmt = Format.formatter_of_buffer buf in
|
||
|
|
let pp_mode fmt = function
|
||
|
|
| None -> Format.fprintf fmt "default"
|
||
|
|
| Some n -> Format.fprintf fmt "%#o" n
|
||
|
|
in
|
||
|
|
let r = contents fmt in
|
||
|
|
Fmt.pf fmt "%!";
|
||
|
|
let f = Buffer.contents buf in
|
||
|
|
let log =
|
||
|
|
Fmt.str "Write to %a (mode: %a, purpose: %s)" Fpath.pp path pp_mode mode
|
||
|
|
purpose
|
||
|
|
in
|
||
|
|
dom (Ok r) (Env.write env path f) [ log ]
|
||
|
|
|
||
|
|
and dry_run : type r. env:Env.t -> r t -> r domain =
|
||
|
|
fun ~env t ->
|
||
|
|
let rec go t ~env log =
|
||
|
|
match t with
|
||
|
|
| Done r -> dom (Ok r) env log
|
||
|
|
| Fail e -> dom (Error (`Msg e)) env log
|
||
|
|
| Run (cmd, k) -> (
|
||
|
|
let domain = interpret_dry ~env cmd in
|
||
|
|
let new_log = List.rev domain.logs @ log in
|
||
|
|
match domain.result with
|
||
|
|
| Ok x -> go (k x) ~env:domain.env new_log
|
||
|
|
| Error _ as e -> dom e domain.env new_log)
|
||
|
|
in
|
||
|
|
let domain = go t ~env [] in
|
||
|
|
{ domain with logs = List.rev domain.logs }
|
||
|
|
|
||
|
|
let dry_run ?(env = env ()) t = dry_run ~env t
|
||
|
|
|
||
|
|
let dry_run_trace ?env t =
|
||
|
|
let domain = dry_run ?env t in
|
||
|
|
List.iter print_endline domain.logs
|
||
|
|
|
||
|
|
let generated_files ?(env = env ~exec:(fun _ -> None) ()) t =
|
||
|
|
let domain = dry_run ~env t in
|
||
|
|
Env.diff_files ~old:env domain.env
|
||
|
|
|
||
|
|
module Infix = struct
|
||
|
|
let ( >>= ) x f = bind ~f x
|
||
|
|
let ( >|= ) x f = map ~f x
|
||
|
|
end
|
||
|
|
|
||
|
|
module Syntax = struct
|
||
|
|
open Infix
|
||
|
|
|
||
|
|
let ( let* ) = ( >>= )
|
||
|
|
let ( let+ ) = ( >|= )
|
||
|
|
end
|
||
|
|
|
||
|
|
module List = struct
|
||
|
|
open Infix
|
||
|
|
|
||
|
|
let iter ~f l = List.fold_left (fun acc e -> acc >>= fun () -> f e) (ok ()) l
|
||
|
|
|
||
|
|
let map ~f l =
|
||
|
|
List.fold_left
|
||
|
|
(fun acc e ->
|
||
|
|
acc >>= fun acc ->
|
||
|
|
f e >|= fun e -> e :: acc)
|
||
|
|
(ok []) l
|
||
|
|
end
|