jbuilder (dune) Beta 17

The 17th beta of jbuilder represents a few months of development. While that’s a bit longer than our usual release cycle for these betas, we do have a larger release than usual. So I’ve decided to write up a little post in addition to just posting the usual change log. I’ll talk about some important new features, and some less important ones as well. A couple of things that I’ll omit are bug fixes and experimental features as this blog post is already long.

Let me start off by reminding everyone that jbuilder will be renamed to dune. The renaming will be done before the release of 1.0. Although the details of the transition are still under discussion, we will try to make the transition as painless as possible for users. A tool that will automatically migrate your projects to dune will be available. This is the last planned beta before the 1.0 release.

Small Usability Improvements

Although dune is a fully featured build system, it’s still relatively young and has a few unpolished corners remaining. We tried to address some of these in the release to avoid the dreaded death by a thousand cuts. Here’s a selection of some of the improvements we’ve made.

Running Tests

A common complaint by users has been that dune is sometimes a little too smart for its own good when running tests. By default, $ jbuilder runtest would rerun only the tests whose dependencies have been modified. This is desirable most of the time, but can still be controlled with the new --force option. Switching on this flag will make sure all the test binaries will rerun.

The --force is actually more general than just for re-running tests. Recall that $ jbuilder runtest just builds the @runtest alias. So we can actually force the building of any alias using the --force flag.

Smarter Variables

Dune allows users to refer to various variables when defining custom rules (to those who are familiar here’s the list). A good chunk of them correspond to a subset of $ ocamlc -config.

These variables end up being quite useful for writing correct rules. For example, here’s an example of using the ${CC} variable to have a custom rule for compiling C code (conceivably useful for compiling and running a bit of C code as part of your build):

 ((targets (foo.exe))
  (deps    (foo.c))
  (action  (run ${CC} -o ${@} ${<} -lfoolib))))

The example above will only work for dune >= 1.0.0+beta17. Previous versions would treat all of these variables as raw strings, which meant that if ${CC} would contain flags, then dune would look for an invalid binary and fail. Now dune is smarter about these variables and understand which variables are just strings which are lists.

To get back the old behavior, where the variable is treated as a string, is just as easily done by quoting it with "${CC}".

Diffing & Promotion

Diffing & Promotion is dune’s neat little answer to working with build artifacts that we’d like to check in and version control in our project. It’s a small feature with wide implications, and introduces a style of work that’s perhaps a bit unfamiliar to many developers. I will present it by using a couple of case studies.

Avoiding OCaml Syntax in jbuild Files

Advanced users might be aware that dune allows you to write OCaml code that will generate a jbuild file on the fly, rather than use the jbuild file syntax. This feature is not recommended, and should only be used as a last resort. The new promotion feature in conjunction with the static include (which I’ll describe shortly) gives a way to avoid its use in situations where we’re just using it to generate boilerplate.

As an example, consider this jbuild file in the Camomile library. We’d like to replace the tuareg syntax with a normal jbuild file and a mechanism to regenerate it when necessary.

First, let me introduce the new (include ..) stanza:

(include <path>)

This stanza will include the source of <path> as part of our source for this jbuild file. <path> is restricted to be a static path that must be present in our source tree.

In conjunction with promotion, this can be used to replace the tuareg syntax:

(include jbuild.inc)

(rule (with-stdout-to jbuild.inc.gen (run ./gen-jbuild.exe)))

Where gen_build.exe will be the binary that will generate our jbuild.inc.gen (repurposed from the jbuild syntax)

We tie this together by defining an alias to do the promotion using the new diff action:

 ((name   jbuild)
  (action (diff jbuild.inc.gen jbuild.inc))))

We can now update our generated jbuild files with:

$ jbuilder build @jbuild --auto-promote

This will make sure the generated files are up to date before copying them to the source.

Correction Files

Another model for updating generated sources is by having the generators write so called corrected (or correction) files. Such a corrected file is the most up to date version of the generated source. It can be diffed against the current version, and eventually accepted by the user.

I’ll motivate the use case with toplevel_expect_test and demonstrate how dune supports such a workflow with the diff? stanza.

Here’s a primitive toplevel_expect_test test (foo.ml):

let x = 1 + 'e'
Line _, characters 12-15:
Error: This expression has type char but an expression was expected of type

The basic idea is that the ocaml-topexpect binary will take this source and evaluate every statement in the toplevel. It will make sure that the output from every evaluated statement matches the contents of the subsequent attribute. When the output indeed matches, the test is considered to be passed, otherwise the test fails.

The great thing about expect tests is that they’re far more useful in the failure case. On failure, ocaml-expect will generate a corrected foo.ml where every %%expect attribute has been updated to make the tests pass and a helpful diff between the two foo.ml’s. If we’re happy with the new results of our tests, we can accept them and overwrite our own foo.ml with the corrected one.

How do we make this work with dune? Here’s a simple stanza to run the expect tests above:

 ((name topexpect)
  (deps (foo.ml))
    (bash "ocaml-expect ${<} || true")
    (diff? foo.ml foo.ml.corrected)))))

A couple of things to note here:

  • we need a silly hack to ignore the exit code from ocaml-expect. This is because ocaml-expect will return a non zero error code indicating failure in addition to a corrected file. Dune only expects the corrected file.

  • We’ve used a diff? rather than a diff action. This action is tailored for commands that produce a new version of the source only when a difference exists, and treat the failure to generate a new source as success - exactly what we expect with corrected files.

To run the tests, we have our @topexpect alias:

$ jbuilder build @topexpect

If there are differences and we are satisfied by them, we can promote the corrected files:

$ jbuilder promote

toplevel_expect_test based tests might not seem very useful, but the workflow is quite general and will also work for ppx_expect and linting. I’ll talk more about those another time, when those features are a bit more ready.

Cross Compilation

The last notable feature we’ve added is the ability to cross compile dune projects. This is an important feature to those who wish to build binaries for Android, iOS, Windows, etc. on their development or CI boxes. This is quite a large feature, so we’ll save the details for another blog post. For now I’ll just link the official documentation for this feature:



I’d like to thank http://ocamllabs.io/, the rest of the dune team, all the external contributors, and everyone who reported bugs or made suggestions.

Being an external contributor can be quite tough. So as a special thanks, I’d like to list all external contributors that have helped out in this release, and invite anyone else to come help with the next version of dune.

Here are github usernames all the external contributors that participated in this release:

Chris00, MiloDavis, seliopou, aantron, xclerc, hhugo.


comments powered by Disqus