package dune-action-plugin

  1. Overview
  2. Docs

Source file dune_action_plugin.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
open Import

module V1 = struct
  module Path = Path
  module Glob = Dune_glob.V1
  open Protocol

  module Execution_error = struct
    exception E of string

    let raise string = raise (E string)

    let raise_on_fs_error = function
      | Error message -> raise message
      | Ok result -> result
    ;;
  end

  module Fs : sig
    val read_directory : string -> (string list, string) result
    val read_file : string -> (string, string) result
    val write_file : string -> string -> (unit, string) result
  end = struct
    let catch_system_exceptions f ~name =
      try Ok (f ()) with
      | Unix.Unix_error (error, syscall, arg) ->
        let error = Unix_error.Detailed.create error ~syscall ~arg in
        Error (name ^ ": " ^ Unix_error.Detailed.to_string_hum error)
      | Sys_error error -> Error (name ^ ": " ^ error)
    ;;

    let read_directory =
      let rec loop dh acc =
        match Unix.readdir dh with
        | "." | ".." -> loop dh acc
        | s -> loop dh (s :: acc)
        | exception End_of_file -> acc
      in
      fun path ->
        catch_system_exceptions ~name:"read_directory" (fun () ->
          let dh = Unix.opendir path in
          Exn.protect
            ~f:(fun () -> loop dh [] |> List.sort ~compare:String.compare)
            ~finally:(fun () -> Unix.closedir dh))
    ;;

    let read_file path =
      catch_system_exceptions ~name:"read_file" (fun () -> Io.String_path.read_file path)
    ;;

    let write_file path data =
      catch_system_exceptions ~name:"write_file" (fun () ->
        Io.String_path.write_file path data)
    ;;
  end

  module Stage = struct
    type 'a t =
      { action : unit -> 'a
      ; dependencies : Dependency.Set.t
      ; targets : String.Set.t
      }

    let map (t : 'a t) ~f = { t with action = (fun () -> f (t.action ())) }

    let both (t1 : 'a t) (t2 : 'b t) =
      { action = (fun () -> t1.action (), t2.action ())
      ; dependencies = Dependency.Set.union t1.dependencies t2.dependencies
      ; targets = String.Set.union t1.targets t2.targets
      }
    ;;
  end

  (* Construction inspired by free monad. *)
  type 'a t =
    | Pure of 'a
    | Stage of 'a t Stage.t

  let lift_stage stage = Stage (Stage.map stage ~f:(fun a -> Pure a))

  let rec map (t : 'a t) ~f =
    match t with
    | Pure a -> Pure (f a)
    | Stage at -> Stage (Stage.map ~f:(map ~f) at)
  ;;

  let rec stage (t : 'a t) ~f =
    match t with
    | Pure a -> f a
    | Stage at -> Stage (Stage.map ~f:(stage ~f) at)
  ;;

  let return a = Pure a

  let rec both (t1 : 'a t) (t2 : 'b t) =
    match t1, t2 with
    | Pure a1, _ -> map ~f:(fun a2 -> a1, a2) t2
    | _, Pure a2 -> map ~f:(fun a1 -> a1, a2) t1
    | Stage at1, Stage at2 ->
      Stage (Stage.both at1 at2 |> Stage.map ~f:(fun (am1, am2) -> both am1 am2))
  ;;

  let read_file ~path =
    let path = Path.to_string path in
    let action () = Fs.read_file path |> Execution_error.raise_on_fs_error in
    lift_stage
      { action
      ; dependencies = Dependency.Set.singleton (File path)
      ; targets = String.Set.empty
      }
  ;;

  let write_file ~path ~data =
    let path = Path.to_string path in
    let action () = Fs.write_file path data |> Execution_error.raise_on_fs_error in
    lift_stage
      { action; dependencies = Dependency.Set.empty; targets = String.Set.singleton path }
  ;;

  (* TODO jstaron: If program tries to read empty directory, dune does not copy
     it to `_build` so we get a "No such file or directory" error. *)
  let read_directory_with_glob ~path ~glob =
    let path = Path.to_string path in
    let action () =
      Fs.read_directory path
      |> Execution_error.raise_on_fs_error
      |> List.filter ~f:(Glob.test glob)
    in
    lift_stage
      { action
      ; dependencies =
          Dependency.Set.singleton (Glob { path; glob = Glob.to_string glob })
      ; targets = String.Set.empty
      }
  ;;

  let rec run_by_dune t context =
    match t with
    | Pure () -> Context.respond context Done
    | Stage at ->
      let allowed_targets = Context.targets context in
      let disallowed_targets = String.Set.diff at.targets allowed_targets in
      (match String.Set.to_list disallowed_targets with
       | [] -> ()
       | [ t ] ->
         Execution_error.raise
           (Printf.sprintf
              "%s is written despite not being declared as a target in dune file. To \
               fix, add it to target list in dune file."
              t)
       | ts ->
         Execution_error.raise
           (Printf.sprintf
              "Following files were written despite not being declared as targets in \
               dune file:\n\
               %sTo fix, add them to target list in dune file."
              (ts |> String.concat ~sep:"\n")));
      let prepared_dependencies = Context.prepared_dependencies context in
      let required_dependencies =
        Dependency.Set.diff at.dependencies prepared_dependencies
      in
      if Dependency.Set.is_empty required_dependencies
      then run_by_dune (at.action ()) context
      else Context.respond context (Need_more_deps required_dependencies)
  ;;

  (* If executable is not run by dune, assume that all dependencies are already
     prepared and no target checking is done. *)
  let rec run_outside_of_dune t =
    match t with
    | Pure () -> ()
    | Stage at -> run_outside_of_dune (at.action ())
  ;;

  let do_run t =
    match Protocol.Context.create () with
    | Run_outside_of_dune -> run_outside_of_dune t
    | Error message ->
      Execution_error.raise
        (Printf.sprintf
           "Error during communication with dune. %s Did you use different dune version \
            to compile the executable?"
           message)
    | Ok context -> run_by_dune t context
  ;;

  let run t =
    try
      do_run t;
      exit 0
    with
    | Execution_error.E message ->
      prerr_endline message;
      exit 1
  ;;

  module O = struct
    let ( let+ ) at f = map at ~f
    let ( and+ ) = both
  end

  module Private = struct
    module Protocol = Protocol

    let do_run = do_run

    module Execution_error = Execution_error
  end
end

module Private = V1.Private
OCaml

Innovation. Community. Security.