open Bos let opam = Cmd.v "opam" type package = { name : string; version : string } let pp fmt p = Format.fprintf fmt "%s.%s" p.name p.version let memoize f = let r = ref None in fun () -> match !r with | Some x -> x | None -> let x = f () in r := Some x; x let get_switch = memoize @@ fun () -> Util.lines_of_process Cmd.(opam % "switch" % "show") |> List.hd let prefix = memoize @@ fun () -> Util.lines_of_process Cmd.(opam % "var" % "--switch" % get_switch () % "prefix") |> List.hd let all_opam_packages = memoize @@ fun () -> let prefix = prefix () in match Bos.OS.Dir.contents Fpath.(v prefix / ".opam-switch" / "packages") with | Error (`Msg msg) -> Logs.err (fun m -> m "Error listing opam packages: %s" msg); [] | Ok contents -> List.filter_map (fun p -> let name = Fpath.basename p in match Astring.String.cut ~sep:"." name with | Some (name, version) -> Some { name; version } | None -> None) contents let pkg_contents { name; _ } = let prefix = Fpath.v (prefix ()) in let changes_file = Format.asprintf "%a/.opam-switch/install/%s.changes" Fpath.pp prefix name in let file = OpamFilename.raw changes_file in let filename = OpamFile.make @@ OpamFilename.raw @@ Filename.basename changes_file in let changed = try OpamFilename.with_contents (fun str -> OpamFile.Changes.read_from_string ~filename @@ (* Field [opam-version] is invalid in [*.changes] files, displaying a warning. *) if String.starts_with ~prefix:"opam-version" str then match OpamStd.String.cut_at str '\n' with | Some (_, str) -> str | None -> assert false else str) file with | OpamSystem.File_not_found s -> Logs.err (fun m -> m "File not found: %s.\n%s\nConsidering it empty." changes_file s); OpamStd.String.Map.empty | OpamPp.Bad_version _ -> Logs.err (fun m -> m "Bad version while parsing %s.\nConsidering it empty." changes_file); OpamStd.String.Map.empty | OpamPp.Bad_format _ -> Logs.err (fun m -> m "Bad format while parsing %s.\nConsidering it empty." changes_file); OpamStd.String.Map.empty in let added = OpamStd.String.Map.fold (fun file x acc -> match x with | OpamDirTrack.Added _ -> ( try if not @@ Sys.is_directory Fpath.(to_string (prefix // v file)) then file :: acc else acc with _ -> acc (* dose (and maybe others) sometimes creates a symlink to something that doesn't exist *) ) | _ -> acc) changed [] in List.map Fpath.v added let deps pkgs = let cmd = Cmd.( opam % "list" % "--recursive" % "-i" % "--columns" % "package" % "--color" % "never" % "-s" % "--or") in let cmd = List.fold_left (fun cmd pkg -> Cmd.(cmd % "--required-by" % pkg)) cmd pkgs in let out = Util.lines_of_process cmd in List.filter_map (fun x -> match Astring.String.cut ~sep:"." x with | Some (name, version) -> Some { name; version } | None -> None) out type doc_file = { kind : [ `Mld | `Asset | `Other ]; file : Fpath.t; rel_path : Fpath.t; } let pp_doc_file fmt { kind; file; rel_path } = Format.fprintf fmt "kind: %a@,file: %a@,rel_path: %a@," (Fmt.of_to_string (function | `Mld -> "`Mld" | `Asset -> "`Asset" | `Other -> "`Other")) kind Fpath.pp file Fpath.pp rel_path type installed_files = { libs : Fpath.set; docs : doc_file list; odoc_config : Fpath.t option; } type package_of_fpath = package Fpath.map (* Here we use an associative list *) type fpaths_of_package = (package * installed_files) list let pp_fpath_set fmt set = Fpath.Set.iter (Format.fprintf fmt "%a@." Fpath.pp) set let pp_fpaths_of_package fmt l = List.iter (fun (p, { libs; docs; odoc_config }) -> Format.fprintf fmt "%a:@,libs: %a@,docs: %a@,odoc_config: %a@," pp p pp_fpath_set libs Fmt.Dump.(list pp_doc_file) docs (Fmt.option Fpath.pp) odoc_config) l let classify_docs prefix only_package contents = let pkg_match pkg = match only_package with None -> true | Some p -> p = pkg in let is_dir f = try Sys.is_directory (Fpath.to_string f) with Sys_error _ -> false in List.fold_left (fun acc fpath -> match Fpath.segs fpath with | "doc" :: pkg :: "odoc-pages" :: _ :: _ when pkg_match pkg && not (is_dir Fpath.(prefix // fpath)) -> Logs.debug (fun m -> m "Found odoc page: %a" Fpath.pp fpath); let kind = match Fpath.get_ext fpath with ".mld" -> `Mld | _ -> `Asset in let rel_path = Fpath.rem_prefix Fpath.(v "doc" / pkg / "odoc-pages") fpath |> Option.get in { kind; file = Fpath.(prefix // fpath); rel_path } :: acc | "doc" :: pkg :: "odoc-assets" :: _ :: _ when pkg_match pkg && not (is_dir Fpath.(prefix // fpath)) -> Logs.debug (fun m -> m "Found odoc page: %a" Fpath.pp fpath); let rel_path = Fpath.rem_prefix Fpath.(v "doc" / pkg / "odoc-assets") fpath |> Option.get in let rel_path = Fpath.(v "_assets" // rel_path) in { kind = `Asset; file = Fpath.(prefix // fpath); rel_path } :: acc | [ "doc"; pkg; _ ] when pkg_match pkg && not (is_dir Fpath.(prefix // fpath)) -> Logs.debug (fun m -> m "Found other doc: %a" Fpath.pp fpath); let rel_path = Fpath.base fpath in { kind = `Other; file = Fpath.(prefix // fpath); rel_path } :: acc | _ -> acc) [] contents let classify_libs prefix only_package contents = let pkg_match pkg = match only_package with None -> true | Some p -> p = pkg in let libs = List.fold_left (fun set fpath -> match Fpath.segs fpath with | "lib" :: "stublibs" :: _ -> set | "lib" :: pkg :: _ :: _ when Fpath.has_ext ".cmi" fpath && pkg_match pkg -> Fpath.Set.add Fpath.(prefix // fpath |> split_base |> fst) set | _ -> set) Fpath.Set.empty contents in libs let find_odoc_config prefix only_package contents = let pkg_match pkg = match only_package with None -> true | Some p -> p = pkg in let opt = List.find_opt (fun fpath -> match Fpath.segs fpath with | [ "doc"; pkg; "odoc-config.sexp" ] -> pkg_match pkg | _ -> false) contents in Option.map (fun p -> Fpath.(prefix // p)) opt let dune_overrides () = let ocamlpath = Sys.getenv_opt "OCAMLPATH" in match ocamlpath with | None -> [] | Some path -> ( (* OCAMLPATH is set in dune to be e.g. /Users/jon/odoc/_build/install/default/lib *) (* Let's strip the 'lib' off and we can find the installed files *) let path = Fpath.v path in match Fpath.segs path |> List.rev with | "lib" :: _ :: "install" :: "_build" :: _ -> ( (* Check it's of the right form *) let base = Fpath.split_base path |> fst in let contents = Bos.OS.Dir.fold_contents (fun x acc -> match Fpath.relativize ~root:base x with | None -> acc | Some r -> r :: acc) [] base in match contents with | Ok contents -> Logs.debug (fun m -> m "dune install contents: %a" Fmt.(Dump.list Fpath.pp) contents); let packages = List.fold_left (fun acc fpath -> match Fpath.segs fpath with | "lib" :: pkg :: _ :: _ -> Util.StringSet.add pkg acc | "doc" :: pkg :: _ :: _ -> Util.StringSet.add pkg acc | _ -> acc) Util.StringSet.empty contents in Logs.debug (fun m -> m "Found packages: %a" Fmt.(Dump.list string) (Util.StringSet.elements packages)); Util.StringSet.fold (fun pkg acc -> let libs = classify_libs base (Some pkg) contents in let docs = classify_docs base (Some pkg) contents in let odoc_config = find_odoc_config base (Some pkg) contents in Logs.debug (fun m -> m "pkg %s Found %d docs" pkg (List.length docs)); ({ name = pkg; version = "dev" }, { libs; docs; odoc_config }) :: acc) packages [] | Error (`Msg msg) -> Logs.err (fun m -> m "Error listing dune install directory: %s" msg); []) | _ -> []) let check pkgs = let cmd = Cmd.( opam % "list" % "-i" % "--columns" % "package" % "--color" % "never" % "-s") in let cmd = List.fold_left Cmd.( % ) cmd pkgs in let out = Util.lines_of_process cmd in let res = List.filter_map (fun x -> match Astring.String.cut ~sep:"." x with | Some (name, _version) -> Some name | None -> None) out in let missing = Util.StringSet.(diff (of_list pkgs) (of_list res)) in let dune_pkgnames = Util.StringSet.of_list (List.map (fun (p, _) -> p.name) (dune_overrides ())) in let missing = Util.StringSet.diff missing dune_pkgnames in if Util.StringSet.cardinal missing = 0 then Ok () else Error missing let pkg_to_dir_map () = let dune_overrides = dune_overrides () in let pkgs = all_opam_packages () in let prefix = prefix () in let pkg_content = List.map (fun p -> let contents = pkg_contents p in let libs = classify_libs (Fpath.v prefix) None contents in let docs = classify_docs (Fpath.v prefix) None contents in let odoc_config = find_odoc_config (Fpath.v prefix) None contents in (p, { libs; docs; odoc_config })) pkgs in (* Remove anything from opam that is present in the dune overrides *) let pkg_content = List.filter (fun (p, _) -> not @@ List.exists (fun (p', _) -> p.name = p'.name) dune_overrides) pkg_content in let pkg_content = pkg_content @ dune_overrides in let map = List.fold_left (fun map (p, { libs; _ }) -> Fpath.Set.fold (fun dir map -> Fpath.Map.update dir (function | None -> Some p | Some x -> Logs.debug (fun m -> m "Multiple packages (%a,%a) found for dir %a" pp x pp p Fpath.pp dir); Some p) map) libs map) Fpath.Map.empty pkg_content in Logs.debug (fun m -> m "pkg_to_dir_map: %a" pp_fpaths_of_package pkg_content); (pkg_content, map)