This note was published on 15th Nov 2013.

Now that OCaml 4.01 has been released, there is a frenzy of commit activity in the development trunk of OCaml as the new features for 4.02 are all integrated. These include some enhancements to the type system such as injectivity, module aliases and extension points as a simpler alternative to syntax extensions.

The best way to ensure that these all play well together is to test against the ever-growing OPAM package database as early as possible. While we’re working on more elaborate continuous building solutions, it’s far easier if a developer can quickly run a bulk build on their own system. The difficulty with doing this is that you also need to install all the external dependencies (e.g. libraries and header files for bindings) needed by the thousands of packages in OPAM.

Enter a hip new lightweight container system called Docker. While containers aren’t quite as secure as type-1 hypervisors such as Xen, they are brilliant for spawning lots of lightweight tasks such as installing (and reverting) package installations. Docker is still under heavy development, but it didn’t take me long to follow the documentation and put together a configuration file for creating an OCaml+OPAM image to let OCaml developers do these bulk builds.

A basic Docker and OPAM setup

I started by spinning up a fresh Ubuntu Saucy VM on the Rackspace Cloud, which has a recent enough kernel version to work out-of-the-box with Docker. The installation instructions worked without any problems.

Next, I created a Dockerfile to represent the set of commands needed to prepare the base Ubuntu image with an OPAM and OCaml environment. You can find the complete repository online at https://github.com/avsm/docker-opam. Let’s walk through the Dockerfile in chunks.

FROM ubuntu:latest
MAINTAINER Anil Madhavapeddy <anil@recoil.org>
RUN apt-get -y install sudo pkg-config git build-essential m4 software-properties-common
RUN git config --global user.email "docker@example.com"
RUN git config --global user.name "Docker CI"
RUN apt-get -y install python-software-properties
RUN echo "yes" | add-apt-repository ppa:avsm/ocaml41+opam11
RUN apt-get -y update -qq
RUN apt-get -y install -qq ocaml ocaml-native-compilers camlp4-extra opam
ADD opam-installext /usr/bin/opam-installext

This sets up a basic OCaml and OPAM environment using the same Ubuntu PPAs as the Travis instructions I posted a few months ago. The final command adds a helper script which uses the new depexts feature in OPAM 1.1 to also install operating system packages that are required by some libraries. I’ll explain in more detail in a later post, but for now all you need to know is that opam installext ctypes will not only install the ctypes OCaml library, but also invoke apt-get install libffi-dev to install the relevant development library first.

RUN adduser --disabled-password --gecos "" opam
RUN passwd -l opam
ADD opamsudo /etc/sudoers.d/opam
USER opam
ENV HOME /home/opam
ENV OPAMVERBOSE 1
ENV OPAMYES 1

The next chunk of the Dockerfile configures the OPAM environment by installing a non-root user (several OPAM packages fail with an error if configured as root). We also set the OPAMVERBOSE and OPAMYES variables to ensure we get the full build logs and non-interactive use, respectively.

Running the bulk tests

We’re now set to build a Docker environment for the exact test that we want to run.

RUN opam init git://github.com/mirage/opam-repository#add-depexts-11
RUN opam install ocamlfind
ENTRYPOINT ["usr/bin/opam-installext"]

This last addition to the Dockerfile initializes our OPAM package set. This is using my development branch which adds a massive diff to populate the OPAM metadata with external dependency information for Ubuntu and Debian.

Building an image from this is a single command:

$ docker build -t avsm/opam github.com/avsm/docker-opam

The ENTRYPOINT tells Docker that our wrapper script is the “root command” to run for this container, so we can install a package in a container by doing this:

$ docker run avsm/opam ctypes

The complete output is logged to stdout and stderr, so we can capture that as easily as a normal shell command. With all these pieces in place, my local bulk build shell script is trivial:

pkg=`opam list -s -a`
RUN=5
mkdir -p /log/$RUN/raw /log/$RUN/err /log/$RUN/ok
for p in $pkg; do
  docker run avsm/opam $p > /log/$RUN/raw/$p 2>&1
  if [ $? != 0 ]; then
    ln -s /log/$RUN/raw/$p /log/$RUN/err/$p
  else
    ln -s /log/$RUN/raw/$p /log/$RUN/ok/$p
  fi
done  

This iterates through a local package set and serially builds everything. Future enhancements I’m working on: parallelising these on a multicore box, and having a linked container that hosts a local package repository so that we don’t require a lot of external bandwidth. Stay tuned!