package uniq
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
A library which introspect and infer dependencies via codept, ocamlfind and opam
Install
dune-project
Dependency
Authors
Maintainers
Sources
uniq-0.1.0.tbz
sha256=998588f1053cf03161a4bded6d7f8068c88de497e77fc0bbca81a5234fa444d5
sha512=53d4ee5ad8c01e19a2e1fbda86266768d118f78566b82f0c736338ce2c12258185a5f4dbb90bf0da37a7528098266ec81861bdcfea29e55daa3122a4f3a1daca
doc/src/uniq.mmeta/uniq_meta.ml.html
Source file uniq_meta.ml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836let src = Logs.Src.create "uniq.meta" module Log = (val Logs.src_log src : Logs.LOG) let error_msgf fmt = Fmt.kstr (fun msg -> Error (`Msg msg)) fmt let ( let* ) = Result.bind let absolute = let cwd = Fpath.v (Sys.getcwd ()) in fun path -> let path = if Fpath.is_rel path then Fpath.(cwd // path) else path in Fpath.normalize path type t = | Node of { name: string; value: string; contents: t list } (** [name] "[value]" ( [contents] ), like [package "lib" ( ... )] *) | Set of { name: string; predicates: predicate list; value: string } (** [name] [(...) as predicates] = [value], like [archive(native) = "lib.cmxa"]*) | Add of { name: string; predicates: predicate list; value: string } (** [name] [(...) as predicates] = [value], like [archive(native) += "lib.cmxa"]*) and predicate = Include of string | Exclude of string let pp_predicate ppf = function | Include p -> Fmt.string ppf p | Exclude p -> Fmt.pf ppf "-%s" p let rec pp ppf = function | Node { name; value; contents } -> Fmt.pf ppf "%s %S (@\n@[<2>%a@]@\n)" name value Fmt.(list ~sep:(any "@\n") pp) contents | Set { name; predicates= []; value } -> Fmt.pf ppf "%s = %S" name value | Set { name; predicates; value } -> Fmt.pf ppf "%s(%a) = %S" name Fmt.(list ~sep:(any ",") pp_predicate) predicates value | Add { name; predicates= []; value } -> Fmt.pf ppf "%s += %S" name value | Add { name; predicates; value } -> Fmt.pf ppf "%s(%a) += %S" name Fmt.(list ~sep:(any ",") pp_predicate) predicates value module Assoc = struct type t = (string * string list) list let add k v t = match List.assoc_opt k t with | Some vs -> let vs = List.sort_uniq String.compare (v :: vs) in (k, vs) :: List.remove_assoc k t | None -> (k, [ v ]) :: t let set k v t = match List.assoc_opt k t with | Some _ -> (k, [ v ]) :: List.remove_assoc k t | None -> (k, [ v ]) :: t end module Path = struct type t = string list let of_string str = let pkg = String.split_on_char '.' str in let rec go = function | [] -> Ok pkg | "" :: _ -> error_msgf "Invalid package name: %S" str | _ :: rest -> go rest in go pkg let of_string_exn str = match of_string str with | Ok pkg -> pkg | Error (`Msg msg) -> invalid_arg msg let pp ppf pkg = Fmt.string ppf (String.concat "." pkg) let equal a b = try List.for_all2 String.equal a b with _ -> false let compare = List.compare String.compare let parent = function | [] -> None | segs -> Some (List.rev (List.tl (List.rev segs))) module Set = Set.Make (struct type nonrec t = t let compare = compare end) module Map = Map.Make (struct type nonrec t = t let compare = compare end) end let incl ~predicates ps = let one = function | Include p -> List.exists (String.equal p) predicates | Exclude p -> not (List.exists (String.equal p) predicates) in List.exists one ps let find_directory ~predicates contents = let rec go result = function | [] -> result | Add { name= "directory"; predicates= []; value } :: rest -> if Stdlib.Option.is_none result then go (Some value) rest else go result rest | Add { name= "directory"; predicates= ps; value } :: rest -> if incl ~predicates ps && Stdlib.Option.is_none result then go (Some value) rest else go result rest | Set { name= "directory"; predicates= []; value } :: rest -> go (Some value) rest | Set { name= "directory"; predicates= ps; value } :: rest -> if incl ~predicates ps then go (Some value) rest else go result rest | _ :: rest -> go result rest in go None contents let compile ~predicates t ks = let rec go ~directory acc t = function | [] -> let rec go acc = function | [] -> let acc = List.remove_assoc "directory" acc in ("directory", [ directory ]) :: acc | Node _ :: rest -> go acc rest | Add { name; predicates= []; value } :: rest -> go (Assoc.add name value acc) rest | Set { name; predicates= []; value } :: rest -> go (Assoc.set name value acc) rest | Add { name; predicates= ps; value } :: rest -> if incl ~predicates ps then go (Assoc.add name value acc) rest else go acc rest | Set { name; predicates= ps; value } :: rest -> if incl ~predicates ps then go (Assoc.set name value acc) rest else go acc rest in go acc t | k :: ks -> ( match t with | [] -> acc | Node { name= "package"; value; contents } :: rest -> let directory' = match find_directory ~predicates contents with | Some v -> Filename.concat directory v | None -> directory in if k = value then go ~directory:directory' acc contents ks else go ~directory acc rest (k :: ks) | _ :: rest -> go ~directory acc rest (k :: ks)) in go ~directory:"" [] t ks exception Parser_error of string let raise_parser_error lexbuf fmt = let p = Lexing.lexeme_start_p lexbuf in let c = p.Lexing.pos_cnum - p.Lexing.pos_bol + 1 in Fmt.kstr (fun msg -> raise (Parser_error msg)) ("%s (l.%d c.%d): " ^^ fmt) p.Lexing.pos_fname p.Lexing.pos_lnum c let pp_token ppf = function | Uniq_meta_lexer.Name name -> Fmt.string ppf name | String str -> Fmt.pf ppf "%S" str | Minus -> Fmt.string ppf "-" | Lparen -> Fmt.string ppf "(" | Rparen -> Fmt.string ppf ")" | Comma -> Fmt.string ppf "," | Equal -> Fmt.string ppf "=" | Plus_equal -> Fmt.string ppf "+=" | Eof -> Fmt.string ppf "#eof" let invalid_token lexbuf token = raise_parser_error lexbuf "Invalid token %a" pp_token token let lparen lexbuf = match Uniq_meta_lexer.token lexbuf with | Lparen -> () | token -> invalid_token lexbuf token let name lexbuf = match Uniq_meta_lexer.token lexbuf with | Name name -> name | token -> invalid_token lexbuf token let string lexbuf = match Uniq_meta_lexer.token lexbuf with | String str -> str | token -> invalid_token lexbuf token let rec predicates lexbuf acc = match Uniq_meta_lexer.token lexbuf with | Rparen -> List.rev acc | Name predicate -> begin match Uniq_meta_lexer.token lexbuf with | Comma -> predicates lexbuf (Include predicate :: acc) | Rparen -> List.rev (Include predicate :: acc) | token -> invalid_token lexbuf token end | Minus -> let predicate = name lexbuf in begin match Uniq_meta_lexer.token lexbuf with | Comma -> predicates lexbuf (Exclude predicate :: acc) | Rparen -> List.rev (Exclude predicate :: acc) | token -> invalid_token lexbuf token end | token -> invalid_token lexbuf token let rec parser lexbuf depth acc = match Uniq_meta_lexer.token lexbuf with | Rparen when depth > 0 -> List.rev acc | Rparen -> raise_parser_error lexbuf "Closing parenthesis without matching opening one" | Eof when depth = 0 -> List.rev acc | Eof -> raise_parser_error lexbuf "%d closing parenthesis missing" depth | Name name -> begin match Uniq_meta_lexer.token lexbuf with | String value -> lparen lexbuf; let contents = parser lexbuf (succ depth) [] in parser lexbuf depth (Node { name; value; contents } :: acc) | Equal -> let value = string lexbuf in parser lexbuf depth (Set { name; predicates= []; value } :: acc) | Plus_equal -> let value = string lexbuf in parser lexbuf depth (Add { name; predicates= []; value } :: acc) | Lparen -> let predicates = predicates lexbuf [] in begin match Uniq_meta_lexer.token lexbuf with | Equal -> let value = string lexbuf in parser lexbuf depth (Set { name; predicates; value } :: acc) | Plus_equal -> let value = string lexbuf in parser lexbuf depth (Add { name; predicates; value } :: acc) | token -> invalid_token lexbuf token end | token -> invalid_token lexbuf token end | token -> invalid_token lexbuf token let error_msgf fmt = Fmt.kstr (fun msg -> Error (`Msg msg)) fmt let parser lexbuf = try Ok (parser lexbuf 0 []) with | Parser_error err -> Error (`Msg err) | Uniq_meta_lexer.Lexical_error (msg, f, l, c) -> error_msgf "%s at l.%d, c.%d: %s" f l c msg let parser path = Log.debug (fun m -> m "parse %a" Fpath.pp path); let ( let@ ) finally fn = Fun.protect ~finally fn in let ic = open_in (Fpath.to_string path) in let@ _ = fun () -> close_in ic in let lexbuf = Lexing.from_channel ic in Lexing.set_filename lexbuf (Fpath.to_string path); parser lexbuf let rec incl us vs = match (us, vs) with | u :: us, v :: vs -> if u = v then incl us vs else false | [], _ | _, [] -> true let rec diff us vs = match (us, vs) with | u :: us, v :: vs -> if u = v then diff us vs else error_msgf "Different paths (%S <> %S)" u v | [], x | x, [] -> Ok x let relativize ~roots path = let rec go = function | [] -> assert false | root :: roots -> if Fpath.is_prefix root path then match Fpath.relativize ~root path with | Some rel -> (root, rel) | None -> go roots else go roots in go roots let search ~roots ?(predicates = [ "native"; "byte" ]) meta_path = let ( >>= ) = Result.bind in let ( >>| ) x fn = Result.map fn x in let elements path = if Sys.is_directory (Fpath.to_string path) then Ok false else if Fpath.basename path = "META" then Ok true else Ok false in let traverse path = if List.exists (Fpath.equal path) roots then Ok true else begin let _, rel = relativize ~roots path in let meta_path' = List.filter (fun s -> s <> "") (Fpath.segs rel) in Ok (incl meta_path meta_path') end in let fold path acc = let root, rel = relativize ~roots path in let package = Fpath.(rem_empty_seg (parent rel)) in let meta_path' = Fpath.(segs package) in match diff meta_path meta_path' >>= fun ks -> parser path >>| fun meta -> compile ~predicates meta ks with | Ok descr -> Fpath.Map.add Fpath.(root // parent rel) descr acc | Error (`Msg msg) -> Log.warn (fun m -> m "impossible to extract the META file of %a: %s" Fpath.pp path msg); acc in let err _path _ = Ok () in Bos.OS.Path.fold ~err ~dotfiles:false ~elements:(`Sat elements) ~traverse:(`Sat traverse) fold Fpath.Map.empty roots >>| Fpath.Map.bindings let requires descr = (* [requires] values are whitespace-separated and frequently span several lines in real META files (e.g. tls, x509); we must split on any blank, not just spaces, otherwise tokens keep a trailing newline and resolution of the dependency closure is silently truncated. *) Stdlib.Option.value ~default:[] (List.assoc_opt "requires" descr) |> List.concat_map (Astring.String.fields ~empty:false ~is_sep:Astring.Char.Ascii.is_white) |> List.map Path.of_string_exn let dependencies_of (_path, descr) = requires descr exception Cycle let get_dependencies (_, path, descr) graph = let deps = dependencies_of (path, descr) in let fn name = match List.find_opt (fun (name', _, _) -> Path.equal name name') graph with | Some node -> [ node ] | None -> [] in List.concat_map fn deps type graph = (Path.t * Fpath.t * Assoc.t) list let dfs (graph : graph) visited start = let rec explore path visited node = if List.mem node path then raise Cycle else if List.mem node visited then visited else let new_path = node :: path in let edges = get_dependencies node graph in let visited = List.fold_left (explore new_path) visited edges in node :: visited in explore [] visited start (* topological sort *) let sort graph = let fn visited node = dfs graph visited node in List.fold_left fn [] graph (* NOTE(dinosaure): [ancestors] exists if we would like to resolve dependencies via [META] files. However, we prefer to resolve dependencies via OCaml objects and their metadata. So, this function is a bit of useless but let's keep it for what it might potentially solve. *) let ancestors ~roots ?(predicates = [ "native"; "byte" ]) mpath = let rec go acc visited = function | [] -> Ok acc | mpath :: todo when List.mem mpath visited -> go acc visited todo | mpath :: todo -> begin match search ~roots ~predicates mpath with | Ok pkgs -> let requires = List.concat (List.map dependencies_of pkgs) in let fn (path, descr) = (mpath, path, descr) in let pkgs = List.map fn pkgs in go (List.rev_append pkgs acc) (mpath :: visited) (List.rev_append requires todo) | Error _ as err -> err end in let* lst = go [] [] [ mpath ] in Ok (sort lst |> List.rev) let to_artifacts pkgs = let ( let* ) = Result.bind in let fn acc (path, pkg) = match acc with | Error _ as err -> err | Ok acc -> let directory = List.assoc_opt "directory" pkg in let* directory = match directory with (* A META [directory] may be a relative {e path} ("foo/bar"), not a single segment, so append it as a path rather than a segment. *) | Some [ dir ] -> ( match Fpath.of_string dir with | Ok rel -> Ok Fpath.(path // rel) | Error _ -> Ok Fpath.(path / dir)) | Some _ -> error_msgf "Multiple directories referenced by %a" Fpath.pp Fpath.(path / "META") | None -> Ok path in let directory = Fpath.to_dir_path directory in (* We keep the linkable OCaml objects: archives ([.cma]/[.cmxa]) and standalone units ([.cmo]/[.cmx]) that single-module packages ship directly. Plugins ([.cmxs]) are not readable as OCaml objects and carry no extra information for us. *) let archive = List.assoc_opt "archive" pkg in let archive = Stdlib.Option.value ~default:[] archive in let keep a = match Filename.extension a with | ".cma" | ".cmxa" | ".cmo" | ".cmx" -> true | _ -> false in let archive = List.filter keep archive in let archive = List.map (Fpath.add_seg directory) archive in let archive = List.filter (fun p -> Sys.file_exists (Fpath.to_string p)) archive in Ok (List.rev_append archive acc) in let* paths = List.fold_left fn (Ok []) pkgs in Uniq_info.vs paths let subpaths (meta : t list) : string list list = let rec go prefix acc = function | [] -> acc | Node { name= "package"; value; contents; _ } :: rest -> let path = prefix @ [ value ] in let sub = go path [] contents in go prefix ((path :: sub) @ acc) rest | _ :: rest -> go prefix acc rest in [] :: go [] [] meta module MSet = Set.Make (Modname) let submodules path = let cmi = Cmi_format.read_cmi path in let fn = function | Types.Sig_module (name, _, _, _, _) -> Some (Modname.v (Ident.name name)) | _ -> None in match List.filter_map fn cmi.cmi_sign with | value -> value | exception _ -> [] (* Given a META-described (sub)package at [meta_dir] with descriptor [descr], return the directory where its .cmi files live. *) let package_directory dname descr = match List.assoc_opt "directory" descr with | Some [ d ] when d <> "" -> begin match Fpath.of_string d with | Ok rel -> Fpath.(dname // rel |> to_dir_path) | Error _ -> Fpath.to_dir_path dname end | _ -> Fpath.to_dir_path dname (* A (sub)package actually ships compiled units only when it declares a non-empty [archive]. Packages without one (virtual bases like [digestif], deprecated redirects like [angstrom.async]) inherit the parent's directory and would otherwise be spuriously credited with the parent's modules. *) let has_archive descr = match List.assoc_opt "archive" descr with | Some archives -> List.exists (fun a -> a <> "") archives | None -> false (* Register [full] (a package) as a provider of [modname] in [acc] when [modname] is among the [targets] we are looking for. *) let register_if_target targets full modname acc = if MSet.mem modname targets then let fn = function | None -> Some [ full ] | Some pkgs -> Some (full :: pkgs) in Modname.Map.update modname fn acc else acc (* Scan the [*.cmi] files inside [dname] and register every top-level module whose name belongs to [targets]. When [and_submodules] is true, also open each [*.cmi] to discover sub-modules (e.g. [Cmdliner] exports [Cmd], [Term]). *) let scan_cmis ~and_submodules ~targets ~full ~dname acc = if Sys.file_exists dname && Sys.is_directory dname then let files = Sys.readdir dname in let fn acc fname = if Filename.check_suffix fname ".cmi" then let base = Filename.chop_suffix fname ".cmi" in let modname = Modname.v (String.capitalize_ascii base) in let acc = register_if_target targets full modname acc in if and_submodules then let subs = submodules (Filename.concat dname fname) in let fn acc modname = register_if_target targets full modname acc in List.fold_left fn acc subs else acc else acc in Array.fold_left fn acc files else acc let with_a_cmi filepath = (* TODO(dinosaure): be more restrictive and also check the current [META] to see if we can manipulate the given [*.mli]. *) let filepath = Fpath.set_ext "cmi" filepath in let filepath = Fpath.to_string filepath in Sys.file_exists filepath && Sys.is_regular_file filepath let scan_mlis ~targets ~full ~dname acc = if Sys.file_exists dname && Sys.is_directory dname then let files = Sys.readdir dname in let fn acc fname = let filepath = Fpath.(v dname / fname) in if Filename.check_suffix fname ".mli" && with_a_cmi filepath then try let filepath = Fpath.(v dname / fname) in let kind = { Read.format= Read.Src; kind= M2l.Signature } in let namespace = Namespaced.make (Filename.chop_suffix fname ".mli") in let v = Comp_unit.read_file Uniq_ml.Param.fault_handler kind (Fpath.to_string filepath) namespace in let modname = Namespaced.module_name namespace in let modules = Uniq_info.collect_modules_on_mli ~modname v.Comp_unit.code in (* NOTE(dinosaure): the objective here is to try to find some modules which exists surely into a "sub-sub-module". A module (the first [_]) can be recognized via [*.cmi]. Inside it, we can find sub-modules (the second [_]). But we are not able to recognize sub-sub-module. For instance, we can have this code: {[ open X509 open Distinguished_name let v = Relative_distinguished_name.empty ]} [codept] will asks where is [Relative_distinguished_name] but it can prove that this module comes from [x509]. So if we still have remaining modules, we will try to introspect [*.mli] and find such modules. It should be noted that this is our last chance and we should not make this method of searching for packages the norm. *) let fn path acc = match Uniq_info.Path.to_list path with | _ :: _ :: rem -> let fn acc m = register_if_target targets full m acc in List.fold_left fn acc rem | _ -> acc in Uniq_info.Path.Set.fold fn modules acc with _exn -> acc else acc in Array.fold_left fn acc files else acc (* Walk every META file under [roots]; for each (sub)package, scan its [*.cmi] directory and populate the module -> packages map. *) let walk_meta_files ~roots ~predicates ~and_submodules ?(intf = `Cmi) ~targets acc = let elements path = let str = Fpath.to_string path in if not (Sys.file_exists str) then Ok false else if Sys.is_directory str then Ok false else Ok (Fpath.basename path = "META") in let fn meta acc = let _, rel = relativize ~roots meta in let segs = Fpath.(segs (rem_empty_seg (parent rel))) in let base = List.filter (fun s -> s <> "") segs in match parser meta with | Error _ -> acc | Ok m -> let metad = let open Fpath in parent meta |> rem_empty_seg |> to_dir_path |> to_string in let fn acc local = let full = base @ local in let descr = compile ~predicates m local in let dname = Fpath.to_string (package_directory Fpath.(parent meta |> rem_empty_seg) descr) in let owns_dir = local = [] || dname <> metad in if has_archive descr && owns_dir then match intf with | `Cmi -> scan_cmis ~and_submodules ~targets ~full ~dname acc | `Mli -> scan_mlis ~targets ~full ~dname acc else acc in List.fold_left fn acc (subpaths m) in let err _path _ = Ok () in Bos.OS.Path.fold ~err ~dotfiles:false ~elements:(`Sat elements) ~traverse:`Any fn acc roots |> Result.value ~default:acc (* Deduplicate packages per module name. *) let dedup result = let seen = Hashtbl.create 16 in let fn modname pkgs acc = let fn pkg = let key = String.concat "." pkg in match Hashtbl.find seen key with | _ -> false | exception Not_found -> Hashtbl.add seen key (); true in let uniques = List.filter fn (List.rev pkgs) in Hashtbl.reset seen; (modname, uniques) :: acc in Modname.Map.fold fn result [] |> List.rev let find_providers ~roots ?(predicates = [ "native"; "byte" ]) modules = let targets = List.fold_left (fun s m -> MSet.add m s) MSet.empty modules in (* Pass 1: match by .cmi filename *) let result = walk_meta_files ~roots ~predicates ~and_submodules:false ~targets Modname.Map.empty in (* Pass 2: for unresolved modules, also check sub-module exports *) let resolved = Modname.Map.fold (fun m _ s -> MSet.add m s) result MSet.empty in let remaining = MSet.diff targets resolved in let result = if MSet.is_empty remaining then result else walk_meta_files ~roots ~predicates ~and_submodules:true ~targets:remaining result in (* Pass 3: for unresolved modules, also check sub-*-module exports via [*.mli] *) let resolved = Modname.Map.fold (fun m _ s -> MSet.add m s) result MSet.empty in let remaining = MSet.diff remaining resolved in let result = if MSet.is_empty remaining then result else walk_meta_files ~roots ~predicates ~and_submodules:true ~intf:`Mli ~targets:remaining result in dedup result type archive = Stdlib of Fpath.t | Library of Path.t * Fpath.t * Assoc.t type package = { pkg: Path.t ; meta_dirpath: Fpath.t ; dirpath: Fpath.t ; descr: Assoc.t } let packages_with_archive ?(predicates = [ "native"; "byte" ]) roots = let elements path = let str = Fpath.to_string path in if not (Sys.file_exists str) then Ok false else if Sys.is_directory str then Ok false else Ok (Fpath.basename path = "META") in let fn meta acc = let _, rel = relativize ~roots meta in let segs = Fpath.(segs (rem_empty_seg (parent rel))) in let base = List.filter (fun s -> s <> "") segs in match parser meta with | Error _ -> acc | Ok m -> let metad = Fpath.(parent meta |> rem_empty_seg) in let fn acc local = let full = base @ local in let descr = compile ~predicates m local in if has_archive descr then let dname = package_directory metad descr in { pkg= full; meta_dirpath= metad; dirpath= dname; descr } :: acc else acc in List.fold_left fn acc (subpaths m) in let err _path _ = Ok () in Bos.OS.Path.fold ~err ~dotfiles:false ~elements:(`Sat elements) ~traverse:`Any fn [] roots |> Result.value ~default:[] let dir_owns_cmi ~dname ~modname ~crc = match crc with | None -> false | Some crc -> let dir = Fpath.to_string dname in if Sys.file_exists dir && Sys.is_directory dir then let same_crc filepath = match Uniq_info.v filepath with | Ok info -> let crc' = Uniq_info.crc_of info modname in Stdlib.Option.map (Uniq_digest.equal crc) crc' |> Stdlib.Option.value ~default:false | Error _ -> false in let check fname = try let base = Filename.chop_suffix fname ".cmi" in (* Filename.check_suffix fname ".cmi" && *) Modname.compare Modname.(v (normalize base)) modname = 0 && same_crc Fpath.(dname / fname) with _exn -> false in Array.exists check (Sys.readdir dir) else false let stdlib_package dir = let dirpath = absolute (Fpath.to_dir_path dir) in Stdlib dirpath let from_cmi_to_impl ~roots ~packages:candidates ?stdlib ?(disambiguate = fun _ paths -> List.hd paths) filepath = if Fpath.mem_ext [ ".cmi" ] filepath = false then invalid_arg "You must give a *.cmi file"; if List.exists (fun root -> Fpath.is_rooted ~root filepath) roots = false then Fmt.invalid_arg "The given *.cmi (%a) is not a part of your roots" Fpath.pp filepath; let* info = Uniq_info.v filepath in if Uniq_info.is_a_cmi info = false then Fmt.invalid_arg "The given *.cmi (%a) is not a valid CMI file" Fpath.pp filepath; let modname = Uniq_info.modname info in let crc = Uniq_info.crc_of info modname in let cmi_dirpath = absolute Fpath.(parent filepath |> to_dir_path) in let is_stdlib = match stdlib with | Some dirpath -> Fpath.equal (absolute (Fpath.to_dir_path dirpath)) cmi_dirpath | None -> false in if is_stdlib then Ok (Some (stdlib_package (Stdlib.Option.get stdlib))) else let owns { dirpath; _ } = dir_owns_cmi ~dname:dirpath ~modname ~crc in let pick { pkg; meta_dirpath; descr; _ } = Library (pkg, meta_dirpath, descr) in let same_dir { dirpath; _ } = Fpath.equal (absolute dirpath) cmi_dirpath in (* Does the archive of this (sub)package actually pack [modname]? Several sibling subpackages may sit in the same directory and all {e own} the shared [*.cmi] (e.g. [fpath] and [fpath.top], or [digestif.c] and [digestif.ocaml]); the one we want is the one whose archive {e packs} the interface's module ([Fpath] lives in [fpath.cmxa], not in [fpath_top.cmxa] which packs [Fpath_top]). *) let packs_module { meta_dirpath; descr; _ } = match to_artifacts [ (meta_dirpath, descr) ] with | Error _ -> false | Ok archives -> let provides info = let fn (path, _) = match List.rev (Uniq_info.Path.to_list path) with | leaf :: _ -> Modname.compare leaf modname = 0 | [] -> false in List.exists fn (Uniq_info.exports info) in List.exists provides archives in (* Among candidates that all locate the [*.cmi], prefer the one whose archive packs [modname]. When several pack it — a genuine choice between equivalent implementations (e.g. [digestif.c] vs [digestif.ocaml]) — we defer to [ambiguity], which the caller may turn into a prompt. *) let choose = function | [] -> None | [ pkg ] -> Some pkg | pkgs -> ( match List.filter packs_module pkgs with | [] -> Some (List.hd pkgs) | [ pkg ] -> Some pkg | _ :: _ as several -> ( let chosen = disambiguate modname (List.map (fun p -> p.pkg) several) in match List.find_opt (fun p -> Path.equal p.pkg chosen) several with | Some pkg -> Some pkg | None -> Some (List.hd several))) in (* Sibling subpackages of [path] under the same parent ocamlfind package. Alternative implementations of one interface live there: [digestif.c] and [digestif.ocaml] each ship their own copy of [digestif.cmi], so whichever copy was picked upstream, the other implementation is a sibling. *) let siblings path = match Path.parent path with | Some (_ :: _ as parent) -> let fn c = (not (Path.equal c.pkg path)) && match Path.parent c.pkg with | Some p -> Path.equal p parent | None -> false in List.filter fn candidates | _ -> [] in let result = match List.filter same_dir candidates with | [ pkg ] -> ( (* Unique in this directory, but a sibling subpackage may implement the same interface ([digestif.c] vs [digestif.ocaml]); when one does, the choice is genuinely ambiguous. *) match siblings pkg.pkg with | [] -> Some pkg | sibs -> choose (pkg :: sibs)) | _ :: _ :: _ as several -> choose several (* No directory match (e.g. the [*.cmi] was relocated): scan by [crc]. *) | [] -> choose (List.filter owns candidates) in Ok (Stdlib.Option.map pick result) let archives_of ~roots ?(predicates = [ "native"; "byte" ]) = function | Stdlib dirpath -> let dirpath = absolute (Fpath.to_dir_path dirpath) in let name = if List.mem "native" predicates then "stdlib.cmxa" else "stdlib.cma" in let path = Fpath.(dirpath / name) in if Sys.file_exists (Fpath.to_string path) then Uniq_info.vs [ path ] else Ok [] | Library (pkg, _, _) -> let* descrs = search ~roots ~predicates pkg in to_artifacts descrs
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>