diff --git a/src/dune_rules/compilation_context.ml b/src/dune_rules/compilation_context.ml index b629d7874d3..d2f6ff63b51 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 rather than here. + TODO: some of the requires can be filtered out using [ocamldep] info. + See issue #4572. *) + 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 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/lib_file_deps.ml b/src/dune_rules/lib_file_deps.ml index 0aa3440da59..639ce2e84c0 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,86 @@ 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 + 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 -> Resolve.Memo.return (Some [ name, None ]) + | 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 + 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..ce2dda2257b 100644 --- a/src/dune_rules/lib_file_deps.mli +++ b/src/dune_rules/lib_file_deps.mli @@ -15,7 +15,13 @@ 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 +(** Compute library file dependencies for all [libs] for the given [cm_kind]. + 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.t * Module.t option) list + -> Dep.Set.t type path_specification = | Allow_all @@ -29,3 +35,18 @@ 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 diff --git a/src/dune_rules/module_compilation.ml b/src/dune_rules/module_compilation.ml index 75bcea17c87..5df2deea74c 100644 --- a/src/dune_rules/module_compilation.ml +++ b/src/dune_rules/module_compilation.ml @@ -286,6 +286,169 @@ 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 for_ = Compilation_context.for_ cctx in + let stanza_modules = Compilation_context.modules cctx in + 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 requires_compile = Compilation_context.requires_compile cctx in + let requires_hidden = Compilation_context.requires_hidden cctx 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 + (* Static deps — always present for all non-skipped modules. *) + let lib_cm_deps_arg : _ Command.Args.t = + if skip_lib_deps || can_filter + then Command.Args.empty + else + (let open Resolve.Memo.O in + let+ direct_libs = requires_compile + and+ hidden_libs = requires_hidden in + Command.Args.Hidden_deps + (Lib_file_deps.deps_of_entries + ~opaque + ~cm_kind + (List.map (direct_libs @ hidden_libs) ~f:(fun lib -> lib, None)))) + |> Resolve.Memo.args + |> Command.Args.memo + in + (* Dynamic deps — additive, alongside static deps. *) + let lib_cm_deps_dyn = + if not can_filter + then Action_builder.return () + else + Action_builder.dyn_deps + (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 (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 + let* flags = Ocaml_flags.get (Compilation_context.flags cctx) mode in + let from_open_flags = Ocaml_flags.extract_open_module_names flags in + let referenced_modules = + Module_name.Set.union from_ocamldep from_open_flags + in + let* from_parameters = + Resolve.Memo.read (Compilation_context.parameters cctx) + >>| Module_name.Set.of_list + in + let+ from_argument_for = + let impl = Compilation_context.implements cctx in + Resolve.Memo.read @@ Virtual_rules.implements_parameter impl m + >>| function + | None -> Module_name.Set.empty + | Some name -> Module_name.Set.singleton name + in + Module_name.Set.union_all + [ referenced_modules; from_parameters; from_argument_for ] + 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 Action_builder.return all_entries + else ( + let filtered = + Lib_file_deps.Lib_index.filter_libs + lib_index + ~referenced_modules:external_modules + in + (* Expand: also include direct library deps of each selected + library. This handles re-exports (module Impl = Impl) where + the consumer references Alias but transitively depends on + Impl through the alias. *) + let selected_libs = List.map filtered ~f:(fun (lib, _) -> lib) in + (* Only expand requires for wrapped libraries (mod_opt = None) + since they use glob deps and may re-export dependencies. *) + let wrapped_selected = + List.filter_map filtered ~f:(fun (lib, mod_opt) -> + match mod_opt with + | None -> Some lib + | Some _ -> None) + in + let+ extra_libs = + Action_builder.List.concat_map wrapped_selected ~f:(fun lib -> + Resolve.Memo.read (Lib.requires lib ~for_)) + in + let already_selected = Lib.Set.of_list selected_libs in + let extra_entries = + List.filter_map extra_libs ~f:(fun lib -> + if Lib.Set.mem already_selected lib + then None + else List.find all_entries ~f:(fun (l, _) -> Lib.equal l lib)) + in + filtered @ extra_entries) + 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 +567,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_dyn >>> Command.run ~dir:(Path.build (Context.build_dir ctx)) compiler @@ -414,6 +578,7 @@ let build_cm ; Command.Args.S obj_dirs ; Command.Args.as_any (Lib_mode.Cm_kind.Map.get (Compilation_context.includes cctx) cm_kind) + ; lib_cm_deps_arg ; extra_args ; As as_parameter_arg ; as_argument_for @@ -512,6 +677,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 +685,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 +717,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..e7e8f7fbff7 100644 --- a/src/dune_rules/ocamldep.ml +++ b/src/dune_rules/ocamldep.ml @@ -199,3 +199,18 @@ let read_immediate_deps_of ~obj_dir ~modules ~ml_kind ~for_ unit = |> 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 Module.source ~ml_kind unit with + | None -> Action_builder.return Module_name.Set.empty + | Some source -> + (match Obj_dir.Module.dep obj_dir ~for_ (Immediate (unit, ml_kind)) with + | None -> Action_builder.return Module_name.Set.empty + | Some ocamldep_output -> + Action_builder.lines_of (Path.build ocamldep_output) + |> Action_builder.map ~f:(fun lines -> + parse_deps_exn ~file:(Module.File.path source) lines + |> List.map ~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/alias-reexport.t b/test/blackbox-tests/test-cases/per-module-lib-deps/alias-reexport.t new file mode 100644 index 00000000000..14e7726f496 --- /dev/null +++ b/test/blackbox-tests/test-cases/per-module-lib-deps/alias-reexport.t @@ -0,0 +1,94 @@ +Incremental builds with library re-exporting a dependency via module alias. + +When library "alias" re-exports library "impl" via (module Impl = Impl), +a consumer that accesses Impl through Alias must be recompiled when +impl.cmi changes. The -opaque flag means soft changes (implementation +only, no cmi change) can safely skip recompilation, but cmi changes +must always trigger it. + +See: https://github.com/ocaml/dune/issues/4572 + + $ cat > dune-project < (lang dune 3.23) + > EOF + +A library where we'll perform the changes: + + $ mkdir impl + $ cat > impl/dune < (library (name impl)) + > EOF + $ cat > impl/impl.ml < let foo = "initial build" + > EOF + +Another library which exposes an alias to impl: + + $ mkdir alias + $ cat > alias/dune < (library (name alias) (libraries impl)) + > EOF + $ cat > alias/alias.ml < module Impl = Impl + > EOF + +A binary which depends on Alias to access Impl. An empty unused file +makes this a multi-module executable: + + $ mkdir bin + $ cat > bin/dune < (executable (name main) (libraries alias)) + > EOF + $ cat > bin/main.ml < let () = print_endline Alias.Impl.foo + > EOF + $ touch bin/unused.ml + +The first build succeeds: + + $ dune exec ./bin/main.exe + initial build + +Soft update — impl.cmi is NOT modified (only implementation changes). +With -opaque, skipping recompilation of main.ml is correct because +main.ml doesn't depend on impl's implementation, only its interface: + + $ cat > impl/impl.ml < let foo = "second build, no change to cmi" + > EOF + + $ dune exec ./bin/main.exe + second build, no change to cmi + +main.cmx is NOT rebuilt (correct — only impl changed, and -opaque +means we don't track impl's implementation): + + $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("Main"))]' + [] + +unused.cmx is also NOT rebuilt (correct — it references nothing): + + $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("Unused"))]' + [] + +Hard update — impl.cmi IS modified (new value added). main.ml must +be recompiled because Alias re-exports Impl and the interface changed: + + $ cat > impl/impl.ml < let new_value = 42 + > let foo = "third build, forced a cmi update" + > EOF + + $ dune exec ./bin/main.exe + third build, forced a cmi update + +Main is rebuilt (necessary — impl.cmi changed and main.ml uses +Impl through the Alias re-export): + + $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("Main"))] | length | . > 0' + true + +Unused is NOT rebuilt (correct — it doesn't reference impl): + + $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("Unused"))] | length' + 0 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..79624037a45 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 @@ -62,4 +62,4 @@ No_use_lib is recompiled even though 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-to-lib-unwrapped.t b/test/blackbox-tests/test-cases/per-module-lib-deps/lib-to-lib-unwrapped.t index 7ca6696af99..5d564f9c474 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 @@ -84,7 +84,7 @@ uses_beta is recompiled even though 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: @@ -101,4 +101,4 @@ uses_alpha is recompiled even though 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..c5ee594fb00 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 @@ -66,4 +66,4 @@ Standalone in middle_lib is recompiled even though 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/multiple-libraries.t b/test/blackbox-tests/test-cases/per-module-lib-deps/multiple-libraries.t index da2810d728b..66429a0c4ba 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 @@ -82,7 +82,7 @@ Uses_other is recompiled even though 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: @@ -99,4 +99,4 @@ Uses_lib is recompiled even though 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..30447771d8f 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 @@ -66,7 +66,7 @@ No_use_lib is recompiled even though 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/stdlib-modules.t b/test/blackbox-tests/test-cases/per-module-lib-deps/stdlib-modules.t index 81bc097ecb7..ae725944508 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 @@ -59,4 +59,4 @@ Uses_stdlib is recompiled even though 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..394fc700a5a 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 @@ -77,4 +77,4 @@ Independent is recompiled even though 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/unwrapped.t b/test/blackbox-tests/test-cases/per-module-lib-deps/unwrapped.t index 908ce2e21b8..eb8e5d688e2 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 @@ -77,7 +77,7 @@ Uses_utils is recompiled even though 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: @@ -94,4 +94,4 @@ Uses_helper is recompiled even though 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..e1a16e4e1f8 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 @@ -71,4 +71,4 @@ Standalone is recompiled even though 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: