Skip to content

Commit 79e5ff2

Browse files
committed
Looking up paths to executables with dune which
The "dune exec" command has three different ways of resolving the names of executables to paths to executables: - public executables defined in the current project - executables in the "bin" directories of dependencies - executables in directories listed in $PATH This can lead to unexpected shadowing, especially in the case of executables from dependecies, as users may not be aware that one of the packages in their project's dependency cone defines an executable with the same name of an executable that's also insalled system-wide. Short of fixing the problem for now, this change introduces a tool for helping investigate specifically which executable will be run by "dune exec". This adds a command "dune which" which prints the path to the executable. Signed-off-by: Stephen Sherratt <stephen@sherra.tt>
1 parent 03eb21f commit 79e5ff2

File tree

7 files changed

+152
-28
lines changed

7 files changed

+152
-28
lines changed

bin/exec.ml

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -111,19 +111,26 @@ let build_prog ~no_rebuild ~prog p =
111111
p
112112
;;
113113

114-
let get_path_and_build_if_necessary sctx ~no_rebuild ~dir ~prog =
114+
let expand ~sctx common cmd_arg = Cmd_arg.expand ~root:(Common.root common) ~sctx cmd_arg
115+
116+
let get_path common ctx_name ~prog =
115117
let open Memo.O in
118+
let* sctx = Super_context.find_exn ctx_name in
119+
let dir =
120+
let context = Dune_rules.Super_context.context sctx in
121+
Path.Build.relative (Context.build_dir context) (Common.prefix_target common "")
122+
in
116123
match Filename.analyze_program_name prog with
117124
| In_path ->
118125
Super_context.resolve_program_memo sctx ~dir ~loc:None prog
119126
>>= (function
120127
| Error (_ : Action.Prog.Not_found.t) -> not_found ~dir ~prog
121-
| Ok p -> build_prog ~no_rebuild ~prog p)
128+
| Ok p -> Memo.return p)
122129
| Relative_to_current_dir ->
123130
let path = Path.relative_to_source_in_build_or_external ~dir prog in
124131
Build_system.file_exists path
125132
>>= (function
126-
| true -> build_prog ~no_rebuild ~prog path
133+
| true -> Memo.return path
127134
| false -> not_found ~dir ~prog)
128135
| Absolute ->
129136
(match
@@ -140,19 +147,22 @@ let get_path_and_build_if_necessary sctx ~no_rebuild ~dir ~prog =
140147
| None -> not_found ~dir ~prog)
141148
;;
142149

143-
let step ~setup ~prog ~args ~common ~no_rebuild ~context ~on_exit () =
150+
let get_path_and_build_if_necessary common ctx_name ~no_rebuild ~prog =
144151
let open Memo.O in
145-
let* sctx = setup >>| Import.Main.find_scontext_exn ~name:context in
146-
let* env = Super_context.context_env sctx in
147-
let expand = Cmd_arg.expand ~root:(Common.root common) ~sctx in
152+
let* path = get_path common ctx_name ~prog in
153+
match Filename.analyze_program_name prog with
154+
| In_path | Relative_to_current_dir -> build_prog ~no_rebuild ~prog path
155+
| Absolute -> Memo.return path
156+
;;
157+
158+
let step ~prog ~args ~common ~no_rebuild ~ctx_name ~on_exit () =
159+
let open Memo.O in
160+
let* sctx = Super_context.find_exn ctx_name in
148161
let* path =
149-
let dir =
150-
let context = Dune_rules.Super_context.context sctx in
151-
Path.Build.relative (Context.build_dir context) (Common.prefix_target common "")
152-
in
153-
let* prog = expand prog in
154-
get_path_and_build_if_necessary sctx ~no_rebuild ~dir ~prog
155-
and* args = Memo.parallel_map args ~f:expand in
162+
let* prog = expand ~sctx common prog in
163+
get_path_and_build_if_necessary common ctx_name ~no_rebuild ~prog
164+
and* args = Memo.parallel_map args ~f:(expand ~sctx common) in
165+
let* env = Super_context.context_env sctx in
156166
Memo.of_non_reproducible_fiber
157167
@@ Dune_engine.Process.run_inherit_std_in_out
158168
~dir:(Path.of_string Fpath.initial_cwd)
@@ -166,7 +176,7 @@ let step ~setup ~prog ~args ~common ~no_rebuild ~context ~on_exit () =
166176

167177
let term : unit Term.t =
168178
let+ builder = Common.Builder.term
169-
and+ context = Common.context_arg ~doc:{|Run the command in this build context.|}
179+
and+ ctx_name = Common.context_arg ~doc:{|Run the command in this build context.|}
170180
and+ prog = Arg.(required & pos 0 (some Cmd_arg.conv) None (Arg.info [] ~docv:"PROG"))
171181
and+ no_rebuild =
172182
Arg.(value & flag & info [ "no-build" ] ~doc:"don't rebuild target before executing")
@@ -182,30 +192,25 @@ let term : unit Term.t =
182192
Scheduler.go_with_rpc_server_and_console_status_reporting ~common ~config
183193
@@ fun () ->
184194
let open Fiber.O in
185-
let* setup = Import.Main.setup () in
186195
let on_exit = Console.printf "Program exited with code [%d]" in
187196
Scheduler.Run.poll
188197
@@
189198
let* () = Fiber.return @@ Scheduler.maybe_clear_screen ~details_hum:[] config in
190-
build @@ step ~setup ~prog ~args ~common ~no_rebuild ~context ~on_exit
199+
build @@ step ~prog ~args ~common ~no_rebuild ~ctx_name ~on_exit
191200
| No ->
192201
Scheduler.go_with_rpc_server ~common ~config
193202
@@ fun () ->
194203
let open Fiber.O in
195204
let* setup = Import.Main.setup () in
196205
build_exn (fun () ->
197206
let open Memo.O in
198-
let* sctx = setup >>| Import.Main.find_scontext_exn ~name:context in
199-
let* env = Super_context.context_env sctx in
200-
let expand = Cmd_arg.expand ~root:(Common.root common) ~sctx in
201-
let* prog =
202-
let dir =
203-
let context = Dune_rules.Super_context.context sctx in
204-
Path.Build.relative (Context.build_dir context) (Common.prefix_target common "")
205-
in
206-
let* prog = expand prog in
207-
get_path_and_build_if_necessary sctx ~no_rebuild ~dir ~prog >>| Path.to_string
208-
and* args = Memo.parallel_map ~f:expand args in
207+
let* sctx = setup >>| Import.Main.find_scontext_exn ~name:ctx_name in
208+
let* env = Super_context.context_env sctx
209+
and* prog =
210+
let* prog = expand ~sctx common prog in
211+
get_path_and_build_if_necessary common ctx_name ~no_rebuild ~prog
212+
>>| Path.to_string
213+
and* args = Memo.parallel_map ~f:(expand ~sctx common) args in
209214
restore_cwd_and_execve (Common.root common) prog args env)
210215
;;
211216

bin/exec.mli

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
11
open Import
22

3+
(** Returns the path to the executable [prog] as it will be resolved by dune:
4+
- if [prog] is the name of an executable defined by the project then the path
5+
to that executable will be returned, and evaluating the returned memo will
6+
build the executable if necessary.
7+
- otherwise if [prog] is the name of an executable in the "bin" directory of
8+
a package in this project's dependency cone then the path to that executable
9+
file will be returned. Note that for this reason all dependencies of the
10+
project will be built when the returned memo is evaluated (unless the first
11+
case is hit).
12+
- otherwise if [prog] is the name of an executable in one of the directories
13+
listed in the PATH environment variable, the path to that executable will be
14+
returned. *)
15+
val get_path : Common.t -> Context_name.t -> prog:string -> Path.t Memo.t
16+
317
val command : unit Cmd.t

bin/main.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ let all : _ Cmdliner.Cmd.t list =
1111
; Install_uninstall.install
1212
; Install_uninstall.uninstall
1313
; Exec.command
14+
; Which.command
1415
; Subst.command
1516
; Print_rules.command
1617
; Utop.command

bin/which.ml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
open Import
2+
3+
let doc =
4+
"Print the path to the executable using the same resolution logic as [dune exec]."
5+
;;
6+
7+
let man =
8+
[ `S "DESCRIPTION"
9+
; `P
10+
{|$(b,dune which NAME) prints the path to the executable NAME using the same logic as:
11+
|}
12+
; `Pre "$ dune exec NAME"
13+
; `P
14+
"Dune will first try to resolve the executable within the public executables in \
15+
the current project, then inside the \"bin\" directory of each package among the \
16+
project's dependencies (when using dune package management), and finally within \
17+
the directories listed in the $PATH environment variable."
18+
]
19+
;;
20+
21+
let info = Cmd.info "which" ~doc ~man
22+
23+
let term : unit Term.t =
24+
let+ builder = Common.Builder.term
25+
and+ ctx_name = Common.context_arg ~doc:{|Run the command in this build context.|}
26+
and+ prog = Arg.(required & pos 0 (some string) None (Arg.info [] ~docv:"PROG")) in
27+
let common, config = Common.init builder in
28+
Scheduler.go_with_rpc_server ~common ~config
29+
@@ fun () ->
30+
build_exn
31+
@@ fun () ->
32+
let open Memo.O in
33+
let+ path = Exec.get_path common ctx_name ~prog >>| Path.to_string in
34+
Console.print [ Pp.verbatim path ]
35+
;;
36+
37+
let command = Cmd.v info term

bin/which.mli

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
open Import
2+
3+
val command : unit Cmd.t

doc/changes/11905.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Add `dune which` command for printing the path to the executable that would
2+
be run by `dune exec` (#11905, @gridbugs)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
Exercise the various ways of resolving executable names with `dune which`.
2+
3+
$ cat > dune-project << EOF
4+
> (lang dune 3.20)
5+
>
6+
> (package
7+
> (name foo))
8+
> EOF
9+
10+
$ cat > dune << EOF
11+
> (executable
12+
> (public_name foo))
13+
> EOF
14+
15+
$ cat > foo.ml << EOF
16+
> let () = print_endline "hello foo"
17+
> EOF
18+
19+
An executable that would be installed by the current package:
20+
$ dune which foo
21+
_build/install/default/bin/foo
22+
23+
An executable from the current project:
24+
$ dune which ./foo.exe
25+
_build/default/foo.exe
26+
27+
Test that executables from dependencies are located correctly:
28+
$ mkdir dune.lock
29+
$ cat > dune.lock/lock.dune << EOF
30+
> (lang package 0.1)
31+
> EOF
32+
$ cat > dune.lock/bar.pkg << EOF
33+
> (version 0.1)
34+
> (install
35+
> (progn
36+
> (write-file %{bin}/bar "#!/bin/sh\necho hello bar")
37+
> (run chmod a+x %{bin}/bar)))
38+
> EOF
39+
40+
$ cat > dune-project << EOF
41+
> (lang dune 3.20)
42+
>
43+
> (package
44+
> (name foo)
45+
> (depends bar))
46+
> EOF
47+
48+
$ dune which bar
49+
_build/_private/default/.pkg/bar/target/bin/bar
50+
51+
Test that executables from PATH are located correctly:
52+
$ mkdir bin
53+
$ cat > bin/baz << EOF
54+
> #!/bin/sh
55+
> echo hello baz
56+
> EOF
57+
58+
$ chmod a+x bin/baz
59+
$ export PATH=$PWD/bin:$PATH
60+
61+
$ dune which baz
62+
$TESTCASE_ROOT/bin/baz

0 commit comments

Comments
 (0)