Workflows

This section documents day-to-day tasks in a project that uses opam-monorepo.

Initial Setup

There are several ways to configure a project to use opam-monorepo, depending on what is checked into Git.

As a summary:

We recommend using the lockfile-in-git workflow. To enable it, first generate a lock file (using opam monorepo lock), add it to your Git repository, together with project.opam, and add duniverse/ in .gitignore.

Manage duniverse explains in more depth how to best manage the files that opam-monorepo generates.

Setting Up Continuous Integration

Note: if you use ocaml-ci, you don't have anything to do. It will detect opam-monorepo builds and replace the opam-based builds by an opam-monorepo aware build that implements the rest of this section.

This section is useful if you want to implement a CI system based on opam-monorepo.

A build consists in three steps:

  1. Set up the environment
  2. Set up the workspace
  3. Build the project

Set Up the Environment

In this context, "the environment" is a set of programs to be put in $PATH. An opam-monorepo build requires an OCaml compiler, Dune, and opam-monorepo. The project might require system packages ("depexts") too. It's possible to rely on opam to install this environment, but it's not technically required.

The following assumes that opam is being used.

To set up the environment:

Setup the Workspace

The next step is to set up a workspace that contains your project and its dependencies.

Build the Project

In this step, the workspace is ready, so dune build commands can succeed. The exact command to run depends on the project, but for CI, dune build followed by dune runtest is usually the right thing to do (or in a single step, dune build @all @runtest). If the goal of that pipeline is to produce release binaries, passing --profile release might be better. Consult the Dune documentation to know more about the difference.

Caching Strategies

This build skeleton can benefit from several caching strategies. The first one is to use a layering approach, e.g., by using Docker.

Most of the steps only require the lock file, so they can be cached and invalidated when the lock file changes.

For the OCaml compiler version, this can be a bit too conservative, as it will cause any change in the lock file to rebuild a switch. Instead, it's possible to hardcode the OCaml compiler version in the Docker file, and only check that the version is correct during the "setup environment" phase.

Doing only this will start all builds with an empty _build directory, so all dependencies need to be built each time. One solution to this is to persist the _build directory between builds. This can even be done across branches (the cache key is just the project name).

For local development, this means that _build is expensive to rebuild (similarly to a local opam switch). It is possible to set up a machine-global dune cache to speed up rebuilds.

Upgrading All Dependencies

To upgrade dependencies, run opam monorepo lock. It will compute a new solution based on the constraints in project.opam and the current state of opam-repository. Then locally run opam monorepo pull to update the duniverse/ folder.

It's recommended to regularly create pull requests to update dependencies. When updating a local copy, developers will need to call opam monorepo pull.

Adding a Dependency

Note: this section also applies to all the cases where there are changes to the dependencies in project.opam, for example if a version constraint is modified.

To add a dependency:

This has the side effect of upgrading all dependencies too. To get a more conservative upgrade, one can do the upgrade in two steps: