Easy Dune (OCaml): Running a Script
Tags: ocaml, dune, tutorial, tooling
Published: 2022-08-07 21:06:00

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](https://tailwindcss.com), 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:

```shell
$ 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](#adding-a-script).

### 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:

```shell
$ mkdir run_task && cd run_task
$ touch dune
$ echo "(lang dune 3.3)" > dune-project
```

At this point you should be able to run:

```shell
$ dune build
```

Without seeing an error, and a new folder `_build` appearing, which confirms we're ready to go:

```shell
.
├── _build
│   ├── default
│   └── log
├── dune
└── dune-project
```

### Configuring Dune with S-Expressions

Let's quickly look at one of the commands above:

```shell
$ 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](https://en.wikipedia.org/wiki/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:

```sexp
(key value value (subkey value))
```

  Here's an imaginary example:

```sexp
(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:

```sexp
(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 <a name="adding-a-script" ></a>

You define a script in a dune project as a [**rule**](https://dune.readthedocs.io/en/stable/dune-files.html#rule). 

### A Simple Rule

For now, copy this basic "hello world" rule into your `dune` file and save it:

```dune
(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:

```shell
$ 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](https://dune.readthedocs.io/en/stable/concepts.html?highlight=actions#user-actions-1). 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](https://github.com/janestreet/shexp) library.

Other common actions are:
 - `(action (system <cmd>))`: executes `<cmd>` with `sh` on unix and `cmd` 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:

```diff
 (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:

```shell
$ 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](https://dune.readthedocs.io/en/stable/concepts.html#dependency-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](https://dune.readthedocs.io/en/stable/dune-files.html?highlight=rule#inferred-rules).

----

## 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, like `dune build @tailwind`.