Enforcing Reproducible OCaml Builds with Dune Explicit Dependencies
Learn how to use Dune's explicit module and library stanzas to eliminate implicit dependencies and ensure reproducible, sandboxed OCaml builds.
25 Jul 2026, 16:27 UTC

Eliminating Implicit Build Dependencies
Hand-written Makefiles and legacy ocamlfind-based builds often allow source files to slip into a library silently or let executables link against undeclared libraries. This creates "works on my machine" syndromes where builds fail in CI despite passing locally. The solution is to use Dune with a declarative configuration that enforces an explicit dependency graph. By declaring a dune-project at the root, listing modules explicitly in library stanzas, and referencing libraries by their Dune names, you ensure reproducible incremental recompilation and sandboxed builds.
The Mechanism of the Dune Dependency Graph
Dune operates by reading a dune-project file to establish the language version and project identity, then scanning dune files in each directory. A stanza (an S-expression block) defines the target type. For example, a (library ...) stanza tells Dune exactly which .ml files form the library, while an (executable ...) stanza defines the entry point and its dependencies.
Dune tracks file timestamps and content hashes to perform incremental builds, recompiling only the affected targets. To prevent implicit environment dependencies—such as a build succeeding only because a specific library happens to be installed in the user's global opam switch—Dune uses a sandbox. The sandbox is an isolated build environment that only exposes files and libraries explicitly declared in the dune files.
Worked Configuration Example
To implement this, create a project root containing a dune-project file and a src directory for source code.
; dune-project (at project root)
(lang dune 3.0)
(name my_project)
; src/dune
(library
(name mylib)
(modules Mylib))
(executable
(name main)
(modules Main)
(libraries mylib))
In this setup, src/Mylib.ml provides the logic and src/Main.ml calls it. The library name mylib is a Dune-internal name, not a global ocamlfind name. This distinction is critical for maintaining the sandbox.
Run the build from the project root using a user account with dune installed:
dune build
Verification: Check the _build/default/src directory for compiled artifacts. To verify the explicit nature of the build, remove Mylib from the (modules ...) list and rerun dune build; the module will be excluded from the compiled library, confirming that Dune does not automatically include every .ml file in the directory.
Limitations and Maintenance Trade-offs
Dune's strictness introduces specific constraints that developers must manage:
- No Automatic Inference: Dune does not infer missing module dependencies. If a file exists in the folder but is omitted from the
(modules ...)list, it is ignored. This prevents accidental inclusions but increases the manual effort required when adding new files. - Isolated Prerequisites: Implicit prerequisites outside the declared tree are ignored. If
Main.mluses a function from a library not listed in the(libraries ...)stanza, the build will fail with a missing symbol at link time, even if the library is installed on the system. - Maintenance Overhead: Explicit module lists can become tedious in large projects. While Dune 3.x offers
(modules :standard)for auto-discovery, this trades some reproducibility for convenience.
Common Configuration Mistakes
Version Mismatches: The behavior of (lang dune ...) is version-sensitive. A project pinned to 2.x handles implicit transitive dependencies differently than 3.x. Always pin a specific version in dune-project to ensure consistent behavior across team members.
OCamlfind vs. Dune Names: Referencing libraries by their ocamlfind names instead of Dune library names can break the sandbox. This allows the build to leak dependencies from the global environment, defeating the purpose of a reproducible build.
Transitive Dependency Assumptions: Dune does not automatically re-export dependencies. If your executable needs a function from Lib_B, and Lib_B is a dependency of mylib, you must still explicitly list Lib_B in the executable's (libraries ...) stanza unless mylib explicitly re-exports it.
Rollback: Because dune build produces artifacts in the _build directory without modifying source files, you can reset the build state by running dune clean.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.