Skip to content

Commit b909b3d

Browse files
committed
feature: unused-libs alias
Introduce an alias to detect unused libraries in libraries and executable stanzas. This stanza relies on information extracted using ocamlobjinfo to detect which modules are actually used Signed-off-by: Rudi Grinberg <[email protected]>
1 parent 5bfb106 commit b909b3d

File tree

16 files changed

+625
-149
lines changed

16 files changed

+625
-149
lines changed

doc/changes/added/12623.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Introduce an `unused-libs` stanza to detect unused libraries (#12623, fixes
2+
#650, @rgrinberg)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@unused-libs
2+
============
3+
4+
This alias is used to detect unused entries in the libraries field of
5+
executables and stanzas.

src/dune_rules/alias0.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ let install = standard "install"
2626
let pkg_install = Alias.Name.of_string "pkg-install"
2727
let ocaml_index = standard "ocaml-index"
2828
let runtest = standard "runtest"
29+
let unused_libs = standard "unused-libs"
2930
let all = standard "all"
3031
let default = standard "default"
3132
let empty = standard "empty"

src/dune_rules/alias0.mli

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ val ocaml_index : Name.t
1515
val install : Name.t
1616
val pkg_install : Name.t
1717
val runtest : Name.t
18+
val unused_libs : Name.t
1819
val empty : Name.t
1920
val all : Name.t
2021
val default : Name.t

src/dune_rules/dep_rules.ml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ let ooi_deps
3636
let ctx = Super_context.context sctx in
3737
Context.ocaml ctx
3838
in
39-
Ocamlobjinfo.rules ocaml ~sandbox ~dir ~unit
39+
Ocamlobjinfo.rules ocaml ~sandbox ~dir ~units:[ unit ]
40+
|> Action_builder.map ~f:(function
41+
| [ x ] -> x
42+
| [] | _ :: _ -> assert false)
4043
in
4144
let add_rule = Super_context.add_rule sctx ~dir in
4245
let read =

src/dune_rules/exe_rules.ml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,18 @@ let executables_rules
208208
in
209209
let lib_config = ocaml.lib_config in
210210
let* requires_compile = Compilation_context.requires_compile cctx in
211+
let* () =
212+
let toolchain = Compilation_context.ocaml cctx in
213+
let direct_requires = Lib.Compile.direct_requires compile_info in
214+
Unused_libs_rules.gen_rules
215+
sctx
216+
toolchain
217+
exes.buildable.loc
218+
~obj_dir
219+
~modules
220+
~dir
221+
~direct_requires
222+
in
211223
let* () =
212224
let* dep_graphs =
213225
(* Building an archive for foreign stubs, we link the corresponding object

src/dune_rules/lib_rules.ml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,17 @@ let library_rules
621621
in
622622
Sub_system.gen_rules
623623
{ super_context = sctx; dir; stanza = lib; scope; source_modules; compile_info }
624+
and+ () =
625+
let toolchain = Compilation_context.ocaml cctx in
626+
let direct_requires = Lib.Compile.direct_requires compile_info in
627+
Unused_libs_rules.gen_rules
628+
sctx
629+
toolchain
630+
lib.buildable.loc
631+
~obj_dir
632+
~modules
633+
~dir
634+
~direct_requires
624635
and+ merlin =
625636
let+ requires_hidden = Compilation_context.requires_hidden cctx
626637
and+ parameters = Compilation_context.parameters cctx in

src/dune_rules/ocamlobjinfo.mli

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,19 @@ val rules
1010
: Ocaml_toolchain.t
1111
-> dir:Path.Build.t
1212
-> sandbox:Sandbox_config.t option
13-
-> unit:Path.t
14-
-> t Action_builder.t
13+
-> units:Path.t list
14+
-> t list Action_builder.t
15+
16+
(** Run ocamlobjinfo on an archive to extract module names defined in it *)
17+
val archive_rules
18+
: Ocaml_toolchain.t
19+
-> dir:Path.Build.t
20+
-> sandbox:Sandbox_config.t option
21+
-> archive:Path.t
22+
-> Module_name.Unique.Set.t Action_builder.t
1523

1624
(** For testing only *)
17-
val parse : string -> t
25+
val parse : string -> t list
26+
27+
(** Parse archive output to extract module names defined in the archive *)
28+
val parse_archive : string -> Module_name.Unique.Set.t

src/dune_rules/ocamlobjinfo.mll

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,34 @@ let ws = [' ' '\t']+
2222
let hash = ['0'-'9' 'a'-'z' '-']+
2323
let name = ['A'-'Z'] ['A'-'Z' 'a'-'z' '0'-'9' '_']*
2424

25-
rule ocamlobjinfo acc = parse
26-
| "Interfaces imported:" newline { intfs acc lexbuf }
27-
| "Implementations imported:" newline { impls acc lexbuf }
28-
| _ { ocamlobjinfo acc lexbuf }
25+
rule ocamlobjinfo acc_units acc = parse
26+
| "Interfaces imported:" newline { intfs acc_units acc lexbuf }
27+
| "Implementations imported:" newline { impls acc_units acc lexbuf }
28+
| _ { ocamlobjinfo acc_units acc lexbuf }
29+
| eof { acc :: acc_units }
30+
and intfs acc_units acc = parse
31+
| ws hash ws (name as name) newline { intfs acc_units (add_intf acc name) lexbuf }
32+
| "Implementations imported:" newline { impls acc_units acc lexbuf }
33+
| "File " [^ '\n']+ newline { ocamlobjinfo (acc :: acc_units) empty lexbuf }
34+
| _ | eof { acc :: acc_units }
35+
and impls acc_units acc = parse
36+
| ws hash ws (name as name) newline { impls acc_units (add_impl acc name) lexbuf }
37+
| "File " [^ '\n']+ newline { ocamlobjinfo (acc :: acc_units) empty lexbuf }
38+
| _ | eof { acc :: acc_units }
39+
40+
and archive acc = parse
41+
| "Unit name:" ws (name as name) { archive (Module_name.Unique.Set.add acc (Module_name.Unique.of_string name)) lexbuf }
42+
| _ { archive acc lexbuf }
2943
| eof { acc }
30-
and intfs acc = parse
31-
| ws hash ws (name as name) newline { intfs (add_intf acc name) lexbuf }
32-
| "Implementations imported:" newline { impls acc lexbuf }
33-
| _ | eof { acc }
34-
and impls acc = parse
35-
| ws hash ws (name as name) newline { impls (add_impl acc name) lexbuf }
36-
| _ | eof { acc }
3744

3845
{
39-
let parse s = ocamlobjinfo empty (Lexing.from_string s)
46+
let parse s = Lexing.from_string s |> ocamlobjinfo [] empty |> List.rev
47+
48+
let parse_archive s =
49+
Lexing.from_string s
50+
|> archive Module_name.Unique.Set.empty
4051

41-
let rules (ocaml : Ocaml_toolchain.t) ~dir ~sandbox ~unit =
52+
let rules (ocaml : Ocaml_toolchain.t) ~dir ~sandbox ~units =
4253
let no_approx =
4354
if Ocaml.Version.ooi_supports_no_approx ocaml.version then
4455
[Command.Args.A "-no-approx"]
@@ -52,7 +63,9 @@ let rules (ocaml : Ocaml_toolchain.t) ~dir ~sandbox ~unit =
5263
[]
5364
in
5465
let observing_facts =
55-
Dep.Facts.singleton (Dep.file unit) (Dep.Fact.nothing)
66+
List.map units ~f:(fun unit ->
67+
Dep.Facts.singleton (Dep.file unit) (Dep.Fact.nothing))
68+
|> Dep.Facts.union_all
5669
in
5770
let open Action_builder.O in
5871
let* action =
@@ -61,7 +74,7 @@ let rules (ocaml : Ocaml_toolchain.t) ~dir ~sandbox ~unit =
6174
(List.concat
6275
[ no_approx
6376
; no_code
64-
; [ Dep unit ]
77+
; [ Deps units ]
6578
])
6679
in
6780
(Dune_engine.Build_system.execute_action_stdout
@@ -73,4 +86,24 @@ let rules (ocaml : Ocaml_toolchain.t) ~dir ~sandbox ~unit =
7386
}
7487
|> Action_builder.of_memo)
7588
>>| parse
89+
90+
let archive_rules (ocaml : Ocaml_toolchain.t) ~dir ~sandbox ~archive =
91+
let observing_facts =
92+
Dep.Facts.singleton (Dep.file archive) (Dep.Fact.nothing)
93+
in
94+
let open Action_builder.O in
95+
let* action =
96+
Command.run' ?sandbox
97+
~dir:(Path.build dir) ocaml.ocamlobjinfo
98+
[ Dep archive ]
99+
in
100+
(Dune_engine.Build_system.execute_action_stdout
101+
~observing_facts
102+
{ Rule.Anonymous_action.action
103+
; loc = Loc.none
104+
; dir
105+
; alias = None
106+
}
107+
|> Action_builder.of_memo)
108+
>>| parse_archive
76109
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
open Import
2+
open Memo.O
3+
4+
let classify_libs sctx libs =
5+
Memo.parallel_map libs ~f:(fun lib ->
6+
let+ modules = Dir_contents.modules_of_lib sctx lib in
7+
lib, modules)
8+
>>| List.partition_map ~f:(fun (lib, modules) ->
9+
match modules with
10+
| Some modules ->
11+
let module_set =
12+
Modules.With_vlib.obj_map modules
13+
|> Module_name.Unique.Map.keys
14+
|> Module_name.Unique.Set.of_list
15+
in
16+
Left (lib, module_set)
17+
| None ->
18+
(match
19+
let archives = Lib.info lib |> Lib_info.archives in
20+
Mode.Dict.get archives Byte
21+
with
22+
| [] -> Left (lib, Module_name.Unique.Set.empty)
23+
| archive :: _ -> Right (lib, archive)))
24+
;;
25+
26+
let gen_rules sctx toolchain loc ~obj_dir ~modules ~dir ~direct_requires =
27+
match
28+
let modules =
29+
Modules.With_vlib.drop_vlib modules
30+
|> Modules.fold ~init:[] ~f:(fun m acc -> m :: acc)
31+
in
32+
let cmis = Obj_dir.Module.L.cm_files obj_dir modules ~kind:(Ocaml Cmi) in
33+
let cmos = Obj_dir.Module.L.cm_files obj_dir modules ~kind:(Ocaml Cmo) in
34+
cmis @ cmos
35+
with
36+
| [] -> Memo.return ()
37+
| units ->
38+
let action =
39+
let open Action_builder.O in
40+
let build_dir = Obj_dir.dir obj_dir in
41+
let* local_modules, external_lib_archives =
42+
let* direct_requires = Resolve.Memo.read direct_requires in
43+
classify_libs sctx direct_requires |> Action_builder.of_memo
44+
in
45+
let* results =
46+
Ocamlobjinfo.rules
47+
toolchain
48+
~dir:build_dir
49+
~sandbox:(Some Sandbox_config.needs_sandboxing)
50+
~units
51+
and* external_modules =
52+
List.map external_lib_archives ~f:(fun (lib, archive) ->
53+
let+ modules =
54+
Ocamlobjinfo.archive_rules
55+
toolchain
56+
~dir:build_dir
57+
~sandbox:(Some Sandbox_config.needs_sandboxing)
58+
~archive
59+
in
60+
lib, modules)
61+
|> Action_builder.all
62+
in
63+
let unused_libs =
64+
let all_imported =
65+
List.fold_left results ~init:Module_name.Unique.Set.empty ~f:(fun acc result ->
66+
let intf_deps = Ml_kind.Dict.get result Intf in
67+
let impl_deps = Ml_kind.Dict.get result Impl in
68+
Module_name.Unique.Set.union
69+
acc
70+
(Module_name.Unique.Set.union intf_deps impl_deps))
71+
in
72+
external_modules @ local_modules
73+
|> Lib.Map.of_list_exn
74+
|> Lib.Map.foldi ~init:[] ~f:(fun lib lib_modules acc ->
75+
(* Skip libraries with no modules *)
76+
if Module_name.Unique.Set.is_empty lib_modules
77+
then acc
78+
else (
79+
(* Check if any module from this library is imported *)
80+
let is_used =
81+
Module_name.Unique.Set.exists lib_modules ~f:(fun mod_name ->
82+
Module_name.Unique.Set.mem all_imported mod_name)
83+
in
84+
if is_used then acc else lib :: acc))
85+
in
86+
match unused_libs with
87+
| [] -> Action_builder.return (Action.progn [])
88+
| libs ->
89+
Action_builder.fail
90+
{ fail =
91+
(fun () ->
92+
(* CR-someday rgrinberg: ideally, we'd use the locations of the
93+
unused libraries, but they've already been discarded. *)
94+
User_error.raise
95+
~loc
96+
[ Pp.text "Unused libraries:"
97+
; Pp.enumerate libs ~f:(fun lib ->
98+
Lib.name lib |> Lib_name.to_string |> Pp.verbatim)
99+
])
100+
}
101+
in
102+
let unused_libs_alias = Alias.make Alias0.unused_libs ~dir in
103+
Rules.Produce.Alias.add_action
104+
unused_libs_alias
105+
~loc
106+
(action |> Action_builder.map ~f:Action.Full.make)
107+
;;

0 commit comments

Comments
 (0)