Dune and odoc

Using Dune

To create docs with odoc and Dune is straightforward, but there is an important point to know: Dune only creates docs for public packages, so you will need a (public_name ...) stanza in your libraries and a corresponding lib.opam file in the root of your project.

The following files are a simple example:

Dune creates the docs for these with this command:

$ dune build @doc

and the results will be in _build/default/_doc/_html/.

Dune's Library Wrapping

Dune has a feature whereby a library may be exposed under a single top-level module. This makes use of an OCaml feature where the use of the compiler flag -no-alias-deps is used to avoid introducing dependencies between compilation units.

We aim to reduce the potential name clashes of modules by only exposing one main module for library users to use, encapsulating all other modules as submodules, while still retaining the usual way of writing OCaml code with one module per file. These individual files are still compiled, and installed, and available in the global namespace, but their names are prefixed with the library's name in order to reduce the possibility of clashes. These prefixed modules are not intended to be used directly, so Dune includes canonical tags for these modules for odoc to ensure they don't 'leak' into the documentation.

Example

Given two modules: A and B, with B referencing types declared in module A:

(** Module A *)
type t
(** Module B *)
type t = A.t

If these modules are to become part of a library called Lib, then Dune will compile these two as if their names were Lib__A and Lib__B and also create a file lib.ml containing the following:

(** @canonical Lib.A *)
module A = Lib__A

(** @canonical Lib.B *)
module B = Lib__B

This will be the one module intended to be used directly by the library's users. This module is in fact compiled first, using the compiler flag --no-alias-deps, which allows it to be compiled without requiring Lib__A and Lib__B to be compiled first.

Dune will then compile a.ml and b.ml, in that order, but ask the compiler to name them Lib__A and Lib__B. It also 'opens' the module Lib, which is what allows B to refer to A.t.

When odoc is used to produce documentation for this, firstly all modules are compiled, but only one module is considered to be visible: Lib. All others have double underscores meaning they are hidden. Only the non-hidden module Lib is linked, and during this process, the the modules A and B are expanded because they are aliases of hidden modules. All references to Lib__A and Lib__B are replaced with the canonical paths Lib.A and Lib.B, so in this way odoc presents the library as entirely contained within the module Lib.

Hand-Written Top-Level Module

In some cases it's desirable to hand-write the top-level library module. This is usually done because some of the modules within the library are intended to be internal only and not exposed. Dune will notice that a module exists with the name of the library (lib.ml in this case), so instead it will create the file lib__.ml. The contents of this are identical to the previous section, with aliases for all modules. The canonical tags on the aliases are, as before, to Lib.A and Lib.B. These are references to module aliases that should be present in lib.ml. If these are not there, odoc won't be able to resolve the canonical references, and any items from these modules that are exposed elsewhere will be hidden. If the items are type aliases they can be replaced, but otherwise they'll be rendered as unresolved links.

For example, consider the following module structure. First, the module Unexposed in file unexposed.mli:

(** Unexposed module *)

type t

The module Wrapping, in file wrapping.mli:

(** Example of Dune's wrapping *)

type t = Unexposed.t

val f : Unexposed.t

and the library module that only exposes the module Wrapping:

module Wrapping = Wrapping

This structure is rendered here.