Elixir Dev Environment With Nix
In a previous article, I explained how to set up Nix on MacOS. This article shows the way I set up a development environment for an Elixir project with Nix.
The instructions in this article assume that you have a working Nix installation on your machine.
Initialize Niv
On my system, I’m usually following the latest stable version of Nixpkgs. In a development environment specific to a project, it is necessary to install specific versions of the build dependencies or languages. Niv can help managing those dependencies independently of your global configuration.
To initialize Niv, go to your project folder and run:
nix-shell -p niv --run 'niv init'
This will create the folder nix
with two files: sources.json
contains the
references and versions of your dependencies, and sources.nix
defines an
object with your sources, which you can import in your scripts.
default.nix
Create the file nix/default.nix
. This is where I define the main dependencies.
A basic version might look like this:
{ sources ? import ./sources.nix
, pkgs ? import sources.nixpkgs { }
}:
with pkgs;
buildEnv {
name = "builder";
paths = [
elixir
nodejs-16_x
postgresql_12
];
}
Let’s start from the top. First, we import sources.nix
to make the
dependencies we added with Niv available, and with with pkgs
, we load the
packages into the current namespace. Finally, we define the packages we need
in our environment. I usually have PostgreSQL running in a Docker container and
only add it here to make psql
and pg_dump
available (e.g. to run
mix ecto.dump
).
shell.nix
We still need to configure the Nix shell to use the dependencies we defined.
Create the file shell.nix
in the root folder of the project.
{ sources ? import ./nix/sources.nix
, pkgs ? import <nixpkgs> { }
}:
with pkgs;
let
inherit (lib) optional optionals;
in
mkShell {
buildInputs = [
(import ./nix/default.nix { inherit pkgs; })
niv
] ++ optional stdenv.isLinux inotify-tools
++ optional stdenv.isDarwin terminal-notifier
++ optionals stdenv.isDarwin (with darwin.apple_sdk.frameworks; [
CoreFoundation
CoreServices
]);
}
Here we import the build environment from default.nix
as well as niv
and add
some packages specific to the development environment and operating system, as
opposed to the packages we need to build the project itself. This split of the
dependencies also allows us to reuse default.nix
when
building Docker images with dockerTools without installing dependencies only relevant for
the dev environment, but this is out of scope of this article.
The optional packages defined here are taken straight from the article Using Nix in Elixir projects.
Direnv
With the setup above, we can get into a Nix shell with the configured
dependencies by running nix-shell
from the root of the project. However, you
probably would prefer to use the customized shell of your choice instead of
being dropped into a bare bash shell. To do this, you can use
direnv.
Update .gitignore
and create the file .envrc
in the root of the project:
echo ".direnv/" >> .gitignore
echo "use_nix" > .envrc
You will see a notice that .envrc
is blocked. Do what the notice tells you and
run:
direnv allow
From now on, whenever you change to the project directory, the Nix shell configuration will be loaded into your environment.
Updating Niv sources
Let’s have a look back at nix/sources.nix
. The referenced nixpkgs
branch may
not refer to the latest version, or the version you need. With niv
being
available now, you can run:
niv update nixpkgs -b nixos-21.05
This command will update the nixpkgs
reference in sources.json
. Refer to the
Niv documentation for more usage examples.
Overrides
Earlier we set elixir
as a dependency. This is less than ideal, since this
will point to whatever OTP and Elixir version the nix channel you are following
is pointing to. It is better to define concrete Erlang/Elixir versions by using
beam.packages.*
:
buildEnv {
paths = [
beam.packages.erlangR23.elixir_1_11
...
];
}
You can see the available Erlang and Elixir versions
here.
Switch to the branch you are following in nix/sources.json
.
However, sometimes you may want to use a version that hasn’t been packaged yet, or you may require a specific patch version. In that case, you can use overrides.
For example, let’s say you need Erlang 23 and Elixir 1.12.2. Change
nix/default.nix
to:
{ sources ? import ./sources.nix
, pkgs ? import sources.nixpkgs { }
}:
with pkgs;
let
elixir = beam.packages.erlangR23.elixir.override {
version = "1.12.2";
sha256 = "1rwmwnqxhjcdx9niva9ardx90p1qi4axxh72nw9k15hhlh2jy29x";
};
in
buildEnv {
name = "builder";
paths = [
elixir
nodejs-14_x
postgresql_12
];
}
Here we override the arguments of the existing Erlang 23 / Elixir derivation. You can get the sha256 value with:
nix-prefetch-url --unpack https://github.com/elixir-lang/elixir/archive/v1.12.2.tar.gz
Sometimes you may also need to override the Erlang derivation. For example, as of this writing, the packaged ErlangR24 derivation does not build on an Apple M1, because the Erlang JIT does not support ARM64 processors yet. To get around that, you can disable JIT with a flag and base your Elixir derivation on that Erlang override.
{ sources ? import ./sources.nix
, pkgs ? import sources.nixpkgs { }
}:
with pkgs;
let
erlang = erlangR24.override {
version = "24.1.7";
sha256 = "1d86yczbb2dndkjcbzc6lcq8aq6gdibh6pkxrg76n07xr5adk2j7";
configureFlags = [ "--disable-jit" ];
};
beamPkg = pkgs.beam.packagesWith erlang;
elixir = beamPkg.elixir.override {
version = "1.13.0";
sha256 = "1rkrx9kbs2nhkmzydm02r4wkb8wxwmg8iv0nqilpzj0skkxd6k8w";
};
in
buildEnv {
name = "builder";
paths = [
elixir
nodejs-16_x
postgresql_12
];
}
You can get the sha256 value for Erlang with:
nix-prefetch-url --unpack https://github.com/erlang/otp/archive/OTP-24.0.5.tar.gz
Overlays
I didn’t have to use any overlays
in my setup yet, but if you need them, I’d suggest to put them in a file called
nix/overlays.nix
.
You can then use them in nix/default.nix
:
{ sources ? import ./sources.nix
, pkgs ? import sources.nixpkgs {
overlays = [ (import ./overlays.nix) ];
}
}:
Or in shell.nix
:
{ sources ? import ./nix/sources.nix
, pkgs ? import sources.nixpkgs {
overlays = [ (import ./nix/overlays.nix) ];
}
}:
Code
You can find the code on Github.