OCaml Onboarding: Introduction to the Dune build system
Welcome to all Camleers
We are back with another practical walkthrough for the newcomers of the OCaml ecosystem. We understand from the feedback we have gathered over the years that getting started with the OCaml Distribution can sometimes be perceived as challenging at first. That's why we keep it in mind when planning each post - to make your onboarding smoother and more approachable.
Case in point: today's topic, which came to us during the making of our latest
opam deep-dive
: Opam 103: Bootstrapping a New OCaml Project with
opam.
It occured to us that we were assuming a level of familiarity with the toolchain that we had never explicitly explained or clarified. We decided to put together a short, practical guide for the newer developers, looking for quick, on-the-fly tutorials for OCaml. 🛠️
A Camleer's basics: Dune
If you're new to OCaml, or any other programming language for that matter, the
first necessities you'll encounter are building, running, and
testing your code. Fortunately, there is a powerful build system called
dune
that we can use. It is widespread and makes project setup and
compilation straightforward. Understanding how dune
works is a key step
towards becoming productive in the OCaml ecosystem.
In this article, we’ll walk you through the essentials of using dune
to
build libraries, executables, and tests, and to manage your
project structure. Whether you're writing your first OCaml program or
stepping into a new dune-based codebase, this guide will help you get up and
running quickly.
We strongly believe that starting from scratch is key when approaching a brand new technical topic — and today's topic is no exception. Anyone who has ever felt lost exploring a new codebase knows that minimal, toy examples are often the best way to build intuition.
Ressources
As said previously, this article was written in the context of the latest
Opam 103: Bootstrapping a New OCaml Project with
opam.
That article explained how an OCaml developer should go about structuring an
OCaml project when they intend to use it with opam
.
The point of today's topic is to focus on the other defining parameter of the
structure of an OCaml project: your build system. The goal is to show how
the workflows of opam
and dune
fit together, while giving you a solid
introduction to the fundamentals of dune.
We're using the same toy
project
helloer
as basis for this rundown. It's a simple, well-scoped example with a
structure that's idiomatic to both opam and dune, making it a great fit for
illustrating the fundamentals without unnecessary complexity.
Note that helloer
was not created using dune init
that we will introduce at
the end of this article. First, it's important to understand how Dune works
under the hood - so you know what it's generating for you, how to modify it
confidently, and how it fits into your overall build workflow.
Consider checking Dune's official reference manual or visiting the official OCaml Discuss forum to reach out to the OCaml Community.
Project metadata and build specification files
dune-project
Let's first start with the dune-project
file since every Dune-driven
project should have one at its root.
This file is the entry point for your project and its contents are its metadata — which Dune uses to understand how your project is structured.
Said metadata includes things like:
- the version of
dune
you're using; - important URLs for your projects lifecycles;
- optional settings like dependencies licensing, documentation;
- and even configuration for automatic opam file generation. More on that in Opam 103.
This information not only guides Dune, but also helps tools like opam understand how to build, distribute, and document your project.
$ cat dune-project
(lang dune 3.15)
(package (name helloer))
(cram enable)
Note: The first line must be (lang dune X.Y)
- with no comments or extra
whitespace. This line determines which features and syntax dune will recognize.
NB: You will find all complementary information in the official docs 👈.
dune file
A dune
file is a build specification file that tells Dune how to compile
the OCaml code within a specific directory.
Usually there's one dune
file per subdirectory, with the description of
what's there - library, executable, or some tests. Since our toy helloer
project is flat in structure, we’ll place this file at the root of the
project.
$ cat dune
(library
(name helloer_lib)
(modules helloer_lib)
)
(executable
(public_name helloer)
(name helloer)
(libraries cmdliner helloer_lib)
(modules helloer)
)
(test
(name test)
(libraries alcotest helloer_lib)
(modules test)
)
In effect, this tells dune
:
- how to build the OCaml files in that directory;
- how libraries, executables, and test targets are defined.
Key stanzas
In the context of Dune, a
stanza
is just a fancy word for a block of configuration. It tells the build
system what kind of artifact you want to define — be it a library, an
executable, a test, a documentation alias, or even an installable binary. Each
stanza lives inside a dune
file and follows a structured, declarative syntax.
They’re usually grouped by purpose, and each type comes with its own expected fields. Each of these stanzas deserves a deeper dive, but here's a quick overview to get you started.
library
stanza
(library
(name helloer_lib)
(modules helloer_lib)
)
A
library
stanza tells Dune how to compile a set of modules into a reusable package.
Purpose of this stanza:
- defines a library named
helloer_lib
; - which will be built from the module
helloer_lib.ml
(by default, each .ml file defines a module with the same name); - and only the exposed modules should be listed here - that is, the modules that are meant to be part of the library's public API and usable by other parts of the project or by external code.
OCaml module names should match the filename, so helloer_lib.ml
is expected
to exist in this directory.
executable
stanza
(executable
(public_name helloer)
(name helloer)
(libraries cmdliner helloer_lib)
(modules helloer)
)
An
executable
stanza explains how to bundle up some code into a runnable binary.
Purposes:
name
: builds an executable namedhelloer
;- needs libraries: external
cmdliner
(for CLI parsing) and internalhelloer_lib
(our own library); public_name helloer
: this makes the executable available publicly. It is used fordune install helloer
in theopam
file for instance.
You can learn about how to find and install
cmdliner
inopam
in the latest Opam 103 blogpost, you'll find a simple breakdown ofopam
files there too .
test
stanza
(test
(name test)
(libraries alcotest helloer_lib)
(modules test)
)
What it does:
- declares a test target named
test
, defined in the filetest.ml
. Atest
stanza registers the executable as part of theruntest
rule alias, meaning it will be compiled and run automatically when you invokedune runtest
(or its aliasdune test
); - uses the
alcotest
testing library; - also uses
helloer_lib
to test its functionality.
Now your project is setup and structured. Next, let’s see how to build it.
Build and run your project
dune build
As you can see below, the dune build @all
command will build all targets
defined in your dune
files, it's the default behaviour of the dune build
command.
$ tree
.
├── dune
├── dune-project
├── helloer_lib.ml
├── helloer.ml
├── helloer.opam
└── test.ml
$ dune build @all
$ tree -L 2
.
├── _build
│ ├── default
│ │ ├── helloer.exe // executable in its build dir
│ │ ├── helloer_lib.cmxs // built library
│ │ ├── test.exe // test executable
│ │ └── [...]
│ ├── install
│ └── log
├── dune
├── dune-project
├── helloer_lib.ml
├── helloer.ml
├── helloer.opam
└── test.ml
Explanation:
@all
is an alias that includes all buildable targets defined in your dune files: executables, libraries, tests, docs, etc;- it is useful for doing a full build to ensure everything compiles.
You can also use custom aliases (like @doc
, @runtest
, etc.), or
define your own in your dune files.
dune build @doc
Once your code builds and your project has a proper dune-project
file, you
can generate documentation using:
$ dune build @doc
What it does:
- uses
odoc
behind the scenes to build API docs from your OCaml code. This implies that installingodoc
is mandatory to benefit from this feature, a simpleopam install odoc
will do just fine; - builds HTML files in
_build/default/_doc/_html/
.
Make sure your dune-project
file includes a (package ...)
stanza, and that
your libraries are properly documented using OCaml comments (** your comment *)
.
You can see generate the doc for the toy project here
NB: You will find all complementary information in the official docs 👈.
After building, you can view the generated docs:
$ open _build/default/_doc/_html/index.html
This is great for checking your module interfaces or publishing documentation online.
dune exec --
This command is used to run executables defined in your project.
So, something like:
$ dune exec -- ./helloer.exe
Hello OCamlers!!
$ dune exec -- ./helloer.exe --gentle
Welcome my dear OCamlers.
This tells dune to build the executable if necessary, then run it. The --
separates the dune options from the executable and its arguments. The first
item after --
is the executable to run
This can be:
- A relative path to a built target, so:
dune exec -- ./path/to/executable
- A public name of an installed executable, meaning:
dune exec -- ./helloer
.
All additional arguments after the executable name (like --gentle
) are passed
to the executable itself.
Essentially, dune exec -- COMMAND
behaves the same way as calling dune install
first and then COMMAND
sequentially.
NB: If you'd like to copy the executable to your project root (outside
_build/
), you can add(promote (until-clean))
to your executable stanza.
Great, our little project builds and runs smoothly, now onto testing it.
Test your project with Dune
In our helloer
project, we use the alcotest
library on our internal
helloer_lib
. This is quite standard. However testing the executable itself
can be done without depending on an external tool with the help of cram
tests.
Cram tests
Dune supports a special kind of test called a cram test, inspired by the original Cram, which checks that command-line examples produce the expected output.
The "expected output" is the shell-session itself and whatever your executable prints, during its test run for that specific call, is checked against it.
To create a cram test, you just write a .t
file that contains a succession of
shell-like sessions separated by empty newlines like so:
$ helloer
Hello OCamlers!!
$ helloer --gentle
Welcome my dear OCamlers.
How it works:
- it runs the commands in
.t
files; - it compares what is printed to
stdout
by our binary to the expected output written in the cram file; - fails if the outputs differ. However, you can use
dune promote
whenever you wish to replace all the failed tests with the new output, which will most often happen when you make changes to your binary's printing tostdout
.
You can test it here.
dune runtest
You can run all your tests using:
$ dune runtest
- it builds
test
targets defined in your project; - it looks for files ending in
.t
or.ml
files marked as tests; - it executes the tests, often using expect style testing (like
ppx_expect
oralcotest
).
It's quite straightforward: if you have an inline_tests
stanza or an
expect
test, it will run them and tell you if anything failed.
For example, a valid cram test will output something like:
$ dune runtest
Testing `Tests'.
This run has ID `N39NJ5ZE'.
[OK] messages 0 normal.
[OK] messages 1 gentle.
Full test results in `~/ocamler/dev/helloer/_build/default/_build/_tests/Tests'.
Test Successful in 0.000s. 2 tests run.
However, if one of these tests were to fail, you would see something like:
$ dune runtest
File "test.t", line 1, characters 0-0:
diff --git a/_build/.sandbox/e6d6dcfb864b62e42104889af2a44f23/default/test.t b/_build/.sandbox/e6d6dcfb864b62e42104889af2a44f23/default/test.t.corrected
index f79b63c..70c7a17 100644
--- a/_build/.sandbox/e6d6dcfb864b62e42104889af2a44f23/default/test.t
+++ b/_build/.sandbox/e6d6dcfb864b62e42104889af2a44f23/default/test.t.corrected
@@ -3,7 +3,7 @@ Default behaviour
Hello OCamlers!!
Gentle behaviour
$ helloer --gentle
- Welcome my deer OCamlers.
+ Welcome my dear OCamlers.
Unknown behaviour
$ helloer --unknown
helloer: unknown option '--unknown'.
File "dune", line 16, characters 7-11:
16 | (name test)
^^^^
Testing `Tests'.
This run has ID `1OS0H3WP'.
[OK] messages 0 normal.
> [FAIL] messages 1 gentle.
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ [FAIL] messages 1 gentle. │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
ASSERT same string
FAIL same string
Expected: `"Welcome my deer OCamlers."'
Received: `"Welcome my dear OCamlers."'
Raised at Alcotest_engine__Test.check in file "src/alcotest-engine/test.ml", lines 216-226, characters 4-19
Called from Alcotest_engine__Core.Make.protect_test.(fun) in file "src/alcotest-engine/core.ml", line 186, characters 17-23
Called from Alcotest_engine__Monad.Identity.catch in file "src/alcotest-engine/monad.ml", line 24, characters 31-35
Logs saved to `~/ocamler/dev/helloer/_build/default/_build/_tests/Tests/messages.001.output'.
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Full test results in `~/ocamler/dev/helloer/_build/default/_build/_tests/Tests'.
1 failure! in 0.000s. 2 tests run.
At this point in the development process, we can assume that you know how to
use the most basic dune
command-lines to make your OCaml projects a
reality!
Now that we’ve explored how Dune works at a foundational level — writing
stanzas by hand, managing libraries and executables, building, running, testing
— you’re probably starting to see patterns. These project ingredients don’t
change much from one small OCaml project to the next. That’s exactly where dune init
comes in.
Scaffolding with dune init
dune init
is the starting point for creating a new OCaml project using Dune.
It scaffolds a working directory structure and sets up the essential files
you’ll need.
Rather than writing every file from scratch, Dune offers a command-line scaffolding tool that sets up a complete, minimal project for you — so you can jump straight to writing code with a solid structure already in place.
This means the following command is all you need to scaffold a basic project:
$ dune init project helloer
What it does:
- creates a new directory
helloer
with a working OCaml project inside; - sets up the dune-project file;
- adds sample source files and their associated dune build files.
The structure you'll get looks like this:
$ tree
helloer/
├── bin/
│ ├── dune
│ └── main.ml
├── dune-project
├── lib
│ ├── dune
├── test
│ ├── dune
│ ├── test_helloer.ml
└── [...]
From here, you can build on this template by adding libraries, tests, and more.
If your project is only a library or binary, you can use the other project template with
dune init lib helloer
ordune init exec helloer
.
Sharp-eyed readers may notice differences between our toy project and the
layout generated by dune init
.
You can see the end result in this branch.
Conclusion
Indeed, you should be comfortable with the basic building blocks of a
dune
-based OCaml project: from initializing it, defining libraries and
executables, to running it and writing tests, and even generating
documentation. dune
takes care of a lot of the heavy lifting, letting you
focus on writing code rather than fiddling with build scripts. As you grow more
confident with OCaml and Dune, you’ll discover even more powerful features—but
for now, you’re well-equipped to start building real-world OCaml applications.
Au sujet d'OCamlPro :
OCamlPro développe des applications à haute valeur ajoutée depuis plus de 10 ans, en utilisant les langages les plus avancés, tels que OCaml, Rust, et WebAssembly (Wasm) visant aussi bien rapidité de développement que robustesse, et en ciblant les domaines les plus exigeants (méthodes formelles, cybersécurité, systèmes distribués/blockchain, conception de DSLs). Fort de plus de 20 ingénieurs R&D, avec une expertise unique sur les langages de programmation, aussi bien théorique (plus de 80% de nos ingénieurs ont une thèse en informatique) que pratique (participation active au développement de plusieurs compilateurs open-source, prototypage de la blockchain Tezos, etc.), diversifiée (OCaml, Rust, Cobol, Python, Scilab, C/C++, etc.) et appliquée à de multiples domaines. Nous dispensons également des [formations sur mesure certifiées Qualiopi sur OCaml, Rust, et les méthodes formelles] (https://training.ocamlpro.com/) Pour nous contacter : contact@ocamlpro.com.
Articles les plus récents
2025
2024
- opam 2.3.0 release!
- Optimisation de Geneweb, 1er logiciel français de Généalogie depuis près de 30 ans
- Alt-Ergo 2.6 is Out!
- Flambda2 Ep. 3: Speculative Inlining
- opam 2.2.0 release!
- Flambda2 Ep. 2: Loopifying Tail-Recursive Functions
- Fixing and Optimizing the GnuCOBOL Preprocessor
- OCaml Backtraces on Uncaught Exceptions
- Opam 102: Pinning Packages
- Flambda2 Ep. 1: Foundational Design Decisions
- Behind the Scenes of the OCaml Optimising Compiler Flambda2: Introduction and Roadmap
- Lean 4: When Sound Programs become a Choice
- Opam 101: The First Steps
2023