From 21e4a51d3e22ae5d211bd5bf0ea262450d6a8b37 Mon Sep 17 00:00:00 2001 From: Liam Stevenson Date: Thu, 21 Aug 2025 16:48:37 -0400 Subject: [PATCH 1/2] Make Merlin look for dot-merlin-reader in the install directory --- src/kernel/mconfig_dot.ml | 39 ++++++++++-- src/utils/std.ml | 13 ++-- .../find-dot-merlin-reader.t | 60 +++++++++++++++++++ tests/test-dirs/config/no-dune.t | 2 +- 4 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 tests/test-dirs/config/dot-merlin-reader/find-dot-merlin-reader.t diff --git a/src/kernel/mconfig_dot.ml b/src/kernel/mconfig_dot.ml index 65d94e76b..bd4f26f87 100644 --- a/src/kernel/mconfig_dot.ml +++ b/src/kernel/mconfig_dot.ml @@ -129,6 +129,34 @@ end = struct | Dot_merlin -> "dot-merlin-reader" | Dune -> "dune" + let find_exe = function + | Dot_merlin -> + (* 1. If DOT_MERLIN_READER_EXE is defined, then use its value for the + dot-merlin-reader exe + 2. If not, look in the same directory as the merlin executable for a + dot-merlin-reader. + 3. If not, fallback to using whatever one is on the PATH. *) + let get_from_env_var = lazy (Sys.getenv_opt "DOT_MERLIN_READER_EXE") in + let get_from_same_dir_as_merlin_exe = + lazy + (let merlin_exe = Sys.executable_name in + let merlin_bin = Filename.dirname merlin_exe in + let dot_merlin_reader_exe = + Filename.concat merlin_bin "dot-merlin-reader" + in + match Sys.file_exists dot_merlin_reader_exe with + | true -> Some dot_merlin_reader_exe + | false -> None) + in + List.find_map_opt + [ get_from_env_var; get_from_same_dir_as_merlin_exe ] + ~f:Lazy.force + |> Option.value ~default:"dot-merlin-reader" + | Dune -> + (* Always use the dune on the PATH *) + (* CR-someday: consider doing something better here *) + "dune" + exception Process_exited module Process = struct @@ -148,10 +176,10 @@ end = struct let prog, args = match cfg with | Dot_merlin -> - let prog = "dot-merlin-reader" in + let prog = find_exe Dot_merlin in (prog, [| prog |]) | Dune -> - let prog = "dune" in + let prog = find_exe Dune in (prog, [| prog; "ocaml-merlin"; "--no-print-directory" |]) in let cwd = Sys.getcwd () in @@ -393,8 +421,11 @@ let get_config { workdir; process_dir; configurator } path_abs = | Unix.Unix_error (ENOENT, "create_process", "dot-merlin-reader") -> let error = Printf.sprintf - "%s could not find `dot-merlin-reader` in the PATH. Please make sure \ - that `dot-merlin-reader` is installed and in the PATH." + "%s could not find `dot-merlin-reader`. Please make sure that \ + `dot-merlin-reader` is installed. `dot-merlin-reader` is expected to \ + be in the same directory as the merlin executable or on the PATH. You \ + may also specify the path to `dot-merlin-reader` via the \ + `DOT_MERLIN_READER_EXE` environment variable." (Lib_config.program_name ()) in (empty_config, [ error ]) diff --git a/src/utils/std.ml b/src/utils/std.ml index af969edcb..26637f52e 100644 --- a/src/utils/std.ml +++ b/src/utils/std.ml @@ -109,12 +109,17 @@ module List = struct | None -> filter_map ~f xs | Some x -> x :: filter_map ~f xs) - let rec find_map ~f = function - | [] -> raise Not_found + let rec find_map_opt ~f = function + | [] -> None | x :: xs -> ( match f x with - | None -> find_map ~f xs - | Some x' -> x') + | None -> find_map_opt ~f xs + | Some x' -> Some x') + + let find_map ~f l = + match find_map_opt ~f l with + | None -> raise Not_found + | Some x -> x let rec map_end ~f l1 l2 = match l1 with diff --git a/tests/test-dirs/config/dot-merlin-reader/find-dot-merlin-reader.t b/tests/test-dirs/config/dot-merlin-reader/find-dot-merlin-reader.t new file mode 100644 index 000000000..1a808fe0a --- /dev/null +++ b/tests/test-dirs/config/dot-merlin-reader/find-dot-merlin-reader.t @@ -0,0 +1,60 @@ + $ cat > .merlin < UNIT_NAME Dot_merlin_was_successfully_read + > EOF + + +In real-world scenarios, ocamlmerlin and dot-merlin-reader are usually installed in the +same directory (for example, via an opam install). In this situation, if dot-merlin-reader +isn't on the PATH, we can still read the .merlin file. + $ mkdir merlin-bin + $ cp $(which dot-merlin-reader) merlin-bin + $ cp $(which ocamlmerlin) merlin-bin + $ cp $(which ocamlmerlin-server) merlin-bin + $ cp $(which ocaml-index) merlin-bin + + $ PATH="" merlin-bin/ocamlmerlin single dump-configuration -filename test.ml \ + > | jq .value.merlin.unit_name -r + Dot_merlin_was_successfully_read + +The dot-merlin-reader in the same directory is prioritized over the one on the PATH. + $ mkdir bad-dot-merlin-reader-bin + $ cat > bad-dot-merlin-reader-bin/dot-merlin-reader < #!/bin/sh + > echo "this is a bad dot-merlin-reader" >&2 + > exit 1 + > EOF + + $ PATH="bad-dot-merlin-reader-bin" \ + > merlin-bin/ocamlmerlin single dump-configuration -filename test.ml \ + > | jq .value.merlin.unit_name -r + Dot_merlin_was_successfully_read + +But we can fall back to the one on the PATH if a dot-merlin-reader doesn't exist in the +same directory. + $ cp -r merlin-bin merlin-bin-no-reader + $ rm merlin-bin-no-reader/dot-merlin-reader + + $ PATH="" \ + > merlin-bin-no-reader/ocamlmerlin single dump-configuration -filename test.ml \ + > | jq .value.merlin.unit_name -r + null + + $ PATH="merlin-bin" \ + > merlin-bin-no-reader/ocamlmerlin single dump-configuration -filename test.ml \ + > | jq .value.merlin.unit_name -r + Dot_merlin_was_successfully_read + +We can override using the DOT_MERLIN_READER_EXE environment variable. + $ cp -r merlin-bin merlin-bin-bad-reader + $ rm merlin-bin-bad-reader/dot-merlin-reader + $ cp bad-dot-merlin-reader-bin/dot-merlin-reader merlin-bin-bad-reader + + $ PATH="merlin-bin-bad-reader" \ + > merlin-bin-bad-reader/ocamlmerlin single dump-configuration -filename test.ml \ + > | jq .value.merlin.unit_name -r + null + + $ PATH="merlin-bin-bad-reader" DOT_MERLIN_READER_EXE="merlin-bin/dot-merlin-reader" \ + > merlin-bin-bad-reader/ocamlmerlin single dump-configuration -filename test.ml \ + > | jq .value.merlin.unit_name -r + Dot_merlin_was_successfully_read diff --git a/tests/test-dirs/config/no-dune.t b/tests/test-dirs/config/no-dune.t index 093914a4d..ecf944de9 100644 --- a/tests/test-dirs/config/no-dune.t +++ b/tests/test-dirs/config/no-dune.t @@ -27,5 +27,5 @@ $ cat output | jq '.value.merlin.failures' [ - "Merlin could not find `dot-merlin-reader` in the PATH. Please make sure that `dot-merlin-reader` is installed and in the PATH." + "Merlin could not find `dot-merlin-reader`. Please make sure that `dot-merlin-reader` is installed. `dot-merlin-reader` is expected to be in the same directory as the merlin executable or on the PATH. You may also specify the path to `dot-merlin-reader` via the `DOT_MERLIN_READER_EXE` environment variable." ] From cb42d4c0b03a568f5de7e44f378329db79f40279 Mon Sep 17 00:00:00 2001 From: Liam Stevenson Date: Fri, 22 Aug 2025 08:47:51 -0400 Subject: [PATCH 2/2] Use realpath --- src/kernel/mconfig_dot.ml | 2 +- .../config/dot-merlin-reader/find-dot-merlin-reader.t | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/kernel/mconfig_dot.ml b/src/kernel/mconfig_dot.ml index bd4f26f87..c146c8733 100644 --- a/src/kernel/mconfig_dot.ml +++ b/src/kernel/mconfig_dot.ml @@ -139,7 +139,7 @@ end = struct let get_from_env_var = lazy (Sys.getenv_opt "DOT_MERLIN_READER_EXE") in let get_from_same_dir_as_merlin_exe = lazy - (let merlin_exe = Sys.executable_name in + (let merlin_exe = Unix.realpath Sys.executable_name in let merlin_bin = Filename.dirname merlin_exe in let dot_merlin_reader_exe = Filename.concat merlin_bin "dot-merlin-reader" diff --git a/tests/test-dirs/config/dot-merlin-reader/find-dot-merlin-reader.t b/tests/test-dirs/config/dot-merlin-reader/find-dot-merlin-reader.t index 1a808fe0a..91c48fbcc 100644 --- a/tests/test-dirs/config/dot-merlin-reader/find-dot-merlin-reader.t +++ b/tests/test-dirs/config/dot-merlin-reader/find-dot-merlin-reader.t @@ -16,6 +16,15 @@ isn't on the PATH, we can still read the .merlin file. > | jq .value.merlin.unit_name -r Dot_merlin_was_successfully_read +If ocamlmerlin is a symlink, it looks in the original install directory. + + $ mkdir merlin-bin-symlink + $ ln -s $(realpath merlin-bin/ocamlmerlin) merlin-bin-symlink/ocamlmerlin + + $ PATH="" merlin-bin-symlink/ocamlmerlin single dump-configuration -filename test.ml \ + > | jq .value.merlin.unit_name -r + Dot_merlin_was_successfully_read + The dot-merlin-reader in the same directory is prioritized over the one on the PATH. $ mkdir bad-dot-merlin-reader-bin $ cat > bad-dot-merlin-reader-bin/dot-merlin-reader <