DEV Community

Patrick Kilgore
Patrick Kilgore

Posted on • Updated on

Easy Dune (OCaml): Running a Script

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

At this point you should be able to run:

$ dune build
Enter fullscreen mode Exit fullscreen mode

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

.
├── _build
│   ├── default
│   └── log
├── dune
└── dune-project
Enter fullscreen mode Exit fullscreen mode

Configuring Dune with S-Expressions

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

$ echo "(lang dune 3.3)" > dune-project
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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 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))
Enter fullscreen mode Exit fullscreen mode

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!")))
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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

 (rule
   (alias helloworld)
-  (deps (universe))
   (action (run echo "Hello World!")))
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)