this repo has no description
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)