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
|