this repo has no description
at universe-builder 313 lines 12 kB view raw
1(* Kinda findlib, sorta *) 2 3type library = { 4 name : string; 5 meta_uri : Uri.t; 6 archive_name : string option; 7 dir : string option; 8 deps : string list; 9 children : library list; 10 mutable loaded : bool; 11} 12 13let rec flatten_libs libs = 14 let handle_lib l = 15 let children = flatten_libs l.children in 16 l :: children 17 in 18 List.map handle_lib libs |> List.flatten 19 20let preloaded = 21 [ 22 "angstrom"; 23 "astring"; 24 "compiler-libs.common"; 25 "compiler-libs.toplevel"; 26 "findlib"; 27 "findlib.top"; 28 "fpath"; 29 "js_of_ocaml-compiler"; 30 "js_of_ocaml-ppx"; 31 "js_of_ocaml-toplevel"; 32 "js_top_worker"; 33 "js_top_worker-rpc"; 34 "logs"; 35 "logs.browser"; 36 "merlin-lib.kernel"; 37 "merlin-lib.ocaml_parsing"; 38 "merlin-lib.query_commands"; 39 "merlin-lib.query_protocol"; 40 "merlin-lib.utils"; 41 "mime_printer"; 42 "uri"; 43 ] 44 45let rec read_libraries_from_pkg_defs ~library_name ~dir meta_uri pkg_expr = 46 try 47 Jslib.log "Reading library: %s" library_name; 48 let pkg_defs = pkg_expr.Fl_metascanner.pkg_defs in 49 (* Try to find archive with various predicates. 50 PPX packages often only define archive(ppx_driver,byte), so we need to 51 check multiple predicate combinations to find the right archive. *) 52 let archive_filename = 53 (* First try with ppx_driver,byte - this catches PPX libraries like ppx_deriving.show *) 54 try Some (Fl_metascanner.lookup "archive" [ "ppx_driver"; "byte" ] pkg_defs) 55 with _ -> ( 56 (* Then try plain byte *) 57 try Some (Fl_metascanner.lookup "archive" [ "byte" ] pkg_defs) 58 with _ -> ( 59 (* Then try native as fallback *) 60 try Some (Fl_metascanner.lookup "archive" [ "native" ] pkg_defs) 61 with _ -> None)) 62 in 63 64 (* Use -ppx_driver predicate for toplevel use - this ensures PPX packages 65 pull in their runtime dependencies (e.g., ppx_deriving.show requires 66 ppx_deriving.runtime when not using ppx_driver) *) 67 let predicates = ["-ppx_driver"] in 68 let deps_str = 69 try Fl_metascanner.lookup "requires" predicates pkg_defs with _ -> "" in 70 let deps = Astring.String.fields ~empty:false deps_str in 71 let subdir = 72 List.find_opt (fun d -> d.Fl_metascanner.def_var = "directory") pkg_defs 73 |> Option.map (fun d -> d.Fl_metascanner.def_value) 74 in 75 let dir = 76 match (dir, subdir) with 77 | None, None -> None 78 | Some d, None -> Some d 79 | None, Some d -> Some d 80 | Some d1, Some d2 -> Some (Filename.concat d1 d2) 81 in 82 let archive_name = 83 Option.bind archive_filename (fun a -> 84 let file_name_len = String.length a in 85 if file_name_len > 0 then Some (Filename.chop_extension a) else None) 86 in 87 Jslib.log "Number of children: %d" (List.length pkg_expr.pkg_children); 88 let children = 89 List.filter_map 90 (fun (n, expr) -> 91 Jslib.log "Found child: %s" n; 92 let library_name = library_name ^ "." ^ n in 93 match 94 read_libraries_from_pkg_defs ~library_name ~dir meta_uri expr 95 with 96 | Ok l -> Some l 97 | Error (`Msg m) -> 98 Jslib.log "Error reading sub-library: %s" m; 99 None) 100 pkg_expr.pkg_children 101 in 102 Ok 103 { 104 name = library_name; 105 archive_name; 106 dir; 107 deps; 108 meta_uri; 109 loaded = false; 110 children; 111 } 112 with Not_found -> Error (`Msg "Failed to read libraries from pkg_defs") 113 114type t = library list 115 116let dcs_filename = "dynamic_cmis.json" 117 118let fetch_dynamic_cmis sync_get url = 119 match sync_get url with 120 | None -> Error (`Msg "Failed to fetch dynamic cmis") 121 | Some json -> 122 let rpc = Jsonrpc.of_string json in 123 Rpcmarshal.unmarshal 124 Js_top_worker_rpc.Toplevel_api_gen.typ_of_dynamic_cmis rpc 125 126let (let*) = Lwt.bind 127 128(** Parse a findlib_index file (JSON or legacy text format) and return 129 the list of META file paths and universe paths. 130 131 JSON format: {"meta_files": ["path/to/META", ...], "universes": ["universe1", ...]} 132 133 meta_files: direct paths to META files 134 universes: paths to other universes (directories containing findlib_index) *) 135let parse_findlib_index content = 136 (* Try JSON format first *) 137 try 138 let json = Yojson.Safe.from_string content in 139 let open Yojson.Safe.Util in 140 (* Support both "meta_files" and "metas" for compatibility *) 141 let meta_files = 142 try json |> member "meta_files" |> to_list |> List.map to_string 143 with _ -> 144 try json |> member "metas" |> to_list |> List.map to_string 145 with _ -> [] 146 in 147 (* Support both "universes" and "deps" for compatibility *) 148 let universes = 149 try json |> member "universes" |> to_list |> List.map to_string 150 with _ -> 151 try json |> member "deps" |> to_list |> List.map to_string 152 with _ -> [] 153 in 154 (meta_files, universes) 155 with _ -> 156 (* Fall back to legacy whitespace-separated format (no universes) *) 157 (Astring.String.fields ~empty:false content, []) 158 159(** Load a single META file and parse it into a library *) 160let load_meta async_get meta_path = 161 let* res = async_get meta_path in 162 match res with 163 | Error (`Msg m) -> 164 Jslib.log "Error fetching findlib meta %s: %s" meta_path m; 165 Lwt.return_none 166 | Ok meta_content -> 167 match Angstrom.parse_string ~consume:All Uri.Parser.uri_reference meta_path with 168 | Ok uri -> ( 169 Jslib.log "Parsed uri: %s" (Uri.path uri); 170 let path = Uri.path uri in 171 let file = Fpath.v path in 172 let base_library_name = 173 if Fpath.basename file = "META" then 174 Fpath.parent file |> Fpath.basename 175 else Fpath.get_ext file 176 in 177 let lexing = Lexing.from_string meta_content in 178 try 179 let meta = Fl_metascanner.parse_lexing lexing in 180 let libraries = 181 read_libraries_from_pkg_defs ~library_name:base_library_name 182 ~dir:None uri meta 183 in 184 Lwt.return (Result.to_option libraries) 185 with _ -> 186 Jslib.log "Failed to parse meta: %s" (Uri.path uri); 187 Lwt.return_none) 188 | Error m -> 189 Jslib.log "Failed to parse uri: %s" m; 190 Lwt.return_none 191 192(** Resolve a path relative to the directory of the base URL. 193 Used for meta_files which are relative to their findlib_index. 194 e.g. base="http://host/demo1/base/findlib_index", path="lib/base/META" 195 => "http://host/demo1/base/lib/base/META" *) 196let resolve_relative_to_dir ~base path = 197 match Angstrom.parse_string ~consume:All Uri.Parser.uri_reference base with 198 | Ok base_uri -> 199 let base_path = Uri.path base_uri in 200 let parent_dir = 201 match Fpath.of_string base_path with 202 | Ok p -> Fpath.parent p |> Fpath.to_string 203 | Error _ -> "/" 204 in 205 let resolved = Filename.concat parent_dir path in 206 Uri.with_path base_uri resolved |> Uri.to_string 207 | Error _ -> path 208 209(** Resolve a path as absolute from root (preserving scheme/host from base). 210 Used for universe paths which are already full paths from root. 211 e.g. base="http://host/demo1/findlib_index", path="demo1/base/findlib_index" 212 => "http://host/demo1/base/findlib_index" *) 213let resolve_from_root ~base path = 214 match Angstrom.parse_string ~consume:All Uri.Parser.uri_reference base with 215 | Ok base_uri -> 216 let resolved = "/" ^ path in 217 Uri.with_path base_uri resolved |> Uri.to_string 218 | Error _ -> "/" ^ path 219 220let init (async_get : string -> (string, [>`Msg of string]) result Lwt.t) findlib_index : t Lwt.t = 221 Jslib.log "Initializing findlib"; 222 (* Track visited universes to avoid infinite loops *) 223 let visited = Hashtbl.create 16 in 224 let rec load_universe index_url = 225 if Hashtbl.mem visited index_url then 226 Lwt.return [] 227 else begin 228 Hashtbl.add visited index_url (); 229 let* findlib_txt = async_get index_url in 230 match findlib_txt with 231 | Error (`Msg m) -> 232 Jslib.log "Error fetching findlib index %s: %s" index_url m; 233 Lwt.return [] 234 | Ok content -> 235 let meta_files, universes = parse_findlib_index content in 236 Jslib.log "Loaded findlib_index %s: %d META files, %d universes" 237 index_url (List.length meta_files) (List.length universes); 238 (* Resolve META paths relative to findlib_index directory *) 239 let resolved_metas = 240 List.map (fun p -> resolve_relative_to_dir ~base:index_url p) meta_files 241 in 242 (* Load META files from this universe *) 243 let* local_libs = Lwt_list.filter_map_p (load_meta async_get) resolved_metas in 244 (* Resolve universe paths from root (they're already full paths) *) 245 let universe_index_urls = 246 List.map (fun u -> 247 resolve_from_root ~base:index_url (Filename.concat u "findlib_index.json")) 248 universes 249 in 250 let* universe_libs = Lwt_list.map_p load_universe universe_index_urls in 251 Lwt.return (local_libs @ List.flatten universe_libs) 252 end 253 in 254 let* all_libs = load_universe findlib_index in 255 Lwt.return (flatten_libs all_libs) 256 257let require ~import_scripts sync_get cmi_only v packages = 258 let rec require dcss package : 259 Js_top_worker_rpc.Toplevel_api_gen.dynamic_cmis list = 260 match List.find (fun lib -> lib.name = package) v with 261 | exception Not_found -> 262 Jslib.log "Package %s not found" package; 263 let available = 264 v 265 |> List.map (fun lib -> 266 Printf.sprintf "%s (%d)" lib.name (List.length lib.children)) 267 |> String.concat ", " 268 in 269 Jslib.log "Available packages: %s" available; 270 dcss 271 | lib -> 272 if lib.loaded then dcss 273 else ( 274 Jslib.log "Loading package %s" lib.name; 275 Jslib.log "lib.dir: %s" (Option.value ~default:"None" lib.dir); 276 let dep_dcs = List.fold_left require dcss lib.deps in 277 let path = Fpath.(v (Uri.path lib.meta_uri) |> parent) in 278 let dir = 279 match lib.dir with 280 | None -> path 281 | Some "+" -> Fpath.parent path (* "+" means parent dir in findlib *) 282 | Some d when String.length d > 0 && d.[0] = '^' -> 283 (* "^" prefix means relative to stdlib dir - treat as parent *) 284 Fpath.parent path 285 | Some d -> Fpath.(path // v d) 286 in 287 let dcs = Fpath.(dir / dcs_filename |> to_string) in 288 let uri = Uri.with_path lib.meta_uri dcs in 289 Jslib.log "uri: %s" (Uri.to_string uri); 290 match fetch_dynamic_cmis sync_get (Uri.to_string uri) with 291 | Ok dcs -> 292 let should_load = 293 (not (List.mem lib.name preloaded)) && not cmi_only 294 in 295 Option.iter 296 (fun archive -> 297 if should_load then begin 298 let archive_js = 299 Fpath.(dir / (archive ^ ".cma.js") |> to_string) 300 in 301 import_scripts 302 [ Uri.with_path uri archive_js |> Uri.to_string ] 303 end) 304 lib.archive_name; 305 lib.loaded <- true; 306 Jslib.log "Finished loading package %s" lib.name; 307 dcs :: dep_dcs 308 | Error (`Msg m) -> 309 Jslib.log "Failed to unmarshal dynamic_cms from url %s: %s" 310 (Uri.to_string uri) m; 311 dcss) 312 in 313 List.fold_left require [] packages