├── .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 | --------------------------------------------------------------------------------