Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions bin/opam2web.ml
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ let make_website user_options universe statistics ds =
O2wBlog.make_menu ~srcurl:user_options.blog_source_uri blog_entries
in
let blog_feed = O2wBlog.make_feed ~root:user_options.root_uri blog_entries in
let criteria = ["name"; "popularity"; "date"] in
let criteria_nostats = ["name"; "date"] in
let criteria = ["name"; "popularity"; "date"; "rev-deps"] in
let criteria_nostats = ["name"; "date"; "rev-deps"] in
let sortby_links =
match statistics with
| None ->
Expand All @@ -106,8 +106,21 @@ let make_website user_options universe statistics ds =
menu_item = No_menu (1, to_html ~active:"date" ~compare_pkg);
menu_srcurl = None;
} in
let revdeps =
let compare_pkg =
O2wPackage.compare_revdeps universe.rev_depends
in
{
menu_source = content_dir;
menu_link = Uri.make ~path:(packages_prefix^"/index-rev-deps.html") ();
menu_link_text = "Packages";
menu_link_html = Html.string "Packages";
menu_item = No_menu (1, to_html ~active:"rev-deps" ~compare_pkg);
menu_srcurl = None;
}
in
match universe.name_popularity with
| None -> [ date ]
| None -> [ revdeps; date ]
| Some s ->
let compare_pkg =
O2wPackage.compare_popularity ~reverse:true s
Expand All @@ -120,7 +133,7 @@ let make_website user_options universe statistics ds =
menu_item = No_menu (1, to_html ~active:"popularity" ~compare_pkg);
menu_srcurl = None;
} in
[ popularity; date ]
[ popularity; revdeps; date ]
in
include_files user_options.out_dir user_options.files_dir;
let about_page =
Expand Down
2 changes: 2 additions & 0 deletions content/universe.xhtml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
<th>Name</th>
<th>Latest version</th>
<th>Description</th>
<th>Rev-deps</th>
<th>Dep-cone</th>
</tr>
</thead>
<t:insert name="pkgs" />
Expand Down
12 changes: 12 additions & 0 deletions src/o2wPackage.ml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ let compare_popularity ?(reverse = false) pkg_stats p1 p2 =
Int64.compare c1 c2
| _ -> compare_alphanum p1 p2

(* Comparison function using the number of reverse dependencies for each package *)
let compare_revdeps rev_depends p1 p2 =
let pkg_count pkg =
match OpamPackage.Map.find_opt pkg rev_depends with
| None -> 0
| Some rdeps ->
OpamPackage.Name.Set.cardinal (OpamPackage.names_of_packages rdeps)
in
match pkg_count p1, pkg_count p2 with
| c1, c2 when c1 <> c2 -> compare c2 c1
| _ -> compare_alphanum p1 p2

(* Comparison function using the publication time of packages *)
let compare_date ?(reverse = false) pkg_dates p1 p2 =
let pkg_date pkg =
Expand Down
4 changes: 4 additions & 0 deletions src/o2wPackage.mli
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ val compare_alphanum: package -> package -> int
val compare_date: ?reverse:bool -> float package_map ->
package -> package -> int

(** Compare packages by number of reverse dependencies, most first *)
val compare_revdeps: package_set package_map ->
package -> package -> int

(** Compare packages by popularity *)
val compare_popularity: ?reverse:bool -> int64 name_map ->
package -> package -> int
Expand Down
4 changes: 4 additions & 0 deletions src/o2wTypes.mli
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ type univ = {
version_downloads: (int64 package_map * (package * package_set) OpamStd.String.Map.t) option;
depends: package_set package_map;
rev_depends: package_set package_map;
(** Number of distinct package names in each package's dependency cone, itself included.
An over-approximation of the output of [opam list --rec --required-by], because we
include packages of every platform (os, arch, ...). *)
dependency_cone_sizes: int package_map;
depopts: package_set package_map;
rev_depopts: package_set package_map;
}
Expand Down
89 changes: 88 additions & 1 deletion src/o2wUniverse.ml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,75 @@ let rev_depends deps =
(OpamPackage.Set.add pkg) OpamPackage.Set.empty))
deps OpamPackage.Map.empty


(* Dependencies used for the dependency cone. They differ from [depends] above.
They are an approximation of what [opam list --required-by] would show because we cannot
resolve variables that need a switch (os, arch, ...). To avoid including dependencies of multiple platforms
(say both linux and macos), we remove disjunctions (by picking a single branch).

[filter_default] is true to match OpamSwitchState.dependencies, which keeps a dependency whose
filter it cannot decide.
*)
let rec remove_disjunction = function
| (OpamFormula.Empty as x) | (Atom _ as x) -> x
| Block x -> remove_disjunction x
| And (a, b) -> And (remove_disjunction a, remove_disjunction b)
| Or (a, _) -> remove_disjunction a

let cone_depends st =
OpamPackage.Map.fold (fun pkg opam ->
let deps =
OpamFormula.packages st.OpamStateTypes.packages @@
remove_disjunction @@
OpamPackageVar.all_depends ~build:true ~post:false ~test:false
~doc:false ~dev_setup:false ~depopts:false ~filter_default:true
st opam
in
OpamPackage.Map.add pkg deps)
st.opams OpamPackage.Map.empty


(* A graph where nodes are packages and there is an edge a->b if package a depends on package b. *)
module PkgGraph = struct
type t = package_set package_map
module V = OpamPackage
let iter_vertex f deps = OpamPackage.Map.iter (fun pkg _ -> f pkg) deps
let iter_succ f deps pkg =
match OpamPackage.Map.find_opt pkg deps with
| None -> ()
| Some pkgs -> OpamPackage.Set.iter f pkgs
end

module PkgTopo = Graph.Topological.Make (PkgGraph)

let dependency_cone_sizes depends =
(* packages in reverse topological order *)
let pkgs = PkgTopo.fold (fun pkg acc -> pkg :: acc) depends [] in
Comment on lines +144 to +159

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need a graph here? Isn't getting the cardinal of max disjunction sufficient?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you mean by "max disjuction". If we computed the dependency cone for each package in the switch independently (using, for ex, OpamSwitchState.dependencies) there'd be too much recomputation.

Argument depends here is already the representation of a directed graph (a map from pkg to list of pkgs, ie, an adjacency list representation). Using this, we can reuse OcamlGraph for traversals to avoid recomputation. Or we can write a custom traversal (but it's still a graph traversal)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, yeah i was originally thinking of a brute force approach + hashtable to avoid recomputation, but it is 25x slower if you take the whole universe. The topological graph approach is clever, neat approximation.

let cones =
List.fold_left (fun acc pkg ->
let deps =
OpamStd.Option.default OpamPackage.Set.empty
(OpamPackage.Map.find_opt pkg depends)
in
(* Some packages include [ocaml] as a dependency. We remove [ocaml] from their cone to avoid including
also ocaml-base-compiler, ocaml-option-bytecode-only, ...
Except for [ocaml] itself, for which we want to see the full cone. *)
let deps = match OpamPackage.Name.to_string (OpamPackage.name pkg) with
| "ocaml" -> deps
| _ -> OpamPackage.Set.filter (fun p -> OpamPackage.Name.to_string p.name <> "ocaml") deps
in
let cone =
OpamPackage.Set.fold (fun dep cone ->
match OpamPackage.Map.find_opt dep acc with
| Some dep_cone -> OpamPackage.Name.Set.union dep_cone cone
| None -> cone)
deps (OpamPackage.Name.Set.singleton pkg.name)
in
OpamPackage.Map.add pkg cone acc)
OpamPackage.Map.empty pkgs
in
OpamPackage.Map.map OpamPackage.Name.Set.cardinal cones

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid having every almost package start at 10 dependencies, we could cheat a little and only show the number of dependencies starting from the ocaml package. What do you think?

Suggested change
OpamPackage.Map.map OpamPackage.Name.Set.cardinal cones
let ocaml_cardinal =
OpamPackage.Name.Set.cardinal
(OpamPackage.Map.find (OpamPackage.of_string "ocaml.5.5.0") cones)
in
OpamPackage.Map.map (fun set ->
Int.max 0 (OpamPackage.Name.Set.cardinal set - ocaml_cardinal))
cones

@ferminr ferminr Aug 15, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good suggestion. But we should not substract ocaml_cardinal unconditionally from every cone, only from those with pacakges which included ocaml explicitly as a dependency. (If we did, many packages would end up with a cone of size < 0). Instead of updating cardinalities I've removed the ocaml dependency earlier in the calculation.

An interesting example. Inspecting the dependency cone of alcobar (opam list --rec --required-by "alcobar.0.3.1") we see jbuilder. I asked an LLM for an explanation, and I got this back:

alcobar depends on alcotest with no version constraint, so that resolves to every alcotest in the repository — 1.0.1 through 1.9.1. alcotest.1.0.1 is a 2020 release, and it wants re {>= "1.7.2"}, which again is open-ended upward but reaches down to re.1.7.2 — a 2018 release, from back when re was built with jbuilder, dune's predecessor.

It further suggests:

If you wanted the number closer to "what would actually get installed", the lever is picking a single version per dependency name — the latest satisfying one, say — rather than all of them. That's a change in cone_depends, where OpamFormula.packages currently returns the full satisfying set.

Not planning to do anything about it in this PR, since opam list behaves the same way.


let to_page ~prefix universe pkg acc =
try
if Unix.isatty Unix.stdout then
Expand Down Expand Up @@ -241,6 +310,8 @@ let load statistics repo_roots =
let rdeps = rev_depends deps in
let depopts = depends st OpamFile.OPAM.depopts in
let rev_depopts = rev_depends depopts in
Printf.printf "++ Computing dependency cones.\n%!";
let dependency_cone_sizes = dependency_cone_sizes (cone_depends st) in
Printf.printf "++ Getting package modification dates from git.\n%!";
let dates = dates st in
let version_downloads, name_popularity =
Expand All @@ -262,6 +333,7 @@ let load statistics repo_roots =
name_popularity;
depends = deps;
rev_depends = rdeps;
dependency_cone_sizes;
depopts;
rev_depopts;
}
Expand Down Expand Up @@ -314,6 +386,19 @@ let to_html ~content_dir ~sortby_links ~active ~compare_pkg univ =
]
| None -> []
in
(* Number of reverse dependencies *)
let pkg_nb_rev_depends =
match OpamPackage.Map.find_opt pkg univ.rev_depends with
| None -> 0
| Some rdeps ->
OpamPackage.Name.Set.cardinal (OpamPackage.names_of_packages rdeps)
in
(* Size of the dependency cone: everything an installation of this
package pulls in, itself included. *)
let pkg_dependency_cone_size =
OpamStd.Option.default 0
(OpamPackage.Map.find_opt pkg univ.dependency_cone_sizes)
in
let tags = String.concat " " (OpamFile.OPAM.tags pkg_info) in
let pkg_tags = if tags = "" then [] else ["Tags: "^tags] in
let pkg_tooltip = String.concat " | " (pkg_download @ pkg_published @ pkg_tags) in
Expand All @@ -329,7 +414,9 @@ let to_html ~content_dir ~sortby_links ~active ~compare_pkg univ =
(Html.a ~href:pkg_href
(Html.string (OpamPackage.name_to_string pkg)))
@ Html.tag "td" (Html.string (OpamPackage.version_to_string pkg))
@ Html.tag "td" synopsis))
@ Html.tag "td" synopsis
@ Html.tag "td" (Html.int pkg_nb_rev_depends)
@ Html.tag "td" (Html.int pkg_dependency_cone_size)))
:: acc)
[]
(List.rev sorted_packages)
Expand Down