···11-(* Library-name index for the active opam switch. *)
11+(* Hierarchical index: opam package -> dune library -> modules.
2233-module String_set = Set.Make (String)
33+ Two structures:
44+ - [pkg_libs]: maps each opam package to a per-package [Dune.Lib_index.t]
55+ so we can answer "what libraries does this package provide" and look
66+ up modules scoped to a specific package.
77+ - [lib_to_pkg]: reverse index for fast [package_of] lookups, since
88+ callers (e.g. monopam.lint dead-lib check) only have a library name. *)
99+1010+type origin = Local | Opam
411512type t = {
66- modules : (string, String_set.t) Hashtbl.t;
77- virtual_impls : (string, unit) Hashtbl.t;
1313+ pkg_libs : (string, Dune.Lib_index.t) Hashtbl.t;
1414+ pkg_origin : (string, origin) Hashtbl.t;
1515+ lib_to_pkg : (string, string) Hashtbl.t;
816}
9171018let load_eio_path eio_path =
···2533 String.uppercase_ascii (String.sub s 0 1)
2634 ^ String.sub s 1 (String.length s - 1)
27352828-(* Capitalised basenames of *.cmi files directly in [pkg_dir]. Skips
2929- wrapped-private modules (containing [__]). Fallback for packages that
3030- pre-date dune-package metadata. *)
3636+(* Capitalised basenames of *.cmi files in [pkg_dir], skipping
3737+ wrapped-private modules. Fallback for packages installed without a
3838+ [dune-package] file. *)
3139let cmi_modules_in_dir pkg_dir =
3240 let entries = try Eio.Path.read_dir pkg_dir with Eio.Io _ -> [] in
3333- List.fold_left
3434- (fun acc entry ->
4141+ List.filter_map
4242+ (fun entry ->
3543 if Filename.check_suffix entry ".cmi" then
3644 let base = Filename.chop_suffix entry ".cmi" in
3737- if contains_double_underscore base then acc
3838- else String_set.add (capitalize base) acc
3939- else acc)
4040- String_set.empty entries
4545+ if contains_double_underscore base then None else Some (capitalize base)
4646+ else None)
4747+ entries
41484242-let parse_libraries content =
4949+(* Stream stanzas through Library.codec while harvesting library names —
5050+ needed for the lib_to_pkg reverse index. Returning the names lets us
5151+ register both indexes in one pass. *)
5252+let library_names_in_dune_package content =
4353 match Sexp.Value.parse_string_many content with
4454 | Error _ -> []
4555 | Ok stanzas ->
4656 List.filter_map
4757 (fun s ->
4848- Sexp.Codec.decode_value Sexp_codecs.Dune.Dune_package.Library.codec s
4949- |> Result.to_option)
5858+ match Sexp.Codec.decode_value Dune.Package.Library.codec s with
5959+ | Ok (lib : Dune.Package.Library.t) -> Some lib.name
6060+ | Error _ -> None)
5061 stanzas
51625252-let scan_pkg_dir t pkg pkg_dir =
5353- let dune_pkg = Eio.Path.(pkg_dir / "dune-package") in
5454- let covered = ref false in
5555- (match load_eio_path dune_pkg with
5656- | None -> ()
6363+let pkg_index t pkg =
6464+ match Hashtbl.find_opt t.pkg_libs pkg with
6565+ | Some i -> i
6666+ | None ->
6767+ let i = Dune.Lib_index.empty () in
6868+ Hashtbl.replace t.pkg_libs pkg i;
6969+ i
7070+7171+let scan_pkg_dir ~origin t pkg pkg_dir =
7272+ let registered = ref false in
7373+ let register () =
7474+ if not !registered then begin
7575+ Hashtbl.replace t.pkg_origin pkg origin;
7676+ registered := true
7777+ end
7878+ in
7979+ match load_eio_path Eio.Path.(pkg_dir / "dune-package") with
5780 | Some content ->
8181+ register ();
8282+ let i = pkg_index t pkg in
8383+ ignore (Dune.Lib_index.add_dune_package i content);
5884 List.iter
5959- (fun (lib : Sexp_codecs.Dune.Dune_package.Library.t) ->
6060- if lib.implements <> None then
6161- Hashtbl.replace t.virtual_impls lib.name ();
6262- if lib.modules <> [] then begin
6363- covered := true;
6464- let existing =
6565- try Hashtbl.find t.modules lib.name
6666- with Not_found -> String_set.empty
6767- in
6868- Hashtbl.replace t.modules lib.name
6969- (String_set.union existing (String_set.of_list lib.modules))
7070- end)
7171- (parse_libraries content));
7272- if not !covered then
7373- let mods = cmi_modules_in_dir pkg_dir in
7474- if not (String_set.is_empty mods) then
7575- let existing =
7676- try Hashtbl.find t.modules pkg with Not_found -> String_set.empty
7777- in
7878- Hashtbl.replace t.modules pkg (String_set.union existing mods)
8585+ (fun lib_name -> Hashtbl.replace t.lib_to_pkg lib_name pkg)
8686+ (library_names_in_dune_package content)
8787+ | None -> (
8888+ match cmi_modules_in_dir pkg_dir with
8989+ | [] -> ()
9090+ | modules ->
9191+ register ();
9292+ let i = pkg_index t pkg in
9393+ ignore (Dune.Lib_index.add_cmi_modules i ~pkg ~modules);
9494+ Hashtbl.replace t.lib_to_pkg pkg pkg)
79958096let build ~fs ~monorepo =
8181- let t = { modules = Hashtbl.create 256; virtual_impls = Hashtbl.create 16 } in
8282- let scan root =
9797+ let t =
9898+ {
9999+ pkg_libs = Hashtbl.create 256;
100100+ pkg_origin = Hashtbl.create 256;
101101+ lib_to_pkg = Hashtbl.create 512;
102102+ }
103103+ in
104104+ let scan ~origin root =
83105 let entries = try Eio.Path.read_dir root with Eio.Io _ -> [] in
8484- List.iter (fun pkg -> scan_pkg_dir t pkg Eio.Path.(root / pkg)) entries
106106+ List.iter
107107+ (fun pkg -> scan_pkg_dir ~origin t pkg Eio.Path.(root / pkg))
108108+ entries
85109 in
86110 let opam_lib =
87111 Eio.Path.(fs / Fpath.to_string Fpath.(monorepo / "_opam" / "lib"))
···92116 / Fpath.to_string
93117 Fpath.(monorepo / "_build" / "install" / "default" / "lib"))
94118 in
9595- scan opam_lib;
9696- scan build_lib;
119119+ scan ~origin:Opam opam_lib;
120120+ scan ~origin:Local build_lib;
97121 t
98122123123+let origin t pkg = Hashtbl.find_opt t.pkg_origin pkg
124124+125125+let packages t =
126126+ Hashtbl.fold (fun k _ acc -> k :: acc) t.pkg_libs []
127127+ |> List.sort String.compare
128128+129129+let libraries t pkg =
130130+ Hashtbl.fold
131131+ (fun lib p acc -> if p = pkg then lib :: acc else acc)
132132+ t.lib_to_pkg []
133133+ |> List.sort String.compare
134134+135135+let package_of t lib = Hashtbl.find_opt t.lib_to_pkg lib
136136+99137let modules t lib =
100100- match Hashtbl.find_opt t.modules lib with
138138+ match package_of t lib with
101139 | None -> []
102102- | Some s -> String_set.elements s
140140+ | Some pkg -> (
141141+ match Hashtbl.find_opt t.pkg_libs pkg with
142142+ | None -> []
143143+ | Some i -> Dune.Lib_index.modules i lib)
103144104104-let is_virtual_implementation t lib = Hashtbl.mem t.virtual_impls lib
145145+let is_virtual_implementation t lib =
146146+ match package_of t lib with
147147+ | None -> false
148148+ | Some pkg -> (
149149+ match Hashtbl.find_opt t.pkg_libs pkg with
150150+ | None -> false
151151+ | Some i -> Dune.Lib_index.is_virtual_implementation i lib)
+59-15
lib/index/monopam_info_index.mli
···11-(** Build-level information about an opam switch's installed libraries.
11+(** Build-level snapshot of the libraries and modules an opam switch makes
22+ available, modelled as the natural hierarchy:
33+ [opam package -> dune library -> modules].
44+55+ Each opam package contributes a [_opam/lib/<pkg>/] directory. Inside, one of
66+ two metadata sources tells us which dune libraries the package provides:
77+88+ - {b dune-package}: dune writes [_opam/lib/<pkg>/dune-package] for any
99+ package built with dune. The file lists every sub-library plus the modules
1010+ each library exposes, with virtual / implements relationships. This is the
1111+ precise source.
1212+ - {b *.cmi listing} (fallback): for packages not built with dune (e.g.
1313+ [zarith], [asn1-combinators], [ounit2]), one library is registered under
1414+ the package name with the [*.cmi] basenames in the install directory as
1515+ its modules. This loses the sub-library hierarchy ([zarith.top] etc.) but
1616+ is enough to answer "is library [L] referenced by name in source?" for the
1717+ top-level library. A future pass could read the findlib [META] file to
1818+ recover the [pkg.subpkg] tree, but that needs the [Meta] module to not
1919+ collide with [compiler-libs.Meta] in mdx-built executables. *)
22033- For each installed package, dune writes a [dune-package] file at
44- [_opam/lib/<pkg>/dune-package] (or [_build/install/<context>/lib/...])
55- listing every sub-library plus the modules it exposes. This module walks
66- those metadata files and aggregates them into a single index keyed by
77- library name. Packages without a [dune-package] (e.g. ones that pre-date
88- dune, like [zarith] or [asn1-combinators]) fall back to listing [*.cmi]
99- basenames in the install directory. *)
2121+type origin =
2222+ | Local
2323+ | Opam
2424+ (** Where a package was discovered:
2525+ - [Local]: built locally and installed under
2626+ [<monorepo>/_build/install/default/lib/]. Takes precedence over an
2727+ opam-installed copy of the same package.
2828+ - [Opam]: only present in [<monorepo>/_opam/lib/], i.e. an external
2929+ opam dependency. *)
10301131type t
1212-(** A library-name index for the active opam switch. *)
3232+(** Snapshot of the libraries available in a monorepo's opam switch. *)
13331434val build : fs:Eio.Fs.dir_ty Eio.Path.t -> monorepo:Fpath.t -> t
1515-(** [build ~fs ~monorepo] scans [<monorepo>/_opam/lib/] and
1616- [<monorepo>/_build/install/default/lib/] and populates the index. *)
3535+(** [build ~fs ~monorepo] scans both [<monorepo>/_opam/lib/] and
3636+ [<monorepo>/_build/install/default/lib/] and aggregates everything into a
3737+ single index. The build-install root is scanned second so locally built
3838+ packages override opam-installed ones, and their {!origin} comes out as
3939+ [Local]. *)
4040+4141+(** {1 Querying by opam package} *)
4242+4343+val packages : t -> string list
4444+(** [packages t] is the sorted list of opam package names known to [t]. *)
4545+4646+val origin : t -> string -> origin option
4747+(** [origin t pkg] is [Some Local] / [Some Opam] for known packages, or [None]
4848+ if [pkg] is not in the index. *)
4949+5050+val libraries : t -> string -> string list
5151+(** [libraries t pkg] is the sorted list of dune library names provided by opam
5252+ package [pkg]. Returns [[]] if [pkg] is unknown. *)
5353+5454+(** {1 Querying by library} *)
5555+5656+val package_of : t -> string -> string option
5757+(** [package_of t lib] is the opam package name that provides dune library
5858+ [lib], or [None] if [lib] is unknown. *)
17591860val modules : t -> string -> string list
1919-(** [modules t lib] is the list of top-level module names exposed by [lib], or
2020- [[]] if [lib] is unknown. Module names are capitalised, in the form
2121- [Sexp_codecs.Dune.Dune_package.Library.t.modules] returns. *)
6161+(** [modules t lib] is the list of top-level module names exposed by dune
6262+ library [lib], or [[]] if [lib] is unknown. Module names are capitalised, in
6363+ the form [Dune.Package.Library.t.modules] returns. *)
22642365val is_virtual_implementation : t -> string -> bool
2466(** [is_virtual_implementation t lib] is [true] iff [lib] has an
2567 [(implements X)] entry in its [dune-package]. Such libraries are link-time
2626- live even when none of their modules appear in source. *)
6868+ live even when none of their modules appear in source. Always [false] for
6969+ libraries discovered via META, since findlib has no notion of virtual
7070+ libraries. *)