Skip to content

Commit b91cfa6

Browse files
committed
Allow dune exec while watch server is running
This change allows a limited version of `dune exec` to run at the same time as dune is running in watch mode. This allows users to run example programs without needing to stop their watch server. This works by sending messages to the RPC server to build the executable if necessary. Signed-off-by: Stephen Sherratt <[email protected]>
1 parent 13c5177 commit b91cfa6

File tree

7 files changed

+191
-42
lines changed

7 files changed

+191
-42
lines changed

bin/build_cmd.ml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,8 @@ let build =
221221
[ Pp.textf
222222
"Your build request is being forwarded to a running Dune instance%s so \
223223
most command-line arguments will be ignored."
224-
(match (lock_held_by : Dune_util.Global_lock.Lock_held_by.t) with
225-
| Unknown -> ""
226-
| Pid_from_lockfile pid -> sprintf " (pid: %d)" pid)
224+
(Dune_util.Global_lock.Lock_held_by.to_string_empty_if_unknown
225+
lock_held_by)
227226
];
228227
build_via_rpc_server ~print_on_success:true ~targets)
229228
| Ok () ->

bin/exec.ml

Lines changed: 123 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,16 @@ module Cmd_arg = struct
6464
let conv = Arg.conv ((fun s -> Ok (parse s)), pp)
6565
end
6666

67-
let not_found ~dir ~prog =
67+
let not_found ~hints ~prog =
68+
User_error.raise
69+
~hints
70+
[ Pp.concat
71+
~sep:Pp.space
72+
[ Pp.text "Program"; User_message.command prog; Pp.text "not found!" ]
73+
]
74+
;;
75+
76+
let not_found_with_suggestions ~dir ~prog =
6877
let open Memo.O in
6978
let+ hints =
7079
(* Good candidates for the "./x.exe" instead of "x.exe" error are
@@ -81,30 +90,25 @@ let not_found ~dir ~prog =
8190
in
8291
User_message.did_you_mean prog ~candidates
8392
in
93+
not_found ~hints ~prog
94+
;;
95+
96+
let program_not_built_yet prog =
8497
User_error.raise
85-
~hints
8698
[ Pp.concat
8799
~sep:Pp.space
88-
[ Pp.text "Program"; User_message.command prog; Pp.text "not found!" ]
100+
[ Pp.text "Program"
101+
; User_message.command prog
102+
; Pp.text "isn't built yet. You need to build it first or remove the"
103+
; User_message.command "--no-build"
104+
; Pp.text "option."
105+
]
89106
]
90107
;;
91108

92109
let build_prog ~no_rebuild ~prog p =
93110
if no_rebuild
94-
then
95-
if Path.exists p
96-
then Memo.return p
97-
else
98-
User_error.raise
99-
[ Pp.concat
100-
~sep:Pp.space
101-
[ Pp.text "Program"
102-
; User_message.command prog
103-
; Pp.text "isn't built yet. You need to build it first or remove the"
104-
; User_message.command "--no-build"
105-
; Pp.text "option."
106-
]
107-
]
111+
then if Path.exists p then Memo.return p else program_not_built_yet prog
108112
else
109113
let open Memo.O in
110114
let+ () = Build_system.build_file p in
@@ -117,14 +121,14 @@ let get_path_and_build_if_necessary sctx ~no_rebuild ~dir ~prog =
117121
| In_path ->
118122
Super_context.resolve_program_memo sctx ~dir ~loc:None prog
119123
>>= (function
120-
| Error (_ : Action.Prog.Not_found.t) -> not_found ~dir ~prog
124+
| Error (_ : Action.Prog.Not_found.t) -> not_found_with_suggestions ~dir ~prog
121125
| Ok p -> build_prog ~no_rebuild ~prog p)
122126
| Relative_to_current_dir ->
123127
let path = Path.relative_to_source_in_build_or_external ~dir prog in
124128
Build_system.file_exists path
125129
>>= (function
126130
| true -> build_prog ~no_rebuild ~prog path
127-
| false -> not_found ~dir ~prog)
131+
| false -> not_found_with_suggestions ~dir ~prog)
128132
| Absolute ->
129133
(match
130134
let prog = Path.of_string prog in
@@ -137,7 +141,7 @@ let get_path_and_build_if_necessary sctx ~no_rebuild ~dir ~prog =
137141
Option.some_if (Path.exists prog) prog)
138142
with
139143
| Some prog -> Memo.return prog
140-
| None -> not_found ~dir ~prog)
144+
| None -> not_found_with_suggestions ~dir ~prog)
141145
;;
142146

143147
let step ~setup ~prog ~args ~common ~no_rebuild ~context ~on_exit () =
@@ -164,17 +168,69 @@ let step ~setup ~prog ~args ~common ~no_rebuild ~context ~on_exit () =
164168
| exit_code -> on_exit exit_code
165169
;;
166170

167-
let term : unit Term.t =
168-
let+ builder = Common.Builder.term
169-
and+ context = Common.context_arg ~doc:{|Run the command in this build context.|}
170-
and+ prog = Arg.(required & pos 0 (some Cmd_arg.conv) None (Arg.info [] ~docv:"PROG"))
171-
and+ no_rebuild =
172-
Arg.(value & flag & info [ "no-build" ] ~doc:"don't rebuild target before executing")
173-
and+ args = Arg.(value & pos_right 0 Cmd_arg.conv [] (Arg.info [] ~docv:"ARGS")) in
174-
(* TODO we should make sure to finalize the current backend before exiting dune.
175-
For watch mode, we should finalize the backend and then restart it in between
176-
runs. *)
177-
let common, config = Common.init builder in
171+
(* Similar to [get_path_and_build_if_necessary] but doesn't require the build
172+
system (ie. it sequences with [Fiber] rather than with [Memo]) and builds
173+
targets via an RPC server. Some functionality is not available but it can be
174+
run concurrently while a second Dune process holds the global build
175+
directory lock.
176+
177+
Returns the absolute path to the executable. *)
178+
let build_prog_via_rpc_if_necessary ~dir ~no_rebuild prog =
179+
match Filename.analyze_program_name prog with
180+
| In_path ->
181+
User_warning.emit
182+
[ Pp.textf
183+
"As this is not the main instance of Dune it is unable to locate the \
184+
executable %S within this project. Dune will attempt to resolve the \
185+
executable's name within your PATH only."
186+
prog
187+
];
188+
let path = Env_path.path Env.initial in
189+
(match Bin.which ~path prog with
190+
| None -> not_found ~hints:[] ~prog
191+
| Some prog_path -> Fiber.return (Path.to_absolute_filename prog_path))
192+
| Relative_to_current_dir ->
193+
let open Fiber.O in
194+
let path = Path.relative_to_source_in_build_or_external ~dir prog in
195+
let+ () =
196+
if no_rebuild
197+
then if Path.exists path then Fiber.return () else program_not_built_yet prog
198+
else (
199+
let target =
200+
Dune_lang.Dep_conf.File
201+
(Dune_lang.String_with_vars.make_text Loc.none (Path.to_string path))
202+
in
203+
Build_cmd.build_via_rpc_server ~print_on_success:false ~targets:[ target ])
204+
in
205+
Path.to_absolute_filename path
206+
| Absolute ->
207+
if Path.exists (Path.of_string prog)
208+
then Fiber.return prog
209+
else not_found ~hints:[] ~prog
210+
;;
211+
212+
let exec_building_via_rpc_server ~common ~prog ~args ~no_rebuild =
213+
let open Fiber.O in
214+
let ensure_terminal = function
215+
| Cmd_arg.Terminal s -> s
216+
| Expandable (_, raw) ->
217+
(* Pforms cannot be expanded without running the build system. *)
218+
User_error.raise
219+
[ Pp.textf
220+
"The term %S contains a pform variable but Dune is unable to expand pform \
221+
variables when building via RPC."
222+
raw
223+
]
224+
in
225+
let context = Common.x common |> Option.value ~default:Context_name.default in
226+
let dir = Context_name.build_dir context in
227+
let prog = ensure_terminal prog in
228+
let args = List.map args ~f:ensure_terminal in
229+
let+ prog = build_prog_via_rpc_if_necessary ~dir ~no_rebuild prog in
230+
restore_cwd_and_execve (Common.root common) prog args Env.initial
231+
;;
232+
233+
let exec_building_directly ~common ~config ~context ~prog ~args ~no_rebuild =
178234
match Common.watch common with
179235
| Yes Passive ->
180236
User_error.raise [ Pp.textf "passive watch mode is unsupported by exec" ]
@@ -209,4 +265,40 @@ let term : unit Term.t =
209265
restore_cwd_and_execve (Common.root common) prog args env)
210266
;;
211267

268+
let term : unit Term.t =
269+
let+ builder = Common.Builder.term
270+
and+ context = Common.context_arg ~doc:{|Run the command in this build context.|}
271+
and+ prog = Arg.(required & pos 0 (some Cmd_arg.conv) None (Arg.info [] ~docv:"PROG"))
272+
and+ no_rebuild =
273+
Arg.(value & flag & info [ "no-build" ] ~doc:"don't rebuild target before executing")
274+
and+ args = Arg.(value & pos_right 0 Cmd_arg.conv [] (Arg.info [] ~docv:"ARGS")) in
275+
(* TODO we should make sure to finalize the current backend before exiting dune.
276+
For watch mode, we should finalize the backend and then restart it in between
277+
runs. *)
278+
let common, config = Common.init builder in
279+
match Dune_util.Global_lock.lock ~timeout:None with
280+
| Error lock_held_by ->
281+
(match Common.watch common with
282+
| Yes _ ->
283+
User_error.raise
284+
[ Pp.textf
285+
"Another instance of dune%s has locked the _build directory. Refusing to \
286+
start a new watch server until no other instances of dune are running."
287+
(Dune_util.Global_lock.Lock_held_by.to_string_empty_if_unknown lock_held_by)
288+
]
289+
| No ->
290+
if not (Common.Builder.equal builder Common.Builder.default)
291+
then
292+
User_warning.emit
293+
[ Pp.textf
294+
"Your build request is being forwarded to a running Dune instance%s. Note \
295+
that certain command line arguments may be ignored."
296+
(Dune_util.Global_lock.Lock_held_by.to_string_empty_if_unknown
297+
lock_held_by)
298+
];
299+
Scheduler.go_without_rpc_server ~common ~config
300+
@@ fun () -> exec_building_via_rpc_server ~common ~prog ~args ~no_rebuild)
301+
| Ok () -> exec_building_directly ~common ~config ~context ~prog ~args ~no_rebuild
302+
;;
303+
212304
let command = Cmd.v info term

doc/changes/11840.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Allow `dune exec` to run concurrently with another instance of dune in watch
2+
mode (#11840, @gridbugs)

src/dune_util/global_lock.ml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ module Lock_held_by = struct
7171
| Pid_from_lockfile of int
7272
| Unknown
7373

74+
let to_string_empty_if_unknown = function
75+
| Unknown -> ""
76+
| Pid_from_lockfile pid -> sprintf " (pid: %d)" pid
77+
;;
78+
7479
let read_lock_file () =
7580
match Io.read_file (Path.build lock_file) with
7681
| exception _ -> Unknown
@@ -127,9 +132,7 @@ let lock_exn ~timeout =
127132
[ Pp.textf
128133
"A running dune%s instance has locked the build directory. If this is not the \
129134
case, please delete %S."
130-
(match lock_held_by with
131-
| Unknown -> ""
132-
| Pid_from_lockfile pid -> sprintf " (pid: %d)" pid)
135+
(Lock_held_by.to_string_empty_if_unknown lock_held_by)
133136
(Path.Build.to_string_maybe_quoted lock_file)
134137
]
135138
;;

src/dune_util/global_lock.mli

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
Before starting rpc, writing to the build dir, this lock should be locked. *)
44

55
module Lock_held_by : sig
6-
type t =
7-
| Pid_from_lockfile of int
8-
| Unknown
6+
type t
7+
8+
(** returns "(pid: X)" where X is the PID if the PID is known, otherwise the
9+
empty string *)
10+
val to_string_empty_if_unknown : t -> string
911
end
1012

1113
(** Attempt to acquire a lock. once a lock is locked, subsequent locks always

test/blackbox-tests/test-cases/watching/dune

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
(applies_to what-dune-watches)
1818
(deps %{bin:strace}))
1919

20-
;; this test sometimes gets stuck and times out
20+
;; These tests sometimes get stuck and time out:
2121

2222
(cram
23-
(applies_to watching-eager-concurrent-build-command)
23+
(applies_to watching-eager-concurrent-build-command
24+
watching-eager-concurrent-exec-command)
2425
(enabled_if false))
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
Demonstrate running "dune exec" concurrently with an eager rpc server.
2+
3+
$ echo '(lang dune 3.18)' > dune-project
4+
$ echo '(executable (name foo))' > dune
5+
$ echo 'let () = print_endline "Hello, World!"' > foo.ml
6+
$ touch README.md
7+
8+
Just watch the readme file so we don't accidentally build foo.exe before
9+
testing the --no-build option:
10+
$ dune build README.md --watch &
11+
Success, waiting for filesystem changes...
12+
Success, waiting for filesystem changes...
13+
Success, waiting for filesystem changes...
14+
15+
Demonstrate handling the --no-build option:
16+
$ dune exec --no-build ./foo.exe
17+
Error: Program './foo.exe' isn't built yet. You need to build it first or
18+
remove the '--no-build' option.
19+
[1]
20+
21+
Demonstrate running an executable from the current project:
22+
$ dune exec ./foo.exe
23+
Hello, World!
24+
25+
Demonstrate running an executable from PATH:
26+
$ dune exec echo "Hello, World!"
27+
Warning: As this is not the main instance of Dune it is unable to locate the
28+
executable "echo" within this project. Dune will attempt to resolve the
29+
executable's name within your PATH only.
30+
Hello, World!
31+
32+
Demonstrate printing a warning if arguments are passed that would be ignored
33+
due to how Dune builds via RPC:
34+
$ dune exec --force ./foo.exe 2>&1 | sed 's/pid: [0-9]*/pid: PID/g'
35+
Warning: Your build request is being forwarded to a running Dune instance
36+
(pid: PID). Note that certain command line arguments may be ignored.
37+
Hello, World!
38+
39+
Demonstrate trying to run exec in watch mode while another watch server is running:
40+
$ dune exec ./foo.exe --watch 2>&1 | sed 's/pid: [0-9]*/pid: PID/g'
41+
Error: Another instance of dune (pid: PID) has locked the _build
42+
directory. Refusing to start a new watch server until no other instances of
43+
dune are running.
44+
45+
Demonstrate running an executable via an absolute path:
46+
$ dune exec $(which echo) "Hello, World!"
47+
Hello, World!
48+
49+
$ dune shutdown
50+
$ wait

0 commit comments

Comments
 (0)