OCaml Best Practices
Workflows presented on this page assume OCaml is Up and Running.
Bootstrap a project
Dune is recommended for bootstrapping projects using dune init
. If opam
or dune
are not installed, please see Up and Running with OCaml.
dune init --help
dune init {library,executable,test,project} NAME [PATH] initialize a new dune component of the specified kind, named NAME, with fields determined by the supplied options.
As shown above, dune init
accepts a kind, NAME
, and optional PATH
to scaffold new code. Let's try it out:
dune init project hello ~/src/ocaml-projects
Success: initialized project component named hello
In the above example, we use:
- "project" as the kind
- "hello" as the name, and
- "~/src/ocaml-projects" as the path to generate the content in
The project
kind creates a library
in ./lib
, an executable
in ./bin
, and links them together in bin/dune
. Additionally, the command creates a test executable and an opam file.
tree ~/src/ocaml-projects/hello/
/home/user/src/ocaml-projects/hello/
├── bin
│ ├── dune
│ └── main.ml
├── hello.opam
├── lib
│ └── dune
└── test
├── dune
└── hello.ml
At this point, you can build the project and run the binary:
cd /home/user/src/ocaml-projects/hello/
dune exec bin/main.exe
Hello, world!
Thus, dune init
can rapidly scaffold new projects, with minimal content. It can also be used to add components (kinds) incrementally to existing projects.
Various community projects offer more comprehensive project scaffolding than dune
as well.
The following projects are not formally supported by the OCaml Platform, but may be of interest to the reader:
Installing dependencies
TL;DR
opam switch create . --deps-only --with-test --with-doc
It is recommended to install the dependencies of a project in a local opam switch to sandbox your development environment.
If you're using opam 2.0.X
, you can do this with:
# if you need external dependencies
opam pin add -n .
opam depext -i <packages>
opam install . --deps-only --with-test --with-doc
If you use opam 2.1.X
, it will install the system dependencies automatically, so you can run:
opam install . --deps-only --with-test --with-doc
Now, if for some reason you prefer to install your dependencies in a global switch, you can run:
opam switch set <switch_name>
opam install . --deps-only --with-test --with-doc
Once the dependencies have been installed successfully, and assuming the project uses dune
as the build system, you can compile it with:
opam exec -- dune build
Or if you set your environment with eval $(opam env)
:
dune build
Updating dependencies
TL;DR
If the project generates the
*.opam
file from thedune-project
, add the dependency in thepackage
stanza and runopam install . --deps-only
. If the project does not generate the*.opam
file, add the dependency in the*.opam
file and runopam install . --deps-only
. To avoid duplicating the project configuration into multiple files, Dune allows to generate the*.opam
file of the project from the package definitions indune-project
when adding the(generate_opam_files true)
stanza.
However, opam remains a central piece of the ecosystem and it's very likely that you will have to work with *.opam
files at some point,
so we don't take a stance on wether you should specify your dependencies in the *.opam
file or in dune-project
.
If the project generates the opam file from the dune-project
(you can tell by the line # This file is generated by dune, edit dune-project instead
at the top of the *.opam
file), you can add your dependencies in the dune-project
in the appropriate package
stanza. It should look like this:
namesynopsis"A short, but powerful statement about your project""An complete and exhaustive description everything your project does."4:with:with
Once you have added your dependency, you can build your project with dune build
which will re-generate the *.opam
files.
If the *.opam
files are not generated, you can add the dependencies in them directly, in the depends
field. If should look like this:
opam-version: "2.0"
synopsis: "A short, but powerful statement about your project"
description: "An complete and exhaustive description everything your project does."
depends: [
"ocaml" {>= "4.08.0"}
"dune"
"alcotest" {with-test}
"odoc" {with-doc}
]
build: [
["dune" "subst"] {pinned}
[
"dune"
"build"
"-p"
name
"-j"
jobs
"@install"
"@runtest" {with-test}
"@doc" {with-doc}
]
]
Either way, once you have added your dependency in the appropriate file, you can run opam install . --deps-only
to update your current switch dependencies.
Updating development dependencies
TL;DR
Follow the workflow "Update dependencies" and add a flag
:with-test
orwith-doc
to your dependency. Opam does not have a notion of development dependencies. Instead, each dependency can be either:
- A normal dependency (used at runtime)
- A build dependency (used to build the project)
- A test dependency (used to test the project)
- A documentation dependency (used to generate the documentation)
When adding a new dependency, as seen in the "Update dependencies" workflow, you can add a flag to your dependency.
For dune-project
, it looks like this:
:with
And for the *.opam
file, it looks like:
"alcotest" {with-test}
The available flags for each dependencies are:
- Normal: no flag
- Build:
build
- Test:
with-test
- Documentation:
with-doc
See opam documentation for more details on the opam syntax.
Selecting a compiler
TL;DR
Use
opam switch set
to manually select the switch to use and usedune-workspace
to automatically run commands in different environment.
Compilation environments are managed with opam switches. The typical workflow is to have a local opam switch for the project, but you may need to select a different compilation environment (i.e. a different compiler version) sometimes. For instance, to run unit tests on an older/newer version of OCaml.
To do this, you'll need to create global opam switches. To create an opam switch with a given version of the compiler, you can use:
opam switch create 4.14.0 ocaml-base-compiler.4.14.0
This will create a new switch called 4.14.0
with the compiler version 4.14.0
.
The list of available compiler version can be retrieved with:
opam switch list-available
This will list the available compiler version for all of the configured Opam repositories.
Once you've created a switch (or you already have a switch you'd like to use), you can run:
opam switch set <switch_name>
eval $(opam env)
to configure the current environment with this switch.
If it is a new switch, you will need to reinstall your dependencies (see "Installing dependencies") with opam install . --deps-only
.
Alternatively, you may want to automatically run commands in a given set of compilation environments. To do this, you can create a file dune-workspace
at the root of your project and list the opam switches you'd like to use there:
(lang dune 2.0)
(context (opam (switch 4.11.0)))
(context (opam (switch 4.12.0)))
(context (opam (switch 4.13.0)))
All the Dune commands you will run, will be run on all of the switches listed. For instance with the definition above:
dune runtest --workspace dune-workspace
Dune will run the tests for OCaml 4.11.0
, 4.12.0
and 4.13.0
.
Running executables
TL;DR
Add an
executable
stanza in your dune file and run the executable withdune exec <executable_path>.exe
ordune exec <public_name>
.
To tell dune to produce an executable, you can use the executable stanza:
namepublic_namelibraries
The <executable_name>
is the name of the executable used internally in the project.
The <public_name>
is the name of the installed binary when installing the package.
Finally, <libraries...>
is the list of libraries to link to the executable.
Once dune has produced the executable with dune build
, you can execute it with dune exec <executable_path>.exe
or dune exec <public_name>
.
For instance, if you've put your dune file in bin/dune
with the following content:
namepublic_name
You can run it with dune exec bin/main.exe
or dune exec my-app
.
Running tests
TL;DR
Add a
test
stanza in your dune file and run the tests withdune build @runtest
.
Tests are created using Dune's test
stanza. The test
stanza is a simple convenience wrapper that will create an executable and add it to the list of tests of the @runtest
target.
For instance, if you add a test in your dune file:
namemodules
with a module dummy_test.ml
:
let () = exit 1
Running dune build @runtest
will fail with the following output:
dummy_test alias src/ocamlorg_web/test/runtest (exit 1)
This means that the test failed because the executable exited with the status code 1
.
The output is not very descriptive. If we want to create suites of unit tests, with several tests per files, and different kind of assertions, we will want to use a test framework such as Alcotest.
Let's modify our dummy test to link to Alcotest:
namemoduleslibraries
With the following module:
open Alcotest
let test_hello_with_name name () =
let greeting = "Hello " ^ name ^ "!" in
let expected = Printf.sprintf "Hello %s!" name in
check string "same string" greeting expected
let suite =
[ "can greet Tom", `Quick, test_hello_with_name "Tom"
; "can greet John", `Quick, test_hello_with_name "John"
]
let () =
Alcotest.run "Dummy" [ "Greeting", suite ]
If we run dune build @runtest
again, the test should be successful and output the following:
Testing `Dummy'.
This run has ID `B5926D16-0DD4-4C97-8C7A-5AFE1F5DF31B'.
[OK] Greeting 0 can greet Tom.
[OK] Greeting 1 can greet John.
Full test results in `_build/default/_build/_tests/Dummy'.
Test Successful in 0.000s. 2 tests run.
Creating libraries
TL;DR
Add a
library
stanza in your dune file.
Creating a library with dune is as simple as adding a library
stanza in your dune file:
namepublic_namelibraries
Where <name>
is the name of the library used inside internally, <public_name>
is the name of the library used by users of the package and <libaries...>
is the list of libraries to link to your library.
Note that if the library does not have a public_name
, it will not be installed when installing the package through opam. As a consequence, you cannot use an internal library that does not have a public_name
in a library or executable that has one.
Publishing packages
TL;DR
Create a
CHANGES.md
file and rundune-release bistro
.
The opam package manager may differ from the package manager you're used to. In order to ensure the highest stability of the ecosystem, each package publication goes through two processes:
- An automated CI pipeline which tests if your package installs using multiple distributions and multiple OCaml compiler versions. It will also check that your new release does not break your reverse dependencies (those packages that require your package). A lower-bound check also ensures that your package installs with the lowest version of your package's dependencies.
- A manual review of the package metadata by an opam-repository maintainer.
This process starts with a PR to the opam-repository, with the addition of a file for the version of the package to publish. The file contains information such as the package name, description, VCS repository, and most importantly, the URL the sources can be downloaded from.
If everything looks good and the CI build passes, the PR is merged and the package becomes available in opam after an opam update
to update the opam-repository.
If there is anything to change, an opam-repository maintainer will comment on the PR with some recommendations.
This is a heavy process, but hopefully, all of it is completely automated on the user side. The recommended way to publish a package is dune-release
.
Once you're ready to publish your package on opam, simply create a CHANGES.md
file with the following format:
# <version>
<release note>
# <older version>
<release note>
and run dune-release bistro
.
Dune Release will run some verification (such as running the tests, linting the opam file, etc.) and will open a PR for you on opam-repository
. From there, all you have to do is wait for the PR to be merged, or for a maintainer to review your package publication.
Setting up VSCode
TL;DR
Install the VSCode extension
ocamllabs.ocaml-platform
and the packagesocaml-lsp-server ocamlformat
in your opam switch.
The official OCaml extension for VSCode is https://marketplace.visualstudio.com/items?itemName=ocamllabs.ocaml-platform.
To get started, you can install it with the following command:
ext install ocamllabs.ocaml-platform
The extension depends on OCaml LSP and ocamlformat. To install them in your switch, you can run:
opam install ocaml-lsp-server ocamlformat
When running vscode
from the terminal, the extension should pick up your current opam switch. If you need to change it, you can click on the package icon in the status bar to select your switch.