I’m learning OCaml via Real World OCaml, and getting a isolated OCaml development environment working with NixOS proved to be quite a struggle. This is a tutorial that describes my way of getting OCaml set up.
Obtaining the OCaml Libraries
OCaml and the more popular libraries are available in Nixpkgs, so one
can write a simple shell.nix
file to make them available in a nix
shell:
with import <nixpkgs> {};
let
ocamlPackages = pkgs.recurseIntoAttrs pkgs.ocamlPackages_latest;
in
{
pkgs.mkShell {
buildInputs = with pkgs; [
dune
] ++ ( with ocamlPackages;
[
ocaml
core
core_extended
findlib
utop
merlin
ocp-indent
]);
}
}
With this, we now have OCaml and the Core
standard library available
in an isolated environment. We also installed Merlin, which provides
context-sensitive completion, and ocp-indent, which provides
auto-formatting of OCaml code.
The issue with many of these OCaml libraries is that they are tied to their compiler versions. In addition, tools like merlin require the ability to locate OCaml libraries, which is tricky business in Nix land.
Merlin and utop distribute their emacs-lisp libraries with the main
executables. This means upon building the Nix derivations, one would
find them in ${package}/share/emacs/site-lisp
. These libraries would
need to be added to the Emacs load-path and loaded. In ordinary
scenarios, I use direnv, which has first-class support for Nix and
nix-shell environments, to automatically load project-specific
libraries and binaries. This didn’t work out. Instead, now I launch a
separate Emacs instance within the nix-shell that would have access to
all the executable and OCaml libraries.
Passing information via shell variables
I use shell variables to pass information to my Emacs configuration.
First, I pass a set IN_NIX_SHELL
to 1
in the nix-shell environment.
This is simple enough:
pkgs.mkShell {
IN_NIX_SHELL = 1;
}
In Emacs, I write a function that checks if I’m in a nix-shell using this variable:
(defun in-nix-shell-p ()
(string-equal (getenv "IN_NIX_SHELL") "1"))
Next, I would also need to pass the site-lisp directories for merlin, utop and ocp-indent to Emacs:
pkgs.mkShell {
UTOP_SITE_LISP = "${ocamlPackages.utop}/share/emacs/site-lisp";
MERLIN_SITE_LISP = "${ocamlPackages.merlin}/share/emacs/site-lisp";
OCP_INDENT_SITE_LISP="${ocamlPackages.ocp-indent}/share/emacs/site-lisp";
}
Here’s how a final shell.nix would look like.
In Emacs I then conditionally load the libraries depending on:
- Whether I’m in a nix-shell.
- Whether these environment variables are passed.
(setq jethro/merlin-site-elisp (getenv "MERLIN_SITE_LISP"))
(setq jethro/utop-site-elisp (getenv "UTOP_SITE_LISP"))
(setq jethro/ocp-site-elisp (getenv "OCP_INDENT_SITE_LISP"))
(use-package merlin
:if (and jethro/merlin-site-elisp
(in-nix-shell-p))
:load-path jethro/merlin-site-elisp
:hook
(tuareg-mode . merlin-mode)
(merlin-mode . company-mode)
:custom
(merlin-command "ocamlmerlin"))
(use-package utop
:if (and jethro/utop-site-elisp
(in-nix-shell-p))
:load-path jethro/utop-site-elisp
:hook
(tuareg-mode . utop-minor-mode))
(use-package ocp-indent
:if (and jethro/ocp-site-elisp
(in-nix-shell-p))
:load-path jethro/ocp-site-elisp)
This part of my Emacs configuration continues to work for my own user-level Emacs daemon, and now performs additional work loading elisp provided by merlin, utop and ocp-indent when launched within my OCaml isolated environment.
Hope this will help others set up their OCaml environment with NixOS! Many thanks to this gist for leading me in the right direction.