Monorepo management for opam overlays
0
fork

Configure Feed

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

monopam-info: extract dune-package library index into monopam-info.index

The dead-lib detection in monopam read dune-package files inline,
duplicating ad-hoc Sexp pattern matching that already had a typed
codec in nox-sexp.codecs.dune.Dune_package. Move that logic into a
new sublib monopam-info.index, which:

- builds the (library -> exposed modules) index from
_opam/lib/<pkg>/dune-package (preferred) and a *.cmi-listing
fallback for packages without dune-package metadata;
- tracks which libraries implement a virtual library so callers can
whitelist them as link-time live.

monopam.lint now consumes the typed Monopam_info_index API instead of
peeking at Sexp values, and monopam-info gains eio/fpath/nox-loc/nox-sexp
deps.

+14 -145
+1
lib/dune
··· 20 20 nox-meta 21 21 nox-meta.bytesrw 22 22 nox-git 23 + monopam-info.index 23 24 re 24 25 retry 25 26 nox-tty
+13 -145
lib/lint.ml
··· 425 425 426 426 (* ---- Dead-lib detection ---- 427 427 428 - Per stanza: a library in [(libraries L)] is dead if none of the 429 - modules it exposes appear in any [.ml] / [.mli] in the same directory. 430 - Module names come from [_opam/lib/<lib>/dune-package] when present 431 - (parsed for [(library (name X) (main_module_name Y) (modules ...))]), 432 - otherwise from a fallback that lists [*.cmi] basenames in the install 433 - directory. *) 434 - 435 - let capitalize s = 436 - if s = "" then s 437 - else 438 - String.uppercase_ascii (String.sub s 0 1) 439 - ^ String.sub s 1 (String.length s - 1) 440 - 441 - (** Walk every (library ...) sub-stanza of a parsed dune-package file and return 442 - a list of [(lib_name, module_set, is_implementation)] tuples. 443 - [is_implementation = true] when the stanza has [(implements X)] — a concrete 444 - impl of a virtual library, always link-time live even when its modules 445 - aren't named in source. *) 446 - let libraries_in_dune_package sexps = 447 - let module_set_of_main fields = 448 - match field "main_module_name" fields with 449 - | Some (Sexp.Atom n :: _) -> String_set.singleton n 450 - | _ -> String_set.empty 451 - in 452 - let rec module_set_of_modules acc s = 453 - match s with 454 - | Sexp.List (Sexp.Atom "module" :: fs) -> ( 455 - match field "obj_name" fs with 456 - | Some (Sexp.Atom n :: _) -> String_set.add (capitalize n) acc 457 - | _ -> acc) 458 - | Sexp.List xs -> List.fold_left module_set_of_modules acc xs 459 - | _ -> acc 460 - in 461 - List.filter_map 462 - (function 463 - | Sexp.List (Sexp.Atom "library" :: fields) -> ( 464 - match field "name" fields with 465 - | Some (Sexp.Atom name :: _) -> 466 - let mods = module_set_of_main fields in 467 - let mods = 468 - match field "modules" fields with 469 - | Some xs -> List.fold_left module_set_of_modules mods xs 470 - | None -> mods 471 - in 472 - let is_impl = field "implements" fields <> None in 473 - Some (name, mods, is_impl) 474 - | _ -> None) 475 - | _ -> None) 476 - sexps 477 - 478 - let contains_double_underscore s = 479 - let n = String.length s in 480 - let rec loop i = 481 - if i + 1 >= n then false 482 - else if s.[i] = '_' && s.[i + 1] = '_' then true 483 - else loop (i + 1) 484 - in 485 - loop 0 486 - 487 - let load_eio_path eio_path = 488 - try Some (Eio.Path.load eio_path) with Eio.Io _ -> None 489 - 490 - (** Capitalised basenames of [*.cmi] files directly in [pkg_dir], excluding 491 - wrapped-private modules (those with [__] in the name). Fallback for 492 - libraries that pre-date dune-package metadata (zarith, asn1-combinators, 493 - ounit2 and similar). *) 494 - let cmi_modules_in_dir pkg_dir = 495 - let entries = try Eio.Path.read_dir pkg_dir with Eio.Io _ -> [] in 496 - List.fold_left 497 - (fun acc entry -> 498 - if Filename.check_suffix entry ".cmi" then 499 - let base = Filename.chop_suffix entry ".cmi" in 500 - if contains_double_underscore base then acc 501 - else String_set.add (capitalize base) acc 502 - else acc) 503 - String_set.empty entries 504 - 505 - type lib_index = { 506 - modules : (string, String_set.t) Hashtbl.t; 507 - (** Library name -> set of exposed top-level modules. *) 508 - virtual_impls : (string, unit) Hashtbl.t; 509 - (** Library names that implement a virtual library — always link-time live 510 - even when none of their modules appear in source. *) 511 - } 512 - 513 - (** Scan [_opam/lib/] and [_build/install/.../lib/] for libraries. For each 514 - package directory, prefer [dune-package]'s precise metadata; fall back to 515 - listing [*.cmi]. *) 516 - let build_lib_index ~fs ~monorepo = 517 - let modules = Hashtbl.create 256 in 518 - let virtual_impls = Hashtbl.create 16 in 519 - let scan root = 520 - let entries = try Eio.Path.read_dir root with Eio.Io _ -> [] in 521 - List.iter 522 - (fun pkg -> 523 - let pkg_dir = Eio.Path.(root / pkg) in 524 - let dune_pkg = Eio.Path.(pkg_dir / "dune-package") in 525 - let covered = ref false in 526 - (match load_eio_path dune_pkg with 527 - | None -> () 528 - | Some content -> 529 - let libs = libraries_in_dune_package (parse_sexps content) in 530 - List.iter 531 - (fun (name, mods, is_impl) -> 532 - if is_impl then Hashtbl.replace virtual_impls name (); 533 - if not (String_set.is_empty mods) then begin 534 - covered := true; 535 - let existing = 536 - try Hashtbl.find modules name 537 - with Not_found -> String_set.empty 538 - in 539 - Hashtbl.replace modules name (String_set.union existing mods) 540 - end) 541 - libs); 542 - if not !covered then 543 - let mods = cmi_modules_in_dir pkg_dir in 544 - if not (String_set.is_empty mods) then 545 - let existing = 546 - try Hashtbl.find modules pkg with Not_found -> String_set.empty 547 - in 548 - Hashtbl.replace modules pkg (String_set.union existing mods)) 549 - entries 550 - in 551 - let opam_lib = 552 - Eio.Path.(fs / Fpath.to_string Fpath.(monorepo / "_opam" / "lib")) 553 - in 554 - let build_lib = 555 - Eio.Path.( 556 - fs 557 - / Fpath.to_string 558 - Fpath.(monorepo / "_build" / "install" / "default" / "lib")) 559 - in 560 - scan opam_lib; 561 - scan build_lib; 562 - { modules; virtual_impls } 428 + Per stanza: a library in [(libraries L)] is dead if none of the modules 429 + it exposes appear in any [.ml] / [.mli] in the same directory. The 430 + library-to-modules index is built by {!Monopam_info_index.Index} from 431 + [dune-package] metadata + a [*.cmi]-listing fallback. *) 563 432 564 433 let module_ref_re = 565 434 Re.compile ··· 659 528 List.filter_map 660 529 (fun lib -> 661 530 if is_builtin lib then None 662 - else if Hashtbl.mem lib_index.virtual_impls lib then None 531 + else if 532 + Monopam_info_index.is_virtual_implementation lib_index 533 + lib 534 + then None 663 535 else 664 - match Hashtbl.find_opt lib_index.modules lib with 665 - | None -> None 536 + match Monopam_info_index.modules lib_index lib with 537 + | [] -> None 666 538 (* Unknown lib: don't flag — could be a sibling 667 539 library with a private name, or a build system 668 540 not reflected in install dirs. *) 669 - | Some mods when String_set.is_empty mods -> None 670 - | Some mods -> 671 - if 672 - String_set.exists 673 - (fun m -> String_set.mem m refs) 674 - mods 541 + | mods -> 542 + if List.exists (fun m -> String_set.mem m refs) mods 675 543 then None 676 544 else 677 545 Some { subtree; kind = Dead_lib; package = lib }) ··· 943 811 let build_lib = Fpath.(monorepo / "_build" / "install" / "default" / "lib") in 944 812 let subdirs = list_subdirs ~fs ~monorepo in 945 813 let sources = load_sources ~fs ~monorepo in 946 - let lib_index = build_lib_index ~fs ~monorepo in 814 + let lib_index = Monopam_info_index.build ~fs ~monorepo in 947 815 let source_issues = 948 816 compute_source_issues ~fs ~monorepo ~sources subdirs |> sort_source_issues 949 817 in