Tweag

opam-nix: Nixify Your OCaml Projects

16 February 2023 — by Alexander Bantyev

opam is a source-based package manager for OCaml. It is the de-facto standard for package management in the OCaml ecosystem. opam’s main package repository contains over 4000 individual packages, on average spanning 7 versions each.

Like many other language-specific package managers (e.g. cargo, cabal, etc.), opam performs four main tasks:

  1. Download the sources.
  2. Resolve the needed dependencies of a package.
  3. Provide those dependencies such that the build system can find them.
  4. Run the build system.

It is pretty good at this. However, there are some problems with step (4):

  • The build is not properly isolated: it can (in theory) fetch arbitrary things from the network, access arbitrary files on the filesystem, and even modify other packages. This allows for irreproducible builds, and makes it easy to forget to explicitly list a system dependency if it happens to be installed on the author’s system.
  • “External” (system) dependencies, such as non-OCaml binaries and libraries, are taken from the user’s distribution repository (using the distribution’s package manager, e.g. apt-get), resulting in version inconsistencies and the possibility for breakage.
  • The builds are not easily cached and reused, meaning you have to compile packages locally.

Also, opam’s user interface is based around imperative commands, meaning that the developer environment setup has to be a script that modifies a switch (opam’s term for an independent collection of interdependent packages), which is fragile, difficult to update, and prone to inconsistencies. While there are tools that solve some of those issues, this can still be painful.

Finally, opam is a package manager for OCaml. It can’t easily integrate with other programming languages. This is a problem for modern software stacks, which often feature multiple programming languages in a single project.

Introducing opam-nix

If you’re familiar with Nix, you might have noticed that it doesn’t have any of the aforementioned problems. However, Nix on its own doesn’t know how to build opam packages. The solution to this? Make a library that “translates” opam packages into a format which Nix can understand. That’s exactly what opam-nix is!

opam-nix provides low-level functions which parse opam files into Nix data structures (using opam-file-format), interpret those data structures, resolve the dependency tree (using opam itself), and turn the dependency tree into derivations. It also has some overrides which ensure that a lot of popular packages build and function correctly.

If that sounds scary, don’t worry, opam-nix also provides high-level tools which make Nixifying your OCaml projects a breeze. In this blog post, we’ll show some examples of how to quickly Nixify your existing opam-based projects. Whether you’re working on improving the onboarding experience on a project or are an OCaml developer yourself, we hope you’ll find it useful.

In the following, we assume that you already have Nix installed and are using flakes. Also, the templates are suited to building opam projects.

Simple package

To get started, fetch the template provided with opam-nix. From your project’s root:

$ nix flake init -t github:tweag/opam-nix
$ git add flake.nix

Nix will create a flake.nix for you. Note that you have to add it to the Git index, otherwise Nix will not pick it up. Open it with your editor, look through the file, and replace the throw with your package name, as specified in the comment:

   outputs = { self, flake-utils, opam-nix, nixpkgs }@inputs:
-    # Don't forget to put the package name instead of `throw':
-    let package = throw "Put the package name here!";
+    let package = "my-package";
     in flake-utils.lib.eachDefaultSystem (system:

Nix is famous for its “shells”: ad-hoc, on-the-fly environments, allowing developers to quickly get started on and switch between projects. opam-nix allows you to leverage this potential to make onboarding to your projects quick and easy.

Now run

$ nix develop

Nix will download and lock opam-nix, nixpkgs, opam-repository, and some other dependencies, then build all the dependencies of your project. Once that’s done, it will drop you into a shell with all the dependencies available.

Unfortunately, that won’t always happen; opam-nix can’t provide perfect compatibility with opam. Most errors you will get are actually symptoms of problematic packaging of your dependencies, e.g. missing a system dependency requirement, arbitrary network access during the build, etc. The proper way to fix such errors is to fix the packaging upstream. However, you can also just override the dependency using the overlay in your flake.nix. You can read more about overlays and package overrides if you are not familiar. For example, you can change the commands used to build a package like this:

         overlay = final: prev:
           {
             # Your overrides go here
+            dune = prev.dune.overrideAttrs (_: { buildPhase = "make release"; });
           };
       in {
         legacyPackages = scope.overrideScope' overlay;

Additionally, if your project requires libraries or tools written in other languages, you can look for them in nixpkgs or package them with Nix (maybe using other *-nix tools). Once you have the package, you can simply then inject it into buildInputs of your project using overrideAttrs inside an overlay.

Once you get to the shell, you can use the usual tools to build your package. Typically, that would be something like dune build or make. You can also use nixpkgs phases to build and test your project using commands specified in the opam file:

$ eval "$prePatch"
$ eval "$configurePhase"
$ eval "$buildPhase"
$ eval "$checkPhase"

Note that this approach combines the benefits of Nix (reproducibility, reliable caching, isolation) for your dependencies with the benefits of your build system (fast, incremental builds, granular caching) for your project itself.

If you want to go all-in on Nix, you can build your project as a Nix derivation too. Just run:

$ nix build

This will build and check your project. You can find the build artifacts in the result folder.

Many developers are not content with simply being able to build the package, though; they also want to have modern amenities such as a language server to get interactive documentation, type information, and code navigation.

Worry not! opam-nix can also help with that. We’ll use a different template here, so make sure to back up any changes to flake.nix you might have made in the previous section.

$ mv flake.nix flake.nix.bkp
$ nix flake init -t github:tweag/opam-nix#multi-package
$ git add flake.nix

Replicate the overrides you made before, if any. You don’t need to specify the package name here, since this template picks up all packages in your repository. It’s also a good idea to look through flake.nix, as it might give you ideas for improving your development experience.

You can now use

nix develop

to get a development environment, as before.

However, now you also get ocaml-lsp-server and ocamlformat available. You can add other OCaml tools to the devPackagesQuery as well. For your editor to pick them up, you’ll have to start it from this environment. Alternatively, since this template provides an .envrc, you can use direnv, which has integrations with many popular editors.

Note that for ocaml-lsp to work, it must be able to find type information for your project. Typically, you can ensure this by running

$ dune build @check

If you wish, you can also build your package with:

$ nix build .#<your-package>

Diving deeper

Since opam-nix is a Nix tool, you get a lot of advantages of Nix, like reproducibility, easy CI with binary caching for even faster developer onboarding, integration with NixOS to get reproducible system deployments, or with Docker for compatibility with most of the world.

Also, because opam-nix follows the philosophy of composing small, low-level parts to make a bigger, user-friendly whole, you can make use of it even if your project doesn’t fit the requirements of buildOpamProject.

For dune-project-based projects, you can use buildDuneProject instead of buildOpamProject.

If you just have a single opam export, that’s then imported with opam import. This setup can be replicated with a snippet like this:

let
  # This is a list of package names installed in the switch
  switch = opam-nix.opamListToQuery (opam-nix.fromOPAM ./opam.export).installed;

  scope = with opam-nix;
    queryToScope { repos = [ opamRepository (makeOpamRepo ./.) ]; }
    (switch // { my-package = "dev"; });
in scope.my-package

Or, if you want to build a Mirage unikernel, you can do so using hillingar, an opam-nix- based tool.

If you want to speed up your project by avoiding Import From Derivation, opam-nix supports haskell.nix-style materialization.

Also, if you’re using jupyenv’s OCaml kernel, you’re actually using opam-nix under the hood; this means all the tricks mentioned previously can also work there

You can check out the documentation for all the public functions provided by opam-nix in the README. Let us know if you make something cool with this!

Inspirations & alternatives

opam-nix wouldn’t be possible without opam and opam-file-format. It uses them directly and reimplements some parts of them in Nix.

The way opam-nix works is similar to crate2nix, cabal2nix, and poetry2nix. Inspiration for many technical decisions was drawn from these projects.

Finally, there are a couple of similar projects which serve a similar purpose but achieve it slightly differently.

  • opam2nix is the original in this space. However, it requires committing generated files into the repository, doesn’t integrate well with Flakes, and is not as flexible.
  • opam-nix-integration is quite similar to opam-nix. However, it’s not as flexible since it doesn’t allow to import opam switches or easily call multiple packages from the same workspace.

Conclusion

opam-nix is a flexible yet user-friendly library to turn opam packages into Nix derivations. It is already mature enough to be used by dune, ocaml-lsp, and others, and yet there are still features and interface improvements waiting to happen. We encourage you to try it on your project and share your experience and feedback in the issue tracker.

About the authors
Alexander BantyevAlexander is a passionate, perpetually learning software engineer and hacker. Lives and breathes FOSS. Loves functional programming and declarative approach to system configuration. He has worked as a DevOps/SRE for typeable.io and serokell.io in the past, and is currently working on improving Developer Productivity at tweag.io.

If you enjoyed this article, you might be interested in joining the Tweag team.

This article is licensed under a Creative Commons Attribution 4.0 International license.

Company

AboutOpen SourceCareersContact Us

Connect with us

© 2024 Modus Create, LLC

Privacy PolicySitemap