diff --git a/bin/main.ml b/bin/main.ml index 61ca4904449..96bd2b6c8bc 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -39,6 +39,7 @@ let all : _ Cmdliner.Cmd.t list = ; Promotion.group ; Pkg.group ; Pkg.Alias.group + ; Tools.group ] in terms @ groups diff --git a/bin/tools/ocamlformat.ml b/bin/tools/ocamlformat.ml new file mode 100644 index 00000000000..9aa692bcec9 --- /dev/null +++ b/bin/tools/ocamlformat.ml @@ -0,0 +1,133 @@ +open! Import +module Pkg_dev_tool = Dune_rules.Pkg_dev_tool + +let exe_path = Path.build @@ Pkg_dev_tool.exe_path Ocamlformat +let exe_name = Pkg_dev_tool.exe_name Ocamlformat + +let run_dev_tool common ~args = + let exe_path_string = Path.to_string exe_path in + Console.print_user_message + (Dune_rules.Pkg_build_progress.format_user_message + ~verb:"Running" + ~object_:(User_message.command (String.concat ~sep:" " (exe_name :: args)))); + Console.finish (); + restore_cwd_and_execve common exe_path_string (exe_path_string :: args) Env.initial +;; + +let dev_tool_exe_exists () = Path.exists exe_path + +let build_dev_tool common = + match dev_tool_exe_exists () with + | true -> + (* Avoid running the build system if the executable already exists + to reduce unnecessary latency in the common case. *) + Fiber.return () + | false -> + let open Fiber.O in + let+ result = + Build_cmd.run_build_system ~common ~request:(fun _build_system -> + Action_builder.path exe_path) + in + (match result with + | Error `Already_reported -> raise Dune_util.Report_error.Already_reported + | Ok () -> ()) +;; + +let is_in_dune_project builder = + Workspace_root.create + ~default_is_cwd:(Common.Builder.default_root_is_cwd builder) + ~specified_by_user:(Common.Builder.root builder) + |> Result.is_ok +;; + +module Fallback = struct + let run_command prog args env = + let prog_string = Path.to_string prog in + let argv = prog_string :: args in + Console.print_user_message + (Dune_rules.Pkg_build_progress.format_user_message + ~verb:"Running" + ~object_:(User_message.command (String.concat ~sep:" " argv))); + Console.finish (); + Proc.restore_cwd_and_execve prog_string argv ~env + ;; + + let run_via_opam args env = + match Bin.which ~path:(Env_path.path env) "opam" with + | None -> Error () + | Some opam_path -> + Console.print_user_message + (User_message.make + [ Pp.textf + "Not in a dune project but opam appears to be installed. Dune will \ + attempt to run %s via opam." + exe_name + ]); + run_command opam_path ([ "exec"; exe_name; "--" ] @ args) env + ;; + + let run_via_path args env = + match Bin.which ~path:(Env_path.path env) exe_name with + | None -> Error () + | Some path -> + Console.print_user_message + (User_message.make + [ Pp.textf + "Not in a dune project but %s appears to be installed. Dune will attempt \ + to run %s from your PATH." + exe_name + exe_name + ]); + run_command path args env + ;; + + (* Attempt to launch ocamlformat from the current opam switch, and + failing that from the PATH. This is necessary so that editors + configured to run ocamlformat via dune can still be used to format + ocaml code outside of dune projects. *) + let run args env = + match run_via_opam args env with + | Ok () -> () + | Error () -> + (match run_via_path args env with + | Ok () -> () + | Error () -> + User_error.raise + [ Pp.textf + "Not in a dune project and %s doesn't appear to be installed." + exe_name + ]) + ;; +end + +let term = + let+ builder = Common.Builder.term + and+ args = Arg.(value & pos_all string [] (info [] ~docv:"ARGS")) in + match is_in_dune_project builder with + | false -> Fallback.run args Env.initial + | true -> + let common, config = Common.init builder in + Scheduler.go ~common ~config (fun () -> + let open Fiber.O in + let* () = Lock_dev_tool.lock_ocamlformat () in + let+ () = build_dev_tool common in + run_dev_tool common ~args) +;; + +let info = + let doc = + {|Wrapper for running ocamlformat intended to be run automatically + by a text editor. All positional arguments will be passed to the + ocamlformat executable (pass flags to ocamlformat after the '--' + argument, such as 'dune ocamlformat -- --help'). If this command + is run from inside a dune project, dune will download and build + the ocamlformat opam package and run the ocamlformat executable + from there rather. Otherwise, dune will attempt to run the + ocamlformat executable from your current opam switch. If opam is + not installed, dune will attempt to run ocamlformat from your + PATH.|} + in + Cmd.info "ocamlformat" ~doc +;; + +let command = Cmd.v info term diff --git a/bin/tools/ocamlformat.mli b/bin/tools/ocamlformat.mli new file mode 100644 index 00000000000..5505206a171 --- /dev/null +++ b/bin/tools/ocamlformat.mli @@ -0,0 +1,3 @@ +open! Import + +val command : unit Cmd.t diff --git a/bin/tools/tools.ml b/bin/tools/tools.ml new file mode 100644 index 00000000000..39a35f4359d --- /dev/null +++ b/bin/tools/tools.ml @@ -0,0 +1,11 @@ +open! Import + +module Exec = struct + let doc = "Command group for running wrapped tools." + let info = Cmd.info ~doc "exec" + let group = Cmd.group info [ Ocamlformat.command ] +end + +let doc = "Command group for wrapped tools." +let info = Cmd.info ~doc "tools" +let group = Cmd.group info [ Exec.group ] diff --git a/bin/tools/tools.mli b/bin/tools/tools.mli new file mode 100644 index 00000000000..d4c5902fcd6 --- /dev/null +++ b/bin/tools/tools.mli @@ -0,0 +1,3 @@ +open Import + +val group : unit Cmd.t diff --git a/src/dune_rules/dune_rules.ml b/src/dune_rules/dune_rules.ml index dc1d7855020..0a560c296b9 100644 --- a/src/dune_rules/dune_rules.ml +++ b/src/dune_rules/dune_rules.ml @@ -69,6 +69,8 @@ module Melange_stanzas = Melange_stanzas module Executables = Executables module Tests = Tests module Stanzas = Stanzas +module Pkg_dev_tool = Pkg_dev_tool +module Pkg_build_progress = Pkg_build_progress module Install_rules = struct let install_file = Install_rules.install_file diff --git a/test/blackbox-tests/test-cases/pkg/ocamlformat/ocamlformat-wrapper-fallback.t b/test/blackbox-tests/test-cases/pkg/ocamlformat/ocamlformat-wrapper-fallback.t new file mode 100644 index 00000000000..c849805aec9 --- /dev/null +++ b/test/blackbox-tests/test-cases/pkg/ocamlformat/ocamlformat-wrapper-fallback.t @@ -0,0 +1,22 @@ +Exercise running the ocamlformat wrapper command outside of a dune +project. + +Make a fake ocamlformat executable and add it to PATH. + $ mkdir -p bin + $ cat > bin/ocamlformat << EOF + > #!/bin/sh + > echo "Hello, World!" + > EOF + $ chmod a+x bin/ocamlformat + $ export PATH=$PWD/bin:$PATH + +This is necessary for dune to act as it normally would outside of a +dune workspace. + $ unset INSIDE_DUNE + +Run the wrapper command from a temporary directory. With INSIDE_DUNE +unset dune would otherwise pick up the dune project itself as the +current workspace. + $ cd $(mktemp -d) + $ dune tools exec ocamlformat 2> /dev/null + Hello, World! diff --git a/test/blackbox-tests/test-cases/pkg/ocamlformat/ocamlformat-wrapper.t b/test/blackbox-tests/test-cases/pkg/ocamlformat/ocamlformat-wrapper.t new file mode 100644 index 00000000000..0d3b459aff4 --- /dev/null +++ b/test/blackbox-tests/test-cases/pkg/ocamlformat/ocamlformat-wrapper.t @@ -0,0 +1,14 @@ +Exercise running the ocamlformat wrapper command. + + $ . ./helpers.sh + $ mkrepo + + $ make_fake_ocamlformat "0.26.2" + $ make_ocamlformat_opam_pkg "0.26.2" + $ make_project_with_dev_tool_lockdir + + $ dune tools exec ocamlformat + Solution for dev-tools.locks/ocamlformat: + - ocamlformat.0.26.2 + Running 'ocamlformat' + formatted with version 0.26.2