this repo has no description
at main 700 lines 27 kB view raw
1let dynamic_cmis_to_json ~cma_units (dcs : Js_top_worker.Impl.dynamic_cmis) = 2 let base = [ 3 ("dcs_url", `String dcs.dcs_url); 4 ("dcs_toplevel_modules", `List (List.map (fun s -> `String s) dcs.dcs_toplevel_modules)); 5 ("dcs_file_prefixes", `List (List.map (fun s -> `String s) dcs.dcs_file_prefixes)); 6 ("dcs_module_crcs", `Assoc (List.map (fun (k, v) -> (k, `String v)) dcs.dcs_module_crcs)); 7 ] in 8 let fields = match cma_units with 9 | [] -> base 10 | units -> 11 let cma_units_json = `Assoc (List.map (fun (archive, mods) -> 12 (archive, `List (List.map (fun m -> `String m) mods))) units) 13 in 14 base @ [("cma_units", cma_units_json)] 15 in 16 `Assoc fields 17 18(** Read the self-CRC from a .cmi file. Returns the hex digest of the 19 compilation unit's interface, or None if the file can't be read. *) 20let cmi_self_crc cmi_path = 21 try 22 let cmi = Cmi_format.read_cmi (Fpath.to_string cmi_path) in 23 let crcs = cmi.Cmi_format.cmi_crcs in 24 if Array.length crcs > 0 then 25 match Import_info.crc crcs.(0) with 26 | Some crc -> Some (Digest.to_hex crc) 27 | None -> None 28 else None 29 with _ -> None 30 31(** For a list of toplevel module names and the directory containing their 32 .cmi files, return a (module_name, hex_crc) association list. *) 33let read_module_crcs dir modules = 34 List.filter_map (fun modname -> 35 let cmi_path = Fpath.(dir / (String.uncapitalize_ascii modname ^ ".cmi")) in 36 match cmi_self_crc cmi_path with 37 | Some crc -> Some (modname, crc) 38 | None -> None) 39 modules 40 41(** Try to relativize a path against findlib_dir. If the result contains 42 ".." (indicating the path is in a different tree), fall back to extracting 43 the path components after "lib" directory. *) 44let relativize_or_fallback ~findlib_dir path = 45 (* First try standard relativize *) 46 let rel = match Fpath.relativize ~root:findlib_dir path with 47 | Some rel -> rel 48 | None -> path (* shouldn't happen for absolute paths, but fallback *) 49 in 50 (* If the result contains "..", use fallback instead *) 51 let segs = Fpath.segs rel in 52 if List.mem ".." segs then begin 53 (* Fallback: use path components after "lib" directory *) 54 let path_segs = Fpath.segs path in 55 let rec find_after_lib = function 56 | [] -> Fpath.v (Fpath.basename path) 57 | "lib" :: rest -> Fpath.v (String.concat Fpath.dir_sep rest) 58 | _ :: rest -> find_after_lib rest 59 in 60 find_after_lib path_segs 61 end else 62 rel 63 64(** Extract implementation unit names from a .cma archive using ocamlobjinfo. *) 65let cma_unit_names ?switch archive = 66 let base_cmd = match switch with 67 | None -> Bos.Cmd.(v "ocamlobjinfo") 68 | Some s -> Bos.Cmd.(v "opam" % "exec" % "--switch" % s % "--" % "ocamlobjinfo") 69 in 70 let cmd = Bos.Cmd.(base_cmd % Fpath.to_string archive) in 71 let lines = Util.lines_of_process cmd in 72 List.filter_map (fun line -> 73 match Astring.String.cut ~sep:"Unit name: " line with 74 | Some ("", name) -> Some (String.trim name) 75 | _ -> None) 76 lines 77 78(** Per-directory CMA unit names, collected during archive compilation 79 and embedded into dynamic_cmis.json. Maps source directory path to 80 a list of (archive_basename, unit_names) pairs. *) 81let cma_units_by_dir : (string, (string * string list) list) Hashtbl.t = 82 Hashtbl.create 16 83 84let record_cma_units dir archive_basename units = 85 let key = Fpath.to_string dir in 86 let existing = match Hashtbl.find_opt cma_units_by_dir key with 87 | Some l -> l 88 | None -> [] 89 in 90 Hashtbl.replace cma_units_by_dir key ((archive_basename, units) :: existing) 91 92let cmi_files dir = 93 Bos.OS.Dir.fold_contents ~traverse:`None ~elements:`Files 94 (fun path acc -> 95 if Fpath.has_ext ".cmi" path then Fpath.filename path :: acc else acc) 96 [] dir 97 98let gen_cmis ?path_prefix cmis = 99 let gen_one (dir, cmis) = 100 let all_cmis = 101 List.map (fun s -> String.sub s 0 (String.length s - 4)) cmis 102 in 103 let hidden, non_hidden = 104 List.partition (fun x -> Astring.String.is_infix ~affix:"__" x) all_cmis 105 in 106 let prefixes = 107 List.filter_map 108 (fun x -> 109 match Astring.String.cuts ~sep:"__" x with 110 | x :: _ -> Some (x ^ "__") 111 | _ -> None) 112 hidden 113 in 114 let prefixes = Util.StringSet.(of_list prefixes |> to_list) in 115 let findlib_dir = Ocamlfind.findlib_dir () |> Fpath.v in 116 let d = relativize_or_fallback ~findlib_dir dir in 117 (* Include path_prefix in dcs_url so it's correct relative to HTTP root *) 118 let dcs_url_path = match path_prefix with 119 | Some prefix -> Fpath.(v prefix / "lib" // d) 120 | None -> Fpath.(v "lib" // d) 121 in 122 let toplevel_modules = List.map String.capitalize_ascii non_hidden in 123 let dcs = 124 { 125 Js_top_worker.Impl.dcs_url = Fpath.to_string dcs_url_path; 126 dcs_toplevel_modules = toplevel_modules; 127 dcs_file_prefixes = prefixes; 128 dcs_module_crcs = read_module_crcs dir toplevel_modules; 129 dcs_cma_units = []; 130 } 131 in 132 let cma_units = match Hashtbl.find_opt cma_units_by_dir (Fpath.to_string dir) with 133 | Some units -> units 134 | None -> [] 135 in 136 ( dir, 137 Yojson.Safe.to_string (dynamic_cmis_to_json ~cma_units dcs) ) 138 in 139 List.map gen_one cmis 140 141(** Read dependency paths from a file (one path per line) *) 142let read_deps_file path = 143 match Bos.OS.File.read_lines (Fpath.v path) with 144 | Ok lines -> List.filter (fun s -> String.length s > 0) lines 145 | Error (`Msg m) -> 146 Format.eprintf "Warning: Failed to read deps file %s: %s\n%!" path m; 147 [] 148 149let copy_extra_files output_dir files = 150 List.iter (fun src -> 151 let src = Fpath.v src in 152 let dst = Fpath.(output_dir / Fpath.filename src) in 153 Util.cp src dst; 154 Format.eprintf "Copied %a to %a\n%!" Fpath.pp src Fpath.pp dst) 155 files 156 157let opam verbose output_dir_str switch libraries no_worker path deps_file extra_files = 158 Opam.switch := switch; 159 (* When --path is specified, only compile the specified libraries (no deps) *) 160 let libraries_with_deps, libraries_only = 161 match Ocamlfind.deps libraries with 162 | Ok l -> 163 let all = Util.StringSet.of_list ("stdlib" :: l) in 164 (* In --path mode, don't auto-add stdlib - only include requested libs *) 165 let only = Util.StringSet.of_list libraries in 166 (all, only) 167 | Error (`Msg m) -> 168 Format.eprintf "Failed to find libs: %s\n%!" m; 169 failwith ("Bad libs: " ^ m) 170 in 171 (* In path mode, only compile the specified packages *) 172 let libraries = if path <> None then libraries_only else libraries_with_deps in 173 (* Read dependency paths from file if specified *) 174 let dep_paths = match deps_file with 175 | Some f -> read_deps_file f 176 | None -> [] 177 in 178 Eio_main.run @@ fun env -> 179 Eio.Switch.run @@ fun sw -> 180 if verbose then Logs.set_level (Some Logs.Debug) else Logs.set_level None; 181 Logs.set_reporter (Logs_fmt.reporter ()); 182 let () = Worker_pool.start_workers env sw 16 in 183 Logs.debug (fun m -> 184 m "Libraries: %a" 185 (Fmt.list ~sep:Fmt.comma Fmt.string) 186 (Util.StringSet.elements libraries)); 187 (* output_dir is always from -o; --path is a subdirectory within it *) 188 let base_output_dir = Fpath.v output_dir_str in 189 let output_dir = 190 match path with 191 | Some p -> Fpath.(base_output_dir // v p) 192 | None -> base_output_dir 193 in 194 let meta_files = 195 List.map 196 (fun lib -> Ocamlfind.meta_file lib) 197 (Util.StringSet.elements libraries) 198 |> Util.StringSet.of_list 199 in 200 let cmi_dirs = 201 match Ocamlfind.deps (Util.StringSet.to_list libraries) with 202 | Ok libs -> 203 let dirs = 204 List.filter_map 205 (fun lib -> 206 match Ocamlfind.get_dir lib with Ok x -> Some x | _ -> None) 207 libs 208 in 209 dirs 210 | Error (`Msg m) -> 211 Format.eprintf "Failed to find libs: %s\n%!" m; 212 [] 213 in 214 Format.eprintf "cmi_dirs: %a\n%!" (Fmt.list ~sep:Fmt.comma Fpath.pp) cmi_dirs; 215 (* In --path mode, only include cmi dirs from specified libraries and their 216 subpackages, not external dependencies *) 217 let cmi_dirs_to_copy = 218 if path <> None then 219 let lib_dirs = 220 List.filter_map 221 (fun lib -> 222 match Ocamlfind.get_dir lib with Ok x -> Some x | _ -> None) 223 (Util.StringSet.to_list libraries) 224 in 225 (* Filter cmi_dirs to include directories that are equal to or subdirectories 226 of lib_dirs. This includes subpackages like base.base_internalhash_types. 227 We check that the relative path doesn't start with ".." *) 228 List.filter 229 (fun dir -> 230 List.exists 231 (fun lib_dir -> 232 Fpath.equal dir lib_dir || 233 match Fpath.relativize ~root:lib_dir dir with 234 | Some rel -> 235 let segs = Fpath.segs rel in 236 (match segs with 237 | ".." :: _ -> false (* Goes outside lib_dir *) 238 | _ -> true) 239 | None -> false) 240 lib_dirs) 241 cmi_dirs 242 else 243 cmi_dirs 244 in 245 let cmis = 246 List.fold_left 247 (fun acc dir -> 248 match cmi_files dir with 249 | Ok files -> (dir, files) :: acc 250 | Error _ -> acc) 251 [] cmi_dirs_to_copy 252 in 253 let ( let* ) = Result.bind in 254 255 let _ = 256 let* _ = Bos.OS.Dir.create output_dir in 257 let findlib_dir = Ocamlfind.findlib_dir () |> Fpath.v in 258 259 List.iter 260 (fun (dir, files) -> 261 let d = relativize_or_fallback ~findlib_dir dir in 262 List.iter 263 (fun f -> 264 let dest_dir = Fpath.(output_dir / "lib" // d) in 265 let dest = Fpath.(dest_dir / f) in 266 let _ = Bos.OS.Dir.create ~path:true dest_dir in 267 match Bos.OS.File.exists dest with 268 | Ok true -> () 269 | Ok false -> Util.cp Fpath.(dir / f) dest 270 | Error _ -> failwith "file exists failed") 271 files) 272 cmis; 273 274 let meta_rels = 275 Util.StringSet.fold 276 (fun meta_file acc -> 277 let meta_file = Fpath.v meta_file in 278 let d = relativize_or_fallback ~findlib_dir meta_file |> Fpath.parent in 279 (meta_file, d) :: acc) 280 meta_files [] 281 in 282 283 List.iter 284 (fun (meta_file, d) -> 285 let dest = Fpath.(output_dir / "lib" // d) in 286 let _ = Bos.OS.Dir.create dest in 287 Util.cp meta_file dest) 288 meta_rels; 289 290 (* Generate findlib_index as JSON with metas field *) 291 let metas_json = 292 List.map 293 (fun (meta_file, d) -> 294 let file = Fpath.filename meta_file in 295 let rel_path = Fpath.(v "lib" // d / file) in 296 `String (Fpath.to_string rel_path)) 297 meta_rels 298 in 299 (* TODO: dep_paths should also contribute META paths once we have full universe info *) 300 let _ = dep_paths in 301 let compiler_field = 302 if no_worker then [] 303 else [("compiler", `Assoc [("worker_url", `String "worker.js")])] 304 in 305 let findlib_json = `Assoc (("meta_files", `List metas_json) :: compiler_field) in 306 Out_channel.with_open_bin 307 Fpath.(output_dir / "findlib_index.json" |> to_string) 308 (fun oc -> Printf.fprintf oc "%s\n" (Yojson.Safe.to_string findlib_json)); 309 310 (* Compile archives for each library AND its subpackages *) 311 Util.StringSet.iter 312 (fun lib -> 313 (* Get subpackages (e.g., base.base_internalhash_types for base) *) 314 let sub_libs = Ocamlfind.sub_libraries lib in 315 let all_libs = Util.StringSet.add lib sub_libs in 316 Util.StringSet.iter 317 (fun sub_lib -> 318 match Ocamlfind.get_dir sub_lib with 319 | Error _ -> () 320 | Ok dir -> 321 let archives = Ocamlfind.archives sub_lib in 322 let archives = List.map (fun x -> Fpath.(dir / x)) archives in 323 let d = relativize_or_fallback ~findlib_dir dir in 324 let dest = Fpath.(output_dir / "lib" // d) in 325 let (_ : (bool, _) result) = Bos.OS.Dir.create dest in 326 let compile_archive archive = 327 let output = Fpath.(dest / (Fpath.filename archive ^ ".js")) in 328 let js_runtime = Ocamlfind.jsoo_runtime sub_lib in 329 let js_files = 330 List.map (fun f -> Fpath.(dir / f |> to_string)) js_runtime 331 in 332 let base_cmd = 333 match switch with 334 | None -> Bos.Cmd.(v "js_of_ocaml") 335 | Some s -> 336 Bos.Cmd.(v "opam" % "exec" % "--switch" % s % "--" % "js_of_ocaml") 337 in 338 let cmd = 339 Bos.Cmd.( 340 base_cmd % "compile" % "--toplevel" % "--include-runtime" 341 % "--effects=disabled") 342 in 343 let cmd = List.fold_left (fun c f -> Bos.Cmd.(c % f)) cmd js_files in 344 let cmd = 345 Bos.Cmd.(cmd % Fpath.to_string archive % "-o" % Fpath.to_string output) 346 in 347 ignore (Util.lines_of_process cmd); 348 (* Record CMA unit names for embedding in dynamic_cmis.json *) 349 let units = cma_unit_names ?switch archive in 350 record_cma_units dir (Fpath.filename archive) units 351 in 352 List.iter compile_archive archives) 353 all_libs) 354 libraries; 355 356 (* Format.eprintf "@[<hov 2>dir: %a [%a]@]\n%!" Fpath.pp dir (Fmt.list ~sep:Fmt.sp Fmt.string) files) cmis; *) 357 Ok () 358 in 359 let init_cmis = gen_cmis ?path_prefix:path cmis in 360 List.iter 361 (fun (dir, dcs) -> 362 let findlib_dir = Ocamlfind.findlib_dir () |> Fpath.v in 363 let d = Fpath.relativize ~root:findlib_dir dir in 364 match d with 365 | None -> 366 Format.eprintf "Failed to relativize %a wrt %a\n%!" Fpath.pp dir 367 Fpath.pp findlib_dir 368 | Some dir -> 369 Format.eprintf "Generating %a\n%!" Fpath.pp dir; 370 let dir = Fpath.(output_dir / "lib" // dir) in 371 let _ = Bos.OS.Dir.create dir in 372 let oc = open_out Fpath.(dir / "dynamic_cmis.json" |> to_string) in 373 Printf.fprintf oc "%s" dcs; 374 close_out oc) 375 init_cmis; 376 Format.eprintf "Number of cmis: %d\n%!" (List.length init_cmis); 377 378 let () = 379 if no_worker then () else Mk_backend.mk switch output_dir 380 in 381 copy_extra_files output_dir extra_files; 382 383 `Ok () 384 385(** Generate a single package's universe directory. 386 Returns (pkg_path, meta_path) where meta_path is the full path to META 387 relative to the output_dir root. *) 388let generate_package_universe ~switch ~output_dir ~findlib_dir ~pkg ~pkg_deps = 389 (* Use package name as directory path *) 390 let pkg_path = pkg in 391 let pkg_output_dir = Fpath.(output_dir / pkg_path) in 392 let _ = Bos.OS.Dir.create ~path:true pkg_output_dir in 393 394 (* Get the package's directory and copy cmi files *) 395 let pkg_dir = match Ocamlfind.get_dir pkg with 396 | Ok d -> d 397 | Error _ -> failwith ("Cannot find package: " ^ pkg) 398 in 399 400 (* Also include subpackages (directories under pkg_dir) *) 401 let all_pkg_dirs = 402 let sub_libs = Ocamlfind.sub_libraries pkg in 403 Util.StringSet.fold (fun sub acc -> 404 match Ocamlfind.get_dir sub with 405 | Ok d -> d :: acc 406 | Error _ -> acc) 407 sub_libs [pkg_dir] 408 |> List.sort_uniq Fpath.compare 409 in 410 411 (* Copy cmi files *) 412 List.iter (fun dir -> 413 match cmi_files dir with 414 | Ok files -> 415 let d = relativize_or_fallback ~findlib_dir dir in 416 List.iter (fun f -> 417 let dest_dir = Fpath.(pkg_output_dir / "lib" // d) in 418 let dest = Fpath.(dest_dir / f) in 419 let _ = Bos.OS.Dir.create ~path:true dest_dir in 420 match Bos.OS.File.exists dest with 421 | Ok true -> () 422 | Ok false -> Util.cp Fpath.(dir / f) dest 423 | Error _ -> ()) 424 files 425 | Error _ -> ()) 426 all_pkg_dirs; 427 428 (* Copy META file *) 429 let meta_file = Fpath.v (Ocamlfind.meta_file pkg) in 430 let meta_rel = relativize_or_fallback ~findlib_dir meta_file |> Fpath.parent in 431 let meta_dest = Fpath.(pkg_output_dir / "lib" // meta_rel) in 432 let _ = Bos.OS.Dir.create ~path:true meta_dest in 433 Util.cp meta_file meta_dest; 434 435 (* Compile archives for main package and all subpackages *) 436 let sub_libs = Ocamlfind.sub_libraries pkg in 437 let all_libs = Util.StringSet.add pkg sub_libs in 438 Util.StringSet.iter (fun lib -> 439 match Ocamlfind.get_dir lib with 440 | Error _ -> () 441 | Ok lib_dir -> 442 let archives = Ocamlfind.archives lib in 443 let archives = List.map (fun x -> Fpath.(lib_dir / x)) archives in 444 let d = relativize_or_fallback ~findlib_dir lib_dir in 445 let dest = Fpath.(pkg_output_dir / "lib" // d) in 446 let _ = Bos.OS.Dir.create ~path:true dest in 447 List.iter (fun archive -> 448 let output = Fpath.(dest / (Fpath.filename archive ^ ".js")) in 449 let js_runtime = Ocamlfind.jsoo_runtime lib in 450 let js_files = List.map (fun f -> Fpath.(lib_dir / f |> to_string)) js_runtime in 451 let base_cmd = match switch with 452 | None -> Bos.Cmd.(v "js_of_ocaml") 453 | Some s -> Bos.Cmd.(v "opam" % "exec" % "--switch" % s % "--" % "js_of_ocaml") 454 in 455 let cmd = Bos.Cmd.(base_cmd % "compile" % "--toplevel" % "--include-runtime" % "--effects=disabled") in 456 let cmd = List.fold_left (fun c f -> Bos.Cmd.(c % f)) cmd js_files in 457 let cmd = Bos.Cmd.(cmd % Fpath.to_string archive % "-o" % Fpath.to_string output) in 458 ignore (Util.lines_of_process cmd); 459 (* Record CMA unit names for embedding in dynamic_cmis.json *) 460 let units = cma_unit_names ?switch archive in 461 record_cma_units lib_dir (Fpath.filename archive) units) 462 archives) 463 all_libs; 464 465 (* Generate dynamic_cmis.json for each directory *) 466 List.iter (fun dir -> 467 match cmi_files dir with 468 | Ok files -> 469 let all_cmis = List.map (fun s -> String.sub s 0 (String.length s - 4)) files in 470 let hidden, non_hidden = List.partition (fun x -> Astring.String.is_infix ~affix:"__" x) all_cmis in 471 let prefixes = List.filter_map (fun x -> 472 match Astring.String.cuts ~sep:"__" x with 473 | x :: _ -> Some (x ^ "__") 474 | _ -> None) hidden in 475 let prefixes = Util.StringSet.(of_list prefixes |> to_list) in 476 let d = relativize_or_fallback ~findlib_dir dir in 477 (* dcs_url is relative to the package's own findlib_index.json *) 478 let toplevel_modules = List.map String.capitalize_ascii non_hidden in 479 let dcs = { 480 Js_top_worker.Impl.dcs_url = Fpath.(v "lib" // d |> to_string); 481 dcs_toplevel_modules = toplevel_modules; 482 dcs_file_prefixes = prefixes; 483 dcs_module_crcs = read_module_crcs dir toplevel_modules; 484 dcs_cma_units = []; 485 } in 486 let cma_units = match Hashtbl.find_opt cma_units_by_dir (Fpath.to_string dir) with 487 | Some units -> units 488 | None -> [] 489 in 490 let dcs_json = Yojson.Safe.to_string (dynamic_cmis_to_json ~cma_units dcs) in 491 let dcs_dir = Fpath.(pkg_output_dir / "lib" // d) in 492 let _ = Bos.OS.Dir.create ~path:true dcs_dir in 493 let oc = open_out Fpath.(dcs_dir / "dynamic_cmis.json" |> to_string) in 494 Printf.fprintf oc "%s" dcs_json; 495 close_out oc 496 | Error _ -> ()) 497 all_pkg_dirs; 498 499 (* Return pkg_path and the META path relative to pkg_path *) 500 let local_meta_path = Fpath.(v "lib" // meta_rel / "META" |> to_string) in 501 (pkg_path, local_meta_path, pkg_deps) 502 503let opam_all verbose output_dir_str switch libraries no_worker all_pkgs extra_files = 504 Opam.switch := switch; 505 506 (* Get all packages and their dependencies *) 507 let all_packages = 508 if all_pkgs then 509 (* Build all installed packages *) 510 Ocamlfind.all () 511 else if libraries = [] then 512 (* No packages specified, just stdlib *) 513 ["stdlib"] 514 else 515 match Ocamlfind.deps libraries with 516 | Ok l -> "stdlib" :: l 517 | Error (`Msg m) -> failwith ("Failed to find libs: " ^ m) 518 in 519 520 (* Remove duplicates and sort *) 521 let all_packages = Util.StringSet.(of_list all_packages |> to_list) in 522 523 Format.eprintf "Generating universes for %d packages\n%!" (List.length all_packages); 524 525 Eio_main.run @@ fun env -> 526 Eio.Switch.run @@ fun sw -> 527 if verbose then Logs.set_level (Some Logs.Debug) else Logs.set_level None; 528 Logs.set_reporter (Logs_fmt.reporter ()); 529 let () = Worker_pool.start_workers env sw 16 in 530 531 let output_dir = Fpath.v output_dir_str in 532 let _ = Bos.OS.Dir.create ~path:true output_dir in 533 let findlib_dir = Ocamlfind.findlib_dir () |> Fpath.v in 534 535 (* Build dependency map: package -> list of dependency packages. 536 Stdlib is implicitly required by everything, so add it for all 537 non-stdlib packages even if ocamlfind doesn't list it. *) 538 let dep_map = Hashtbl.create 64 in 539 let all_packages_set = Util.StringSet.of_list all_packages in 540 List.iter (fun pkg -> 541 let deps = match Ocamlfind.deps [pkg] with 542 | Ok l -> List.filter (fun d -> d <> pkg) l 543 | Error _ -> [] 544 in 545 (* Add stdlib as implicit dependency for non-stdlib packages *) 546 let deps = 547 if pkg <> "stdlib" && not (List.mem "stdlib" deps) 548 && Util.StringSet.mem "stdlib" all_packages_set 549 then "stdlib" :: deps 550 else deps 551 in 552 Hashtbl.add dep_map pkg deps) 553 all_packages; 554 555 (* Generate each package and collect results *) 556 let pkg_results = List.map (fun pkg -> 557 Format.eprintf "Generating %s...\n%!" pkg; 558 let pkg_deps = Hashtbl.find dep_map pkg in 559 generate_package_universe ~switch ~output_dir ~findlib_dir ~pkg ~pkg_deps) 560 all_packages 561 in 562 563 (* Build a map from package name to full META path *) 564 let meta_path_map = Hashtbl.create 64 in 565 List.iter (fun (pkg_path, local_meta_path, _deps) -> 566 let full_meta_path = pkg_path ^ "/" ^ local_meta_path in 567 Hashtbl.add meta_path_map pkg_path full_meta_path) 568 pkg_results; 569 570 (* Generate findlib_index for each package. 571 - meta_files: only this package's own META (relative to its own dir) 572 - universes: relative paths to dependency package dirs (e.g., "../stdlib") *) 573 List.iter (fun (pkg_path, local_meta_path, deps) -> 574 let dep_universes = List.filter_map (fun dep -> 575 if Hashtbl.mem meta_path_map dep then 576 Some ("../" ^ dep) 577 else begin 578 Format.eprintf "Warning: no universe found for dep %s\n%!" dep; 579 None 580 end) 581 deps 582 in 583 let fields = [("meta_files", `List [`String local_meta_path])] in 584 let fields = if dep_universes = [] then fields 585 else fields @ [("universes", `List (List.map (fun s -> `String s) dep_universes))] in 586 let findlib_json = `Assoc fields in 587 Out_channel.with_open_bin Fpath.(output_dir / pkg_path / "findlib_index.json" |> to_string) 588 (fun oc -> Printf.fprintf oc "%s\n" (Yojson.Safe.to_string findlib_json))) 589 pkg_results; 590 591 (* Generate root findlib_index.json with all META paths *) 592 let all_metas = List.map (fun (pkg_path, local_meta_path, _) -> 593 pkg_path ^ "/" ^ local_meta_path) 594 pkg_results 595 in 596 let compiler_field = 597 if no_worker then [] 598 else [("compiler", `Assoc [("worker_url", `String "worker.js")])] 599 in 600 let root_index = `Assoc (("meta_files", `List (List.map (fun s -> `String s) all_metas)) :: compiler_field) in 601 Out_channel.with_open_bin Fpath.(output_dir / "findlib_index.json" |> to_string) 602 (fun oc -> Printf.fprintf oc "%s\n" (Yojson.Safe.to_string root_index)); 603 604 Format.eprintf "Generated root findlib_index.json with %d META files\n%!" (List.length pkg_results); 605 606 (* Generate worker.js if requested *) 607 let () = if no_worker then () else Mk_backend.mk switch output_dir in 608 copy_extra_files output_dir extra_files; 609 610 `Ok () 611 612open Cmdliner 613 614let opam_cmd = 615 let libraries = Arg.(value & pos_all string [] & info [] ~docv:"LIB") in 616 let output_dir = 617 let doc = 618 "Output directory in which to put all outputs. This should be the root \ 619 directory of the HTTP server. Ignored when --path is specified." 620 in 621 Arg.(value & opt string "html" & info [ "o"; "output" ] ~doc) 622 in 623 let verbose = 624 let doc = "Enable verbose logging" in 625 Arg.(value & flag & info [ "v"; "verbose" ] ~doc) in 626 let no_worker = 627 let doc = "Do not create worker.js" in 628 Arg.(value & flag & info [ "no-worker" ] ~doc) 629 in 630 let switch = 631 let doc = "Opam switch to use" in 632 Arg.(value & opt (some string) None & info [ "switch" ] ~doc) 633 in 634 let path = 635 let doc = 636 "Full output path for this package (e.g., universes/abc123/base/v0.17.1/). \ 637 When specified, only the named packages are compiled (not dependencies)." 638 in 639 Arg.(value & opt (some string) None & info [ "path" ] ~doc) 640 in 641 let deps_file = 642 let doc = 643 "File containing dependency paths, one per line. Each path should be \ 644 relative to the HTTP root (e.g., universes/xyz789/sexplib0/v0.17.0/)." 645 in 646 Arg.(value & opt (some string) None & info [ "deps-file" ] ~doc) 647 in 648 let extra_files = 649 let doc = 650 "Copy file into the output directory (e.g., --copy-file /path/to/x-ocaml.js). \ 651 May be repeated." 652 in 653 Arg.(value & opt_all string [] & info [ "copy-file" ] ~doc) 654 in 655 let info = Cmd.info "opam" ~doc:"Generate opam files" in 656 Cmd.v info 657 Term.(ret (const opam $ verbose $ output_dir $ switch $ libraries $ no_worker $ path $ deps_file $ extra_files)) 658 659let opam_all_cmd = 660 let libraries = Arg.(value & pos_all string [] & info [] ~docv:"LIB") in 661 let output_dir = 662 let doc = 663 "Output directory for all universes. Each package gets its own subdirectory." 664 in 665 Arg.(value & opt string "html" & info [ "o"; "output" ] ~doc) 666 in 667 let verbose = 668 let doc = "Enable verbose logging" in 669 Arg.(value & flag & info [ "v"; "verbose" ] ~doc) 670 in 671 let no_worker = 672 let doc = "Do not create worker.js" in 673 Arg.(value & flag & info [ "no-worker" ] ~doc) 674 in 675 let switch = 676 let doc = "Opam switch to use" in 677 Arg.(value & opt (some string) None & info [ "switch" ] ~doc) 678 in 679 let all_pkgs = 680 let doc = "Build all installed packages (from ocamlfind list)" in 681 Arg.(value & flag & info [ "all" ] ~doc) 682 in 683 let extra_files = 684 let doc = 685 "Copy file into the output directory (e.g., --copy-file /path/to/x-ocaml.js). \ 686 May be repeated." 687 in 688 Arg.(value & opt_all string [] & info [ "copy-file" ] ~doc) 689 in 690 let info = Cmd.info "opam-all" ~doc:"Generate universes for all packages and their dependencies" in 691 Cmd.v info 692 Term.(ret (const opam_all $ verbose $ output_dir $ switch $ libraries $ no_worker $ all_pkgs $ extra_files)) 693 694let main_cmd = 695 let doc = "An odoc notebook tool" in 696 let info = Cmd.info "odoc-notebook" ~version:"%%VERSION%%" ~doc in 697 let default = Term.(ret (const (`Help (`Pager, None)))) in 698 Cmd.group info ~default [ opam_cmd; opam_all_cmd ] 699 700let () = exit (Cmd.eval main_cmd)