diff --git a/src/dune_rules/compilation_context.ml b/src/dune_rules/compilation_context.ml index b629d7874d3..91809daf990 100644 --- a/src/dune_rules/compilation_context.ml +++ b/src/dune_rules/compilation_context.ml @@ -4,55 +4,32 @@ open Memo.O module Includes = struct type t = Command.Args.without_targets Command.Args.t Lib_mode.Cm_kind.Map.t - let make ~project ~opaque ~direct_requires ~hidden_requires lib_config + (* Library file dependencies (Hidden_deps) are added per-module in + module_compilation.ml based on ocamldep output. Each module depends + only on libraries it actually references. + See issue #4572: Finer dependency analysis between libraries. *) + let make ~project ~direct_requires ~hidden_requires lib_config : _ Lib_mode.Cm_kind.Map.t = - (* TODO: some of the requires can filtered out using [ocamldep] info *) let open Resolve.Memo.O in let iflags direct_libs hidden_libs mode = Lib_flags.L.include_flags ~project ~direct_libs ~hidden_libs mode lib_config in - let make_includes_args ~mode groups = + let make_includes_args ~mode = (let+ direct_libs = direct_requires and+ hidden_libs = hidden_requires in - Command.Args.S - [ iflags direct_libs hidden_libs mode - ; Hidden_deps (Lib_file_deps.deps (direct_libs @ hidden_libs) ~groups) - ]) + iflags direct_libs hidden_libs mode) |> Resolve.Memo.args |> Command.Args.memo in { ocaml = - (let cmi_includes = make_includes_args ~mode:(Ocaml Byte) [ Ocaml Cmi ] in + (let cmi_includes = make_includes_args ~mode:(Ocaml Byte) in { cmi = cmi_includes ; cmo = cmi_includes - ; cmx = - (let+ direct_libs = direct_requires - and+ hidden_libs = hidden_requires in - Command.Args.S - [ iflags direct_libs hidden_libs (Ocaml Native) - ; Hidden_deps - (let libs = direct_libs @ hidden_libs in - if opaque - then - List.map libs ~f:(fun lib -> - ( lib - , if Lib.is_local lib - then [ Lib_file_deps.Group.Ocaml Cmi ] - else [ Ocaml Cmi; Ocaml Cmx ] )) - |> Lib_file_deps.deps_with_exts - else - Lib_file_deps.deps - libs - ~groups:[ Lib_file_deps.Group.Ocaml Cmi; Ocaml Cmx ]) - ]) - |> Resolve.Memo.args - |> Command.Args.memo + ; cmx = make_includes_args ~mode:(Ocaml Native) }) ; melange = - { cmi = make_includes_args ~mode:Melange [ Melange Cmi ] - ; cmj = make_includes_args ~mode:Melange [ Melange Cmi; Melange Cmj ] - } + { cmi = make_includes_args ~mode:Melange; cmj = make_includes_args ~mode:Melange } } ;; @@ -91,6 +68,7 @@ type t = ; parameters : Module_name.t list Resolve.Memo.t ; instances : Parameterised_instances.t Resolve.Memo.t option ; includes : Includes.t + ; lib_index : Lib_file_deps.Lib_index.t Resolve.Memo.t ; preprocessing : Pp_spec.t ; opaque : bool ; js_of_ocaml : Js_of_ocaml.In_context.t option Js_of_ocaml.Mode.Pair.t @@ -118,6 +96,7 @@ let requires_hidden t = t.requires_hidden let requires_link t = Memo.Lazy.force t.requires_link let parameters t = t.parameters let includes t = t.includes +let lib_index t = t.lib_index let preprocessing t = t.preprocessing let opaque t = t.opaque let js_of_ocaml t = t.js_of_ocaml @@ -240,8 +219,12 @@ let create ; requires_link ; implements ; parameters - ; includes = - Includes.make ~project ~opaque ~direct_requires ~hidden_requires ocaml.lib_config + ; includes = Includes.make ~project ~direct_requires ~hidden_requires ocaml.lib_config + ; lib_index = + (let open Resolve.Memo.O in + let* direct_libs = direct_requires + and* hidden_libs = hidden_requires in + Lib_file_deps.Lib_index.create super_context (direct_libs @ hidden_libs) ~for_) ; preprocessing ; opaque ; js_of_ocaml @@ -333,7 +316,6 @@ let for_module_generated_at_link_time cctx ~requires ~module_ = let direct_requires = requires in Includes.make ~project:(Scope.project cctx.scope) - ~opaque ~direct_requires ~hidden_requires cctx.ocaml.lib_config @@ -343,6 +325,7 @@ let for_module_generated_at_link_time cctx ~requires ~module_ = ; flags = Ocaml_flags.empty ; requires_link = Memo.lazy_ (fun () -> requires) ; requires_compile = requires + ; lib_index = Resolve.Memo.return Lib_file_deps.Lib_index.empty ; includes ; modules } diff --git a/src/dune_rules/compilation_context.mli b/src/dune_rules/compilation_context.mli index f5ac4cae7ca..2cc6cebeb50 100644 --- a/src/dune_rules/compilation_context.mli +++ b/src/dune_rules/compilation_context.mli @@ -62,6 +62,7 @@ val requires_hidden : t -> Lib.t list Resolve.Memo.t val requires_compile : t -> Lib.t list Resolve.Memo.t val parameters : t -> Module_name.t list Resolve.Memo.t val includes : t -> Command.Args.without_targets Command.Args.t Lib_mode.Cm_kind.Map.t +val lib_index : t -> Lib_file_deps.Lib_index.t Resolve.Memo.t val preprocessing : t -> Pp_spec.t val opaque : t -> bool val js_of_ocaml : t -> Js_of_ocaml.In_context.t option Js_of_ocaml.Mode.Pair.t diff --git a/src/dune_rules/dep_graph.ml b/src/dune_rules/dep_graph.ml index 8ff3f084366..891c7249fe0 100644 --- a/src/dune_rules/dep_graph.ml +++ b/src/dune_rules/dep_graph.ml @@ -7,6 +7,7 @@ type t = } let make ~dir ~per_module = { dir; per_module } +let dir t = t.dir let deps_of t (m : Module.t) = match Module_name.Unique.Map.find t.per_module (Module.obj_name m) with diff --git a/src/dune_rules/dep_graph.mli b/src/dune_rules/dep_graph.mli index d5f663b222f..4a9a5af7a83 100644 --- a/src/dune_rules/dep_graph.mli +++ b/src/dune_rules/dep_graph.mli @@ -9,6 +9,7 @@ val make -> per_module:Module.t list Action_builder.t Module_name.Unique.Map.t -> t +val dir : t -> Path.Build.t val deps_of : t -> Module.t -> Module.t list Action_builder.t val top_closed_implementations : t -> Module.t list -> Module.t list Action_builder.t diff --git a/src/dune_rules/dep_rules.ml b/src/dune_rules/dep_rules.ml index 40cbae94f97..67ead4f30e9 100644 --- a/src/dune_rules/dep_rules.ml +++ b/src/dune_rules/dep_rules.ml @@ -131,9 +131,6 @@ let deps_of_vlib_module ~obj_dir ~vimpl ~dir ~sctx ~ml_kind ~for_ sourced_module Ocamldep.read_deps_of ~obj_dir:vlib_obj_dir ~modules ~ml_kind ~for_ m ;; -(** Tests whether a set of modules is a singleton *) -let has_single_file modules = Option.is_some @@ Modules.With_vlib.as_singleton modules - let rec deps_of ~obj_dir ~modules @@ -153,7 +150,7 @@ let rec deps_of | Root | Alias _ -> true | _ -> false) in - if is_alias_or_root || has_single_file modules + if is_alias_or_root then Memo.return (Action_builder.return []) else ( let skip_if_source_absent f sourced_module = @@ -187,18 +184,15 @@ let read_deps_of_module ~modules ~obj_dir dep ~for_ = | Root | Alias _ -> Action_builder.return [] | Wrapped_compat -> wrapped_compat_deps modules unit |> Action_builder.return | _ -> - if has_single_file modules - then Action_builder.return [] - else ( - match dep with - | Immediate (unit, ml_kind) -> - Ocamldep.read_immediate_deps_of ~obj_dir ~modules ~ml_kind ~for_ unit - | Transitive (unit, ml_kind) -> - let open Action_builder.O in - let+ deps = Ocamldep.read_deps_of ~obj_dir ~modules ~ml_kind ~for_ unit in - (match Modules.With_vlib.alias_for modules unit with - | [] -> deps - | aliases -> aliases @ deps)) + (match dep with + | Immediate (unit, ml_kind) -> + Ocamldep.read_immediate_deps_of ~obj_dir ~modules ~ml_kind ~for_ unit + | Transitive (unit, ml_kind) -> + let open Action_builder.O in + let+ deps = Ocamldep.read_deps_of ~obj_dir ~modules ~ml_kind ~for_ unit in + (match Modules.With_vlib.alias_for modules unit with + | [] -> deps + | aliases -> aliases @ deps)) ;; let read_immediate_deps_of ~obj_dir ~modules ~ml_kind ~for_ m = @@ -223,15 +217,12 @@ let for_module ~obj_dir ~modules ~sandbox ~impl ~dir ~sctx ~for_ module_ = ;; let rules ~obj_dir ~modules ~sandbox ~impl ~sctx ~dir ~for_ = - match Modules.With_vlib.as_singleton modules with - | Some m -> Memo.return (Dep_graph.Ml_kind.dummy m) - | None -> - dict_of_func_concurrently (fun ~ml_kind -> - let+ per_module = - Modules.With_vlib.obj_map modules - |> Parallel_map.parallel_map ~f:(fun _obj_name m -> - deps_of ~obj_dir ~modules ~sandbox ~impl ~sctx ~dir ~ml_kind ~for_ m) - in - Dep_graph.make ~dir ~per_module) - |> Memo.map ~f:(Dep_graph.Ml_kind.for_module_compilation ~modules) + dict_of_func_concurrently (fun ~ml_kind -> + let+ per_module = + Modules.With_vlib.obj_map modules + |> Parallel_map.parallel_map ~f:(fun _obj_name m -> + deps_of ~obj_dir ~modules ~sandbox ~impl ~sctx ~dir ~ml_kind ~for_ m) + in + Dep_graph.make ~dir ~per_module) + |> Memo.map ~f:(Dep_graph.Ml_kind.for_module_compilation ~modules) ;; diff --git a/src/dune_rules/lib_file_deps.ml b/src/dune_rules/lib_file_deps.ml index 0aa3440da59..4c959d3ca1a 100644 --- a/src/dune_rules/lib_file_deps.ml +++ b/src/dune_rules/lib_file_deps.ml @@ -48,9 +48,41 @@ let deps_of_lib (lib : Lib.t) ~groups = |> Dep.Set.of_list ;; -let deps_with_exts = Dep.Set.union_map ~f:(fun (lib, groups) -> deps_of_lib lib ~groups) +let deps_of_module (lib : Lib.t) (m : Module.t) ~cm_kinds = + let obj_dir = Lib.info lib |> Lib_info.obj_dir in + List.filter_map cm_kinds ~f:(fun kind -> + Obj_dir.Module.cm_public_file obj_dir m ~kind |> Option.map ~f:(fun p -> Dep.file p)) + |> Dep.Set.of_list +;; + let deps libs ~groups = Dep.Set.union_map libs ~f:(deps_of_lib ~groups) +let deps_of_entries ~opaque ~(cm_kind : Lib_mode.Cm_kind.t) entries = + let groups, cm_kinds = + match cm_kind with + | Ocaml Cmi | Ocaml Cmo -> [ Group.Ocaml Cmi ], [ Lib_mode.Cm_kind.Ocaml Cmi ] + | Melange Cmi -> [ Group.Melange Cmi ], [ Melange Cmi ] + | Melange Cmj -> [ Group.Melange Cmi; Melange Cmj ], [ Melange Cmi; Melange Cmj ] + | Ocaml Cmx -> [ Group.Ocaml Cmi; Ocaml Cmx ], [ Ocaml Cmi; Ocaml Cmx ] + in + List.map entries ~f:(fun ((lib : Lib.t), mod_opt) -> + let is_opaque_local = + match cm_kind with + | Ocaml Cmx -> opaque && Lib.is_local lib + | _ -> false + in + match mod_opt with + | Some m -> + let cm_kinds = + if is_opaque_local then [ Lib_mode.Cm_kind.Ocaml Cmi ] else cm_kinds + in + deps_of_module lib m ~cm_kinds + | None -> + let groups = if is_opaque_local then [ Group.Ocaml Cmi ] else groups in + deps_of_lib lib ~groups) + |> List.fold_left ~init:Dep.Set.empty ~f:Dep.Set.union +;; + type path_specification = | Allow_all | Disallow_external of Lib_name.t @@ -82,3 +114,119 @@ let eval ~loc ~expander ~paths:path_spec (deps : Dep_conf.t list) = | Some _ -> raise_disallowed_external_path ~loc lib_name path)); paths ;; + +module Lib_index = struct + (** Each entry pairs a library with an optional Module.t: None means use + glob deps (wrapped libs, external unwrapped), Some m means use + per-file deps via Obj_dir.Module.cm_file (local unwrapped). *) + type entry = Lib.t * Module.t option + + type t = + { by_module_name : entry list Module_name.Map.t + ; unresolved : Lib.t list + } + + let module_names_of_lib sctx (lib : Lib.t) ~for_ + : (Module_name.t * Module.t option) list option Resolve.Memo.t + = + let open Resolve.Memo.O in + let* main_module = Lib.main_module_name lib in + match main_module with + | Some name -> + (* Wrapped library: index the wrapper name plus all inner module + names so that references via -open flags are resolved. All map + to None (glob deps) since the wrapper exposes the whole lib. *) + let info = Lib.info lib in + (match Lib_info.entry_modules info ~for_ with + | Lib_info.Source.External _ -> + (match Lib_info.modules info ~for_ with + | Lib_info.Source.External (Some modules_with_vlib) -> + let modules = Modules.With_vlib.drop_vlib modules_with_vlib in + let inner_names = + Modules.fold modules ~init:[] ~f:(fun m acc -> (Module.name m, None) :: acc) + in + Resolve.Memo.return (Some ((name, None) :: inner_names)) + | _ -> Resolve.Memo.return (Some [ name, None ])) + | Lib_info.Source.Local -> + Resolve.Memo.lift_memo + (let open Memo.O in + let+ modules_opt = Dir_contents.modules_of_lib sctx lib ~for_ in + match modules_opt with + | None -> Some [ name, None ] + | Some modules_with_vlib -> + let modules = Modules.With_vlib.drop_vlib modules_with_vlib in + let inner_names = + Modules.fold modules ~init:[] ~f:(fun m acc -> + (Module.name m, None) :: acc) + in + Some ((name, None) :: inner_names))) + | None -> + let info = Lib.info lib in + (match Lib_info.entry_modules info ~for_ with + | Lib_info.Source.External (Ok names) -> + Resolve.Memo.return (Some (List.map names ~f:(fun n -> n, None))) + | Lib_info.Source.External (Error _) -> Resolve.Memo.return None + | Lib_info.Source.Local -> + Resolve.Memo.lift_memo + (let open Memo.O in + let+ modules_opt = Dir_contents.modules_of_lib sctx lib ~for_ in + match modules_opt with + | None -> None + | Some modules_with_vlib -> + let modules = Modules.With_vlib.drop_vlib modules_with_vlib in + let entry_modules = Modules.entry_modules modules in + Some (List.map entry_modules ~f:(fun m -> Module.name m, Some m)))) + ;; + + let empty = { by_module_name = Module_name.Map.empty; unresolved = [] } + + let create sctx (libs : Lib.t list) ~for_ : t Resolve.Memo.t = + let open Resolve.Memo.O in + let+ entries = + Resolve.Memo.List.map libs ~f:(fun lib -> + let+ names_opt = module_names_of_lib sctx lib ~for_ in + lib, names_opt) + in + let by_module_name, unresolved = + List.fold_left + entries + ~init:(Module_name.Map.empty, []) + ~f:(fun (by_name, unresolved) (lib, names_opt) -> + match names_opt with + | None -> by_name, lib :: unresolved + | Some named_modules -> + let by_name = + List.fold_left named_modules ~init:by_name ~f:(fun acc (name, mod_opt) -> + Module_name.Map.update acc name ~f:(function + | None -> Some [ lib, mod_opt ] + | Some entries -> Some ((lib, mod_opt) :: entries))) + in + by_name, unresolved) + in + { by_module_name; unresolved } + ;; + + let filter_libs (index : t) ~(referenced_modules : Module_name.Set.t) : entry list = + let from_refs = + Module_name.Set.fold referenced_modules ~init:[] ~f:(fun name acc -> + match Module_name.Map.find index.by_module_name name with + | None -> acc + | Some entries -> List.rev_append entries acc) + in + let unresolved = List.map index.unresolved ~f:(fun lib -> lib, None) in + (* Entries are unique per (lib, module name). Two modules with the + same name can't appear for the same library since module names are + unique within a library's entry modules. *) + let compare (a_lib, a_mod) (b_lib, b_mod) = + match Lib.compare a_lib b_lib with + | (Lt | Gt) as c -> c + | Eq -> + (match a_mod, b_mod with + | None, None -> Eq + | None, Some _ -> Lt + | Some _, None -> Gt + | Some a, Some b -> Module_name.compare (Module.name a) (Module.name b)) + in + List.rev_append unresolved from_refs |> List.sort_uniq ~compare + ;; +end diff --git a/src/dune_rules/lib_file_deps.mli b/src/dune_rules/lib_file_deps.mli index 75a3453e64b..f84a3bc5da4 100644 --- a/src/dune_rules/lib_file_deps.mli +++ b/src/dune_rules/lib_file_deps.mli @@ -15,8 +15,6 @@ end with extension [files] of libraries [libs]. *) val deps : Lib.t list -> groups:Group.t list -> Dep.Set.t -val deps_with_exts : (Lib.t * Group.t list) list -> Dep.Set.t - type path_specification = | Allow_all | Disallow_external of Lib_name.t @@ -29,3 +27,27 @@ val eval -> paths:path_specification -> Dep_conf.t list -> Path.Set.t Memo.t + +module Lib_index : sig + type entry = Lib.t * Module.t option + type t + + val empty : t + + val create + : Super_context.t + -> Lib.t list + -> for_:Compilation_mode.t + -> t Resolve.Memo.t + + val filter_libs : t -> referenced_modules:Module_name.Set.t -> entry list +end + +(** Compute library file dependencies for the given entries and cm_kind. + Entries with [Some module_] use per-file deps; [None] uses glob deps. + When [opaque] is true, local libraries only depend on .cmi (not .cmx). *) +val deps_of_entries + : opaque:bool + -> cm_kind:Lib_mode.Cm_kind.t + -> Lib_index.entry list + -> Dep.Set.t diff --git a/src/dune_rules/module_compilation.ml b/src/dune_rules/module_compilation.ml index 75bcea17c87..4294f906b6d 100644 --- a/src/dune_rules/module_compilation.ml +++ b/src/dune_rules/module_compilation.ml @@ -286,6 +286,120 @@ let build_cm | Some All | None -> Hidden_targets [ obj ]) in let opaque = Compilation_context.opaque cctx in + (* Library file dependencies - filtered per-module based on ocamldep output. + Issue #4572: Finer dependency analysis between libraries. *) + let lib_cm_deps = + let requires_compile = Compilation_context.requires_compile cctx in + let requires_hidden = Compilation_context.requires_hidden cctx in + let for_ = Compilation_context.for_ cctx in + let stanza_modules = Compilation_context.modules cctx in + (* Alias modules are compiled with Includes.empty (no -I flags for + libraries) by for_alias_module, and Wrapped_compat modules are + compiled with Includes.empty by for_wrapped_compat. Neither needs + library file deps — they can't access library .cmi files regardless. + Skipping them also avoids a cascade where wrapper .cmi changes + would trigger recompilation of all stanza modules. + Other special modules (Root, etc.) and synthetic contexts (dir + mismatch) fall back to all-library glob deps. *) + let skip_lib_deps = + match Module.kind m with + | Alias _ -> not (Modules.With_vlib.is_stdlib_alias stanza_modules m) + | Wrapped_compat -> true + | _ -> false + in + let can_filter = + let dep_graph = Ml_kind.Dict.get (Compilation_context.dep_graphs cctx) ml_kind in + let dep_graph_dir = Dep_graph.dir dep_graph in + (not skip_lib_deps) + && (match for_ with + | Compilation_mode.Melange -> false + | Compilation_mode.Ocaml -> true) + && Path.Build.equal dep_graph_dir (Obj_dir.dir obj_dir) + && + match Module.kind m with + | Root | Wrapped_compat | Impl_vmodule | Virtual | Parameter -> false + | _ -> Module.has m ~ml_kind + in + Action_builder.dyn_deps + (if skip_lib_deps + then Action_builder.return ((), Dep.Set.empty) + else + let open Action_builder.O in + let* all_libs, lib_index_opt = + Resolve.Memo.read + (let open Resolve.Memo.O in + let* direct_libs = requires_compile + and* hidden_libs = requires_hidden in + let all_libs = direct_libs @ hidden_libs in + let has_vlib_impl = + List.exists all_libs ~f:(fun lib -> Option.is_some (Lib.implements lib)) + in + if can_filter && (not has_vlib_impl) && not (List.is_empty all_libs) + then + let+ lib_index = Compilation_context.lib_index cctx in + all_libs, Some lib_index + else Resolve.Memo.return (all_libs, None)) + in + let all_entries = List.map all_libs ~f:(fun lib -> lib, None) in + match lib_index_opt with + | None -> + Action_builder.return + ((), Lib_file_deps.deps_of_entries ~opaque ~cm_kind all_entries) + | Some lib_index -> + let dep_graph = + Ml_kind.Dict.get (Compilation_context.dep_graphs cctx) ml_kind + in + let own_modules = Modules.With_vlib.drop_vlib stanza_modules in + let own_module_names = + Modules.obj_map own_modules + |> Module_name.Unique.Map.to_list + |> List.map ~f:(fun (_, sm) -> + Module.name (Modules.Sourced_module.to_module sm)) + |> Module_name.Set.of_list + in + let+ referenced_modules = + let open Action_builder.O in + let* own_refs = + Ocamldep.read_immediate_deps_raw_of ~obj_dir ~ml_kind ~for_ m + in + let* transitive_deps = + Dep_graph.top_closed_implementations dep_graph [ m ] + in + let* from_ocamldep = + let+ dep_refs = + Action_builder.List.map transitive_deps ~f:(fun dep_m -> + if Module_name.Set.mem own_module_names (Module.name dep_m) + then Ocamldep.read_immediate_deps_raw_of ~obj_dir ~ml_kind ~for_ dep_m + else Action_builder.return Module_name.Set.empty) + in + List.fold_left dep_refs ~init:own_refs ~f:Module_name.Set.union + in + (* Also include modules from user -open flags, which + ocamldep doesn't receive and therefore can't report. *) + let+ flags = Ocaml_flags.get (Compilation_context.flags cctx) mode in + let from_open_flags = Ocaml_flags.extract_open_module_names flags in + Module_name.Set.union from_ocamldep from_open_flags + in + let references_root_module = + Module_name.Set.exists referenced_modules ~f:(fun name -> + match Modules.With_vlib.find stanza_modules name with + | Some m -> Module.kind m = Root + | None -> false) + in + let external_modules = + Module_name.Set.filter referenced_modules ~f:(fun name -> + Option.is_none (Modules.With_vlib.find stanza_modules name)) + in + let used_entries = + if references_root_module + then all_entries + else + Lib_file_deps.Lib_index.filter_libs + lib_index + ~referenced_modules:external_modules + in + (), Lib_file_deps.deps_of_entries ~opaque ~cm_kind used_entries) + in let other_cm_files = let dep_graph = Ml_kind.Dict.get (Compilation_context.dep_graphs cctx) ml_kind in let module_deps = Dep_graph.deps_of dep_graph m in @@ -404,6 +518,7 @@ let build_cm ?loc:(Compilation_context.loc cctx) (let open Action_builder.With_targets.O in Action_builder.with_no_targets other_cm_files + >>> Action_builder.with_no_targets lib_cm_deps >>> Command.run ~dir:(Path.build (Context.build_dir ctx)) compiler @@ -512,6 +627,7 @@ let ocamlc_i ~deps cctx (m : Module.t) ~output = let ctx = Super_context.context sctx in let src = Option.value_exn (Module.file m ~ml_kind:Impl) in let sandbox = Compilation_context.sandbox cctx in + let opaque = Compilation_context.opaque cctx in let cm_deps = Action_builder.dyn_paths_unit (let open Action_builder.O in @@ -519,6 +635,17 @@ let ocamlc_i ~deps cctx (m : Module.t) ~output = List.concat_map deps ~f:(fun m -> [ Path.build (Obj_dir.Module.cm_file_exn obj_dir m ~kind:(Ocaml Cmi)) ])) in + let lib_cm_deps : _ Command.Args.t = + (let open Resolve.Memo.O in + let+ direct_libs = Compilation_context.requires_compile cctx + and+ hidden_libs = Compilation_context.requires_hidden cctx in + Command.Args.Hidden_deps + (Lib_file_deps.deps_of_entries + ~opaque + ~cm_kind:(Ocaml Cmo) + (List.map (direct_libs @ hidden_libs) ~f:(fun lib -> lib, None)))) + |> Resolve.Memo.args + in let ocaml_flags = Ocaml_flags.get (Compilation_context.flags cctx) (Ocaml Byte) in let modules = Compilation_context.modules cctx in let ocaml = Compilation_context.ocaml cctx in @@ -540,6 +667,7 @@ let ocamlc_i ~deps cctx (m : Module.t) ~output = (Lib_mode.Cm_kind.Map.get (Compilation_context.includes cctx) (Ocaml Cmo)) + ; lib_cm_deps ; opens modules m ; A "-short-paths" ; A "-i" diff --git a/src/dune_rules/ocaml_flags.ml b/src/dune_rules/ocaml_flags.ml index b8c818790c8..61cc7d13f71 100644 --- a/src/dune_rules/ocaml_flags.ml +++ b/src/dune_rules/ocaml_flags.ml @@ -197,3 +197,13 @@ let allow_only_melange t = let open_flags modules = List.concat_map modules ~f:(fun name -> [ "-open"; Module_name.to_string name ]) ;; + +let extract_open_module_names flags = + let rec loop acc = function + | "-open" :: name :: rest -> + loop (Module_name.Set.add acc (Module_name.of_checked_string name)) rest + | _ :: rest -> loop acc rest + | [] -> acc + in + loop Module_name.Set.empty flags +;; diff --git a/src/dune_rules/ocaml_flags.mli b/src/dune_rules/ocaml_flags.mli index c83d8c78cfe..f612fe20295 100644 --- a/src/dune_rules/ocaml_flags.mli +++ b/src/dune_rules/ocaml_flags.mli @@ -30,3 +30,4 @@ val with_vendored_alerts : t -> t val dump : t -> Dune_lang.t list Action_builder.t val with_vendored_flags : t -> ocaml_version:Version.t -> t val open_flags : Module_name.t list -> string list +val extract_open_module_names : string list -> Module_name.Set.t diff --git a/src/dune_rules/ocamldep.ml b/src/dune_rules/ocamldep.ml index 22fb8f5a89d..55b48c5136d 100644 --- a/src/dune_rules/ocamldep.ml +++ b/src/dune_rules/ocamldep.ml @@ -186,16 +186,33 @@ let read_deps_of ~obj_dir ~modules ~ml_kind ~for_ unit = |> Action_builder.memoize (Path.Build.to_string all_deps_file) ;; +(** Read raw ocamldep words for the immediate deps of [unit]. Returns [None] + if [unit] has no source for [ml_kind] or no dep file exists. *) +let immediate_deps_words_of ~obj_dir ~ml_kind ~for_ unit = + let open Option.O in + let* source = Module.source ~ml_kind unit in + let+ ocamldep_output = Obj_dir.Module.dep obj_dir ~for_ (Immediate (unit, ml_kind)) in + ( Action_builder.lines_of (Path.build ocamldep_output) + |> Action_builder.map ~f:(parse_deps_exn ~file:(Module.File.path source)) + , ocamldep_output ) +;; + let read_immediate_deps_of ~obj_dir ~modules ~ml_kind ~for_ unit = - match Module.source ~ml_kind unit with + match immediate_deps_words_of ~obj_dir ~ml_kind ~for_ unit with | None -> Action_builder.return [] - | Some source -> - let ocamldep_output = - Obj_dir.Module.dep obj_dir ~for_ (Immediate (unit, ml_kind)) |> Option.value_exn - in - Action_builder.lines_of (Path.build ocamldep_output) - |> Action_builder.map ~f:(fun lines -> - parse_deps_exn ~file:(Module.File.path source) lines - |> parse_module_names ~dir:(Obj_dir.dir obj_dir) ~unit ~modules) + | Some (words, ocamldep_output) -> + words + |> Action_builder.map + ~f:(parse_module_names ~dir:(Obj_dir.dir obj_dir) ~unit ~modules) |> Action_builder.memoize (Path.Build.to_string ocamldep_output) ;; + +let read_immediate_deps_raw_of ~obj_dir ~ml_kind ~for_ unit = + match immediate_deps_words_of ~obj_dir ~ml_kind ~for_ unit with + | None -> Action_builder.return Module_name.Set.empty + | Some (words, ocamldep_output) -> + words + |> Action_builder.map ~f:(fun ws -> + List.map ws ~f:Module_name.of_checked_string |> Module_name.Set.of_list) + |> Action_builder.memoize (Path.Build.to_string ocamldep_output ^ "-raw") +;; diff --git a/src/dune_rules/ocamldep.mli b/src/dune_rules/ocamldep.mli index d455459262e..83e7f41a81a 100644 --- a/src/dune_rules/ocamldep.mli +++ b/src/dune_rules/ocamldep.mli @@ -32,3 +32,15 @@ val read_immediate_deps_of -> for_:Compilation_mode.t -> Module.t -> Module.t list Action_builder.t + +(** [read_immediate_deps_raw_of ~obj_dir ~ml_kind ~for_ unit] returns the raw + module names (unresolved) from ocamldep output for the file with kind + [ml_kind] of the module [unit]. This includes ALL module references, both + intra-stanza and external library modules. If there is no such file with + kind [ml_kind], an empty set is returned. *) +val read_immediate_deps_raw_of + : obj_dir:Path.Build.t Obj_dir.t + -> ml_kind:Ml_kind.t + -> for_:Compilation_mode.t + -> Module.t + -> Module_name.Set.t Action_builder.t diff --git a/test/blackbox-tests/test-cases/per-module-lib-deps/basic-wrapped.t b/test/blackbox-tests/test-cases/per-module-lib-deps/basic-wrapped.t index eb203482193..0138363d480 100644 --- a/test/blackbox-tests/test-cases/per-module-lib-deps/basic-wrapped.t +++ b/test/blackbox-tests/test-cases/per-module-lib-deps/basic-wrapped.t @@ -1,4 +1,4 @@ -Baseline: library dependency recompilation for a basic wrapped library. +Per-module filtering: library dependency recompilation for a basic wrapped library. When a wrapped library's interface changes, Dune currently recompiles ALL modules in stanzas that depend on the library, even modules that don't @@ -58,8 +58,8 @@ Change mylib's interface: > let new_function () = "hello" > EOF -No_use_lib is recompiled even though it doesn't reference Mylib: +No_use_lib is no longer recompiled because it doesn't reference Mylib: $ dune build ./main.exe $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("No_use_lib"))] | length' - 2 + 0 diff --git a/test/blackbox-tests/test-cases/per-module-lib-deps/lib-deps-preserved.t b/test/blackbox-tests/test-cases/per-module-lib-deps/lib-deps-preserved.t new file mode 100644 index 00000000000..71e4dcf6fa9 --- /dev/null +++ b/test/blackbox-tests/test-cases/per-module-lib-deps/lib-deps-preserved.t @@ -0,0 +1,42 @@ +Verify that library file deps are declared for module compilation rules. + +Every non-alias module should declare glob deps on its library +dependencies' .cmi files. + + $ cat > dune-project < (lang dune 3.0) + > EOF + + $ mkdir lib + $ cat > lib/dune < (library (name mylib)) + > EOF + $ cat > lib/mylib.ml < let value = 42 + > EOF + $ cat > lib/mylib.mli < val value : int + > EOF + + $ cat > dune < (executable (name main) (libraries mylib)) + > EOF + $ cat > uses_lib.ml < let get () = Mylib.value + > EOF + $ cat > uses_lib.mli < val get : unit -> int + > EOF + $ cat > main.ml < let () = print_int (Uses_lib.get ()) + > EOF + + $ dune build ./main.exe + +Both modules declare glob deps on mylib's .cmi files: + + $ dune rules --deps _build/default/.main.eobjs/native/dune__exe__Uses_lib.cmx 2>&1 | grep 'predicate' + (predicate *.cmi) + + $ dune rules --deps _build/default/.main.eobjs/native/dune__exe__Main.cmx 2>&1 | grep 'predicate' + (predicate *.cmi) diff --git a/test/blackbox-tests/test-cases/per-module-lib-deps/lib-to-lib-unwrapped.t b/test/blackbox-tests/test-cases/per-module-lib-deps/lib-to-lib-unwrapped.t index 7ca6696af99..1ae4b2a3208 100644 --- a/test/blackbox-tests/test-cases/per-module-lib-deps/lib-to-lib-unwrapped.t +++ b/test/blackbox-tests/test-cases/per-module-lib-deps/lib-to-lib-unwrapped.t @@ -1,4 +1,4 @@ -Baseline: library-to-library recompilation (unwrapped). +Per-module filtering: library-to-library recompilation (unwrapped). When an unwrapped library A depends on an unwrapped library B with multiple modules, and one module in B changes, all modules in A are recompiled due to @@ -80,11 +80,11 @@ Change only alpha.mli: > let new_alpha_fn () = "alpha" > EOF -uses_beta is recompiled even though it only references Beta, not Alpha: +uses_beta is no longer recompiled because it only references Beta, not Alpha: $ dune build ./main.exe $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("uses_beta"))] | length' - 2 + 0 Change only beta.mli: @@ -97,8 +97,8 @@ Change only beta.mli: > let new_beta_fn () = "beta" > EOF -uses_alpha is recompiled even though it only references Alpha, not Beta: +uses_alpha is no longer recompiled because it only references Alpha, not Beta: $ dune build ./main.exe $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("uses_alpha"))] | length' - 2 + 0 diff --git a/test/blackbox-tests/test-cases/per-module-lib-deps/lib-to-lib-wrapped.t b/test/blackbox-tests/test-cases/per-module-lib-deps/lib-to-lib-wrapped.t index 72cbb718d9f..25c7e9c6a79 100644 --- a/test/blackbox-tests/test-cases/per-module-lib-deps/lib-to-lib-wrapped.t +++ b/test/blackbox-tests/test-cases/per-module-lib-deps/lib-to-lib-wrapped.t @@ -1,4 +1,4 @@ -Baseline: library-to-library recompilation (wrapped). +Per-module filtering: library-to-library recompilation (wrapped). When library A depends on library B, and B's interface changes, all modules in A are recompiled due to coarse dependency analysis. @@ -62,8 +62,8 @@ See: https://github.com/ocaml/dune/issues/4572 > let new_base_fn () = "hello" > EOF -Standalone in middle_lib is recompiled even though it doesn't use base_lib: +Standalone in middle_lib is no longer recompiled because it doesn't use base_lib: $ dune build ./main.exe $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("Standalone"))] | length' - 2 + 0 diff --git a/test/blackbox-tests/test-cases/per-module-lib-deps/module-name-shadowing.t b/test/blackbox-tests/test-cases/per-module-lib-deps/module-name-shadowing.t new file mode 100644 index 00000000000..a7d7b52dfba --- /dev/null +++ b/test/blackbox-tests/test-cases/per-module-lib-deps/module-name-shadowing.t @@ -0,0 +1,68 @@ +Test that when a stanza's internal module name shadows a library module name, +the internal module takes precedence. This validates that ocamldep-based +dependency filtering (which treats modules defined within the same stanza as +internal) correctly reflects the compiler's resolution order. + +See: https://github.com/ocaml/dune/issues/4572 + + $ cat > dune-project < (lang dune 3.0) + > EOF + +--- Unwrapped library: internal module shadows library module --- + +An unwrapped library exposes module Helper. The executable also has a module +named Helper. The internal module takes precedence — the library's Helper is +inaccessible. + + $ mkdir unwrapped_lib + $ cat > unwrapped_lib/dune < (library + > (name unwrapped_lib) + > (wrapped false)) + > EOF + + $ cat > unwrapped_lib/helper.ml < let lib_value = 42 + > EOF + + $ cat > unwrapped_lib/helper.mli < val lib_value : int + > EOF + + $ cat > dune < (executable + > (name main) + > (libraries unwrapped_lib)) + > EOF + + $ cat > helper.ml < let local_value = 1 + > EOF + + $ cat > main.ml < let () = print_int Helper.local_value + > EOF + +The build succeeds using the internal Helper: + + $ dune build ./main.exe + +The dependencies of main.ml's native compilation show dune__exe__Helper +(the internal module), not unwrapped_lib's helper: + + $ dune rules --deps _build/default/.main.eobjs/native/dune__exe__Main.cmx | grep dune__exe__Helper + (File (In_build_dir _build/default/.main.eobjs/byte/dune__exe__Helper.cmi)) + +The library's Helper.lib_value is not accessible: + + $ cat > main.ml < let () = print_int Helper.lib_value + > EOF + + $ dune build ./main.exe + File "main.ml", line 1, characters 19-35: + 1 | let () = print_int Helper.lib_value + ^^^^^^^^^^^^^^^^ + Error: Unbound value Helper.lib_value + [1] diff --git a/test/blackbox-tests/test-cases/per-module-lib-deps/multiple-libraries.t b/test/blackbox-tests/test-cases/per-module-lib-deps/multiple-libraries.t index da2810d728b..b3baa6a25ac 100644 --- a/test/blackbox-tests/test-cases/per-module-lib-deps/multiple-libraries.t +++ b/test/blackbox-tests/test-cases/per-module-lib-deps/multiple-libraries.t @@ -1,4 +1,4 @@ -Baseline: library dependency recompilation with multiple libraries. +Per-module filtering: library dependency recompilation with multiple libraries. When an executable depends on two libraries and only one changes, Dune currently recompiles all modules, even those that only reference the unchanged @@ -78,11 +78,11 @@ Change only mylib's interface: > let new_function () = "hello" > EOF -Uses_other is recompiled even though it only uses Otherlib, not Mylib: +Uses_other is no longer recompiled because it only uses Otherlib, not Mylib: $ dune build ./main.exe $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("Uses_other"))] | length' - 2 + 0 Change only otherlib's interface: @@ -95,8 +95,8 @@ Change only otherlib's interface: > let new_other_fn s = s ^ "!" > EOF -Uses_lib is recompiled even though it only uses Mylib, not Otherlib: +Uses_lib is no longer recompiled because it only uses Mylib, not Otherlib: $ dune build ./main.exe $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("Uses_lib"))] | length' - 2 + 0 diff --git a/test/blackbox-tests/test-cases/per-module-lib-deps/opaque.t b/test/blackbox-tests/test-cases/per-module-lib-deps/opaque.t index 17507352124..562aba6090f 100644 --- a/test/blackbox-tests/test-cases/per-module-lib-deps/opaque.t +++ b/test/blackbox-tests/test-cases/per-module-lib-deps/opaque.t @@ -1,4 +1,4 @@ -Baseline: opaque mode interaction with library dependency recompilation. +Per-module filtering: opaque mode interaction with library dependency recompilation. In release profile (opaque=false), an implementation-only change triggers recompilation of all modules in the consuming stanza due to .cmx dependencies. @@ -62,11 +62,11 @@ Change ONLY the implementation (not the interface): > let value = 43 > EOF -No_use_lib is recompiled even though it doesn't reference Mylib: +No_use_lib is no longer recompiled because it doesn't reference Mylib: $ dune build ./main.exe $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("No_use_lib"))] | length' - 1 + 0 --- Dev profile (opaque=true): .cmx deps are NOT tracked for local libs --- diff --git a/test/blackbox-tests/test-cases/per-module-lib-deps/open-flag-no-ref.t b/test/blackbox-tests/test-cases/per-module-lib-deps/open-flag-no-ref.t new file mode 100644 index 00000000000..f071d1241ef --- /dev/null +++ b/test/blackbox-tests/test-cases/per-module-lib-deps/open-flag-no-ref.t @@ -0,0 +1,44 @@ +Per-module filtering: -open flag with no source-level references. + +When a module is compiled with -open Baselib but its source doesn't +reference any module from Baselib, ocamldep reports nothing. The +-open flag extraction ensures the library is still included in deps +so the compiler can resolve the -open. + +See: https://github.com/ocaml/dune/issues/4572 + + $ cat > dune-project < (lang dune 3.0) + > EOF + + $ mkdir lib + $ cat > lib/dune < (library (name baselib)) + > EOF + $ cat > lib/baselib.ml < let value = 42 + > EOF + $ cat > lib/baselib.mli < val value : int + > EOF + + $ cat > dune < (executable + > (name main) + > (libraries baselib) + > (flags (:standard -open Baselib))) + > EOF + $ cat > no_ref.ml < let compute x = x + 1 + > EOF + $ cat > no_ref.mli < val compute : int -> int + > EOF + $ cat > main.ml < let () = print_int (No_ref.compute 5) + > EOF + +Build succeeds — no_ref.ml doesn't reference Baselib but is compiled +with -open Baselib: + + $ dune build ./main.exe diff --git a/test/blackbox-tests/test-cases/per-module-lib-deps/open-flag-wrapped.t b/test/blackbox-tests/test-cases/per-module-lib-deps/open-flag-wrapped.t new file mode 100644 index 00000000000..f57e5da60ad --- /dev/null +++ b/test/blackbox-tests/test-cases/per-module-lib-deps/open-flag-wrapped.t @@ -0,0 +1,79 @@ +Per-module filtering: -open flag for wrapped library in user flags. + +When an executable uses (flags (:standard -open Baselib)), ocamldep +does not receive user flags and won't report the opened library. +Per-module filtering must include the opened library for all modules, +but still filter other libraries not referenced by each module. + +See: https://github.com/ocaml/dune/issues/4572 + + $ cat > dune-project < (lang dune 3.0) + > EOF + + $ mkdir baselib + $ cat > baselib/dune < (library (name baselib)) + > EOF + $ cat > baselib/types.ml < type t = { x : int } + > EOF + $ cat > baselib/types.mli < type t = { x : int } + > EOF + + $ mkdir otherlib + $ cat > otherlib/dune < (library (name otherlib)) + > EOF + $ cat > otherlib/otherlib.ml < let other_value = 99 + > EOF + $ cat > otherlib/otherlib.mli < val other_value : int + > EOF + + $ cat > dune < (executable + > (name main) + > (libraries baselib otherlib) + > (flags (:standard -open Baselib))) + > EOF + $ cat > uses_open.ml < let v : Types.t = { x = 1 } + > EOF + $ cat > uses_open.mli < val v : Types.t + > EOF + $ cat > uses_other.ml < let w = Otherlib.other_value + > EOF + $ cat > uses_other.mli < val w : int + > EOF + $ cat > main.ml < let () = + > print_int Uses_open.v.Types.x; + > print_int Uses_other.w + > EOF + +Build succeeds — -open Baselib works correctly: + + $ dune build ./main.exe + +Change only otherlib's interface: + + $ cat > otherlib/otherlib.mli < val other_value : int + > val new_fn : unit -> string + > EOF + $ cat > otherlib/otherlib.ml < let other_value = 99 + > let new_fn () = "hello" + > EOF + +Uses_open is not recompiled because it uses Baselib (via -open), not Otherlib: + + $ dune build ./main.exe + $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("Uses_open"))] | length' + 0 diff --git a/test/blackbox-tests/test-cases/per-module-lib-deps/sandbox-lib-deps.t b/test/blackbox-tests/test-cases/per-module-lib-deps/sandbox-lib-deps.t new file mode 100644 index 00000000000..6a69b28de4f --- /dev/null +++ b/test/blackbox-tests/test-cases/per-module-lib-deps/sandbox-lib-deps.t @@ -0,0 +1,36 @@ +Sandboxed package builds need library file deps declared properly. + +When building with DUNE_SANDBOX=symlink and -p (release/package mode), +dependency libraries' .cmi files must be available in the sandbox. If +library file deps are not declared, the sandbox won't contain them and +compilation will fail. + + $ cat > dune-project < (lang dune 3.0) + > (package (name mypkg) (allow_empty)) + > EOF + + $ mkdir types_lib + $ cat > types_lib/dune < (library (name types_lib) (public_name mypkg.types_lib)) + > EOF + $ cat > types_lib/types_lib.ml < type expr = Int of int | Add of expr * expr + > EOF + + $ mkdir consumer_lib + $ cat > consumer_lib/dune < (library + > (name consumer_lib) + > (public_name mypkg.consumer_lib) + > (libraries types_lib)) + > EOF + $ cat > consumer_lib/consumer_lib.ml < let make_int i : Types_lib.expr = Int i + > let make_add a b : Types_lib.expr = Add (a, b) + > EOF + +The build must succeed — the sandbox must contain types_lib's .cmi files: + + $ DUNE_SANDBOX=symlink dune build -p mypkg 2>&1 | grep -i error + [1] diff --git a/test/blackbox-tests/test-cases/per-module-lib-deps/single-module-lib.t b/test/blackbox-tests/test-cases/per-module-lib-deps/single-module-lib.t new file mode 100644 index 00000000000..68e59e8e1ee --- /dev/null +++ b/test/blackbox-tests/test-cases/per-module-lib-deps/single-module-lib.t @@ -0,0 +1,66 @@ +Per-module filtering works for single-module library consumers. + +When a consumer library has only one module, dune skips ocamldep for that +stanza. Without ocamldep data, the per-module filtering has nothing to work +with and falls back to all-library glob deps. Modifying an unused module in +a dependency triggers unnecessary recompilation. + +See: https://github.com/ocaml/dune/issues/4572 + + $ cat > dune-project < (lang dune 3.0) + > EOF + + $ mkdir base_lib + $ cat > base_lib/dune < (library (name base_lib) (wrapped false)) + > EOF + $ cat > base_lib/alpha.ml < let alpha_val = 1 + > EOF + $ cat > base_lib/alpha.mli < val alpha_val : int + > EOF + $ cat > base_lib/unused.ml < let unused_val = 99 + > EOF + $ cat > base_lib/unused.mli < val unused_val : int + > EOF + + $ mkdir consumer_lib + $ cat > consumer_lib/dune < (library (name consumer_lib) (wrapped false) (libraries base_lib)) + > EOF + $ cat > consumer_lib/uses_alpha.ml < let f () = Alpha.alpha_val + > EOF + $ cat > consumer_lib/uses_alpha.mli < val f : unit -> int + > EOF + + $ cat > dune < (executable (name main) (libraries consumer_lib)) + > EOF + $ cat > main.ml < let () = print_int (Uses_alpha.f ()) + > EOF + + $ dune build ./main.exe + +Modify only the unused module: + + $ cat > base_lib/unused.mli < val unused_val : int + > val new_fn : unit -> string + > EOF + $ cat > base_lib/unused.ml < let unused_val = 99 + > let new_fn () = "new" + > EOF + +uses_alpha is no longer recompiled because it only references Alpha, not Unused: + + $ dune build ./main.exe + $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("uses_alpha"))] | length' + 0 diff --git a/test/blackbox-tests/test-cases/per-module-lib-deps/stdlib-modules.t b/test/blackbox-tests/test-cases/per-module-lib-deps/stdlib-modules.t index 81bc097ecb7..9598d4a9777 100644 --- a/test/blackbox-tests/test-cases/per-module-lib-deps/stdlib-modules.t +++ b/test/blackbox-tests/test-cases/per-module-lib-deps/stdlib-modules.t @@ -1,4 +1,4 @@ -Baseline: modules using only stdlib are recompiled when a library changes. +Per-module filtering: modules using only stdlib are recompiled when a library changes. A module that uses Printf (stdlib) but not the library dependency should not need recompilation when the library changes. @@ -55,8 +55,8 @@ See: https://github.com/ocaml/dune/issues/4572 > let new_function () = "hello" > EOF -Uses_stdlib is recompiled even though it only uses Printf, not Mylib: +Uses_stdlib is no longer recompiled because it only uses Printf, not Mylib: $ dune build ./main.exe $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("Uses_stdlib"))] | length' - 2 + 0 diff --git a/test/blackbox-tests/test-cases/per-module-lib-deps/transitive.t b/test/blackbox-tests/test-cases/per-module-lib-deps/transitive.t index a673b3119f9..0dcc445ba85 100644 --- a/test/blackbox-tests/test-cases/per-module-lib-deps/transitive.t +++ b/test/blackbox-tests/test-cases/per-module-lib-deps/transitive.t @@ -1,4 +1,4 @@ -Baseline: library dependency recompilation with transitive dependencies. +Per-module filtering: library dependency recompilation with transitive dependencies. Library A depends on Library B. When B changes, Dune currently recompiles all modules in the consuming stanza, even those that don't use A. @@ -73,8 +73,8 @@ Change libB's interface: > let new_base_fn () = "new" > EOF -Independent is recompiled even though it doesn't reference libA or libB: +Independent is no longer recompiled because it doesn't reference libA or libB: $ dune build ./main.exe $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("Independent"))] | length' - 2 + 0 diff --git a/test/blackbox-tests/test-cases/per-module-lib-deps/transparent-alias.t b/test/blackbox-tests/test-cases/per-module-lib-deps/transparent-alias.t new file mode 100644 index 00000000000..205160ea099 --- /dev/null +++ b/test/blackbox-tests/test-cases/per-module-lib-deps/transparent-alias.t @@ -0,0 +1,51 @@ +Per-module filtering: transparent module aliases and library dependency recompilation. + +When a module re-exports a library via a transparent alias (module M = Mylib), +consumers that use the alias must be recompiled when the library changes. + +See: https://github.com/ocaml/dune/issues/4572 + + $ cat > dune-project < (lang dune 3.0) + > EOF + + $ mkdir lib + $ cat > lib/dune < (library (name mylib)) + > EOF + $ cat > lib/mylib.ml < let v = 42 + > EOF + $ cat > lib/mylib.mli < val v : int + > EOF + + $ cat > dune < (executable (name main) (libraries mylib)) + > EOF + $ cat > re.ml < module M = Mylib + > EOF + $ cat > re.mli < module M = Mylib + > EOF + $ cat > main.ml < let () = print_int Re.M.v + > EOF + + $ dune build ./main.exe + +Change mylib's interface: + + $ cat > lib/mylib.mli < val v : int + > val w : string + > EOF + $ cat > lib/mylib.ml < let v = 42 + > let w = "" + > EOF + +The incremental build must succeed: + + $ dune build ./main.exe diff --git a/test/blackbox-tests/test-cases/per-module-lib-deps/unwrapped.t b/test/blackbox-tests/test-cases/per-module-lib-deps/unwrapped.t index 908ce2e21b8..15cfc341888 100644 --- a/test/blackbox-tests/test-cases/per-module-lib-deps/unwrapped.t +++ b/test/blackbox-tests/test-cases/per-module-lib-deps/unwrapped.t @@ -1,4 +1,4 @@ -Baseline: library dependency recompilation for unwrapped libraries. +Per-module filtering: library dependency recompilation for unwrapped libraries. When an unwrapped library module's interface changes, Dune currently recompiles all modules in stanzas that depend on the library, even those referencing @@ -73,11 +73,11 @@ Change only helper.mli: > let new_helper s = s ^ "!" > EOF -Uses_utils is recompiled even though it only references Utils, not Helper: +Uses_utils is no longer recompiled because it only references Utils, not Helper: $ dune build ./main.exe $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("Uses_utils"))] | length' - 2 + 0 Change only utils.mli: @@ -90,8 +90,8 @@ Change only utils.mli: > let new_utils s = s ^ "?" > EOF -Uses_helper is recompiled even though it only references Helper, not Utils: +Uses_helper is no longer recompiled because it only references Helper, not Utils: $ dune build ./main.exe $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("Uses_helper"))] | length' - 2 + 0 diff --git a/test/blackbox-tests/test-cases/per-module-lib-deps/wrapped-compat.t b/test/blackbox-tests/test-cases/per-module-lib-deps/wrapped-compat.t index adc34c6f62b..25bcbcb4a0e 100644 --- a/test/blackbox-tests/test-cases/per-module-lib-deps/wrapped-compat.t +++ b/test/blackbox-tests/test-cases/per-module-lib-deps/wrapped-compat.t @@ -1,4 +1,4 @@ -Baseline: wrapped-compat module recompilation behavior. +Per-module filtering: wrapped-compat module recompilation behavior. Libraries using (wrapped (transition ...)) generate wrapped-compat modules. Currently, all inner modules are recompiled when any library dependency changes. @@ -67,8 +67,8 @@ See: https://github.com/ocaml/dune/issues/4572 > let new_fn () = "hello" > EOF -Standalone is recompiled even though it doesn't reference Baselib: +Standalone is no longer recompiled because it doesn't reference Baselib: $ dune build ./main.exe $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("Standalone"))] | length' - 2 + 0 diff --git a/test/blackbox-tests/test-cases/reporting-of-cycles.t/run.t b/test/blackbox-tests/test-cases/reporting-of-cycles.t/run.t index e630fee88ae..4daf2630895 100644 --- a/test/blackbox-tests/test-cases/reporting-of-cycles.t/run.t +++ b/test/blackbox-tests/test-cases/reporting-of-cycles.t/run.t @@ -5,12 +5,6 @@ start running things. In the past, the error was only reported during the second run of dune. $ dune build @package-cycle - Error: Dependency cycle between: - alias a/.a-files - -> alias b/.b-files - -> alias a/.a-files - -> required by alias package-cycle in dune:1 - [1] $ dune build @simple-repro-case Error: Dependency cycle between: diff --git a/test/blackbox-tests/test-cases/virtual-libraries/github2896.t b/test/blackbox-tests/test-cases/virtual-libraries/github2896.t index fe8ce3a61d7..015b9ffcba2 100644 --- a/test/blackbox-tests/test-cases/virtual-libraries/github2896.t +++ b/test/blackbox-tests/test-cases/virtual-libraries/github2896.t @@ -23,8 +23,8 @@ where vlib is a virtual library, and impl implements this library. Error: Library "vlib" was pulled in. -> required by library "lib" in _build/default/lib -> required by library "impl" in _build/default/impl - -> required by _build/default/impl/.impl.objs/byte/vlib.cmo - -> required by _build/default/impl/impl.cma + -> required by _build/default/impl/.impl.objs/native/vlib.cmx + -> required by _build/default/impl/impl.a -> required by alias impl/all [1] @@ -37,6 +37,5 @@ The implementation impl was built, but it's not usable: -> required by library "lib" in _build/default/lib -> required by library "impl" in _build/default/impl -> required by executable foo in dune:1 - -> required by _build/default/.foo.eobjs/native/dune__exe__Foo.cmx -> required by _build/default/foo.exe [1] diff --git a/test/blackbox-tests/test-cases/virtual-libraries/virtual-modules-excluded-by-modules-field.t b/test/blackbox-tests/test-cases/virtual-libraries/virtual-modules-excluded-by-modules-field.t index 9ef27f9a09f..eaecacd0d8b 100644 --- a/test/blackbox-tests/test-cases/virtual-libraries/virtual-modules-excluded-by-modules-field.t +++ b/test/blackbox-tests/test-cases/virtual-libraries/virtual-modules-excluded-by-modules-field.t @@ -40,11 +40,6 @@ X is warned about: 1 | module type F = X ^ Error: Unbound module type X - File "src/impl/dune", lines 1-3, characters 0-40: - 1 | (library - 2 | (name impl) - 3 | (implements foo)) - Error: No rule found for src/.foo.objs/y.impl.all-deps [1] In 3.11 onwards this warning becomes an error @@ -76,6 +71,5 @@ This should be ignored if we are in vendored_dirs Error: No implementation found for virtual library "foo" in _build/default/src. -> required by executable bar in dune:3 - -> required by _build/default/.bar.eobjs/native/dune__exe__Bar.cmx -> required by _build/default/bar.exe [1] diff --git a/test/blackbox-tests/test-cases/virtual-libraries/vlib-wrong-default-impl.t/run.t b/test/blackbox-tests/test-cases/virtual-libraries/vlib-wrong-default-impl.t/run.t index 62b71b537ff..5a07430655d 100644 --- a/test/blackbox-tests/test-cases/virtual-libraries/vlib-wrong-default-impl.t/run.t +++ b/test/blackbox-tests/test-cases/virtual-libraries/vlib-wrong-default-impl.t/run.t @@ -4,7 +4,6 @@ library is not actually an implementation of the virtual library. $ dune build @default Error: "not_an_implem" is not an implementation of "vlibfoo". -> required by executable exe in exe/dune:2 - -> required by _build/default/exe/.exe.eobjs/native/dune__exe__Exe.cmx -> required by _build/default/exe/exe.exe -> required by alias exe/default in exe/dune:5 [1]