File: exec.ml

package info (click to toggle)
ocaml-dune 3.20.2-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 33,564 kB
  • sloc: ml: 175,178; asm: 28,570; ansic: 5,251; sh: 1,096; lisp: 625; makefile: 148; python: 125; cpp: 48; javascript: 10
file content (328 lines) | stat: -rw-r--r-- 12,233 bytes parent folder | download
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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
open Import

let doc = "Execute a command in a similar environment as if installation was performed."

let man =
  [ `S "DESCRIPTION"
  ; `P
      {|$(b,dune exec -- COMMAND) should behave in the same way as if you
          do:|}
  ; `Pre "  \\$ dune install\n  \\$ COMMAND"
  ; `P
      {|In particular if you run $(b,dune exec ocaml), you will have
          access to the libraries defined in the workspace using your usual
          directives ($(b,#require) for instance)|}
  ; `P
      {|When a leading / is present in the command (absolute path), then the
          path is interpreted as an absolute path|}
  ; `P
      {|When a / is present at any other position (relative path), then the
          path is interpreted as relative to the build context + current
          working directory (or the value of $(b,--root) when ran outside of
          the project root)|}
  ; `Blocks Common.help_secs
  ; Common.examples
      [ "Run the executable named `my_exec'", "dune exec my_exec"
      ; ( "Run the executable defined in `foo.ml' with the argument `arg'"
        , "dune exec -- ./foo.exe arg" )
      ]
  ]
;;

let info = Cmd.info "exec" ~doc ~man

module Cmd_arg = struct
  type t =
    | Expandable of Dune_lang.String_with_vars.t * string
    | Terminal of string

  let parse s =
    match Arg.conv_parser Arg.dep s with
    | Ok (File sw) when Dune_lang.String_with_vars.has_pforms sw -> Expandable (sw, s)
    | _ -> Terminal s
  ;;

  let pp pps = function
    | Expandable (_, s) -> Format.fprintf pps "%s" s
    | Terminal s -> Format.fprintf pps "%s" s
  ;;

  let expand t ~root ~sctx =
    let open Memo.O in
    match t with
    | Terminal s -> Memo.return s
    | Expandable (sw, _) ->
      let+ path, _ =
        Target.expand_path_from_root root sctx sw
        |> Action_builder.evaluate_and_collect_facts
      in
      let context = Dune_rules.Super_context.context sctx in
      (* TODO Why are we stringifying this path? *)
      Path.to_string (Path.build (Path.Build.relative (Context.build_dir context) path))
  ;;

  let conv = Arg.conv ((fun s -> Ok (parse s)), pp)
end

let not_found ~hints ~prog =
  User_error.raise
    ~hints
    [ Pp.concat
        ~sep:Pp.space
        [ Pp.text "Program"; User_message.command prog; Pp.text "not found!" ]
    ]
;;

let not_found_with_suggestions ~dir ~prog =
  let open Memo.O in
  let+ hints =
    (* Good candidates for the "./x.exe" instead of "x.exe" error are
       executables present in the current directory. Note: we do not
       check directory targets here; even if they do indeed include a
       matching executable, they would be located in a subdirectory of
       [dir], so it's unclear if that's what the user wanted. *)
    let+ candidates =
      let+ filename_set = Build_system.files_of ~dir:(Path.build dir) in
      Filename_set.filenames filename_set
      |> Filename.Set.to_list
      |> List.filter ~f:(fun filename -> Filename.extension filename = ".exe")
      |> List.map ~f:(fun filename -> "./" ^ filename)
    in
    User_message.did_you_mean prog ~candidates
  in
  not_found ~hints ~prog
;;

let program_not_built_yet prog =
  User_error.raise
    [ Pp.concat
        ~sep:Pp.space
        [ Pp.text "Program"
        ; User_message.command prog
        ; Pp.text "isn't built yet. You need to build it first or remove the"
        ; User_message.command "--no-build"
        ; Pp.text "option."
        ]
    ]
;;

let build_prog ~no_rebuild ~prog p =
  if no_rebuild
  then if Path.exists p then Memo.return p else program_not_built_yet prog
  else
    let open Memo.O in
    let+ () = Build_system.build_file p in
    p
;;

let dir_of_context common sctx =
  let context = Dune_rules.Super_context.context sctx in
  Path.Build.relative (Context.build_dir context) (Common.prefix_target common "")
;;

let get_path common sctx ~prog =
  let open Memo.O in
  let dir = dir_of_context common sctx in
  match Filename.analyze_program_name prog with
  | In_path ->
    Super_context.resolve_program_memo sctx ~dir ~loc:None prog
    >>= (function
     | Error (_ : Action.Prog.Not_found.t) -> not_found_with_suggestions ~dir ~prog
     | Ok p -> Memo.return p)
  | Relative_to_current_dir ->
    let path = Path.relative_to_source_in_build_or_external ~dir prog in
    Build_system.file_exists path
    >>= (function
     | true -> Memo.return path
     | false -> not_found_with_suggestions ~dir ~prog)
  | Absolute ->
    (match
       let prog = Path.of_string prog in
       if Path.exists prog
       then Some prog
       else if not Sys.win32
       then None
       else (
         let prog = Path.extend_basename prog ~suffix:Bin.exe in
         Option.some_if (Path.exists prog) prog)
     with
     | Some prog -> Memo.return prog
     | None -> not_found_with_suggestions ~dir ~prog)
;;

let get_path_and_build_if_necessary common sctx ~no_rebuild ~prog =
  let open Memo.O in
  let* path = get_path common sctx ~prog in
  match Filename.analyze_program_name prog with
  | In_path | Relative_to_current_dir -> build_prog ~no_rebuild ~prog path
  | Absolute -> Memo.return path
;;

let step ~prog ~args ~common ~no_rebuild ~context ~on_exit () =
  let open Memo.O in
  let* sctx = Super_context.find_exn context in
  let* path =
    let* prog = Cmd_arg.expand ~root:(Common.root common) ~sctx prog in
    get_path_and_build_if_necessary common sctx ~no_rebuild ~prog
  and* args =
    Memo.parallel_map args ~f:(Cmd_arg.expand ~root:(Common.root common) ~sctx)
  in
  let* env = Super_context.context_env sctx in
  Memo.of_non_reproducible_fiber
  @@ Dune_engine.Process.run_inherit_std_in_out
       ~dir:(Path.of_string Fpath.initial_cwd)
       ~env
       path
       args
  >>| function
  | 0 -> ()
  | exit_code -> on_exit exit_code
;;

(* Similar to [get_path_and_build_if_necessary] but doesn't require the build
   system (ie. it sequences with [Fiber] rather than with [Memo]) and builds
   targets via an RPC server. Some functionality is not available but it can be
   run concurrently while a second Dune process holds the global build
   directory lock.

   Returns the absolute path to the executable. *)
let build_prog_via_rpc_if_necessary ~dir ~no_rebuild prog =
  match Filename.analyze_program_name prog with
  | In_path ->
    (* This case is reached if [dune exec] is passed the name of an
       executable (rather than a path to an executable). When dune is running
       directly, dune will try to resolve the executbale name within the public
       executables defined in the current project and its dependencies, and
       only if no executable with the given name is found will dune then
       resolve the name within the $PATH variable instead. Looking up an
       executable's name within the current project requires running the
       build system, but running the build system is not allowed while
       another dune instance holds the global build directory lock. In this
       case dune will only resolve the executable's name within $PATH.
       Because this behaviour is different from the default, print a warning
       so users are hopefully less surprised.
    *)
    User_warning.emit
      [ Pp.textf
          "As this is not the main instance of Dune it is unable to locate the \
           executable %S within this project. Dune will attempt to resolve the \
           executable's name within your PATH only."
          prog
      ];
    let path = Env_path.path Env.initial in
    (match Bin.which ~path prog with
     | None -> not_found ~hints:[] ~prog
     | Some prog_path -> Fiber.return (Path.to_absolute_filename prog_path))
  | Relative_to_current_dir ->
    let open Fiber.O in
    let path = Path.relative_to_source_in_build_or_external ~dir prog in
    let+ () =
      if no_rebuild
      then if Path.exists path then Fiber.return () else program_not_built_yet prog
      else (
        let target =
          Dune_lang.Dep_conf.File
            (Dune_lang.String_with_vars.make_text Loc.none (Path.to_string path))
        in
        Build.build_via_rpc_server ~print_on_success:false ~targets:[ target ])
    in
    Path.to_absolute_filename path
  | Absolute ->
    if Path.exists (Path.of_string prog)
    then Fiber.return prog
    else not_found ~hints:[] ~prog
;;

let exec_building_via_rpc_server ~common ~prog ~args ~no_rebuild =
  let open Fiber.O in
  let ensure_terminal v =
    match (v : Cmd_arg.t) with
    | Terminal s -> s
    | Expandable (_, raw) ->
      (* Variables cannot be expanded without running the build system. *)
      User_error.raise
        [ Pp.textf
            "The term %S contains a variable but Dune is unable to expand variables when \
             building via RPC."
            raw
        ]
  in
  let context = Common.x common |> Option.value ~default:Context_name.default in
  let dir = Context_name.build_dir context in
  let prog = ensure_terminal prog in
  let args = List.map args ~f:ensure_terminal in
  let+ prog = build_prog_via_rpc_if_necessary ~dir ~no_rebuild prog in
  restore_cwd_and_execve (Common.root common) prog args Env.initial
;;

let exec_building_directly ~common ~config ~context ~prog ~args ~no_rebuild =
  match Common.watch common with
  | Yes Passive ->
    User_error.raise [ Pp.textf "passive watch mode is unsupported by exec" ]
  | Yes Eager ->
    Scheduler.go_with_rpc_server_and_console_status_reporting ~common ~config
    @@ fun () ->
    let open Fiber.O in
    let on_exit = Console.printf "Program exited with code [%d]" in
    Scheduler.Run.poll
    @@
    let* () = Fiber.return @@ Scheduler.maybe_clear_screen ~details_hum:[] config in
    build @@ step ~prog ~args ~common ~no_rebuild ~context ~on_exit
  | No ->
    Scheduler.go_with_rpc_server ~common ~config
    @@ fun () ->
    let open Fiber.O in
    let* setup = Import.Main.setup () in
    build_exn (fun () ->
      let open Memo.O in
      let* sctx = setup >>| Import.Main.find_scontext_exn ~name:context in
      let* env = Super_context.context_env sctx
      and* prog =
        let* prog = Cmd_arg.expand ~root:(Common.root common) ~sctx prog in
        get_path_and_build_if_necessary common sctx ~no_rebuild ~prog >>| Path.to_string
      and* args =
        Memo.parallel_map ~f:(Cmd_arg.expand ~root:(Common.root common) ~sctx) args
      in
      restore_cwd_and_execve (Common.root common) prog args env)
;;

let term : unit Term.t =
  let+ builder = Common.Builder.term
  and+ context = Common.context_arg ~doc:{|Run the command in this build context.|}
  and+ prog = Arg.(required & pos 0 (some Cmd_arg.conv) None (Arg.info [] ~docv:"PROG"))
  and+ no_rebuild =
    Arg.(value & flag & info [ "no-build" ] ~doc:"don't rebuild target before executing")
  and+ args = Arg.(value & pos_right 0 Cmd_arg.conv [] (Arg.info [] ~docv:"ARGS")) in
  (* TODO we should make sure to finalize the current backend before exiting dune.
     For watch mode, we should finalize the backend and then restart it in between
     runs. *)
  let common, config = Common.init builder in
  match Dune_util.Global_lock.lock ~timeout:None with
  | Error lock_held_by ->
    (match Common.watch common with
     | Yes _ ->
       User_error.raise
         [ Pp.textf
             "Another instance of dune%s has locked the _build directory. Refusing to \
              start a new watch server until no other instances of dune are running."
             (match lock_held_by with
              | Unknown -> ""
              | Pid_from_lockfile pid -> sprintf " (pid: %d)" pid)
         ]
     | No ->
       if not (Common.Builder.equal builder Common.Builder.default)
       then
         User_warning.emit
           [ Pp.textf
               "Your build request is being forwarded to a running Dune instance%s. Note \
                that certain command line arguments may be ignored."
               (match lock_held_by with
                | Unknown -> ""
                | Pid_from_lockfile pid -> sprintf " (pid: %d)" pid)
           ];
       Scheduler.go_without_rpc_server ~common ~config
       @@ fun () -> exec_building_via_rpc_server ~common ~prog ~args ~no_rebuild)
  | Ok () -> exec_building_directly ~common ~config ~context ~prog ~args ~no_rebuild
;;

let command = Cmd.v info term