this repo has no description
at main 455 lines 18 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 20(* Runtime check for packages compiled into the worker binary. 21 When #require loads a package that is already linked into the worker, 22 we must NOT call import_scripts on its .cma.js — doing so would 23 re-register CMI files that are already in the virtual filesystem, 24 causing a Sys_error "file already exists". 25 26 Instead of maintaining a hand-written list, we use Symtable to check 27 whether any of the package's toplevel modules are already in the 28 bytecode global table (i.e. linked into the binary). *) 29 30exception Crc_mismatch of string 31 32let is_module_available module_name = 33 match Hashtbl.find_opt Js_top_worker.Impl.symtable_memo module_name with 34 | Some v -> v 35 | None -> 36 let v = 37 let id = Ident.create_persistent module_name in 38 let in_symtable = 39 match Symtable.Global.of_ident id with 40 | None -> false 41 | Some g -> Symtable.is_global_defined g 42 in 43 if in_symtable then true 44 else 45 (* Symtable only knows about modules loaded via the bytecode linker. 46 Modules statically compiled into the jsoo binary are registered 47 in caml_global_data by caml_register_global but are invisible to 48 Symtable. Check the JS runtime as a fallback. *) 49 let open Js_of_ocaml in 50 let global_data = Js.Unsafe.pure_js_expr "globalThis.jsoo_runtime.caml_get_global_data()" in 51 Js.Optdef.test (Js.Unsafe.get global_data (Js.string module_name)) 52 in 53 Hashtbl.replace Js_top_worker.Impl.symtable_memo module_name v; 54 v 55 56let binary_crc module_name = 57 match Js_top_worker.Impl.lookup_binary_crc module_name with 58 | Some _ as v -> v 59 | None -> 60 let v = 61 try Some (Digest.to_hex (Env.crc_of_unit (module_name |> Compilation_unit.Name.of_string))) 62 with Not_found -> None 63 in 64 Option.iter (Js_top_worker.Impl.memo_binary_crc module_name) v; 65 v 66 67let universe_crc module_name (dcs : Js_top_worker.Impl.dynamic_cmis) = 68 match Js_top_worker.Impl.lookup_server_crc module_name with 69 | Some _ as v -> v 70 | None -> List.assoc_opt module_name dcs.dcs_module_crcs 71 72let is_package_preloaded (dcs : Js_top_worker.Impl.dynamic_cmis) = 73 let open Js_top_worker.Impl in 74 match check_preload_status 75 ~is_available:is_module_available 76 ~get_binary_crc:binary_crc 77 ~get_universe_crc:(fun m -> universe_crc m dcs) 78 dcs 79 with 80 | Preloaded -> true 81 | Not_loaded -> false 82 | Partially_loaded { loaded; missing } -> 83 Jslib.log 84 "WARNING: package partially preloaded (%d/%d modules). \ 85 Loaded: [%s], missing: [%s]" 86 (List.length loaded) (List.length loaded + List.length missing) 87 (String.concat ", " loaded) 88 (String.concat ", " missing); 89 false 90 | Crc_mismatch ms -> 91 raise (Crc_mismatch (Printf.sprintf 92 "Package preloaded but CRC mismatch: %s" 93 (String.concat ", " ms))) 94 95(** Check if the implementation units from a specific CMA archive are already 96 loaded in the toplevel. This is more precise than is_package_preloaded 97 because it checks only the units in the actual archive, not all CMIs 98 in the directory (which may contain units from other findlib packages). *) 99let is_cma_preloaded units = 100 match units with 101 | [] -> false 102 | _ -> List.for_all is_module_available units 103 104let rec read_libraries_from_pkg_defs ~library_name ~dir meta_uri pkg_expr = 105 try 106 Jslib.log "Reading library: %s" library_name; 107 let pkg_defs = pkg_expr.Fl_metascanner.pkg_defs in 108 (* Try to find archive with various predicates. 109 PPX packages often only define archive(ppx_driver,byte), so we need to 110 check multiple predicate combinations to find the right archive. *) 111 let archive_filename = 112 (* First try with ppx_driver,byte - this catches PPX libraries like ppx_deriving.show *) 113 try Some (Fl_metascanner.lookup "archive" [ "ppx_driver"; "byte" ] pkg_defs) 114 with _ -> ( 115 (* Then try plain byte *) 116 try Some (Fl_metascanner.lookup "archive" [ "byte" ] pkg_defs) 117 with _ -> ( 118 (* Then try native as fallback *) 119 try Some (Fl_metascanner.lookup "archive" [ "native" ] pkg_defs) 120 with _ -> None)) 121 in 122 123 (* Use -ppx_driver predicate for toplevel use - this ensures PPX packages 124 pull in their runtime dependencies (e.g., ppx_deriving.show requires 125 ppx_deriving.runtime when not using ppx_driver) *) 126 let predicates = ["-ppx_driver"] in 127 let deps_str = 128 try Fl_metascanner.lookup "requires" predicates pkg_defs with _ -> "" in 129 let deps = Astring.String.fields ~empty:false deps_str in 130 let subdir = 131 List.find_opt (fun d -> d.Fl_metascanner.def_var = "directory") pkg_defs 132 |> Option.map (fun d -> d.Fl_metascanner.def_value) 133 in 134 let dir = 135 match (dir, subdir) with 136 | None, None -> None 137 | Some d, None -> Some d 138 | None, Some d -> Some d 139 | Some d1, Some d2 -> Some (Filename.concat d1 d2) 140 in 141 let archive_name = 142 Option.bind archive_filename (fun a -> 143 let file_name_len = String.length a in 144 if file_name_len > 0 then Some (Filename.chop_extension a) else None) 145 in 146 Jslib.log "Number of children: %d" (List.length pkg_expr.pkg_children); 147 let children = 148 List.filter_map 149 (fun (n, expr) -> 150 Jslib.log "Found child: %s" n; 151 let library_name = library_name ^ "." ^ n in 152 match 153 read_libraries_from_pkg_defs ~library_name ~dir meta_uri expr 154 with 155 | Ok l -> Some l 156 | Error (`Msg m) -> 157 Jslib.log "Error reading sub-library: %s" m; 158 None) 159 pkg_expr.pkg_children 160 in 161 Ok 162 { 163 name = library_name; 164 archive_name; 165 dir; 166 deps; 167 meta_uri; 168 loaded = false; 169 children; 170 } 171 with Not_found -> Error (`Msg "Failed to read libraries from pkg_defs") 172 173type t = library list 174 175let dcs_filename = "dynamic_cmis.json" 176 177let fetch_dynamic_cmis sync_get url = 178 match sync_get url with 179 | None -> Error (`Msg "Failed to fetch dynamic cmis") 180 | Some json_str -> 181 (try 182 let json = Yojson.Safe.from_string json_str in 183 let open Yojson.Safe.Util in 184 let dcs_url = json |> member "dcs_url" |> to_string in 185 let dcs_toplevel_modules = 186 json |> member "dcs_toplevel_modules" |> to_list |> List.map to_string in 187 let dcs_file_prefixes = 188 json |> member "dcs_file_prefixes" |> to_list |> List.map to_string in 189 let dcs_module_crcs = 190 match json |> member "dcs_module_crcs" with 191 | `Assoc pairs -> 192 List.filter_map (fun (k, v) -> 193 match v with `String s -> Some (k, s) | _ -> None) pairs 194 | _ -> [] 195 in 196 let dcs_cma_units = 197 match json |> member "cma_units" with 198 | `Assoc pairs -> 199 List.filter_map (fun (k, v) -> 200 match v with 201 | `List units -> 202 Some (k, List.filter_map (fun u -> 203 match u with `String s -> Some s | _ -> None) units) 204 | _ -> None) pairs 205 | _ -> [] 206 in 207 Ok { Js_top_worker.Impl.dcs_url; dcs_toplevel_modules; dcs_file_prefixes; 208 dcs_module_crcs; dcs_cma_units } 209 with e -> 210 Error (`Msg (Printf.sprintf "Failed to parse dynamic_cmis JSON: %s" (Printexc.to_string e)))) 211 212let (let*) = Lwt.bind 213 214(** Parse a findlib_index file (JSON or legacy text format) and return 215 the list of META file paths and universe paths. 216 217 JSON format: [{"meta_files": [...], "universes": [...]}] 218 219 meta_files: direct paths to META files 220 universes: paths to other universes (directories containing findlib_index) *) 221let parse_findlib_index content = 222 (* Try JSON format first *) 223 try 224 let json = Yojson.Safe.from_string content in 225 let open Yojson.Safe.Util in 226 (* Support both "meta_files" and "metas" for compatibility *) 227 let meta_files = 228 try json |> member "meta_files" |> to_list |> List.map to_string 229 with _ -> 230 try json |> member "metas" |> to_list |> List.map to_string 231 with _ -> [] 232 in 233 (* Support both "universes" and "deps" for compatibility *) 234 let universes = 235 try json |> member "universes" |> to_list |> List.map to_string 236 with _ -> 237 try json |> member "deps" |> to_list |> List.map to_string 238 with _ -> [] 239 in 240 (meta_files, universes) 241 with _ -> 242 (* Fall back to legacy whitespace-separated format (no universes) *) 243 (Astring.String.fields ~empty:false content, []) 244 245(** Load a single META file and parse it into a library *) 246let load_meta async_get meta_path = 247 let* res = async_get meta_path in 248 match res with 249 | Error (`Msg m) -> 250 Jslib.log "Error fetching findlib meta %s: %s" meta_path m; 251 Lwt.return_none 252 | Ok meta_content -> 253 match Angstrom.parse_string ~consume:All Uri.Parser.uri_reference meta_path with 254 | Ok uri -> ( 255 Jslib.log "Parsed uri: %s" (Uri.path uri); 256 let path = Uri.path uri in 257 let file = Fpath.v path in 258 let base_library_name = 259 if Fpath.basename file = "META" then 260 Fpath.parent file |> Fpath.basename 261 else Fpath.get_ext file 262 in 263 let lexing = Lexing.from_string meta_content in 264 try 265 let meta = Fl_metascanner.parse_lexing lexing in 266 let libraries = 267 read_libraries_from_pkg_defs ~library_name:base_library_name 268 ~dir:None uri meta 269 in 270 Lwt.return (Result.to_option libraries) 271 with _ -> 272 Jslib.log "Failed to parse meta: %s" (Uri.path uri); 273 Lwt.return_none) 274 | Error m -> 275 Jslib.log "Failed to parse uri: %s" m; 276 Lwt.return_none 277 278(** Resolve a path relative to the directory of the base URL. 279 Used for meta_files which are relative to their findlib_index. 280 e.g. base="http://host/demo1/base/findlib_index", path="lib/base/META" 281 => "http://host/demo1/base/lib/base/META" *) 282let resolve_relative_to_dir ~base path = 283 match Angstrom.parse_string ~consume:All Uri.Parser.uri_reference base with 284 | Ok base_uri -> 285 let base_path = Uri.path base_uri in 286 let parent_dir = 287 match Fpath.of_string base_path with 288 | Ok p -> Fpath.parent p |> Fpath.to_string 289 | Error _ -> "/" 290 in 291 let resolved = Filename.concat parent_dir path in 292 Uri.with_path base_uri resolved |> Uri.to_string 293 | Error _ -> path 294 295(** Resolve a path as absolute from root (preserving scheme/host from base). 296 Used for universe paths which are already full paths from root. 297 e.g. base="http://host/demo1/findlib_index", path="demo1/base/findlib_index" 298 => "http://host/demo1/base/findlib_index" *) 299let resolve_from_root ~base path = 300 match Angstrom.parse_string ~consume:All Uri.Parser.uri_reference base with 301 | Ok base_uri -> 302 let resolved = "/" ^ path in 303 Uri.with_path base_uri resolved |> Uri.to_string 304 | Error _ -> "/" ^ path 305 306let init (async_get : string -> (string, [>`Msg of string]) result Lwt.t) findlib_index : t Lwt.t = 307 Jslib.log "Initializing findlib"; 308 (* Track visited universes to avoid infinite loops *) 309 let visited = Hashtbl.create 16 in 310 let rec load_universe index_url = 311 if Hashtbl.mem visited index_url then 312 Lwt.return [] 313 else begin 314 Hashtbl.add visited index_url (); 315 let* findlib_txt = 316 let* result = async_get index_url in 317 match result with 318 | Ok _ as ok -> Lwt.return ok 319 | Error _ when Filename.check_suffix index_url ".json" -> 320 (* Backward compat: try without .json extension *) 321 let legacy_url = Filename.chop_suffix index_url ".json" in 322 Jslib.log "Retrying without .json: %s" legacy_url; 323 async_get legacy_url 324 | err -> Lwt.return err 325 in 326 match findlib_txt with 327 | Error (`Msg m) -> 328 Jslib.log "Error fetching findlib index %s: %s" index_url m; 329 Lwt.return [] 330 | Ok content -> 331 let meta_files, universes = parse_findlib_index content in 332 Jslib.log "Loaded findlib_index %s: %d META files, %d universes" 333 index_url (List.length meta_files) (List.length universes); 334 (* Resolve META paths relative to findlib_index directory *) 335 let resolved_metas = 336 List.map (fun p -> resolve_relative_to_dir ~base:index_url p) meta_files 337 in 338 (* Load META files from this universe *) 339 let* local_libs = Lwt_list.filter_map_p (load_meta async_get) resolved_metas in 340 (* Resolve universe paths relative to this findlib_index's directory. 341 Universe paths can be relative (e.g., "../stdlib") or absolute from 342 root (e.g., "packages/stdlib"). Relative paths are the common case 343 from opam-all; absolute paths are kept for backward compatibility. *) 344 let universe_index_urls = 345 List.map (fun u -> 346 let index_path = Filename.concat u "findlib_index.json" in 347 if String.length u > 0 && u.[0] = '.' then 348 (* Relative path (e.g., "../stdlib") — resolve from this index's dir *) 349 resolve_relative_to_dir ~base:index_url index_path 350 else 351 (* Absolute-from-root path — legacy behavior *) 352 resolve_from_root ~base:index_url index_path) 353 universes 354 in 355 let* universe_libs = Lwt_list.map_p load_universe universe_index_urls in 356 Lwt.return (local_libs @ List.flatten universe_libs) 357 end 358 in 359 let* all_libs = load_universe findlib_index in 360 Lwt.return (flatten_libs all_libs) 361 362let require ~import_scripts sync_get cmi_only v packages = 363 let rec require dcss package : 364 Js_top_worker.Impl.dynamic_cmis list = 365 match List.find (fun lib -> lib.name = package) v with 366 | exception Not_found -> 367 Jslib.log "Package %s not found" package; 368 let available = 369 v 370 |> List.map (fun lib -> 371 Printf.sprintf "%s (%d)" lib.name (List.length lib.children)) 372 |> String.concat ", " 373 in 374 Jslib.log "Available packages: %s" available; 375 dcss 376 | lib -> 377 if lib.loaded then dcss 378 else ( 379 Jslib.log "Loading package %s" lib.name; 380 Jslib.log "lib.dir: %s" (Option.value ~default:"None" lib.dir); 381 let dep_dcs = List.fold_left require dcss lib.deps in 382 let path = Fpath.(v (Uri.path lib.meta_uri) |> parent) in 383 let dir = 384 match lib.dir with 385 | None -> path 386 | Some "+" -> Fpath.parent path (* "+" means parent dir in findlib *) 387 | Some d when String.length d > 0 && d.[0] = '^' -> 388 (* "^" prefix means relative to stdlib dir - treat as parent *) 389 Fpath.parent path 390 | Some d -> Fpath.(path // v d) 391 in 392 let dcs = Fpath.(dir / dcs_filename |> to_string) in 393 let uri = Uri.with_path lib.meta_uri dcs in 394 Jslib.log "uri: %s" (Uri.to_string uri); 395 match fetch_dynamic_cmis sync_get (Uri.to_string uri) with 396 | Ok dcs -> 397 (* Check whether to load the .cma.js archive. 398 We use per-archive unit lists (from .units.json) rather than 399 the directory-wide dynamic_cmis.json, because multiple findlib 400 packages may share a directory (e.g., compiler-libs.common and 401 compiler-libs.optcomp both live in compiler-libs/). *) 402 Option.iter 403 (fun archive -> 404 if not cmi_only then begin 405 let cma_name = archive ^ ".cma" in 406 let already_loaded = 407 match List.assoc_opt cma_name dcs.dcs_cma_units with 408 | Some units -> 409 let preloaded = is_cma_preloaded units in 410 if preloaded then 411 Jslib.log "Archive %s already preloaded (%d units), skipping" 412 cma_name (List.length units) 413 else 414 Jslib.log "Archive %s not preloaded, loading %d units" 415 cma_name (List.length units); 416 preloaded 417 | None -> 418 (* No cma_units in dynamic_cmis.json — fall back to directory-wide check *) 419 Jslib.log "No cma_units for %s, falling back to directory check" cma_name; 420 is_package_preloaded dcs 421 in 422 if not already_loaded then begin 423 let archive_js = 424 Fpath.(dir / (archive ^ ".cma.js") |> to_string) 425 in 426 import_scripts 427 [ Uri.with_path uri archive_js |> Uri.to_string ] 428 end 429 end) 430 lib.archive_name; 431 lib.loaded <- true; 432 Jslib.log "Finished loading package %s" lib.name; 433 dcs :: dep_dcs 434 | Error (`Msg m) -> 435 Jslib.log "Failed to unmarshal dynamic_cms from url %s: %s" 436 (Uri.to_string uri) m; 437 dcss) 438 in 439 List.fold_left require [] packages 440 441let find_dcs_url v package_name = 442 match List.find_opt (fun lib -> lib.name = package_name) v with 443 | None -> None 444 | Some lib -> 445 let path = Fpath.(v (Uri.path lib.meta_uri) |> parent) in 446 let dir = 447 match lib.dir with 448 | None -> path 449 | Some "+" -> Fpath.parent path 450 | Some d when String.length d > 0 && d.[0] = '^' -> 451 Fpath.parent path 452 | Some d -> Fpath.(path // v d) 453 in 454 let dcs = Fpath.(dir / dcs_filename |> to_string) in 455 Some (Uri.with_path lib.meta_uri dcs |> Uri.to_string)