diff --git a/dune-project b/dune-project index 1eb3587..8529ff4 100644 --- a/dune-project +++ b/dune-project @@ -36,4 +36,5 @@ base64 astring bos - ppxlib)) + (opam-state + (>= 2.1.2)))) diff --git a/platform.opam b/platform.opam index e5b005e..a137456 100644 --- a/platform.opam +++ b/platform.opam @@ -28,7 +28,7 @@ depends: [ "base64" "astring" "bos" - "ppxlib" + "opam-state" {>= "2.1.2"} "odoc" {with-doc} ] build: [ diff --git a/src/bin/main.ml b/src/bin/main.ml index b785c8b..489b6a0 100644 --- a/src/bin/main.ml +++ b/src/bin/main.ml @@ -142,4 +142,6 @@ let main () = in Stdlib.exit @@ Cmd.eval' (Cmd.v info term) -let () = main () +let () = + Platform.Opam.Queries.init (); + main () diff --git a/src/lib/binary_package.ml b/src/lib/binary_package.ml index 4792bdb..403ed9b 100644 --- a/src/lib/binary_package.ml +++ b/src/lib/binary_package.ml @@ -6,9 +6,9 @@ open Bos type t = { name : string; ver : string } (** Name and version of the binary package corresponding to a given package. *) -let binary_name ~ocaml_version ~name ~ver ~pure_binary = +let binary_name ~ocaml ~name ~ver ~pure_binary = let name = if pure_binary then name else name ^ "+bin+platform" in - let ocaml_version = Ocaml_version.to_string ocaml_version in + let ocaml_version = Opam.Conversions.version_of_pkg ocaml in { name; ver = ver ^ "-ocaml" ^ ocaml_version } let name_to_string { name; ver } = name ^ "." ^ ver @@ -17,33 +17,41 @@ let name { name; ver = _ } = name let has_binary_package repo { name; ver } = Repo.has_pkg (Binary_repo.repo repo) ~pkg:name ~ver -let generate_opam_file original_name pure_binary archive_path ocaml_version = +let generate_opam_file original_name pure_binary archive_path ocaml = let conflicts = if pure_binary then None else Some [ original_name ] in Repo.Opam_file.v ~install:[ [ "cp"; "-pPR"; "."; "%{prefix}%" ] ] - ~depends:[ ("ocaml", Some ("=", Ocaml_version.to_string ocaml_version)) ] + ~depends:[ ("ocaml", Some ("=", Opam.Conversions.version_of_pkg ocaml)) ] ?conflicts ~url:archive_path let should_remove = Fpath.(is_prefix (v "lib")) let process_path prefix path = - let+ ex = Bos.OS.File.exists path in - if not ex then None - else - match Fpath.rem_prefix prefix path with - | None -> None - | Some path -> - if should_remove path then None else Some Fpath.(base prefix // path) + match Fpath.of_string path with + | Error (`Msg s) -> Error (`Msg s) + | Ok path -> ( + let+ ex = Bos.OS.File.exists path in + if not ex then None + else + match Fpath.rem_prefix prefix path with + | None -> None + | Some path -> + if should_remove path then None + else Some Fpath.(base prefix // path)) (** Binary is already in the sandbox. Add this binary as a package in the local repo *) -let make_binary_package opam_opts ~ocaml_version sandbox repo - ({ name; ver } as bname) ~name:query_name ~pure_binary = +let make_binary_package opam_opts ~ocaml sandbox repo ({ name; ver } as bname) + ~name:query_name ~pure_binary = let prefix = Sandbox_switch.switch_path_prefix sandbox in let archive_path = Binary_repo.archive_path repo ~unique_name:(name_to_string bname ^ ".tar.gz") in - Sandbox_switch.list_files opam_opts sandbox ~pkg:query_name >>= fun paths -> + Opam.Queries.( + with_switch_state + ~dir_name:(Sandbox_switch.get_sandbox_root sandbox) + (files_installed_by_pkg query_name)) + >>= fun paths -> let* paths = paths |> Result.List.filter_map (process_path prefix) @@ -61,7 +69,5 @@ let make_binary_package opam_opts ~ocaml_version sandbox repo if not archive_created then Error (`Msg "Couldn't generate the package archive for unknown reason.") else - let opam = - generate_opam_file query_name pure_binary archive_path ocaml_version - in + let opam = generate_opam_file query_name pure_binary archive_path ocaml in Repo.add_package opam_opts (Binary_repo.repo repo) ~pkg:name ~ver opam diff --git a/src/lib/binary_package.mli b/src/lib/binary_package.mli index 2e32298..22fe824 100644 --- a/src/lib/binary_package.mli +++ b/src/lib/binary_package.mli @@ -6,11 +6,7 @@ open! Import type t val binary_name : - ocaml_version:Ocaml_version.t -> - name:string -> - ver:string -> - pure_binary:bool -> - t + ocaml:OpamPackage.t -> name:string -> ver:string -> pure_binary:bool -> t val name_to_string : t -> string val name : t -> string @@ -20,7 +16,7 @@ val has_binary_package : Binary_repo.t -> t -> bool val make_binary_package : Opam.GlobalOpts.t -> - ocaml_version:Ocaml_version.t -> + ocaml:OpamPackage.t -> Sandbox_switch.t -> Binary_repo.t -> t -> diff --git a/src/lib/dune b/src/lib/dune index 1b9ce13..568167e 100644 --- a/src/lib/dune +++ b/src/lib/dune @@ -1,3 +1,3 @@ (library (name platform) - (libraries astring angstrom ocaml-version bos.setup)) + (libraries astring angstrom ocaml-version bos.setup opam-state)) diff --git a/src/lib/opam.ml b/src/lib/opam.ml index b836877..819f3a1 100644 --- a/src/lib/opam.ml +++ b/src/lib/opam.ml @@ -136,59 +136,102 @@ module Repository = struct Bos.Cmd.(v "repository" % "--this-switch" % "remove" % name) end -module Show = struct - let list_files opam_opts pkg_name = - Cmd.run_l opam_opts Bos.Cmd.(v "show" % "--list-files" % pkg_name) - - let available_versions opam_opts pkg_name = - let open Result.Syntax in - let+ output = - Cmd.run_s opam_opts - Bos.Cmd.(v "show" % "-f" % "available-versions" % pkg_name) +module Queries = struct + let init () = + OpamFormatConfig.init (); + let root = OpamStateConfig.opamroot () in + OpamStateConfig.load_defaults root |> ignore; + OpamStateConfig.init ~root_dir:root () + + let latest_version ~metadata_universe ~pkg_universe ~ocaml package = + let package = OpamPackage.Name.of_string package in + let compatible_ones = + OpamPackage.Set.filter + (fun pkg -> + OpamPackage.Name.equal (OpamPackage.name pkg) package + && + let pkg_opam_file = OpamPackage.Map.find pkg metadata_universe in + let dependencies = OpamFile.OPAM.depends pkg_opam_file in + let env _ = None in + OpamFormula.verifies + (OpamPackageVar.filter_depends_formula ~env dependencies) + ocaml) + pkg_universe in - Astring.String.cuts ~sep:" " output |> List.rev - - let installed_version opam_opts pkg_name = - match - Cmd.run_s opam_opts - Bos.Cmd.( - v "show" % pkg_name % "-f" % "installed-version" % "--normalise") - with - | Ok "--" -> Ok None - | Ok s -> Ok (Some s) - | Error e -> Error e - - let installed_versions opam_opts pkg_names = - let parse = - let open Angstrom in - let is_whitespace = function - | '\x20' | '\x0a' | '\x0d' | '\x09' -> true - | _ -> false - in - let whitespace = take_while is_whitespace in - let word = whitespace *> take_till is_whitespace in - let field f = whitespace *> string f *> word in - let parse_double_line = both (field "name") (field "installed-version") in - let parse = many parse_double_line in - fun s -> - match parse_string ~consume:Consume.All parse s with - | Ok e -> Ok e - | Error e -> Result.errorf "Error in parsing installed versions: %s" e + try Some (OpamPackage.max_version compatible_ones package) + with Not_found -> None + + let installed_versions pkg_names + (sel : OpamTypes.switch_selections) = + let installed_pkgs = sel.sel_installed in + List.map + (fun name -> + let version = + OpamPackage.package_of_name_opt installed_pkgs + @@ OpamPackage.Name.of_string name + in + (name, version)) + pkg_names + + let files_installed_by_pkg pkg_name + (switch_state : 'lock OpamStateTypes.switch_state) = + let changes_f = + OpamPath.Switch.changes switch_state.switch_global.root + switch_state.switch + (OpamPackage.Name.of_string pkg_name) in - let* res = - Cmd.run_s opam_opts - Bos.Cmd.( - v "show" %% of_list pkg_names % "-f" % "name,installed-version" - % "--normalise") - in - let+ res = parse res in - List.map (function a, "--" -> (a, None) | a, s -> (a, Some s)) res - - let depends opam_opts pkg_name = - Cmd.run_l opam_opts Bos.Cmd.(v "show" % "-f" % "depends:" % pkg_name) + match OpamFile.Changes.read_opt changes_f with + | None -> + Result.errorf + "Something went wrong looking for the files installed by %s" pkg_name + | Some changes -> + Ok + (OpamDirTrack.check + (OpamPath.Switch.root switch_state.switch_global.root + switch_state.switch) + changes + |> List.fold_left + (fun acc file -> + match file with + | filename, _ -> OpamFilename.to_string filename :: acc) + []) + + let get_pkg_universe (switch_state : 'lock OpamStateTypes.switch_state) = + switch_state.available_packages + + let get_metadata_universe (switch_state : 'lock OpamStateTypes.switch_state) = + switch_state.opams + +let get_switch = function + | None -> OpamStateConfig.get_switch () + | Some dir -> + OpamSwitch.of_dirname + (Fpath.to_string dir |> OpamFilename.Dir.of_string) + + let with_switch_state ?dir_name f = + let switch = get_switch dir_name in + OpamGlobalState.with_ `Lock_read (fun global_state -> + OpamSwitchState.with_ `Lock_read global_state ~switch + (fun switch_state -> f switch_state)) + + let with_switch_state_sel ?dir_name f = + let switch = get_switch dir_name in + OpamGlobalState.with_ `Lock_read (fun global_state -> + let sel = OpamSwitchState.load_selections ~lock_kind:`Lock_read global_state switch in + f sel) + + let with_virtual_state f = + OpamGlobalState.with_ `Lock_read (fun global_state -> + OpamRepositoryState.with_ `Lock_read global_state (fun repo_state -> + let virtual_state = + OpamSwitchState.load_virtual global_state repo_state + in + f virtual_state)) +end - let version opam_opts pkg_name = - Cmd.run_l opam_opts Bos.Cmd.(v "show" % "-f" % "version" % pkg_name) +module Conversions = struct + let version_of_pkg pkg = + OpamPackage.version pkg |> OpamPackage.Version.to_string end let install opam_opts pkgs = diff --git a/src/lib/opam.mli b/src/lib/opam.mli index 6e4e32e..fd0359c 100644 --- a/src/lib/opam.mli +++ b/src/lib/opam.mli @@ -51,26 +51,46 @@ module Repository : sig val remove : GlobalOpts.t -> string -> (unit, [> `Msg of string ]) result end -module Show : sig - val list_files : - GlobalOpts.t -> string -> (string list, [> `Msg of string ]) result +module Queries : sig + val init : unit -> unit - val available_versions : - GlobalOpts.t -> string -> (string list, [> `Msg of string ]) result + val files_installed_by_pkg : + string -> + 'lock OpamStateTypes.switch_state -> + (string list, [> `Msg of string ]) result + + val get_pkg_universe : + 'lock OpamStateTypes.switch_state -> OpamTypes.package_set lazy_t - val installed_version : - GlobalOpts.t -> string -> (string option, [> `Msg of string ]) result + val get_metadata_universe : + 'lock OpamStateTypes.switch_state -> OpamFile.OPAM.t OpamTypes.package_map + + val latest_version : + metadata_universe:OpamFile.OPAM.t OpamTypes.package_map -> + pkg_universe:OpamTypes.package_set -> + ocaml:OpamPackage.t -> + string -> + OpamPackage.t option val installed_versions : - GlobalOpts.t -> - string list -> - ((string * string option) list, 'a) Result.or_msg + string list -> + OpamTypes.switch_selections -> (string * OpamPackage.t option) list + + val with_switch_state : + ?dir_name:Fpath.t -> + ([< OpamStateTypes.unlocked > `Lock_read `Lock_write ] + OpamStateTypes.switch_state -> + 'a) -> + 'a - val depends : - GlobalOpts.t -> string -> (string list, [> `Msg of string ]) result + val with_switch_state_sel : ?dir_name:Fpath.t -> (OpamTypes.switch_selections -> 'a) -> 'a + + val with_virtual_state : + (OpamStateTypes.unlocked OpamStateTypes.switch_state -> 'a) -> 'a +end - val version : - GlobalOpts.t -> string -> (string list, [> `Msg of string ]) result +module Conversions : sig + val version_of_pkg : OpamPackage.t -> string end val install : GlobalOpts.t -> string list -> (unit, [> `Msg of string ]) result diff --git a/src/lib/sandbox_switch.ml b/src/lib/sandbox_switch.ml index 309048b..e0a90cd 100644 --- a/src/lib/sandbox_switch.ml +++ b/src/lib/sandbox_switch.ml @@ -15,6 +15,8 @@ type t = { [init] and removed during [deinit]. *) } +let get_sandbox_root sb = sb.sandbox_root + let compiler_tools = [ "ocaml"; @@ -88,8 +90,8 @@ let with_var_sys_ocaml_version opam_opts ~ocaml_version f = let* () = Opam.Config.Var.set opam_opts ~global var ocaml_version in Fun.protect ~finally:restore_var f -let init opam_opts ~ocaml_version = - let ocaml_version = Ocaml_version.to_string ocaml_version in +let init opam_opts ~ocaml = + let ocaml_version = Opam.Conversions.version_of_pkg ocaml in (* Directory in which to create the switch. *) let* sandbox_root = OS.Dir.tmp "ocaml-platform-sandbox-%s" in let* compiler_path = @@ -129,14 +131,10 @@ let install _opam_opts t ~pkg = let pkg = pkg_to_string pkg in Opam.install t.sandbox_opts [ pkg ] -let list_files _opam_opts t ~pkg = - let+ files = Opam.Show.list_files t.sandbox_opts pkg in - List.map Fpath.v files - let switch_path_prefix t = t.prefix -let with_sandbox_switch opam_opts ~ocaml_version f = - let* sandbox = init opam_opts ~ocaml_version in +let with_sandbox_switch opam_opts ~ocaml f = + let* sandbox = init opam_opts ~ocaml in Fun.protect ~finally:(fun () -> deinit opam_opts sandbox) (fun () -> f sandbox) diff --git a/src/lib/sandbox_switch.mli b/src/lib/sandbox_switch.mli index 0f1e025..65e3078 100644 --- a/src/lib/sandbox_switch.mli +++ b/src/lib/sandbox_switch.mli @@ -2,20 +2,19 @@ open Import type t +val get_sandbox_root : t -> Fpath.t + val install : Opam.GlobalOpts.t -> t -> pkg:string * string option -> (unit, 'e) Result.or_msg -val list_files : - Opam.GlobalOpts.t -> t -> pkg:string -> (Fpath.t list, 'e) Result.or_msg - val switch_path_prefix : t -> Fpath.t val with_sandbox_switch : Opam.GlobalOpts.t -> - ocaml_version:Ocaml_version.t -> + ocaml:OpamPackage.t -> (t -> ('a, 'e) Result.or_msg) -> ('a, 'e) Result.or_msg (** Create a sandbox switch, call the passed function and finally remove the diff --git a/src/lib/tools.ml b/src/lib/tools.ml index 11f002a..a55d50a 100644 --- a/src/lib/tools.ml +++ b/src/lib/tools.ml @@ -1,7 +1,6 @@ open! Import open Astring open Bos -module OV = Ocaml_version open Result.Syntax type tool = { name : string; pure_binary : bool; version : string option } @@ -9,117 +8,67 @@ type tool = { name : string; pure_binary : bool; version : string option } [OpamPackage.Name.t] for the type of [name] and something like ... for the type of [compiler_constr].*) -let parse_constraints s = - let open Angstrom in - let is_whitespace = function - | '\x20' | '\x0a' | '\x0d' | '\x09' -> true - | _ -> false - in - let whitespace = take_while is_whitespace in - let whitespaced p = whitespace *> p <* whitespace in - let quoted p = whitespaced @@ (char '"' *> p) <* char '"' in - let bracketed p = whitespaced @@ (char '{' *> p) <* char '}' in - let quoted_ocaml = quoted @@ string "ocaml" in - let quoted_version = - quoted @@ take_till (( = ) '"') >>| fun version_string -> - OV.of_string_exn version_string - in - let comparator = - whitespaced @@ take_till is_whitespace >>= function - | "<" -> return `Lt - | "<=" -> return `Le - | ">" -> return `Gt - | ">=" -> return `Ge - | "=" -> return `Eq - | _ -> fail "not a comparator" - in - let constraint_ = both comparator quoted_version in - let constraints = sep_by (whitespaced @@ char '&') constraint_ in - let finally = quoted_ocaml *> bracketed constraints <* end_of_input in - match parse_string ~consume:Consume.All finally s with - | Ok a -> Ok a - | Error m -> Error (`Msg m) - -let verify_constraint version (op, constraint_version) = - let d = OV.compare version constraint_version in - match op with - | `Le -> d <= 0 - | `Lt -> d < 0 - | `Ge -> d >= 0 - | `Gt -> d > 0 - | `Eq -> d = 0 - -let verify_constraints version constraints = - List.for_all (verify_constraint version) constraints - -let best_available_version opam_opts ocaml_version name = - let open Result.Syntax in - let+ versions = Opam.Show.available_versions opam_opts name in - let version = - versions - |> List.find (fun version -> - let ocaml_depends = - let+ depends = - Opam.Show.depends opam_opts (name ^ "." ^ version) - in - List.find_opt (String.is_prefix ~affix:"\"ocaml\"") depends - in - match ocaml_depends with - | Ok (Some ocaml_constraint) -> - let result = - parse_constraints ocaml_constraint >>| fun constraints -> - verify_constraints ocaml_version constraints - in - Result.value ~default:false result - | Ok None -> true - | _ -> false) - in - version - -let best_version_of_tool opam_opts ocaml_version tool = +let best_version ~metadata_universe ~pkg_universe ~ocaml tool = (match tool.version with | Some ver -> Ok ver - | None -> best_available_version opam_opts ocaml_version tool.name) + | None -> ( + match + Opam.Queries.latest_version ~metadata_universe ~pkg_universe:(Lazy.force pkg_universe) ~ocaml + tool.name + with + | Some ver -> Ok (Opam.Conversions.version_of_pkg ver) + | None -> + Result.errorf + "Something went wrong trying to find the best version for %s" + tool.name)) >>| fun ver -> - Binary_package.binary_name ~ocaml_version ~name:tool.name ~ver + Binary_package.binary_name ~ocaml ~name:tool.name ~ver ~pure_binary:tool.pure_binary -let make_binary_package opam_opts ~ocaml_version sandbox repo bname tool = +let make_binary_package opam_opts ~ocaml sandbox repo bname tool = let { name; pure_binary; _ } = tool in Sandbox_switch.install opam_opts sandbox ~pkg:(tool.name, tool.version) >>= fun () -> - Binary_package.make_binary_package opam_opts ~ocaml_version sandbox repo bname - ~name ~pure_binary + Binary_package.make_binary_package opam_opts ~ocaml sandbox repo bname ~name + ~pure_binary let install opam_opts tools = let binary_repo_path = Fpath.( opam_opts.Opam.GlobalOpts.root / "plugins" / "ocaml-platform" / "cache") in - let* ovraw = Opam.Show.installed_version opam_opts "ocaml" in - (match ovraw with - | None -> Result.errorf "Cannot install tools: No switch is selected." - | Some s -> OV.of_string s) - >>= fun ocaml_version -> + let tools_names = List.map (fun tool -> tool.name) tools in + let installed = + Opam.Queries.( + with_switch_state_sel (installed_versions ("ocaml" :: tools_names))) + in + (match List.assoc_opt "ocaml" installed with + | Some (Some s) -> Ok s + | _ -> + Result.errorf "Cannot install tools: No switch with compiler is selected.") + >>= fun ocaml -> Binary_repo.init opam_opts binary_repo_path >>= fun repo -> (* [tools_to_build] is the list of tools that need to be built and placed in the cache. [tools_to_install] is the names of the packages to install into the user's switch, each string is a suitable argument to [opam install]. *) Logs.app (fun m -> m "Inferring tools version..."); let* tools_to_build, tools_to_install = - let* version_list = - Opam.Show.installed_versions opam_opts - (List.map (fun tool -> tool.name) tools) + let pkg_universe, metadata_universe = + Opam.Queries.( + with_virtual_state (fun state -> + (get_pkg_universe state, get_metadata_universe state))) in Result.List.fold_left (fun (to_build, to_install) tool -> - let pkg_version = List.assoc_opt tool.name version_list in + let pkg_version = List.assoc_opt tool.name installed in match pkg_version with | Some (Some _) -> Logs.info (fun m -> m "%s is already installed" tool.name); Ok (to_build, to_install) | _ -> - let+ bname = best_version_of_tool opam_opts ocaml_version tool in + let+ bname = + best_version ~metadata_universe ~pkg_universe ~ocaml tool + in Logs.info (fun m -> m "%s will be installed as %s" tool.name (Binary_package.name_to_string bname)); @@ -134,13 +83,11 @@ let install opam_opts tools = | [] -> Ok () | _ :: _ -> Logs.app (fun m -> m "Creating a sandbox to build the tools..."); - Sandbox_switch.with_sandbox_switch opam_opts ~ocaml_version - (fun sandbox -> + Sandbox_switch.with_sandbox_switch opam_opts ~ocaml (fun sandbox -> Result.List.fold_left (fun () (tool, bname) -> Logs.app (fun m -> m "Building %s..." tool.name); - make_binary_package opam_opts ~ocaml_version sandbox repo bname - tool) + make_binary_package opam_opts ~ocaml sandbox repo bname tool) () tools_to_build)) >>= fun () -> match tools_to_install with