There's a lot to dune, the OCaml build system. It can probably do what you want, but the documentation—while thorough—never bothers to explain how to complete common tasks in a way a busy developer would find helpful.
This is a "how to" to run a script with dune. By the end, you'll be able to trigger the execution of an arbitrary program with dune to complete a useful task for your code-base. Think
"scripts" in a
package.json, or a
.PHONY target in a
Makefile. For example, you might run a code-generation CLI like tailwindcss, or push build artifacts to S3 with an
- You know your way around a terminal and basic POSIX shell commands
- You've got a working installation of dune version 3:
$ dune --version 3.4.1
If you've already got a dune project, locate the
dune file in which you'd like to define the command, change directories to that folder, and skip ahead.
Scripts are defined in a file named
dune at the root of your project (or library, or executable). Dune has a suite of
init commands to help create this file, but these create a lot of unnecessary noise too, so let's make the minimum setup manually:
$ mkdir run_task && cd run_task $ touch dune $ echo "(lang dune 3.3)" > dune-project
At this point you should be able to run:
$ dune build
Without seeing an error, and a new folder
_build appearing, which confirms we're ready to go:
. ├── _build │ ├── default │ └── log ├── dune └── dune-project
Let's quickly look at one of the commands above:
$ echo "(lang dune 3.3)" > dune-project
You might be wondering what
lang dune 3.3 means and why it's in parentheses. It's called an S-Expression, and like
TOML is a language that can be used for, among other things, configuration files. Unlike the latter few formats, S-Expressions are not frequently seen outside OCaml, but they are a common way in OCaml to serialize data, much in the way
You might find it a bizarre choice for a programming tool and language hoping to attract new users (I do). But regardless, it's how we'll "program"
dune to do what we want.
Fortunately, you don't need to know much about s-expressions to do what we need here. For now, you can think of them as named, nested arrays:
(key value value (subkey value))
Here's an imaginary example:
(package (name my-package)(version 1.0)) (dependencies lib-1 lib-2 lib-3 (testing-only tool-a tool-b)(development-only lib-a))
The trick is knowing which
keys are significant to
dune, and how
dune responds to different values for those keys, similar to how you need to know in a
npm project that the
"files" requires an array of file patterns as the value.
They can be a pain to read without adding white space, so I suggest adding whatever white space makes it easiest for you. The below is equivalent to the above, made a little easier to read with strategic white space:
(package (name my-package) (version 1.0)) (dependencies lib-1 lib-2 lib-3 (testing-only tool-a tool-b) (development-only lib-a))
You define a script in a dune project as a rule.
For now, copy this basic "hello world" rule into your
dune file and save it:
(rule (alias helloworld) (deps (universe)) (action (run echo "Hello World!")))
If you already have entries in your dune file, add this below everything else, un-nested.
You should now be able to run:
$ dune build @helloworld Hello World!
Awesome! We're 90% of the way there for invoking simple scripts.
Each rule can have an alias, which you invoke with the
build command by prefixing it with an
Note that you can alias a lot of things in
dune, for example, a set of dependencies. Be careful when referencing the docs!
An action is the command
dune runs to satisfy the rule. The basic syntax is demonstrated above, but there's an internal language that can get quite complex. And you can always use the simple syntax to invoke a small program to perform any more complicated build task! For example, if you really like S-Expressions you can write some crazy shell scripts with the shexp library.
Other common actions are:
(action (system <cmd>)): executes
shon unix and
(action (bash <cmd>)): executes
<cmd>in a bash shell
Unlike scripts in the
dune have dependencies, which are tracked by
dune to avoid unnecessary work.
To illustrate this, remove this line from the example "hello world" above:
(rule (alias helloworld) - (deps (universe)) (action (run echo "Hello World!")))
And try running
dune build @helloworld twice. You'll notice it works as you might expect once, then appears to stop working:
$ dune build @helloworld Hello World! $ dune build @helloworld $ # Huh? No echo?
That's because, without defining the
dune knows that since the last time we ran the
@helloworld build command... nothing changed! And so
dune does not do what it believes to be unnecessary work, and does not run the command we have defined the second time.
This is particularly powerful for something like code generation, where if none of the input files have changed we can avoid wasting time to create build artifacts.
You can check out the full dependency definition specification to experiment, but for now know that
(deps (universe)) will act similarly to a
npm script or
.PHONY target and execute every single time it is invoked.
There's still once major concept we haven't covered: the
(target ...) settings for a rule. Targets are any file(s) you're creating with a rule, and they let do you more advanced things with
dune. The good news is that if you don't want or need more advanced dune features, you're fine ignoring this piece of configuration.
For example, the command above is just an ephemeral side effect, so there's no point. Note, however, that
dune will actually infer your targets for certain actions, and so the implications of configuring a target might pop up even if you neglect to explicitly set one. Most of the time, things will still "just work". But as you go deeper creating your own rules and you see strange behavior or errors mentioning targets, I would recommend reading up on them.
- Running arbitrary tasks in your projects ("scripts"), is called a
- Rules are written in a
dunefile as S-Expressions.
- Rules have names called "aliases".
- Rules have dependencies (and will not run if dependencies have not changed since the last time the rule was invoked).
- Rules have actions, which define the command that is run to satisfy the rule
- Rules can be invoked by running
dune build @with the alias following, like
dune build @tailwind.