Table of contents

OCaml Onboarding: Dune essentials

Date: 2025-05-27
Category: Trainings



A camel sitting atop a dune in the middle of the desert. He wears his hard hat as he takes a break from all the building and running of OCaml code.

A camel sitting atop a dune in the middle of the desert. He wears his hard hat as he takes a break from all the building and running of OCaml code.

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 boarding onto the OCaml Distribution can sometimes be perceived as challenging at first and so we try to stay aware of this when coming up with these blog ideas.

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 figured that we could write a little something for the newer devs out there who are looking for quick, on-the-fly tutorials for OCaml.

In comes the topic of today: the beginner's guide to the essentials of the Dune build system. πŸ› οΈ

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. Learning about it will definitely help you get started in the OCaml World.

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 productive 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 dev 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 both workflows (that of opam and dune) fit together while learning the fundamentals of dune at the same time.

That is why we are 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 β€” even though it wasn’t created using its initialization command. This makes it a great fit for illustrating the fundamentals without unnecessary complexity.

With that in mind, we will introduce dune init at the end of this article, so we can first focus on the core mechanics that make Dune work β€” before handing over control to its helpful scaffolding features.

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 β€” it's how Dune knows you're working in a structured project. Said metadata can be anything from the version of dune you're using, important URLs for your project's lifecycles, the definition of dependencies, and even licensing - all this and more is there to inform opam and its users.

$ cat dune-project
(lang dune 3.15)
(package (name helloer))

(cram enable)

Purposes:

  • Declares your project name, version, and many more things.
  • Specifies which Dune language version you're using (minimal compatibility).
  • Can configure features like package names for opam, documentation, and more.
  • It can even generate the opam file for you. More on that in Opam 103.

NB: You will find all complementary information in the official docs πŸ‘ˆ.

dune file

A dune file is a build specification file, typically found in each subdirectory of your project. 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. For example, a library stanza tells Dune how to compile a set of modules into a reusable package. An executable stanza explains how to bundle up some code into a runnable binary. A test stanza sets up code that will be run only during test passes.

This dune file defines three primary stanzas: one for the library, one for the executable, and one for tests.

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)
)

Purpose of this stanza:

  • Defines a library named helloer_lib.
  • This will build it from the file named helloer_lib.ml.
  • Only the exposed modules should be listed here (in this case, just helloer_lib).

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)
)

Purposes:

  • name: builds an executable named helloer.
  • Needs libraries: external cmdliner (for CLI parsing) and internal helloer_lib (our own library).
  • public_name helloer: This makes the executable available publicly. It is used for dune install helloer in the opam file for instance.

You can learn about how to find and install cmdliner in opam here!

test stanza
(test
 (name test)
 (libraries alcotest helloer_lib)
 (modules test)
)

What it does:

  • Declares a test target named test, defined in the file test.ml.
  • 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.

$ 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 installing odoc is mandatory to benefit from this feature, a simple opam 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 that are part of your OCaml project from the root:

dune exec -- ./path/to/executable

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. Everything after -- is passed to the program you’re running.

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.

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:

Ξ» cat tests/hello.t
  $ helloer
  Hello OCamlers!!

  $ helloer --gentle
  Welcome my dear OCamlers.

How it works:

  • Runs the commands in .t files.
  • Compares the actual output to the expected output.
  • Fails if the outputs differ, allowing easy updating with dune promote.

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.

You cant 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 or alcotest).

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 or dune 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 resulte in this branch and how files reorganise in this git compare.

Conclusion

Indeed, you should be comfortable with the basic building blocks of a dune-based OCaml project: from initializing it, and 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.



About OCamlPro:

OCamlPro is a R&D lab founded in 2011, with the mission to help industrial users benefit from experts with a state-of-the-art knowledge of programming languages theory and practice.

  • We provide audit, support, custom developer tools and training for both the most modern languages, such as Rust, Wasm and OCaml, and for legacy languages, such as COBOL or even home-made domain-specific languages;
  • We design, create and implement software with great added-value for our clients. High complexity is not a problem for our PhD-level experts. For example, we helped the French Income Tax Administration re-adapt and improve their internally kept M language, we designed a DSL to model and express revenue streams in the Cinema Industry, codename Niagara, and we also developed the prototype of the Tezos proof-of-stake blockchain from 2014 to 2018.
  • We have a long history of creating open-source projects, such as the Opam package manager, the LearnOCaml web platform, and contributing to other ones, such as the Flambda optimizing compiler, or the GnuCOBOL compiler.
  • We are also experts of Formal Methods, developing tools such as our SMT Solver Alt-Ergo (check our Alt-Ergo Users' Club) and using them to prove safety or security properties of programs.

Please reach out, we'll be delighted to discuss your challenges: contact@ocamlpro.com or book a quick discussion.