Perfect Elixir: Environment Setup

jonlauridsen

Jon Lauridsen

Posted on March 18, 2024

Perfect Elixir: Environment Setup

We need Erlang and Elixir installed, which might sound simple, but there are trade-offs to consider for a shared team environment. We'll also add a PostgreSQL database to keep our explorations relevant to real-world scenarios.

Let's explore different approaches and discuss their pros and cons.

Table of Contents

 

Just… Install the Dependencies?

Why not just install the dependencies as suggested by each tool's website?

I’m on macOS and erlang.org, elixir-lang.org, and postgresql.org all recommend installing via Homebrew.

So, first we install Homebrew:

$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Enter fullscreen mode Exit fullscreen mode

🖥️ Terminal

Install Homebrew

Then install our tools:

$ brew install erlang elixir postgresql@15
Enter fullscreen mode Exit fullscreen mode

🖥️ Terminal

Brew install erlang, elixir, and postgresql

And… we’re done!?

But there are critical issues with this approach:

  • No versioning - There's no control over what versions we just installed, because Homebrew is not designed for versioning. It leads to a totally unpredictable mix of versions as different developers will install their tools at different times.
  • Globally installed - Homebrew tools are globally installed, meaning they affect all other projects. That leads to unpredictable behavior as one project requires an upgrade that ruins another project, and Homebrew doesn't offer a way to switch between versions.

So no, “just installing” isn’t viable at all. That's not a Homebrew problem because Homebrew was never designed to solve versions, but for our needs we must find a solution that installs exactly the right versions on all developer machines and environments. Let's go explore the tools are designed for that.

 

asdf

asdf is a version manager with plugins for Erlang, Elixir, and Postgres. The installation guide suggests first installing some system dependencies via Homebrew and then cloning the asdf repository:

$ brew install coreutils curl git
…
$ git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.13.1
$ echo '. "$HOME/.asdf/asdf.sh"' >> ~/.zshrc
Enter fullscreen mode Exit fullscreen mode

🖥️ Terminal

Brew install coreutils, curl, and git

ℹ️ BTW it's quite odd to install asdf via git clone. Although it can be installed via Homebrew the asdf guide recommends using Git so that's what I'm going with here.

Next, add plugins:

$ asdf plugin add erlang https://github.com/asdf-vm/asdf-erlang.git
$ asdf plugin add elixir https://github.com/asdf-vm/asdf-elixir.git
$ asdf plugin add postgres
Enter fullscreen mode Exit fullscreen mode

🖥️ Terminal

Add plugins to asdf

And then we need to go through each plugin's GitHub repository's documentation to derive a list of additional dependencies that are needed:

$ brew install autoconf openssl@1.1 openssl libxslt fop gcc readline zlib curl ossp-uuid
$ echo 'export KERL_CONFIGURE_OPTIONS="--without-javac --with-ssl=$(brew --prefix openssl@1.1)"' >> ~/.zshrc
Enter fullscreen mode Exit fullscreen mode

🖥️ Terminal

Add plugin dependencies

ℹ️ BTW it's really unnerving each plugin requires their own set of Homebrew-installed system dependencies, because it throws all the versioning right out the window! But let's keep going…

Then, create a .tool-versions file to specify the tools and versions:

$ cat << EOF >> .tool-versions
erlang 26.2.1
elixir 1.16.0
postgres 15.5
EOF
Enter fullscreen mode Exit fullscreen mode

And then the last command is to install the specified tools:

$ asdf install$ which erl
/Users/cloud/.asdf/shims/erl
$ which elixir
/Users/cloud/.asdf/shims/elixir
$ which psql
/Users/cloud/.asdf/shims/psql
Enter fullscreen mode Exit fullscreen mode

🖥️ Terminal

Run asdf install

We now have all our tools installed 🎉

direnv

But just having the tools installed isn't really enough: Developers will have to manually run asdf install to stay in sync with the specified versions, can't that be automated?

direnv is a common tool for keeping developer environments in sync, because it can trigger commands upon entering a folder. So let's install and configure direnv:

$ asdf plugin add direnv
$ echo direnv 2.30.0 >> .tool-versions 
$ asdf install
$ asdf direnv setup --shell zsh --version 2.30.0
$ asdf direnv local
$ echo "use asdf" > .envrc
Enter fullscreen mode Exit fullscreen mode

🖥️ Terminal

Add asdf direnv

Now, if we simulate a change to .tools-versions by updating the Erlang version, we'll see direnv automatically prompts to re-install dependencies:

$ cd perfect-elixir/
direnv: loading ~/perfect-elixir/.envrc
direnv: using asdf
direnv: Creating env file /Users/cloud/.cache/asdf-direnv/env/1510633598-1931737049-390094659-716574907
direnv: erlang 26.2.2 not installed. Run 'asdf direnv install' to install.
direnv: referenced  does not exist
$ asdf direnv install
Downloading 26.2.2 to /Users/cloud/.asdf/downloads/erlang/26.2.2...
...
$ which erl
/Users/cloud/.asdf/installs/erlang/26.2.2/bin/erl
Enter fullscreen mode Exit fullscreen mode

🖥️ Terminal

Show how asdf direnv automatically checks for dependencies on entering the folder

We now have an workflow backed by asdf that automatically keep our environments in sync, even as our team upgrades tool versions 🎉

 

mise

Mise is a recent replacement for asdf, leveraging all the existing asdf plugins but promising to dramatically simplify the steps to get everything work. So let's check it out.

Install Mise via Homebrew:

$ brew install mise
Enter fullscreen mode Exit fullscreen mode

Activate it for your shell (assuming zsh):

$ echo 'eval "$(mise activate zsh)"' >> "${ZDOTDIR-$HOME}/.zshrc"
$ source ~/.zshrc
Enter fullscreen mode Exit fullscreen mode

Create a .mise.toml file to specify dependencies:

$ cat .mise.toml
[tools]
erlang = '26.2.1'
elixir = '1.16.0'
postgres = '15.5'
Enter fullscreen mode Exit fullscreen mode

Install the dependencies:

$ mise install
mise ⚠️ postgres is a community-developed plugin – https://github.com/smashedtoatoms/asdf-postgres
Would you like to install postgres? Yes
…
mise elixir@1.16.0 ✓ installed  
Enter fullscreen mode Exit fullscreen mode

🖥️ Terminal

Running mise install command

And just like that every tool is available:

$ which erl
/Users/cloud/.local/share/mise/installs/erlang/26.2.1/bin/erl

$ which elixir
/Users/cloud/.local/share/mise/installs/elixir/1.16.0/bin/elixir

$ which psql
/Users/cloud/.local/share/mise/installs/postgres/15.5/bin/psql
Enter fullscreen mode Exit fullscreen mode

And the tools are automatically only activated inside the folder:

$ cd ..
$ which erl
erl not found

$ cd perfect-elixir
$ which erl
/Users/cloud/.local/share/mise/installs/erlang/26.2.1/bin/erl
Enter fullscreen mode Exit fullscreen mode

That's slick! 🎉

 

Nix

Nix is a tool "for reproducible and declarative configuration management", available for macOS and Linux. Let’s give it a try!

First, install Nix:

$ sh <(curl -L https://nixos.org/nix/install)
Enter fullscreen mode Exit fullscreen mode

🖥️ Terminal

Installing nix

ℹ️ BTW The installer requires sudo, and it creates a new “Nix Store” drive-volume and 32 hidden new users. I immediately find that really intrusive, is that really necessary to install some system tools?

Nix uses a custom pseudo programming language for specifying dependencies. I struggled greatly to understand Nix guides and tutorials but I think we have to enable some experimental features and create a flake.nix file:

$ mkdir -p ~/.config/nix && echo "experimental-features = nix-command flakes" > ~/.config/nix/nix.conf

$ nix flake new .
wrote: /Users/cloud/Documents/nix/flake.nix

$ cat flake.nix
{
  description = "A very basic flake";
  outputs = { self, nixpkgs }: {
    packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;
    packages.x86_64-linux.default = self.packages.x86_64-linux.hello;
  };
}
Enter fullscreen mode Exit fullscreen mode

ℹ️ BTW I don't understand why a new flake references "legacy" packages, or why they point to Linux packages when I run this on a Mac… but these are just minor confusions in the journey to get Nix working properly.

Then edit flake.nix to specify dependencies:

$ cat flake.nix
{
  description = "A flake";
  outputs = { self, nixpkgs }: {
    devShells.x86_64-darwin = {
      default = nixpkgs.legacyPackages.x86_64-darwin.mkShell {
        buildInputs = [
          nixpkgs.legacyPackages.x86_64-darwin.erlangR26
          nixpkgs.legacyPackages.x86_64-darwin.elixir_1_16
          nixpkgs.legacyPackages.x86_64-darwin.postgresql_15
        ];
      };
    };
  };
}
Enter fullscreen mode Exit fullscreen mode

Now we can activate the flake by running nix develop:

$ nix develop
...
$ which erl
/nix/store/49qw7cw30wszrfn3sa23qnlskyvbnbhi-erlang-26.2.2/bin/erl
$ which elixir
/nix/store/rr6immch9mp8dphv1jvgxym35za4b7jy-elixir-1.16.1/bin/elixir
$ which psql
/nix/store/v5ym92k3kss1af7n1788653vis1d6qsc-postgresql-15.5/bin/psql
Enter fullscreen mode Exit fullscreen mode

🖥️ Terminal

Run nix develop

And if we exit the shell, the tools are no longer available:

macOS-14:perfect-elixir cloud$ which erl
/nix/store/49qw7cw30wszrfn3sa23qnlskyvbnbhi-erlang-26.2.2/bin/erl
macOS-14:perfect-elixir cloud$ exit
exit
$ which erl
erl not found
Enter fullscreen mode Exit fullscreen mode

🖥️ Terminal

Exiting the Nix shell makes tools unavailable again

We now have a reproducible specification of our environment, pretty nice!

direnv

But just as with #asdf we would like the tools to be made automatically upon entering the folder. Let's once again automate it with direnv.

First, install direnv via Nix:

$ nix-env -iA nixpkgs.direnv;
$ echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc
$ source ~/.zshrc
Enter fullscreen mode Exit fullscreen mode

🖥️ Terminal

Install direnv via Nix

And activate the flake with direnv:

$ echo "use flake" > .envrc
$ direnv allow
direnv: loading ~/Documents/nix/.envrc
direnv: using flake
…
$ which erl
/nix/store/rp1c50s0w039grl22q086h0dyrygk0p2-erlang-26.2.1/bin/erl
$ which elixir
/nix/store/66f9b1d1c4fmhz6bd3fpcny6brjm0fk7-elixir-1.16.0/bin/elixir
$ which psql
/nix/store/zhk6mf2y5c07zqf519zjkm3fm2nazmvj-postgresql-15.5/bin/psql
Enter fullscreen mode Exit fullscreen mode

🖥️ Terminal

Activate Nix Flake via Direnv

Now, our Nix environment automatically activates when we enter the folder 🎉

ℹ️ BTW direnv requires running direnv allow whenever the .envrc file changes to prevent malicious code from executing. Always review .envrc before allowing.

 

pkgx


pkgx has the tagline "RUN ANYTHING", which sounds promising. Let's try it out.

First, install and activate pkgx:

$ brew install pkgxdev/made/pkgx
$ eval "$(pkgx integrate)"
Enter fullscreen mode Exit fullscreen mode

🖥️ Terminal

Install pkgx via Homebrew, then activate pkgx

Create a .pkgx.yml file to specify dependencies:

$ cat .pkgx.yml
dependencies:
  erlang.org: =26.2.1
  elixir-lang.org: =1.16.0
  postgresql.org: =15.2.0
Enter fullscreen mode Exit fullscreen mode

Activate the dependencies:

$ dev
env +erlang.org=26.2.1 +elixir-lang.org=1.16.0 +postgresql.org=15.2.0
$ which erl
/Users/cloud/.pkgx/erlang.org/v26.2.1/bin/erl
Enter fullscreen mode Exit fullscreen mode

🖥️ Terminal

Running dev command

And… huh, that’s it?! The tools are automatically made available when inside the folder, and disappear when not:

$ cd ..
env -erlang.org=26.2.1 -elixir-lang.org=1.16.0 -postgresql.org=15.2.0
$ which erl
erl not found
$ cd perfect-elixir
env +erlang.org=26.2.1 +elixir-lang.org=1.16.0 +postgresql.org=15.2.0
$ which erl
/Users/cloud/.pkgx/erlang.org/v26.2.1/bin/erl
Enter fullscreen mode Exit fullscreen mode

🖥️ Terminal

Leaving folder disables pkgx-provided tools

Hard to get any simpler than that, and although not shown here pkgx also supports specifying core tools such as bash, grep, etc.

 

In Conclusion

asdf Thoughts

asdf seems to be a popular choice judging from all the articles that mention it, but I found it quite cumbersome and old-fashioned to use. I don't mean to be too offensive, and I'm sure asdf has helped developers for decades which is undeniably amazing, but I think asdf is probably popular more for historical reasons than for how it compares to its present-day peers. I would recommend against using asdf.

Mise Thoughts

Mise dramatically simplifies the asdf experience, removing the major ergonomic painpoints of asdf. It's really remarkably simple to use, and it deserves praise for that. But also: Mise doesn't support system tools such as bash, grep, etc., and those are very common sources of errors in projects because e.g. MacOS' grep is very different from GNU grep and some projects often end up requiring one or the other.

As a result Mise-based projects must also maintain a list of Homebrew dependencies that developers should install, which causes the problems we saw in Just... Install the Dependencies section: Homebrew dependencies lack versioning and are globally installed, and just isn't precise enough to build a project on. I do not recommend Mise.

Nix Thoughts

Nix is clearly powerful, but also very hard to learn. Like, way over the top hard, complete with lacking documentation and hard to grasp jargon. It definitely offers unmatched control over dependencies, but it also requires such a significant learning curve it stands in strong contrast to our needs of just wanting a handful of system tools installed.

I'm sure Nix is a great tool for sophisticated needs such as specifying all dependencies for an entire operating system, but for installing Elixir and Bash? Nix is likely overkill for that purpose. That's not to say Nix is automatically a poor choice, but you should carefully consider its learning curve before adopting it, including how it will impact every person who will ever work on this project, including future hires.

pkgx Thoughts

pkgx is impressively simple and easy to use: Easy to install, easy to configure, and easy to use. It's crazy simple all the way: No need to invoke sudo, and the installer automatically integrates itself with your preferred shell, and its dev command is enormously convenient in how everything just works out of the box.

Despite being a new tool, the pkgx registry already includes all manner of packages, crucially including core tools such as bash and grep. That'll become extremely valuable when we start writing scripts, because it means we can truly rely on the whole team having the same tools available.


Ultimately the best tool depends on your specific needs and preferences, but for me pkgx stands out as the most user-friendly and comprehensive option. It is an easy recommendation, and it is the tool I'll use going forward in this article series.

💖 💪 🙅 🚩
jonlauridsen
Jon Lauridsen

Posted on March 18, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related