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 aws
CLI invocation. We'll refer to these kinds of things as "scripts", as we might in a node/JavaScript project.
Assumptions
- 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
From Scratch
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.
Creating Your dune
File
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
Configuring Dune with S-Expressions
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 JSON
, YAML
or 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 JSON
is a common way to serialize data in JavaScript.
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 key
s 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 package.json
key "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))
Adding a Script: Rules
You define a script in a dune project as a rule.
A Simple 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.
Understanding Simple Rules
Aliases
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!
Action
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<cmd>
withsh
on unix andcmd
on windows. -
(action (bash <cmd>))
: executes<cmd>
in a bash shell
Dependencies
Unlike scripts in the npm
ecosystem, rules
in 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 deps
as universe
, 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.
Elephant In The Room: Targets
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.
Summary
- Running arbitrary tasks in your projects ("scripts"), is called a
dune
"rule". - Rules are written in a
dune
file 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, likedune build @tailwind
.
Top comments (0)