DEV Community

Caleb
Caleb

Posted on

Nix Quick Tips - Flake for OCaml

TL;DR Here is the code for those in a hurry. I'll break down what is happening for those interested.

{
  description = "My OCaml project";
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
    ocaml-overlay.url = "github:nix-ocaml/nix-overlays";
    ocaml-overlay.inputs.nixpkgs.follows = "nixpkgs";
  };
  outputs = { self, nixpkgs, flake-utils, ocaml-overlay }: 
    flake-utils.lib.eachDefaultSystem (system: 
      let
        pkgs = import nixpkgs {
          inherit system;
          overlays = [
            ocaml-overlay.overlays.default
          ];
        };
      in {
        devShell = pkgs.mkShell {
          buildInputs = [
            pkgs.ocaml
            pkgs.ocamlPackages.dune_2
            pkgs.ocamlPackages.merlin
            pkgs.ocamlPackages.utop
            pkgs.ocamlPackages.dream # This isn't required, you can place any  packages you want in here.
          ];
          shellHook = ''
            eval $(opam env)
          '';
        };
      }     
    );
}

Enter fullscreen mode Exit fullscreen mode

Requirements

You'll need to have nix installed and experimental features enabled for use with flakes.

The what and why

If you want a reproducible build, one way you can do this is with nix and nix flakes. I wont go into great detail on them here (that will be for another post) but I will cover how this flake is working.

In very broad terms, nix is a functional language you can use to define declarative builds. There is also NixOS which is a Linux distribution built on nix and uses it for it's package management, but you can install nix separate from NixOS.

A flake is used for managing dependencies between nix expressions. In simple terms, you can think of a flake as its own self contained "environment" like python's virtual environment. There is more to it than that but in general that is an easy way to think about it.

Brief overview of flakes

A flake has two main parts, the inputs and the outputs. The inputs declare dependencies to pass to the outputs. The outputs define what will happen when running certain nix commands. In this example our inputs are nixpkgs, flake-utils, and ocmal-overlay. Our only output is the devShell. In this case I haven't created a build output, only a devShell used for development.

How do I use this flake?

Simply place the flake in the root of your ocaml project and to activate the develop environment run the command nix develop. It will fetch all the dependencies defined in the flake then drop you in a shell with them. These dependencies are only available in this shell. If you exit it or go to another shell you'll have to run nix develop again. If you want to avoid running this command every time you can use direnv. Install direnv then create a file called .envrc in your project. Add the line use flake to the file. Now every time you cd into the project directory it will activate the flake. You may have to run direnv allow the first time around.

Ok ok, what is the flake actually doing?

I'll go through this line by line;

description = "My OCaml project";
Enter fullscreen mode Exit fullscreen mode

Just a description of your flake, this can be whatever.

inputs = {
Enter fullscreen mode Exit fullscreen mode

Inputs are a required part of a flake. This defines what dependencies will be passed to use in your outputs.

nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
Enter fullscreen mode Exit fullscreen mode

This defines a dependency nixpkgs. We pass the github "url" for where to fetch it. It's the format of github:<repo-owner>/<repo-name/<branch-name>

flake-utils.url = "github:numtide/flake-utils";
Enter fullscreen mode Exit fullscreen mode

This is exactly what the name says. It imports a flake with some useful functionality for creating flakes. Not strictly necessary but it makes doing certain things easier.

    ocaml-overlay.url = "github:nix-ocaml/nix-overlays";
    ocaml-overlay.inputs.nixpkgs.follows = "nixpkgs";
Enter fullscreen mode Exit fullscreen mode

The first line is getting the ocaml overlay. Overlays are, as the name says, a way to overlay modifications on a package. Say you want a different version or extra functionality in a package from nixpkgs. You can create an overlay that takes in the original package and returns an modified one, essentially overlaying your changes. This overlay adds some things we'll need to get ocaml working. The last line just tells this flake to use our provided nixpkgs version in its definition.

outputs = { self, nixpkgs, flake-utils, ocaml-overlay }:
Enter fullscreen mode Exit fullscreen mode

Here we're defining out outputs, and what to be passed in from our inputs. If you add/remove inputs, make sure to update this to reflect that.

flake-utils.lib.eachDefaultSystem (system:
Enter fullscreen mode Exit fullscreen mode

Our flake-utils are back! Usually you need to define the different systems you're building for in your flake. Using this flake-utils function provides us an easy way to define this by providing us a system constant that will be the system we're running on. (I think that's how it works, haven't dived too deeply into flake-utils. Feel free to correct me.)

 let
        pkgs = import nixpkgs {
          inherit system;
          overlays = [
            ocaml-overlay.overlays.default
          ];
        };
      in {
Enter fullscreen mode Exit fullscreen mode

I'm going to cover this section as one chunk. We are declaring a constant to be passed to the next expression with let in syntax. What we're defining is the pkgs we're going to use. We set it equal to import nixpkgs which essentially expands nixpkgs (which we get from out inputs) and applies any changes we include in the {}. In this case we inherit system; which defines our current system as the one to use for these packages. Then we define an overlay which is set to the one we got from out inputs, ocaml-overlay. Now any packages created or modified by ocaml-overlay are applied to our pkgs.

in {
        devShell = pkgs.mkShell {
          buildInputs = [
            pkgs.ocaml
            pkgs.ocamlPackages.dune_2
            pkgs.ocamlPackages.merlin
            pkgs.ocamlPackages.utop
            pkgs.ocamlPackages.dream # This isn't required, you can place any  packages you want in here.
          ];
          shellHook = ''
            eval $(opam env)
          '';
        };
      }    
Enter fullscreen mode Exit fullscreen mode

Deep breath, we're almost done. This last chunk of code is just defining our devShell. This is the shell environment that will be created when we run nix develop. The in {} is simply bringing in the definitions from our let expression (pkgs).

We create our devShell and set it equal to pkgs.mkShell which is a function provided by nixpkgs (now named pkgs) to create a shell environment. We pass to it our definition of this environment. The buildInputs in the definition is a list of packages to include in this shell. So any packages defined in nixpkgs or our ocaml-overlay go here. Finally, we have a shellHook which is run before you're dropped into the shell. Any extra environment variables or scripts you want to provide to the shell can be dropped here. In this case we're just adding eval $(opam env) which will add opam's environment variables to our shell that we need for working with it.

Now you can run nix develop and get dropped into a shell with these dependencies and environment variables available to you after a short install process. These are only available in the project, they won't be installed system wide. Neat, right?

And that's it!

I'm still no expert on nix or flakes. As I journey through learning them though and run into hiccups I had to figure out I'll try to provide my solutions with explanations for future learners. Let me know your thoughts and if you have a better way of doing things I've explained, let me know that too! Thanks for reading.

Top comments (0)