Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .ocamlformat
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
profile = conventional
version = 0.29.0
margin = 72
4 changes: 2 additions & 2 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
## 0.1.0 - 2026-05-06

- Initial opam release.
- Add `doctor check` with diagnostics for common OCaml, opam, dune, LSP,
formatter, PATH, and editor setup issues.
- Add `doctor check` with diagnostics for common OCaml, opam, dune, LSP, formatter,
PATH, and editor setup issues.
- Document the text report and exit codes.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
`doctor` is a read-only CLI for checking whether a local OCaml development
environment looks usable.

It reports missing core tools, opam initialization and switch state, likely
shell environment mismatches, selected opam packages, and the VS Code OCaml
Platform extension when the `code` command is available.
It reports missing core tools, opam initialization and switch state, likely shell
environment mismatches, selected opam packages, and the VS Code OCaml Platform
extension when the `code` command is available.

## Installation

Expand Down Expand Up @@ -83,7 +83,7 @@ opam exec -- dune runtest
opam exec -- dune exec doctor -- check
```

Tests fake process execution, so they do not depend on the host opam setup, VS
Code, or a particular shell.
Tests fake process execution, so they do not depend on the host opam setup, VS Code,
or a particular shell.

Maintainer release notes are in [RELEASE.md](RELEASE.md).
19 changes: 9 additions & 10 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Release Process

Notes for publishing `doctor` to opam-repository. This is a release checklist,
not release automation.
Notes for publishing `doctor` to opam-repository.
This is a release checklist, not release automation.

Run from a clean checkout. Replace `0.1.0` with the version being released.

Expand Down Expand Up @@ -30,8 +30,8 @@ opam pin remove doctor -y

## Tag

Commit the release metadata, then create the tag. Push the branch and tag after
checking the final diff.
Commit the release metadata, then create the tag.
Push the branch and tag after checking the final diff.

```console
git status --short
Expand All @@ -40,9 +40,9 @@ git tag -a 0.1.0 -m "Release 0.1.0"

## Publish

Before the first release, verify current `opam-publish` usage with the installed
help or the current opam documentation. The tool is used to open the
opam-repository pull request for the tagged release.
Before the first release, verify current `opam-publish` usage with the installed help
or the current opam documentation. The tool is used to open the opam-repository pull
request for the tagged release.

```console
opam install opam-publish
Expand All @@ -59,6 +59,5 @@ doctor version
doctor check
```

The package metadata currently uses the maintainer's GitHub noreply address.
Change it only when you intentionally want a different public maintainer address
on opam.
The package metadata currently uses the maintainer's GitHub noreply address. Change
it only when you intentionally want a different public maintainer address on opam.
13 changes: 8 additions & 5 deletions bin/main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ let exit_infos =
Cmd.Exit.info ~doc:"one or more errors." 2;
Cmd.Exit.info ~doc:"unexpected internal failure." 3;
]
@ List.filter (fun info -> Cmd.Exit.info_code info <> 0) Cmd.Exit.defaults
@ List.filter
(fun info -> Cmd.Exit.info_code info <> 0)
Cmd.Exit.defaults

let check_cmd =
let doc = "Run OCaml development environment diagnostics." in
Expand All @@ -43,15 +45,16 @@ let version_cmd =

let default_cmd =
let doc =
"Inspect an OCaml development environment and print actionable diagnostics."
"Inspect an OCaml development environment and print actionable \
diagnostics."
in
let man =
[
`S Manpage.s_description;
`P
"doctor checks for common OCaml, opam, dune, LSP, formatter, shell \
environment, and VS Code setup issues. It does not modify your \
machine.";
"doctor checks for common OCaml, opam, dune, LSP, formatter, \
shell environment, and VS Code setup issues. It does not \
modify your machine.";
]
in
Cmd.group
Expand Down
1 change: 1 addition & 0 deletions doctor.opam
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and editor integration hints.

maintainer: "Thomas B. <29905917+funwithcthulhu@users.noreply.github.com>"
authors: "Thomas B."

license: "MIT"

tags: [
Expand Down
1 change: 1 addition & 0 deletions dune-project
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

(authors "Thomas B.")
(license MIT)

(maintainers "Thomas B. <29905917+funwithcthulhu@users.noreply.github.com>")

(package
Expand Down
33 changes: 20 additions & 13 deletions lib/check.ml
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,17 @@ let max_severity left right =

let aggregate diagnostics =
List.fold_left
(fun severity diagnostic -> max_severity severity diagnostic.severity)
(fun severity diagnostic ->
max_severity severity diagnostic.severity)
Ok diagnostics

let exit_code diagnostics =
match aggregate diagnostics with Ok -> 0 | Warn -> 1 | Error -> 2

let clean_output output =
output |> String.split_on_char '\n' |> List.map String.trim
output
|> String.split_on_char '\n'
|> List.map String.trim
|> List.filter (fun line -> line <> "")

let first_output_line (result : Process.result) =
Expand Down Expand Up @@ -86,14 +89,16 @@ let command_diagnostic ~(run : Process.runner) spec =
make
~id:("command." ^ spec.command)
~title:(Printf.sprintf "%s command failed" spec.label)
~detail:(Process.summary result) ~suggestion:spec.missing_suggestion
spec.missing_severity
~detail:(Process.summary result)
~suggestion:spec.missing_suggestion spec.missing_severity

let lsp_command_diagnostic ~(run : Process.runner) =
let primary = run "ocaml-lsp-server" [ "--version" ] in
match primary.status with
| Process.Exited 0 ->
let version = primary |> first_output_line |> Option.map String.trim in
let version =
primary |> first_output_line |> Option.map String.trim
in
let title = title_with_version "OCaml LSP" version in
make ~id:"command.ocaml-lsp-server" ~title Ok
| Process.Spawn_error _ -> (
Expand All @@ -114,24 +119,25 @@ let lsp_command_diagnostic ~(run : Process.runner) =
make ~id:"command.ocaml-lsp-server"
~title:"OCaml LSP command not found"
~detail:
"Checked `ocaml-lsp-server` and `ocamllsp`; neither command is \
available on PATH."
"Checked `ocaml-lsp-server` and `ocamllsp`; neither \
command is available on PATH."
~suggestion:"opam install ocaml-lsp-server" Warn
| _ ->
make ~id:"command.ocaml-lsp-server"
~title:"OCaml LSP found, but its version could not be read"
~detail:(Process.summary fallback)
~suggestion:
"Try running `ocamllsp --version` directly, or reinstall it with \
opam."
"Try running `ocamllsp --version` directly, or reinstall \
it with opam."
Warn)
| _ ->
make ~id:"command.ocaml-lsp-server"
~title:"ocaml-lsp-server found, but its version could not be read"
~title:
"ocaml-lsp-server found, but its version could not be read"
~detail:(Process.summary primary)
~suggestion:
"Try running `ocaml-lsp-server --version` directly, or reinstall it \
with opam."
"Try running `ocaml-lsp-server --version` directly, or \
reinstall it with opam."
Warn

let command_diagnostics ~run =
Expand All @@ -151,7 +157,8 @@ let command_diagnostics ~run =
label = "OCaml";
missing_severity = Error;
missing_suggestion =
"Create or select an opam switch, then sync your shell environment.";
"Create or select an opam switch, then sync your shell \
environment.";
version_parser = parse_ocaml_version;
};
{
Expand Down
14 changes: 9 additions & 5 deletions lib/editor.ml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
let has_extension extensions extension =
extensions |> String.split_on_char '\n' |> List.map String.trim
extensions
|> String.split_on_char '\n'
|> List.map String.trim
|> List.exists (String.equal extension)

let diagnostics ~(run : Process.runner) =
Expand All @@ -14,10 +16,12 @@ let diagnostics ~(run : Process.runner) =
let extensions = run "code" [ "--list-extensions" ] in
match extensions.status with
| Process.Exited 0
when has_extension extensions.stdout "ocamllabs.ocaml-platform" ->
when has_extension extensions.stdout "ocamllabs.ocaml-platform"
->
[
Check.make ~id:"editor.vscode.ocaml-platform"
~title:"VS Code OCaml Platform extension detected" Check.Ok;
~title:"VS Code OCaml Platform extension detected"
Check.Ok;
]
| Process.Exited 0 ->
[
Expand All @@ -33,8 +37,8 @@ let diagnostics ~(run : Process.runner) =
~title:"could not list VS Code extensions"
~detail:(Process.summary extensions)
~suggestion:
"Open VS Code and check whether ocamllabs.ocaml-platform is \
installed."
"Open VS Code and check whether \
ocamllabs.ocaml-platform is installed."
Check.Warn;
])
| _ ->
Expand Down
52 changes: 34 additions & 18 deletions lib/opam.ml
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
let non_empty_lines output =
output |> String.split_on_char '\n' |> List.map String.trim
output
|> String.split_on_char '\n'
|> List.map String.trim
|> List.filter (fun line -> line <> "")

let parse_active_switch output =
match non_empty_lines output with
| [] -> None
| line :: _ ->
let lower = String.lowercase_ascii line in
if String.length lower >= 7 && String.sub lower 0 7 = "[error]" then None
if String.length lower >= 7 && String.sub lower 0 7 = "[error]"
then None
else Some line

let parse_switch_list output =
Expand All @@ -30,7 +33,8 @@ let parse_installed_packages output =
|> List.map (fun line ->
match words line with package :: _ -> package | [] -> line)

let has_package packages package = List.exists (String.equal package) packages
let has_package packages package =
List.exists (String.equal package) packages

let opam_available ~(run : Process.runner) =
match (run "opam" [ "--version" ]).status with
Expand All @@ -47,12 +51,15 @@ let initialized_diagnostic ~(run : Process.runner) =
~detail:(Printf.sprintf "Root: %s" root)
Check.Ok
| [] ->
Check.make ~id:"opam.initialized" ~title:"opam root could not be read"
~detail:(Process.summary result) ~suggestion:"opam init" Check.Warn)
Check.make ~id:"opam.initialized"
~title:"opam root could not be read"
~detail:(Process.summary result)
~suggestion:"opam init" Check.Warn)
| _ ->
Check.make ~id:"opam.initialized"
~title:"opam does not appear initialized"
~detail:(Process.summary result) ~suggestion:"opam init" Check.Warn
~detail:(Process.summary result)
~suggestion:"opam init" Check.Warn

let switch_diagnostics ~(run : Process.runner) os =
let show = run "opam" [ "switch"; "show" ] in
Expand All @@ -75,16 +82,18 @@ let switch_diagnostics ~(run : Process.runner) os =
if switch_list = [] then "opam switch create 5.2.0"
else Platform.environment_sync_suggestion os
in
Check.make ~id:"opam.switch.active" ~title:"opam switch not active"
~detail:"opam did not report an active switch." ~suggestion
Check.Error)
Check.make ~id:"opam.switch.active"
~title:"opam switch not active"
~detail:"opam did not report an active switch."
~suggestion Check.Error)
| _ ->
let suggestion =
if switch_list = [] then "opam switch create 5.2.0"
else Platform.environment_sync_suggestion os
in
Check.make ~id:"opam.switch.active" ~title:"opam switch not active"
~detail:(Process.summary show) ~suggestion Check.Error
Check.make ~id:"opam.switch.active"
~title:"opam switch not active" ~detail:(Process.summary show)
~suggestion Check.Error
in
let list_diagnostic =
match switches.status with
Expand All @@ -99,7 +108,8 @@ let switch_diagnostics ~(run : Process.runner) os =
~title:(Printf.sprintf "opam switches available: %d" count)
?detail Check.Ok
| _ ->
Check.make ~id:"opam.switch.list" ~title:"could not list opam switches"
Check.make ~id:"opam.switch.list"
~title:"could not list opam switches"
~detail:(Process.summary switches)
~suggestion:"Run `opam switch list` to inspect your switches."
Check.Warn
Expand All @@ -124,7 +134,8 @@ let switch_bin_diagnostic ~(run : Process.runner) os =
| _ -> None
in
match first_path with
| Some path when Platform.is_path_under ~parent:switch_bin path ->
| Some path
when Platform.is_path_under ~parent:switch_bin path ->
[
Check.make ~id:"opam.env.sync"
~title:"shell environment appears synced with opam"
Expand All @@ -134,10 +145,12 @@ let switch_bin_diagnostic ~(run : Process.runner) os =
| Some path ->
[
Check.make ~id:"opam.env.sync"
~title:"shell environment may be out of sync with opam"
~title:
"shell environment may be out of sync with opam"
~detail:
(Printf.sprintf
"ocaml resolves to %s, but the active switch bin is %s."
"ocaml resolves to %s, but the active switch \
bin is %s."
path switch_bin)
~suggestion:(Platform.environment_sync_suggestion os)
Check.Warn;
Expand All @@ -153,7 +166,8 @@ let package_diagnostic packages package ~optional =
Check.Ok
else
let title =
if optional then Printf.sprintf "%s not installed (optional)" package
if optional then
Printf.sprintf "%s not installed (optional)" package
else Printf.sprintf "%s not installed" package
in
Check.make
Expand All @@ -178,7 +192,8 @@ let package_diagnostics ~(run : Process.runner) =
Check.make ~id:"opam.packages"
~title:"could not read installed opam packages"
~detail:(Process.summary result)
~suggestion:"Run `opam list --installed --short` to inspect packages."
~suggestion:
"Run `opam list --installed --short` to inspect packages."
Check.Warn;
]

Expand All @@ -192,6 +207,7 @@ let diagnostics ~(run : Process.runner) os =
[
Check.make ~id:"opam.initialized"
~title:"opam checks skipped because opam is missing"
~suggestion:"Install opam from https://opam.ocaml.org/doc/Install.html"
~suggestion:
"Install opam from https://opam.ocaml.org/doc/Install.html"
Check.Error;
]
Loading
Loading