Synechepedia

Super Simple CI for OCaml on CircleCI

Intro

These notes record the steps I took to set up a simple continuous integration system for an OCaml project using CircleCI.

A working, minimal exemplification of these notes can be found at https://github.com/shonfeder/ocaml-circleci-example.

I will keep improving these notes and the approach they document until it is superseded by something better.

How to use this note

  • You can follow step-by-step if this is all new to you and you’re starting from scratch.
  • You should skip anything you already understand or already have configured.
  • If you have any questions, find that anything doesn’t work correctly, or think that anything could be improved, Please open an issue or a PR on the source repository for these notes.

My approach to basic CI

Maximal Flexibility

I want a drop-in CI configuration that will provide quick builds for all my projects with no fuss. I am therefore inclined to opt for a relatively heavyweight container configuration, so I needn’t fiddle with common dependencies for every project.

Minimal CI Configuration

IMO, the current state of CI configuration and management is pretty miserable. The state of the art in build systems and package management is significantly more advanced (even if it still leaves lots to be desired). Thus, as much as possible, I aim to keep build and packaging logic out of the CI configuration and in the build system and package manager.

Prerequisites

A GitHub repository for the project

A CircleCI account with access to your repository

An OCaml project built with dune

Setup

A minimally testable project

Where . is the root directory of your project:

   .
   ├── .circleci
   │   └── config.yml
   ├── dune-project
   ├── myproject.install
   ├── myproject.opam
   └── test
       ├── dune
       └── mytest.ml

The top-level directories and files

.circleci
The configuration for your CI consulted by the CircleCI service
dune-project
Auto generated by dune the first time you run a dune command that builds the project
myproject.install
Augo generated by dune
myproject.opam
Configuration of your project as an opam package
test
The testing components of your project

The three manually created components are described in the following three sections.

Configure opam to install your package

A minimal package configuration for our example

$ cat myproject.opam

     opam-version: "2.0"
     name: "myproject"
     version: "0.1"
     synopsis: "Description of my project"
     maintainer: "Me <my.self@sub.domain>"
     depends: [
       "ocaml" { build }
       "ocamlfind" { build }
       "dune" { build & >= "1.5.0" }

       "base" { >= "0.12.0" }
       "alcotest" { with-test }
     ]
     build: [
       ["dune" "build" "-p" name]
     ]
  • The dependencies marked as build depends — ocaml, ocamlfind, and dune — should be included in all projects,
  • but the base and alcotest dependencies are only included as examples of, respectively, an external library dependency and a test dependency. (For the sake of minimality, these dependencies aren’t actually used in the example project.)

Validate your opam configuration

To validate the syntax and configuration your opam package, run

      $ opam lint

NOTE: If you are using the minimal example configuration, you should see some warnings for missing fields. You can supply these if you wish. Consult opam packaging documentation for more info.

TODO Supply link to opam packaging reference

Locally test the installation

To locally test the installation from within the root directory of your project, run

      $ opam install . --with-test -y

Configure dune to run your tests

The following setup provides a minimum configuration for failing test:

$ cat ./test/dune

     (test
      (name mytest))

$ cat ./test/mytest.ml

     let () = assert false

We can run our tests locally with $ dune runtest. The resulting failure isn’t pretty, but it proves the point:

    $ dune runtest
          mytest alias test/runtest (exit 2)
    (cd _build/default/test && ./mytest.exe)
    Fatal error: exception Assert_failure("test/mytest.ml", 1, 9)

TODO Provide link

Follow the dune documentation on configuring and running tests with your preferred testing tools.

Write your CircleCI configuration

$ cat .circleci/config.yml

    version: 2
    jobs:
      build:
        docker:
          - image: shonfeder/ocaml-ci-docker

        steps:
          - checkout

          - run:
              name: Install Package Dependencies
              command: opam install . --with-test -y

          - run:
              name: Run tests
              command: |
                eval $(opam env)
                dune runtest

Conclusion

Voila: super simple CI for OCaml on CircleCI.

This configuration has plenty of limitations and shortcomings, but it suffice get a project up and running with CI.

Anticipated improvements [0/5]

  • [ ] Local caching of build artifacts and built dependencies
  • [ ] A smaller Docker base image
  • [ ] Escape the nightmare of YAML of programming
  • [ ] An entirely OCaml-defined CI system?

A more robust examples