Monorepo management for opam overlays
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

monopam: lint detects missing-pin opam deps

A runtime dep declared in some monorepo opam file may resolve fine on
the current developer's switch yet fail [opam install] from a fresh
switch, because the dep was installed from a local pin
([_opam/.opam-switch/overlay/<pkg>]) rather than from a public opam
repository. The publish path tripped on this for [tw] (pulled by
[irmin]) and [helix] (pulled by [globe]).

Add a [Missing_pin] kind to [Lint.kind] and a check that flags any
runtime dep that is:
- not provided by any monorepo subtree's opam file,
- not a [conf-*] / known builtin / implicit dep, and
- either currently sitting in [_opam/.opam-switch/overlay/<pkg>] or
absent from the active switch entirely.

Both [pp_table] and [pp_plain] in the lint CLI gain a "Missing pin"
column / "missing pin: ..." line, and the summary string includes
[N missing-pin].

+77 -3
+12 -1
bin/cmd_lint.ml
··· 43 43 Tty.Table.column ~max_width:50 "Missing"; 44 44 Tty.Table.column ~max_width:50 "Unused"; 45 45 Tty.Table.column ~max_width:50 "Dead lib"; 46 + Tty.Table.column ~max_width:50 "Missing pin"; 46 47 ] 47 48 in 48 49 let rows = ··· 52 53 let missing = pkgs_of_kind Monopam.Lint.Missing issues in 53 54 let unused = pkgs_of_kind Monopam.Lint.Unused issues in 54 55 let dead = pkgs_of_kind Monopam.Lint.Dead_lib issues in 56 + let missing_pin = pkgs_of_kind Monopam.Lint.Missing_pin issues in 55 57 [ 56 58 Tty.Span.text subtree; 57 59 Tty.Span.styled ··· 63 65 Tty.Span.styled 64 66 Tty.Style.(fg (Tty.Color.ansi `Magenta)) 65 67 (String.concat " " dead); 68 + Tty.Span.styled 69 + Tty.Style.(fg (Tty.Color.ansi `Red)) 70 + (String.concat " " missing_pin); 66 71 ]) 67 72 order 68 73 in ··· 77 82 let missing = pkgs_of_kind Monopam.Lint.Missing issues in 78 83 let unused = pkgs_of_kind Monopam.Lint.Unused issues in 79 84 let dead = pkgs_of_kind Monopam.Lint.Dead_lib issues in 85 + let missing_pin = pkgs_of_kind Monopam.Lint.Missing_pin issues in 80 86 if missing <> [] then 81 87 Fmt.pr "%s missing: %s@." subtree (String.concat " " missing); 82 88 if unused <> [] then 83 89 Fmt.pr "%s unused: %s@." subtree (String.concat " " unused); 84 90 if dead <> [] then 85 - Fmt.pr "%s dead lib: %s@." subtree (String.concat " " dead)) 91 + Fmt.pr "%s dead lib: %s@." subtree (String.concat " " dead); 92 + if missing_pin <> [] then 93 + Fmt.pr "%s missing pin: %s@." subtree (String.concat " " missing_pin)) 86 94 order 87 95 88 96 let pp_source_issues source_issues = ··· 173 181 let n_missing = count_kind Monopam.Lint.Missing issues in 174 182 let n_unused = count_kind Monopam.Lint.Unused issues in 175 183 let n_dead = count_kind Monopam.Lint.Dead_lib issues in 184 + let n_missing_pin = count_kind Monopam.Lint.Missing_pin issues in 176 185 let n_source = List.length source_issues in 177 186 let n_warn = List.length dune_warning_issues in 178 187 List.filter_map Fun.id ··· 180 189 (if n_missing > 0 then Some (Fmt.str "%d missing" n_missing) else None); 181 190 (if n_unused > 0 then Some (Fmt.str "%d unused" n_unused) else None); 182 191 (if n_dead > 0 then Some (Fmt.str "%d dead-lib" n_dead) else None); 192 + (if n_missing_pin > 0 then Some (Fmt.str "%d missing-pin" n_missing_pin) 193 + else None); 183 194 (if n_source > 0 then Some (Fmt.str "%d source" n_source) else None); 184 195 (if n_warn > 0 then Some (Fmt.str "%d dune-warnings" n_warn) else None); 185 196 ]
+57 -2
lib/lint.ml
··· 449 449 450 450 (* ---- Types ---- *) 451 451 452 - type kind = Missing | Unused | Dead_lib 452 + type kind = Missing | Unused | Dead_lib | Missing_pin 453 453 type issue = { subtree : string; kind : kind; package : string } 454 454 455 455 (* ---- Dead-lib detection ---- ··· 800 800 (fun (a : issue) (b : issue) -> 801 801 match String.compare a.subtree b.subtree with 802 802 | 0 -> 803 - let rank = function Missing -> 0 | Unused -> 1 | Dead_lib -> 2 in 803 + let rank = function 804 + | Missing -> 0 805 + | Unused -> 1 806 + | Dead_lib -> 2 807 + | Missing_pin -> 3 808 + in 804 809 let ka = rank a.kind in 805 810 let kb = rank b.kind in 806 811 if ka <> kb then compare ka kb else String.compare a.package b.package ··· 835 840 | Ok s -> s 836 841 | Error _ -> Sources_registry.empty 837 842 843 + (** Walk every subtree's [*.opam] files and collect the union of declared 844 + package names. Used to decide whether a runtime dep is satisfiable from 845 + inside the monorepo. *) 846 + let monorepo_packages ~fs ~monorepo subdirs = 847 + List.fold_left 848 + (fun acc subtree -> 849 + let subtree_path = Fpath.(monorepo / subtree) in 850 + let pkgs = scan_opam_files ~fs subtree_path in 851 + List.fold_left 852 + (fun acc (pkg_name, _, _) -> String_set.add pkg_name acc) 853 + acc pkgs) 854 + String_set.empty subdirs 855 + 856 + (** Pinned packages live as opam overlays in [_opam/.opam-switch/overlay/<pkg>]. 857 + They were installed from a local source or git pin, never from a public 858 + opam-repo, so [opam install <pkg>] from a fresh switch (the publish path) 859 + will fail. *) 860 + let pinned_packages ~fs ~monorepo = 861 + let overlay = 862 + Eio.Path.( 863 + fs / Fpath.to_string monorepo / "_opam" / ".opam-switch" / "overlay") 864 + in 865 + match Eio.Path.read_dir overlay with 866 + | entries -> String_set.of_list entries 867 + | exception Eio.Io _ -> String_set.empty 868 + 869 + (** A runtime dep is missing_pin when it's neither provided by a monorepo 870 + subtree nor installable from a public opam-repo. We approximate the second 871 + leg by checking whether the dep is currently a pinned overlay 872 + ([_opam/.opam-switch/overlay/<pkg>]) or absent from the active switch 873 + altogether — both cases will trip up [opam install] from a fresh switch. *) 874 + let missing_pin_packages ~pinned_pkgs ~lib_index ~monorepo_pkgs ~subtree 875 + (_pkg_name, runtime_deps, _all_deps) = 876 + String_set.fold 877 + (fun dep acc -> 878 + if String_set.mem dep monorepo_pkgs then acc 879 + else if String_set.mem dep implicit_deps then acc 880 + else if is_conf_pkg dep then acc 881 + else if String_set.mem dep pinned_pkgs then 882 + { subtree; kind = Missing_pin; package = dep } :: acc 883 + else if Monopam_info_index.origin lib_index dep <> None then acc 884 + else { subtree; kind = Missing_pin; package = dep } :: acc) 885 + runtime_deps [] 886 + 838 887 let run ~fs ~monorepo () = 839 888 let index = build_library_index ~fs ~monorepo in 840 889 let build_lib = Fpath.(monorepo / "_build" / "install" / "default" / "lib") in ··· 848 897 compute_dune_warning_issues ~fs ~monorepo subdirs 849 898 |> sort_dune_warning_issues 850 899 in 900 + let monorepo_pkgs = monorepo_packages ~fs ~monorepo subdirs in 901 + let pinned_pkgs = pinned_packages ~fs ~monorepo in 851 902 let issues = ref [] in 852 903 let scanned = ref 0 in 853 904 List.iter ··· 877 928 (reexports_of_pkg ~fs ~build_lib ~index p)) 878 929 direct_deps direct_deps 879 930 in 931 + issues := 932 + missing_pin_packages ~pinned_pkgs ~lib_index ~monorepo_pkgs 933 + ~subtree pkg 934 + @ !issues; 880 935 let new_issues = 881 936 check_package ~index ~build_lib ~dune_pkgs ~dune_owner_pkgs 882 937 ~own_set ~all_deps ~subtree pkg ~fs
+8
lib/lint.mli
··· 21 21 (** Library is in a stanza's [(libraries ...)] but none of the modules its 22 22 [_opam/lib/<lib>/dune-package] (or [.cmi] files) expose appear in any 23 23 [.ml] / [.mli] in the same directory. *) 24 + | Missing_pin 25 + (** Package is declared in opam [depends] but [opam install] from a fresh 26 + switch can't find it: it's not provided by any monorepo subtree, not a 27 + known builtin / conf-* / implicit dep, and either currently sitting as 28 + a local pin in [_opam/.opam-switch/overlay/<pkg>] or absent from the 29 + active switch entirely. The fix is usually to either publish the dep 30 + upstream or declare a [pin-depends:] entry in this package's 31 + [<pkg>.opam.template]. *) 24 32 25 33 type issue = { 26 34 subtree : string; (** Monorepo subdirectory *)