Skip to content

Commit 7373af6

Browse files
committed
feat: implement per-module library dependency filtering (issue #4572)
Use ocamldep output to filter cross-library file dependencies per module. Instead of every module depending on all libraries, each module now depends only on libraries whose entry modules it actually references. - Lib_index maps library entry module names back to libraries, computed once per stanza and stored in Compilation_context - read_immediate_deps_raw_of returns raw (unresolved) module names from ocamldep, including cross-library references previously discarded - build_cm filters library deps using the index; conservative fallbacks for root module references and unresolvable names - deps_of_entries extended to support per-file deps for unwrapped libraries via Lib_index.entry (Lib.t * Module.t option) - Dep_graph.dir accessor added for link-time module detection Signed-off-by: Robin Bate Boerop <me@robinbb.com>
1 parent a0cbf2e commit 7373af6

17 files changed

+279
-95
lines changed

src/dune_rules/compilation_context.ml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ module Includes = struct
55
type t = Command.Args.without_targets Command.Args.t Lib_mode.Cm_kind.Map.t
66

77
(* Note: Library file dependencies (Hidden_deps) are now added per-module
8-
in module_compilation.ml. This allows future work to depend only on
9-
libraries that a module actually uses.
8+
in module_compilation.ml based on ocamldep output. This allows us to
9+
only depend on libraries that a module actually uses.
1010
See issue #4572: Finer dependency analysis between libraries. *)
1111
let make ~project ~direct_requires ~hidden_requires lib_config
1212
: _ Lib_mode.Cm_kind.Map.t
@@ -68,6 +68,7 @@ type t =
6868
; parameters : Module_name.t list Resolve.Memo.t
6969
; instances : Parameterised_instances.t Resolve.Memo.t option
7070
; includes : Includes.t
71+
; lib_index : Lib_file_deps.Lib_index.t Resolve.Memo.t
7172
; preprocessing : Pp_spec.t
7273
; opaque : bool
7374
; js_of_ocaml : Js_of_ocaml.In_context.t option Js_of_ocaml.Mode.Pair.t
@@ -95,6 +96,7 @@ let requires_hidden t = t.requires_hidden
9596
let requires_link t = Memo.Lazy.force t.requires_link
9697
let parameters t = t.parameters
9798
let includes t = t.includes
99+
let lib_index t = t.lib_index
98100
let preprocessing t = t.preprocessing
99101
let opaque t = t.opaque
100102
let js_of_ocaml t = t.js_of_ocaml
@@ -218,6 +220,11 @@ let create
218220
; implements
219221
; parameters
220222
; includes = Includes.make ~project ~direct_requires ~hidden_requires ocaml.lib_config
223+
; lib_index =
224+
(let open Resolve.Memo.O in
225+
let* direct_libs = direct_requires
226+
and* hidden_libs = hidden_requires in
227+
Lib_file_deps.Lib_index.create super_context (direct_libs @ hidden_libs) ~for_)
221228
; preprocessing
222229
; opaque
223230
; js_of_ocaml
@@ -318,6 +325,7 @@ let for_module_generated_at_link_time cctx ~requires ~module_ =
318325
; flags = Ocaml_flags.empty
319326
; requires_link = Memo.lazy_ (fun () -> requires)
320327
; requires_compile = requires
328+
; lib_index = Resolve.Memo.return Lib_file_deps.Lib_index.empty
321329
; includes
322330
; modules
323331
}

src/dune_rules/compilation_context.mli

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ val requires_hidden : t -> Lib.t list Resolve.Memo.t
6262
val requires_compile : t -> Lib.t list Resolve.Memo.t
6363
val parameters : t -> Module_name.t list Resolve.Memo.t
6464
val includes : t -> Command.Args.without_targets Command.Args.t Lib_mode.Cm_kind.Map.t
65+
val lib_index : t -> Lib_file_deps.Lib_index.t Resolve.Memo.t
6566
val preprocessing : t -> Pp_spec.t
6667
val opaque : t -> bool
6768
val js_of_ocaml : t -> Js_of_ocaml.In_context.t option Js_of_ocaml.Mode.Pair.t

src/dune_rules/dep_graph.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ type t =
77
}
88

99
let make ~dir ~per_module = { dir; per_module }
10+
let dir t = t.dir
1011

1112
let deps_of t (m : Module.t) =
1213
match Module_name.Unique.Map.find t.per_module (Module.obj_name m) with

src/dune_rules/dep_graph.mli

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ val make
99
-> per_module:Module.t list Action_builder.t Module_name.Unique.Map.t
1010
-> t
1111

12+
val dir : t -> Path.Build.t
1213
val deps_of : t -> Module.t -> Module.t list Action_builder.t
1314
val top_closed_implementations : t -> Module.t list -> Module.t list Action_builder.t
1415

src/dune_rules/lib_file_deps.ml

Lines changed: 116 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,23 +48,39 @@ let deps_of_lib (lib : Lib.t) ~groups =
4848
|> Dep.Set.of_list
4949
;;
5050

51+
let deps_of_module (lib : Lib.t) (m : Module.t) ~cm_kinds =
52+
let obj_dir = Lib.info lib |> Lib_info.obj_dir in
53+
List.filter_map cm_kinds ~f:(fun kind ->
54+
Obj_dir.Module.cm_file obj_dir m ~kind |> Option.map ~f:(fun p -> Dep.file p))
55+
|> Dep.Set.of_list
56+
;;
57+
5158
let deps libs ~groups = Dep.Set.union_map libs ~f:(deps_of_lib ~groups)
5259

53-
let deps_of_entries ~opaque ~(cm_kind : Lib_mode.Cm_kind.t) (libs : Lib.t list) =
54-
let groups =
60+
let deps_of_entries ~opaque ~(cm_kind : Lib_mode.Cm_kind.t) entries =
61+
let groups, cm_kinds =
5562
match cm_kind with
56-
| Ocaml Cmi | Ocaml Cmo -> [ Group.Ocaml Cmi ]
57-
| Melange Cmi -> [ Group.Melange Cmi ]
58-
| Melange Cmj -> [ Group.Melange Cmi; Melange Cmj ]
59-
| Ocaml Cmx -> [ Group.Ocaml Cmi; Ocaml Cmx ]
63+
| Ocaml Cmi | Ocaml Cmo -> [ Group.Ocaml Cmi ], [ Lib_mode.Cm_kind.Ocaml Cmi ]
64+
| Melange Cmi -> [ Group.Melange Cmi ], [ Melange Cmi ]
65+
| Melange Cmj -> [ Group.Melange Cmi; Melange Cmj ], [ Melange Cmi; Melange Cmj ]
66+
| Ocaml Cmx -> [ Group.Ocaml Cmi; Ocaml Cmx ], [ Ocaml Cmi; Ocaml Cmx ]
6067
in
61-
Dep.Set.union_map libs ~f:(fun lib ->
62-
let groups =
68+
List.map entries ~f:(fun ((lib : Lib.t), mod_opt) ->
69+
let is_opaque_local =
6370
match cm_kind with
64-
| Ocaml Cmx when opaque && Lib.is_local lib -> [ Group.Ocaml Cmi ]
65-
| _ -> groups
71+
| Ocaml Cmx -> opaque && Lib.is_local lib
72+
| _ -> false
6673
in
67-
deps_of_lib lib ~groups)
74+
match mod_opt with
75+
| Some m ->
76+
let cm_kinds =
77+
if is_opaque_local then [ Lib_mode.Cm_kind.Ocaml Cmi ] else cm_kinds
78+
in
79+
deps_of_module lib m ~cm_kinds
80+
| None ->
81+
let groups = if is_opaque_local then [ Group.Ocaml Cmi ] else groups in
82+
deps_of_lib lib ~groups)
83+
|> List.fold_left ~init:Dep.Set.empty ~f:Dep.Set.union
6884
;;
6985

7086
type path_specification =
@@ -98,3 +114,92 @@ let eval ~loc ~expander ~paths:path_spec (deps : Dep_conf.t list) =
98114
| Some _ -> raise_disallowed_external_path ~loc lib_name path));
99115
paths
100116
;;
117+
118+
module Lib_index = struct
119+
(** Each entry pairs a library with an optional Module.t: None means use
120+
glob deps (wrapped libs, external unwrapped), Some m means use
121+
per-file deps via Obj_dir.Module.cm_file (local unwrapped). *)
122+
type entry = Lib.t * Module.t option
123+
124+
type t =
125+
{ by_module_name : entry list Module_name.Map.t
126+
; unresolved : Lib.t list
127+
}
128+
129+
let module_names_of_lib sctx (lib : Lib.t) ~for_
130+
: (Module_name.t * Module.t option) list option Resolve.Memo.t
131+
=
132+
let open Resolve.Memo.O in
133+
let* main_module = Lib.main_module_name lib in
134+
match main_module with
135+
| Some name -> Resolve.Memo.return (Some [ name, None ])
136+
| None ->
137+
let info = Lib.info lib in
138+
(match Lib_info.entry_modules info ~for_ with
139+
| Lib_info.Source.External (Ok names) ->
140+
Resolve.Memo.return (Some (List.map names ~f:(fun n -> n, None)))
141+
| Lib_info.Source.External (Error _) -> Resolve.Memo.return None
142+
| Lib_info.Source.Local ->
143+
Resolve.Memo.lift_memo
144+
(let open Memo.O in
145+
let+ modules_opt = Dir_contents.modules_of_lib sctx lib ~for_ in
146+
match modules_opt with
147+
| None -> None
148+
| Some modules_with_vlib ->
149+
let modules = Modules.With_vlib.drop_vlib modules_with_vlib in
150+
let entry_modules = Modules.entry_modules modules in
151+
Some (List.map entry_modules ~f:(fun m -> Module.name m, Some m))))
152+
;;
153+
154+
let empty = { by_module_name = Module_name.Map.empty; unresolved = [] }
155+
156+
let create sctx (libs : Lib.t list) ~for_ : t Resolve.Memo.t =
157+
let open Resolve.Memo.O in
158+
let+ entries =
159+
Resolve.Memo.List.map libs ~f:(fun lib ->
160+
let+ names_opt = module_names_of_lib sctx lib ~for_ in
161+
lib, names_opt)
162+
in
163+
let by_module_name, unresolved =
164+
List.fold_left
165+
entries
166+
~init:(Module_name.Map.empty, [])
167+
~f:(fun (by_name, unresolved) (lib, names_opt) ->
168+
match names_opt with
169+
| None -> by_name, lib :: unresolved
170+
| Some named_modules ->
171+
let by_name =
172+
List.fold_left named_modules ~init:by_name ~f:(fun acc (name, mod_opt) ->
173+
Module_name.Map.update acc name ~f:(function
174+
| None -> Some [ lib, mod_opt ]
175+
| Some entries -> Some ((lib, mod_opt) :: entries)))
176+
in
177+
by_name, unresolved)
178+
in
179+
{ by_module_name; unresolved }
180+
;;
181+
182+
let filter_libs (index : t) ~(referenced_modules : Module_name.Set.t) : entry list =
183+
let from_refs =
184+
Module_name.Set.fold referenced_modules ~init:[] ~f:(fun name acc ->
185+
match Module_name.Map.find index.by_module_name name with
186+
| None -> acc
187+
| Some entries -> List.rev_append entries acc)
188+
in
189+
let unresolved = List.map index.unresolved ~f:(fun lib -> lib, None) in
190+
(* Entries are unique per (lib, module name). Two modules with the
191+
same name can't appear for the same library since module names are
192+
unique within a library's entry modules. *)
193+
let compare (a_lib, a_mod) (b_lib, b_mod) =
194+
match Lib.compare a_lib b_lib with
195+
| (Lt | Gt) as c -> c
196+
| Eq ->
197+
(match a_mod, b_mod with
198+
| None, None -> Eq
199+
| None, Some _ -> Lt
200+
| Some _, None -> Gt
201+
| Some a, Some b -> Module_name.compare (Module.name a) (Module.name b))
202+
in
203+
List.rev_append unresolved from_refs |> List.sort_uniq ~compare
204+
;;
205+
end

src/dune_rules/lib_file_deps.mli

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@ end
1515
with extension [files] of libraries [libs]. *)
1616
val deps : Lib.t list -> groups:Group.t list -> Dep.Set.t
1717

18-
(** Compute library file dependencies for all [libs] for the given [cm_kind].
19-
When [opaque] is true, local libraries only depend on .cmi (not .cmx). *)
20-
val deps_of_entries : opaque:bool -> cm_kind:Lib_mode.Cm_kind.t -> Lib.t list -> Dep.Set.t
21-
2218
type path_specification =
2319
| Allow_all
2420
| Disallow_external of Lib_name.t
@@ -31,3 +27,27 @@ val eval
3127
-> paths:path_specification
3228
-> Dep_conf.t list
3329
-> Path.Set.t Memo.t
30+
31+
module Lib_index : sig
32+
type entry = Lib.t * Module.t option
33+
type t
34+
35+
val empty : t
36+
37+
val create
38+
: Super_context.t
39+
-> Lib.t list
40+
-> for_:Compilation_mode.t
41+
-> t Resolve.Memo.t
42+
43+
val filter_libs : t -> referenced_modules:Module_name.Set.t -> entry list
44+
end
45+
46+
(** Compute library file dependencies for the given entries and cm_kind.
47+
Entries with [Some module_] use per-file deps; [None] uses glob deps.
48+
When [opaque] is true, local libraries only depend on .cmi (not .cmx). *)
49+
val deps_of_entries
50+
: opaque:bool
51+
-> cm_kind:Lib_mode.Cm_kind.t
52+
-> Lib_index.entry list
53+
-> Dep.Set.t

src/dune_rules/module_compilation.ml

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -286,28 +286,86 @@ let build_cm
286286
| Some All | None -> Hidden_targets [ obj ])
287287
in
288288
let opaque = Compilation_context.opaque cctx in
289-
(* Library file dependencies, added per-module. Alias and Wrapped_compat
290-
modules are compiled with Includes.empty and need no library file deps.
291-
All other modules depend on all libraries in the stanza's requires. *)
289+
(* Library file dependencies - filtered per-module based on ocamldep output.
290+
Issue #4572: Finer dependency analysis between libraries. *)
292291
let lib_cm_deps =
292+
let requires_compile = Compilation_context.requires_compile cctx in
293+
let requires_hidden = Compilation_context.requires_hidden cctx in
294+
let for_ = Compilation_context.for_ cctx in
295+
let stanza_modules = Compilation_context.modules cctx in
296+
(* Alias modules are compiled with Includes.empty (no -I flags for
297+
libraries) by for_alias_module, and Wrapped_compat modules are
298+
compiled with Includes.empty by for_wrapped_compat. Neither needs
299+
library file deps — they can't access library .cmi files regardless.
300+
Skipping them also avoids a cascade where wrapper .cmi changes
301+
would trigger recompilation of all stanza modules.
302+
Other special modules (Root, etc.) and synthetic contexts (dir
303+
mismatch) fall back to all-library glob deps. *)
293304
let skip_lib_deps =
294305
match Module.kind m with
295306
| Alias _ | Wrapped_compat -> true
296307
| _ -> false
297308
in
309+
let can_filter =
310+
let dep_graph = Ml_kind.Dict.get (Compilation_context.dep_graphs cctx) ml_kind in
311+
let dep_graph_dir = Dep_graph.dir dep_graph in
312+
(not skip_lib_deps)
313+
&& Path.Build.equal dep_graph_dir (Obj_dir.dir obj_dir)
314+
&&
315+
match Module.kind m with
316+
| Root | Wrapped_compat | Impl_vmodule | Virtual | Parameter -> false
317+
| _ -> Module.has m ~ml_kind
318+
in
298319
Action_builder.dyn_deps
299320
(if skip_lib_deps
300321
then Action_builder.return ((), Dep.Set.empty)
301322
else
302323
let open Action_builder.O in
303-
let+ all_libs =
324+
let* all_libs, lib_index_opt =
304325
Resolve.Memo.read
305326
(let open Resolve.Memo.O in
306-
let+ direct_libs = Compilation_context.requires_compile cctx
307-
and+ hidden_libs = Compilation_context.requires_hidden cctx in
308-
direct_libs @ hidden_libs)
327+
let* direct_libs = requires_compile
328+
and* hidden_libs = requires_hidden in
329+
let all_libs = direct_libs @ hidden_libs in
330+
if can_filter && not (List.is_empty all_libs)
331+
then
332+
let+ lib_index = Compilation_context.lib_index cctx in
333+
all_libs, Some lib_index
334+
else Resolve.Memo.return (all_libs, None))
309335
in
310-
(), Lib_file_deps.deps_of_entries ~opaque ~cm_kind all_libs)
336+
let all_entries = List.map all_libs ~f:(fun lib -> lib, None) in
337+
match lib_index_opt with
338+
| None ->
339+
Action_builder.return
340+
((), Lib_file_deps.deps_of_entries ~opaque ~cm_kind all_entries)
341+
| Some lib_index ->
342+
let+ referenced_modules =
343+
Ocamldep.read_immediate_deps_raw_of ~obj_dir ~ml_kind ~for_ m
344+
in
345+
let references_root_module =
346+
Module_name.Set.exists referenced_modules ~f:(fun name ->
347+
match Modules.With_vlib.find stanza_modules name with
348+
| Some m -> Module.kind m = Root
349+
| None -> false)
350+
in
351+
let external_modules =
352+
Module_name.Set.filter referenced_modules ~f:(fun name ->
353+
Option.is_none (Modules.With_vlib.find stanza_modules name))
354+
in
355+
let used_entries =
356+
if references_root_module
357+
then all_entries
358+
else (
359+
let used =
360+
Lib_file_deps.Lib_index.filter_libs
361+
lib_index
362+
~referenced_modules:external_modules
363+
in
364+
if List.is_empty used && not (Module_name.Set.is_empty external_modules)
365+
then all_entries
366+
else used)
367+
in
368+
(), Lib_file_deps.deps_of_entries ~opaque ~cm_kind used_entries)
311369
in
312370
let other_cm_files =
313371
let dep_graph = Ml_kind.Dict.get (Compilation_context.dep_graphs cctx) ml_kind in

0 commit comments

Comments
 (0)