├── .gitignore
├── .ocamlformat
├── README.md
├── bin
├── dune
├── ocamlformat_vmux.ml
├── ocamlformat_vmux.mli
├── ocamlformat_vmux_shim.ml
└── ocamlformat_vmux_shim.mli
├── dune-project
├── lib
├── config.ml
├── config.mli
├── dune
├── find_ocamlformat_config.ml
├── find_ocamlformat_config.mli
├── import.ml
├── ocamlformat_vmux.ml
└── ocamlformat_vmux.mli
└── ocamlformat-vmux.opam
/.gitignore:
--------------------------------------------------------------------------------
1 | _build
2 | _opam
3 |
--------------------------------------------------------------------------------
/.ocamlformat:
--------------------------------------------------------------------------------
1 | version = 0.17.0
2 | profile = conventional
3 |
4 | parse-docstrings
5 | module-item-spacing = compact
6 | dock-collection-brackets = false
7 | break-infix = fit-or-vertical
8 | break-separators = before
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OCamlformat version multiplexer
2 |
3 |
🚧 Works on my machine™ but very poorly tested. 🚧
4 |
5 | As of today, OCamlformat is normally consumed via installing the appropriate version into a dev switch. This is a bit painful, since the OCamlformat dependency tree is quite large and can conflict with other common dev switch inclusions (especially Odoc and Ppxlib).
6 |
7 | This project provides an awful hack to get around this problem: install all the versions, and provide a shim `ocamlformat` binary that proxies to the appropriate one at runtime. With this shim in your `$PATH`, you can stop installing OCamlformat in Opam switches :tada:
8 |
9 | ## Usage
10 |
11 | ```bash
12 | ; opam pin add -n ocamlformat-vmux git+https://github.com/CraigFe/ocamlformat-vmux
13 | ; opam install ocamlformat-vmux
14 |
15 | # Pull all installed OCamlformat versions into ~/.local/bin/
16 | ; ocamlformat-vmux steal ~/.local/bin
17 |
18 | # Put the shim `ocamlformat` binary somewhere in your path
19 | ; ln -s "$(which ocamlformat-vmux-shim)" ~/.local/bin/ocamlformat
20 |
21 | # Purge opam-installed `ocamlformat` from your life
22 | ; opam switch -s | xargs -n1 -I{} opam remove ocamlformat --switch={} --yes
23 | ```
24 |
--------------------------------------------------------------------------------
/bin/dune:
--------------------------------------------------------------------------------
1 | (executable
2 | (name ocamlformat_vmux)
3 | (modules ocamlformat_vmux)
4 | (public_name ocamlformat-vmux)
5 | (libraries ocamlformat_vmux fpath))
6 |
7 | (executable
8 | (name ocamlformat_vmux_shim)
9 | (modules ocamlformat_vmux_shim)
10 | (public_name ocamlformat-vmux-shim)
11 | (libraries ocamlformat_vmux fpath))
12 |
--------------------------------------------------------------------------------
/bin/ocamlformat_vmux.ml:
--------------------------------------------------------------------------------
1 | let () =
2 | match Sys.argv with
3 | | [| _; "steal"; into |] -> Ocamlformat_vmux.steal ~into
4 | | [| _; "inferred_version" |] ->
5 | Ocamlformat_vmux.inferred_version ~from:(Fpath.v (Sys.getcwd ()))
6 | | _ ->
7 | Fmt.epr "usage: %s [ steal | inferred_version ]@."
8 | Sys.argv.(0);
9 | exit 1
10 |
--------------------------------------------------------------------------------
/bin/ocamlformat_vmux.mli:
--------------------------------------------------------------------------------
1 | (* Intentionally empty *)
2 |
--------------------------------------------------------------------------------
/bin/ocamlformat_vmux_shim.ml:
--------------------------------------------------------------------------------
1 | let () = Ocamlformat_vmux.shim ~from:(Fpath.v (Sys.getcwd ()))
2 |
--------------------------------------------------------------------------------
/bin/ocamlformat_vmux_shim.mli:
--------------------------------------------------------------------------------
1 | (* Intentionally empty *)
2 |
--------------------------------------------------------------------------------
/dune-project:
--------------------------------------------------------------------------------
1 | (lang dune 2.0)
2 | (name ocamlformat-vmux)
3 | (generate_opam_files true)
4 |
5 | (source (github mirage/alcotest))
6 | (license MIT)
7 | (authors "Craig Ferguson ")
8 | (maintainers "Craig Ferguson ")
9 |
10 | (package
11 | (name ocamlformat-vmux)
12 | (synopsis "OCamlformat version multiplexer")
13 | (depends
14 | (ocaml (>= 4.08))
15 | (dune-private-libs (>= 2.6.0))
16 | astring
17 | bos
18 | fmt
19 | fpath
20 | logs
21 | opam-state
22 | re
23 | rresult
24 | sexplib))
25 |
--------------------------------------------------------------------------------
/lib/config.ml:
--------------------------------------------------------------------------------
1 | open! Import
2 |
3 | let ( / ) = Filename.concat
4 | let parent_loc = Xdg.config_dir / "ocamlformat-vmux"
5 | let location = parent_loc / "config"
6 |
7 | let read () =
8 | let sexp = Sexp.load_sexp location in
9 |
10 | match sexp with
11 | | List (Atom "versions" :: xs) ->
12 | List.fold_left xs ~init:String.Map.empty ~f:(fun m -> function
13 | | Sexp.List [ Atom version; Atom path ] ->
14 | String.Map.add version (Fpath.v path) m
15 | | x -> Fmt.failwith "Invalid version specification: %a" Sexp.pp x)
16 | | x -> Fmt.failwith "Invalid `versions` stanza: %a" Sexp.pp x
17 |
18 | let write_diff m =
19 | let absent : bool =
20 | Bos.OS.Dir.create (Fpath.v parent_loc) |> Rresult.R.failwith_error_msg
21 | in
22 | let initial = if absent then String.Map.empty else read () in
23 | let to_write = String.Map.union (fun _ _ new_ -> Some new_) initial m in
24 | let versions =
25 | String.Map.bindings to_write
26 | |> List.map ~f:(fun (v, p) ->
27 | Sexp.List [ Atom v; Atom (Fpath.to_string p) ])
28 | in
29 | let sexp = Sexp.List (Atom "versions" :: versions) in
30 | Sexp.save_hum location sexp
31 |
--------------------------------------------------------------------------------
/lib/config.mli:
--------------------------------------------------------------------------------
1 | open! Import
2 |
3 | val location : string
4 | val write_diff : Fpath.t String.Map.t -> unit
5 | val read : unit -> Fpath.t String.Map.t
6 |
--------------------------------------------------------------------------------
/lib/dune:
--------------------------------------------------------------------------------
1 | (library
2 | (name ocamlformat_vmux)
3 | (libraries logs fmt opam-state bos rresult fpath re astring logs.fmt fmt.tty
4 | sexplib dune-private-libs.xdg))
5 |
--------------------------------------------------------------------------------
/lib/find_ocamlformat_config.ml:
--------------------------------------------------------------------------------
1 | open! Import
2 |
3 | (* Extracted from OCamlformat's [lib/Conf.ml]. *)
4 |
5 | let local =
6 | let project_root_witnesses = [ ".git"; ".hg"; "dune-project" ] in
7 | let ( / ) = Fpath.( / ) in
8 | let rec scan_upwards curr =
9 | let here = curr / ".ocamlformat" in
10 | match Fpath.exists here with
11 | | true -> Some here
12 | | false -> (
13 | match
14 | List.exists project_root_witnesses ~f:(fun x ->
15 | Fpath.exists (curr / x))
16 | with
17 | | true -> None
18 | | false ->
19 | let parent = Fpath.parent curr in
20 | if parent == curr then None else scan_upwards parent)
21 | in
22 | fun ~from -> scan_upwards from
23 |
24 | let global () =
25 | let xdg_config_home =
26 | match Sys.getenv_opt "XDG_CONFIG_HOME" with
27 | | None | Some "" -> (
28 | match Sys.getenv_opt "HOME" with
29 | | None | Some "" -> None
30 | | Some home -> Some Fpath.(v home / ".config"))
31 | | Some xdg_config_home -> Some (Fpath.v xdg_config_home)
32 | in
33 | match xdg_config_home with
34 | | Some xdg_config_home ->
35 | let filename = Fpath.(xdg_config_home / "ocamlformat") in
36 | if Fpath.exists filename then Some filename else None
37 | | None -> None
38 |
--------------------------------------------------------------------------------
/lib/find_ocamlformat_config.mli:
--------------------------------------------------------------------------------
1 | val local : from:Fpath.t -> Fpath.t option
2 | val global : unit -> Fpath.t option
3 |
--------------------------------------------------------------------------------
/lib/import.ml:
--------------------------------------------------------------------------------
1 | include Sexplib
2 | include Stdlib.StdLabels
3 | include Astring
4 | include Bos
5 |
6 | module Fpath = struct
7 | include Fpath
8 |
9 | let exists p = to_string p |> Sys.file_exists
10 | end
11 |
12 | module String = struct
13 | include String
14 |
15 | module Map = struct
16 | include Map
17 |
18 | let iter ~f m = iter f m
19 | end
20 | end
21 |
22 | module Fmt = struct
23 | include Fmt
24 |
25 | let buf = Buffer.create 512
26 | let buf_ppf = Format.formatter_of_buffer buf
27 |
28 | let () =
29 | Fmt_tty.setup_std_outputs ();
30 | Fmt.set_style_renderer buf_ppf (Fmt.style_renderer Fmt.stdout)
31 |
32 | let str_styled : type a. (a, Format.formatter, unit, string) format4 -> a =
33 | fun fmt ->
34 | Format.kdprintf
35 | (fun theta ->
36 | theta buf_ppf;
37 | Format.pp_print_flush buf_ppf ();
38 | let s = Buffer.contents buf in
39 | Buffer.reset buf;
40 | s)
41 | fmt
42 | end
43 |
44 | let print fmt = Format.kdprintf (Format.printf "@[%t@]@.") fmt
45 |
46 | let constf f fmt =
47 | Format.kdprintf (fun theta ppf -> f (fun ppf () -> theta ppf) ppf ()) fmt
48 |
--------------------------------------------------------------------------------
/lib/ocamlformat_vmux.ml:
--------------------------------------------------------------------------------
1 | open! Import
2 |
3 | let () =
4 | Logs.set_reporter
5 | (Logs_fmt.reporter
6 | ~pp_header:
7 | (fun ppf -> function
8 | | App, _ -> Fmt.(styled `Green (const string "→ ")) ppf ()
9 | | Error, _ -> Fmt.(styled `Red (const string "Error: ")) ppf ()
10 | | _ -> ())
11 | ())
12 |
13 | let cyan = Fmt.(styled `Cyan string)
14 |
15 | let get_opam_installed_versions () =
16 | OpamGlobalState.with_ `Lock_none (fun global_state ->
17 | let ocamlformat_versions =
18 | OpamPackage.Name.of_string "ocamlformat"
19 | |> OpamGlobalState.installed_versions global_state
20 | in
21 |
22 | OpamPackage.Map.fold
23 | (fun p switches m ->
24 | let version = OpamPackage.Version.to_string (OpamPackage.version p) in
25 | match String.Map.find version m with
26 | | Some _ -> m
27 | | None ->
28 | let selected_switch = List.hd switches in
29 | let entry =
30 | ( OpamSwitch.to_string selected_switch
31 | , OpamSwitch.get_root global_state.OpamStateTypes.root
32 | selected_switch )
33 | in
34 | String.Map.add version entry m)
35 | ocamlformat_versions String.Map.empty)
36 |
37 | let versioned_ocamlformat = Printf.sprintf "ocamlformat-%s"
38 |
39 | let steal ~into =
40 | let install_location ~version =
41 | Printf.sprintf "%s/%s" into (versioned_ocamlformat version)
42 | in
43 |
44 | Logs.app (fun f ->
45 | f "Getting the currently-installed OCamlformat versions:\n");
46 | let ocamlformat_versions = get_opam_installed_versions () in
47 |
48 | String.Map.iter ocamlformat_versions ~f:(fun k (v, _) ->
49 | print " - %s\t%t" k (constf Fmt.(styled `Faint) "(switch: %s)" v));
50 |
51 | print "";
52 | Logs.app (fun f -> f "Stealing these versions into `%a`\n" cyan into);
53 |
54 | String.Map.iter ocamlformat_versions ~f:(fun version (_, x) ->
55 | let target = install_location ~version in
56 | let source =
57 | Printf.sprintf "%s/bin/ocamlformat" (OpamFilename.Dir.to_string x)
58 | in
59 | let cp = Cmd.(v "cp" % source % target) in
60 | match OS.Cmd.(run_status cp) |> Rresult.R.failwith_error_msg with
61 | | `Exited 0 ->
62 | print " %s %t %s" target (constf Fmt.(styled `Faint) "↦") source
63 | | n ->
64 | Fmt.failwith "Non-zero return status for `cp`: %a" OS.Cmd.pp_status n);
65 |
66 | print "";
67 | Logs.app (fun f -> f "Writing this state to `%a`" cyan Config.location);
68 | let stolen_installation =
69 | ocamlformat_versions
70 | |> String.Map.mapi (fun version _ -> Fpath.v (install_location ~version))
71 | in
72 | Config.write_diff stolen_installation;
73 | Logs.app (fun f -> f "Done!")
74 |
75 | let read_version_from_ocamlformat_config =
76 | let re = Re.Pcre.re " *version *= *(.*) *$" |> Re.compile in
77 | fun file ->
78 | let ic = open_in (Fpath.to_string file) in
79 | try
80 | let rec aux () =
81 | let line = input_line ic in
82 | match Re.Group.get (Re.exec re line) 1 with
83 | | v -> Some v
84 | | exception Not_found -> aux ()
85 | in
86 | aux ()
87 | with End_of_file ->
88 | close_in ic;
89 | None
90 |
91 | let get_required_version path =
92 | let config =
93 | match Find_ocamlformat_config.local ~from:path with
94 | | Some x -> Some x
95 | | None -> Find_ocamlformat_config.global ()
96 | in
97 | Option.bind config (fun file ->
98 | read_version_from_ocamlformat_config file
99 | |> Option.map (fun x -> (x, file)))
100 |
101 | let inferred_version ~from:path =
102 | match get_required_version path with
103 | | None ->
104 | Fmt.pr "None\t%t@."
105 | (constf
106 | Fmt.(styled `Faint)
107 | "(no local `.ocamlformat` file for %a or in $XDG_CONFIG_HOME)"
108 | Fpath.pp path)
109 | | Some (v, p) ->
110 | Fmt.pr "Some %S\t%t@." v
111 | (constf Fmt.(styled `Faint) "(from: `%a`)" Fpath.pp p)
112 |
113 | let shim ~from =
114 | let available_versions = Config.read () in
115 | let selected_binary =
116 | match get_required_version from with
117 | | None -> snd (String.Map.get_max_binding available_versions)
118 | | Some (version, config_file) -> (
119 | match String.Map.find_opt version available_versions with
120 | | Some path -> path
121 | | None ->
122 | Logs.err (fun f ->
123 | f "@[%a@]" Fmt.text
124 | (Fmt.str_styled
125 | "OCamlformat seems to want version %s (read from file \
126 | `%a`), but the binary `%a` isn't available.\n\n\
127 | Either create this binary manually or install it in an \
128 | opam switch and re-run `%a`."
129 | version
130 | Fmt.(styled `Cyan Fpath.pp)
131 | config_file cyan
132 | (versioned_ocamlformat version)
133 | cyan "ocamlformat-vmux steal"));
134 | exit 1)
135 | in
136 | Unix.execv (Fpath.to_string selected_binary) Sys.argv
137 |
--------------------------------------------------------------------------------
/lib/ocamlformat_vmux.mli:
--------------------------------------------------------------------------------
1 | val steal : into:string -> unit
2 | (** Copy the opam-installed [ocamlformat] binaries into the given directory. *)
3 |
4 | val inferred_version : from:Fpath.t -> unit
5 | (** Print the version OCamlformat seems to expect when run from the given path. *)
6 |
7 | val shim : from:Fpath.t -> unit
8 | (** [exec] the appropriate stolen [ocamlformat] binary, assuming it exists in
9 | the path. *)
10 |
--------------------------------------------------------------------------------
/ocamlformat-vmux.opam:
--------------------------------------------------------------------------------
1 | # This file is generated by dune, edit dune-project instead
2 | opam-version: "2.0"
3 | synopsis: "OCamlformat version multiplexer"
4 | maintainer: ["Craig Ferguson "]
5 | authors: ["Craig Ferguson "]
6 | license: "MIT"
7 | homepage: "https://github.com/mirage/alcotest"
8 | bug-reports: "https://github.com/mirage/alcotest/issues"
9 | depends: [
10 | "dune" {>= "2.0"}
11 | "ocaml" {>= "4.08"}
12 | "dune-private-libs" {>= "2.6.0"}
13 | "astring"
14 | "bos"
15 | "fmt"
16 | "fpath"
17 | "logs"
18 | "opam-state"
19 | "re"
20 | "rresult"
21 | "sexplib"
22 | ]
23 | build: [
24 | ["dune" "subst"] {pinned}
25 | [
26 | "dune"
27 | "build"
28 | "-p"
29 | name
30 | "-j"
31 | jobs
32 | "@install"
33 | "@runtest" {with-test}
34 | "@doc" {with-doc}
35 | ]
36 | ]
37 | dev-repo: "git+https://github.com/mirage/alcotest.git"
38 |
--------------------------------------------------------------------------------