Developing with Elixir requires a fair amount of configuration.

You need:

  • Erlang
  • Elixir
  • Hex package manager

And usually you want all these to be locked at a specific version.

There are several solutions out there, the most popular probably being https://github.com/asdf-vm/asdf but for Nix fans, how do we setup a nix-shell and do this the “nix way”?

The Nix community seems to reccomend to nixify your projects, examples exist for

  • Ruby https://github.com/nix-community/bundix
  • Node https://nixos.wiki/wiki/Node.js
  • Elixir https://discourse.nixos.org/t/announcing-mixnix-build-elixir-projects-with-nix/2444

I’ve been using a different pattern with most of my projects to achieve this, and it works for both Ruby, Elixir and many other languages.

The trick is find the env variables that allow us to “isolate” locally the dependency installation, in our example these are MIX_HOME and HEX_HOME.

By setting these two variables to the local directory within the nix-shell we allow mix to install the packages locally instead of trying to add them to the nix-store path (/nix...) that is readonly.

Summing it up

Here’s the shell.nix file that I use in my elixir projects

with import <nixpkgs> {};
let
  # define packages to install with special handling for OSX
  basePackages = [
    gnumake
    gcc
    readline
    openssl
    zlib
    libxml2
    curl
    libiconv
    elixir_1_9
    glibcLocales
    nodejs-12_x
    yarn
    postgresql
  ];

  inputs = basePackages
    ++ lib.optional stdenv.isLinux inotify-tools
    ++ lib.optionals stdenv.isDarwin (with darwin.apple_sdk.frameworks; [
        CoreFoundation
        CoreServices
      ]);

  # define shell startup command
  hooks = ''
    # this allows mix to work on the local directory
    mkdir -p .nix-mix
    mkdir -p .nix-hex
    export MIX_HOME=$PWD/.nix-mix
    export HEX_HOME=$PWD/.nix-hex
    export PATH=$MIX_HOME/bin:$PATH
    export PATH=$HEX_HOME/bin:$PATH
    export LANG=en_US.UTF-8
    export ERL_AFLAGS="-kernel shell_history enabled"
  '';

in mkShell {
  buildInputs = inputs;
  shellHook = hooks;
}

save this in your project directory as shell.nix then type nix-shell and you will be in a bash shell that has mix and elixir ready to be used!

Bonus: Add gigalixir support

https://www.gigalixir.com is one of the simplest ways to deploy Elixir applications. Similarly to Heroku, you subscribe, create an account, install the cli and you are off to the races.

Jan 2022 Update

Gigalixir is now on nixpkgs! If you are on a recent version of nixpkgs (I tested 21.11) all you need to do is add gigalixir to the list of packages above.

Note: the package is currently broken in nixpkgs-unstable, afterall it is called “unstable” :)

Old install process

The problem is that our nix-shell does not allow us to run pip install gigalixir and install the gigalixir command line utility.

Setting up python in nix requires a little more configuration.

First we need to add Python to our nix-shell and ensure that our python install has the pip package manager, a detailed explaination can be found here https://nixos.wiki/wiki/Python.

In short you can do something like this

  my-python-packages = python-packages: with python-packages; [
    pip
    setuptools
  ];

  python-with-my-packages = pkgs.python3.withPackages my-python-packages;

Then, like we did for Elixir, we have to force Python to install its stuff locally, we do this by setting PIP_PREFIX and PYTHONPATH;

 alias pip="PIP_PREFIX='$(pwd)/_build/pip_packages' \pip"
 export PYTHONPATH="$(pwd)/_build/pip_packages/lib/python3.7/site-packages:$PYTHONPATH"

The resulting shell.nix file is then as follows

with import <nixpkgs> {};
let
  my-python-packages = python-packages: with python-packages; [
    pip
    setuptools
  ];

  python-with-my-packages = pkgs.python3.withPackages my-python-packages;

  # define packages to install with special handling for OSX
  basePackages = [
    gnumake
    gcc
    readline
    openssl
    zlib
    libxml2
    curl
    libiconv
    elixir_1_9
    glibcLocales
    nodejs-12_x
    yarn
    postgresql
    inotify-tools
    python-with-my-packages
  ];


  inputs = basePackages
    ++ lib.optional stdenv.isLinux inotify-tools
    ++ lib.optionals stdenv.isDarwin (with darwin.apple_sdk.frameworks; [
        CoreFoundation
        CoreServices
      ]);
			
  # define shell startup command
  hooks = ''
    export PS1='\n\[\033[1;32m\][nix-shell:\w]($(git rev-parse --abbrev-ref HEAD))\$\[\033[0m\] '

    # this allows python to work locally
    alias pip="PIP_PREFIX='$(pwd)/_build/pip_packages' \pip"
    export PYTHONPATH="$(pwd)/_build/pip_packages/lib/python3.7/site-packages:$PYTHONPATH"
    unset SOURCE_DATE_EPOCH

    # this allows mix to work on the local directory
    mkdir -p .nix-mix
    mkdir -p .nix-hex
    export MIX_HOME=$PWD/.nix-mix
    export HEX_HOME=$PWD/.nix-hex
    export PATH=$MIX_HOME/bin:$PATH
    export PATH=$HEX_HOME/bin:$PATH
    export LANG=en_US.UTF-8
    export PATH=$PATH:$(pwd)/_build/pip_packages/bin
    export ERL_AFLAGS="-kernel shell_history enabled"
  '';

in mkShell {
  buildInputs = inputs;
  shellHook = hooks;
}

At this point we can type nix-shell and then pip install gigalixir and we are ready to go!

Notes:

The full code can be found here in this gist

After publishing this post I found this one that presents a simpler approach and with a shorter shell configuration, I updated my notes above to use mkShell and a nicer syntax that uses lib.optionals to load the MacOS only packages

Another good resource I only found today is here