open Import open Jbuild open Build.O module SC = Super_context let (++) = Path.relative let lib_unique_name lib = let name = Lib.name lib in match Lib.status lib with | Installed -> assert false | Public _ -> name | Private scope_name -> SC.Scope_key.to_string name scope_name let pkg_or_lnu lib = match Lib.package lib with | Some p -> Package.Name.to_string p | None -> lib_unique_name lib type target = | Lib of Lib.t | Pkg of Package.Name.t module Gen (S : sig val sctx : SC.t end) = struct open S let context = SC.context sctx module Paths = struct let root = context.Context.build_dir ++ "_doc" let odocs m = root ++ ( match m with | Lib lib -> sprintf "_odoc/lib/%s" (lib_unique_name lib) | Pkg pkg -> sprintf "_odoc/pkg/%s" (Package.Name.to_string pkg) ) let html_root = root ++ "_html" let html m = html_root ++ ( match m with | Pkg pkg -> Package.Name.to_string pkg | Lib lib -> pkg_or_lnu lib ) let gen_mld_dir (pkg : Package.t) = root ++ "_mlds" ++ (Package.Name.to_string pkg.name) end module Dep = struct let html_alias m = Build_system.Alias.doc ~dir:(Paths.html m) let alias = Build_system.Alias.make ".odoc-all" let deps requires = Build.of_result_map requires ~f:(fun libs -> Build.path_set ( List.fold_left libs ~init:Path.Set.empty ~f:(fun acc (lib : Lib.t) -> if Lib.is_local lib then let dir = Paths.odocs (Lib lib) in Path.Set.add acc (Build_system.Alias.stamp_file (alias ~dir)) else acc))) let alias m = alias ~dir:(Paths.odocs m) (* let static_deps t lib = Build_system.Alias.dep (alias t lib) *) let setup_deps m files = SC.add_alias_deps sctx (alias m) files end let odoc = SC.resolve_program sctx "odoc" ~hint:"opam install odoc" let odoc_ext = ".odoc" module Mld : sig type t val create : Path.t -> t val odoc_file : doc_dir:Path.t -> t -> Path.t val odoc_input : t -> Path.t end = struct type t = Path.t let create p = p let odoc_file ~doc_dir t = let t = Filename.chop_extension (Path.basename t) in Path.relative doc_dir (sprintf "page-%s%s" t odoc_ext) let odoc_input t = t end let module_deps (m : Module.t) ~doc_dir ~(dep_graphs:Ocamldep.Dep_graphs.t) = Build.dyn_paths ((match m.intf with | Some _ -> Ocamldep.Dep_graph.deps_of dep_graphs.intf m | None -> (* When a module has no .mli, use the dependencies for the .ml *) Ocamldep.Dep_graph.deps_of dep_graphs.impl m) >>^ List.map ~f:(Module.odoc_file ~doc_dir)) let compile_module (m : Module.t) ~obj_dir ~includes:(file_deps, iflags) ~dep_graphs ~doc_dir ~pkg_or_lnu = let odoc_file = Module.odoc_file m ~doc_dir in SC.add_rule sctx (file_deps >>> module_deps m ~doc_dir ~dep_graphs >>> Build.run ~context ~dir:doc_dir odoc [ A "compile" ; A "-I"; Path doc_dir ; iflags ; As ["--pkg"; pkg_or_lnu] ; A "-o"; Target odoc_file ; Dep (Module.cmti_file m ~obj_dir) ]); (m, odoc_file) let compile_mld (m : Mld.t) ~includes ~doc_dir ~pkg = let odoc_file = Mld.odoc_file m ~doc_dir in SC.add_rule sctx (includes >>> Build.run ~context ~dir:doc_dir odoc [ A "compile" ; Dyn (fun x -> x) ; As ["--pkg"; Package.Name.to_string pkg] ; A "-o"; Target odoc_file ; Dep (Mld.odoc_input m) ]); odoc_file type odoc = { odoc_input: Path.t ; html_dir: Path.t ; html_file: Path.t ; html_alias: Build_system.Alias.t ; typ: [`Module | `Mld] } let odoc_include_flags requires = Arg_spec.of_result_map requires ~f:(fun libs -> let paths = libs |> List.fold_left ~f:(fun paths lib -> if Lib.is_local lib then ( Path.Set.add paths (Paths.odocs (Lib lib)) ) else ( paths ) ) ~init:Path.Set.empty in Arg_spec.S (List.concat_map (Path.Set.to_list paths) ~f:(fun dir -> [Arg_spec.A "-I"; Path dir]))) let to_html (odoc_file : odoc) ~deps ~requires = let to_remove, jbuilder_keep = match odoc_file.typ with | `Mld -> odoc_file.html_file, [] | `Module -> let jbuilder_keep = Build.create_file (odoc_file.html_dir ++ Config.jbuilder_keep_fname) in odoc_file.html_dir, [jbuilder_keep] in SC.add_rule sctx (deps >>> Build.progn ( Build.remove_tree to_remove :: Build.mkdir odoc_file.html_dir :: Build.run ~context ~dir:Paths.html_root odoc [ A "html" ; odoc_include_flags requires ; A "-o"; Path Paths.html_root ; Dep odoc_file.odoc_input ; Hidden_targets [odoc_file.html_file] ] :: jbuilder_keep ) ); odoc_file.html_file let css_file = Paths.html_root ++ "odoc.css" let toplevel_index = Paths.html_root ++ "index.html" let setup_library_odoc_rules (library : Library.t) ~scope ~modules ~requires ~(dep_graphs:Ocamldep.Dep_graph.t Ml_kind.Dict.t) = let lib = Option.value_exn (Lib.DB.find_even_when_hidden (Scope.libs scope) library.name) in (* Using the proper package name doesn't actually work since odoc assumes that a package contains only 1 library *) let pkg_or_lnu = pkg_or_lnu lib in let doc_dir = Paths.odocs (Lib lib) in let obj_dir = Lib.obj_dir lib in let includes = (Dep.deps requires, odoc_include_flags requires) in let modules_and_odoc_files = List.map (Module.Name.Map.values modules) ~f:( compile_module ~obj_dir ~includes ~dep_graphs ~doc_dir ~pkg_or_lnu) in Dep.setup_deps (Lib lib) (List.map modules_and_odoc_files ~f:snd |> Path.Set.of_list) let setup_css_rule () = SC.add_rule sctx (Build.run ~context ~dir:context.build_dir odoc [ A "css"; A "-o"; Path Paths.html_root ; Hidden_targets [css_file] ]) let sp = Printf.sprintf let setup_toplevel_index_rule () = let list_items = Super_context.packages sctx |> Package.Name.Map.to_list |> List.filter_map ~f:(fun (name, pkg) -> let name = Package.Name.to_string name in let link = sp {|%s|} name name in let version_suffix = match pkg.Package.version_from_opam_file with | None -> "" | Some v -> sp {| %s|} v in Some (sp "
  • %s%s
  • " link version_suffix)) in let list_items = String.concat ~sep:"\n " list_items in let html = sp {| index

    OCaml package documentation

      %s
    |} list_items in SC.add_rule sctx @@ Build.write_file toplevel_index html let html_alias pkg = Build_system.Alias.doc ~dir:( Path.append context.build_dir pkg.Package.path ) let libs_of_pkg ~pkg = match Package.Name.Map.find (SC.libs_by_package sctx) pkg with | None -> Lib.Set.empty | Some (_, libs) -> libs let load_all_odoc_rules_pkg ~pkg = let pkg_libs = libs_of_pkg ~pkg in SC.load_dir sctx ~dir:(Paths.odocs (Pkg pkg)); Lib.Set.iter pkg_libs ~f:(fun lib -> SC.load_dir sctx ~dir:(Paths.odocs (Lib lib))); pkg_libs let create_odoc ~target odoc_input = let html_alias = Dep.html_alias target in let html_base = Paths.html target in match target with | Lib _ -> let html_dir = html_base ++ ( Path.basename odoc_input |> Filename.chop_extension |> Stdune.String.capitalize ) in { odoc_input ; html_dir ; html_file = html_dir ++ "index.html" ; typ = `Module ; html_alias } | Pkg _ -> { odoc_input ; html_dir = html_base ; html_file = html_base ++ sprintf "%s.html" ( Path.basename odoc_input |> Filename.chop_extension |> String.drop_prefix ~prefix:"page-" |> Option.value_exn ) ; typ = `Mld ; html_alias } let setup_pkg_html_rules = let loaded = Package.Name.Table.create ~default_value:false in let odoc_glob = Re.compile (Re.seq [Re.(rep1 any) ; Re.str ".odoc" ; Re.eos]) in fun ~pkg ~libs -> if not (Package.Name.Table.get loaded pkg) then begin Package.Name.Table.set loaded ~key:pkg ~data:true; let odocs = let odocs target = let dir = Paths.odocs target in SC.eval_glob sctx ~dir odoc_glob |> List.map ~f:(fun d -> create_odoc (Path.relative dir d) ~target) in List.concat ( odocs (Pkg pkg) :: (List.map libs ~f:(fun lib -> odocs (Lib lib))) ) in let html_files = let closure = match Lib.closure libs with | Ok closure -> closure | Error _ -> (* CR diml for rgrinberg: this branch needs a comment, I don't understand why we fallback to not taking the transitive closure in case of error. *) libs in let deps = Dep.deps (Ok closure) in List.map odocs ~f:(to_html ~deps ~requires:(Ok closure)) in List.iter ( Dep.html_alias (Pkg pkg) :: List.map ~f:(fun lib -> Dep.html_alias (Lib lib)) libs ) ~f:(fun alias -> SC.add_alias_deps sctx alias (Path.Set.of_list [ css_file ; toplevel_index ]) ); List.combine odocs html_files |> List.iter ~f:(fun (odoc, html) -> SC.add_alias_deps sctx odoc.html_alias (Path.Set.singleton html) ); end let gen_rules ~dir:_ rest = match rest with | ["_html"] -> setup_css_rule (); setup_toplevel_index_rule () | "_mlds" :: _pkg :: _ | "_odoc" :: "pkg" :: _pkg :: _ -> () (* rules were already setup lazily in gen_rules *) | "_odoc" :: "lib" :: lib :: _ -> let lib, lib_db = SC.Scope_key.of_string sctx lib in begin match Lib.DB.find lib_db lib with | Error _ -> () | Ok lib -> SC.load_dir sctx ~dir:(Lib.src_dir lib) end | "_html" :: lib_unique_name_or_pkg :: _ -> let setup_html_rules pkg = setup_pkg_html_rules ~pkg ~libs:( Lib.Set.to_list (load_all_odoc_rules_pkg ~pkg) ) in (* TODO we can be a better with the error handling in the case where lib_unique_name_or_pkg is neither a valid pkg or lnu *) let lib, lib_db = SC.Scope_key.of_string sctx lib_unique_name_or_pkg in begin match Lib.DB.find lib_db lib with | Error _ -> () | Ok lib -> Option.iter (Lib.package lib) ~f:setup_html_rules end; Option.iter (Package.Name.Map.find (SC.packages sctx) (Package.Name.of_string lib_unique_name_or_pkg)) ~f:(fun pkg -> setup_html_rules pkg.name) | _ -> () let setup_package_aliases (pkg : Package.t) = let alias = html_alias pkg in SC.add_alias_deps sctx alias ( Dep.html_alias (Pkg pkg.name) :: (libs_of_pkg ~pkg:pkg.name |> Lib.Set.to_list |> List.map ~f:(fun lib -> Dep.html_alias (Lib lib))) |> List.map ~f:Build_system.Alias.stamp_file |> Path.Set.of_list ) let entry_modules ~(pkg : Package.t) ~entry_modules_by_lib = libs_of_pkg ~pkg:pkg.name |> Lib.Set.to_list |> List.filter_map ~f:(fun l -> if Lib.is_local l then ( Some (l, entry_modules_by_lib l) ) else ( None )) |> Lib.Map.of_list_exn let default_index entry_modules = let b = Buffer.create 512 in Lib.Map.to_list entry_modules |> List.sort ~compare:(fun (x, _) (y, _) -> String.compare (Lib.name x) (Lib.name y)) |> List.iter ~f:(fun (lib, modules) -> Printf.bprintf b "{1 Library %s}\n" (Lib.name lib); Buffer.add_string b ( match modules with | [ x ] -> sprintf "The entry point of this library is the module:\n{!module-%s}.\n" (Module.Name.to_string (Module.name x)) | _ -> sprintf "This library exposes the following toplevel modules:\n\ {!modules:%s}.\n" (modules |> List.sort ~compare:(fun x y -> Module.Name.compare (Module.name x) (Module.name y)) |> List.map ~f:(fun m -> Module.Name.to_string (Module.name m)) |> String.concat ~sep:" ") ); ); Buffer.contents b let check_mlds_no_dupes ~pkg ~mlds = match List.map mlds ~f:(fun mld -> (Path.basename mld, mld)) |> String_map.of_list with | Ok m -> m | Error (_, p1, p2) -> die "Package %s has two mld's with the same basename %s, %s" (Package.Name.to_string pkg.Package.name) (Path.to_string_maybe_quoted p1) (Path.to_string_maybe_quoted p2) let setup_package_odoc_rules ~pkg ~mlds ~entry_modules_by_lib = let mlds = check_mlds_no_dupes ~pkg ~mlds in let mlds = if String_map.mem mlds "index" then mlds else let entry_modules = entry_modules ~pkg ~entry_modules_by_lib in let gen_mld = Paths.gen_mld_dir pkg ++ "index.mld" in SC.add_rule sctx ( Build.write_file gen_mld (default_index entry_modules) ); String_map.add mlds "index" gen_mld in let odocs = List.map (String_map.values mlds) ~f:(fun mld -> compile_mld (Mld.create mld) ~pkg:pkg.name ~doc_dir:(Paths.odocs (Pkg pkg.name)) ~includes:(Build.arr (fun _ -> Arg_spec.As [])) ) in Dep.setup_deps (Pkg pkg.name) (Path.Set.of_list odocs) let init ~modules_by_lib ~mlds_of_dir = let docs_by_package = let map = lazy ( SC.stanzas sctx |> List.concat_map ~f:(fun (w : SC.Dir_with_jbuild.t) -> List.filter_map w.stanzas ~f:(function | Jbuild.Stanza.Documentation (d : Jbuild.Documentation.t) -> Some (d.package.name, (w.ctx_dir, d)) | _ -> None )) |> Package.Name.Map.of_list_multi ) in fun (p : Package.t) -> Option.value (Package.Name.Map.find (Lazy.force map) p.name) ~default:[] in let modules_by_lib = let module M = Map.Make( struct type t = Path.t * string let compare (d1, l1) (d2, l2) = match Path.compare d1 d2 with | Ordering.Eq -> String.compare l1 l2 | o -> o end) in let lib_to_library = lazy ( SC.stanzas sctx |> List.concat_map ~f:(fun (w : SC.Dir_with_jbuild.t) -> List.filter_map w.stanzas ~f:(function | Jbuild.Stanza.Library (l : Library.t) -> Some ((w.ctx_dir, Library.best_name l), l) | _ -> None )) |> M.of_list_exn ) in fun lib -> let dir = Lib.src_dir lib in let library = Option.value_exn ( M.find (Lazy.force lib_to_library) (dir, Lib.name lib) ) in modules_by_lib ~dir library in SC.packages sctx |> Package.Name.Map.iter ~f:(fun (pkg : Package.t) -> let rules = lazy ( setup_package_odoc_rules ~pkg ~mlds:( docs_by_package pkg |> List.concat_map ~f:(fun (dir, doc) -> mlds_of_dir doc ~dir) ) ~entry_modules_by_lib:modules_by_lib ) in List.iter [ Paths.odocs (Pkg pkg.name) ; Paths.gen_mld_dir pkg ] ~f:(fun dir -> SC.on_load_dir sctx ~dir ~f:(fun () -> Lazy.force rules)); (* setup @doc to build the correct html for the package *) setup_package_aliases pkg; ); Super_context.add_alias_deps sctx (Build_system.Alias.private_doc ~dir:context.build_dir) (SC.stanzas sctx |> List.concat_map ~f:(fun (w : SC.Dir_with_jbuild.t) -> List.filter_map w.stanzas ~f:(function | Jbuild.Stanza.Library (l : Jbuild.Library.t) -> begin match l.public with | Some _ -> None | None -> let scope = SC.find_scope_by_dir sctx w.ctx_dir in Some (Option.value_exn ( Lib.DB.find_even_when_hidden (Scope.libs scope) l.name) ) end | (_ : Jbuild.Stanza.t) -> None )) |> List.map ~f:(fun (lib : Lib.t) -> Build_system.Alias.stamp_file (Dep.alias (Lib lib))) |> Path.Set.of_list ) end