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)"
Then install our tools:
$ brew install erlang elixir postgresql@15
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
ℹ️ 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
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
ℹ️ 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
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
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
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
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
Activate it for your shell (assuming zsh):
$ echo 'eval "$(mise activate zsh)"' >> "${ZDOTDIR-$HOME}/.zshrc"
$ source ~/.zshrc
Create a .mise.toml
file to specify dependencies:
$ cat .mise.toml
[tools]
erlang = '26.2.1'
elixir = '1.16.0'
postgres = '15.5'
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
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
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
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)
ℹ️ 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;
};
}
ℹ️ 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
];
};
};
};
}
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
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
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
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
Now, our Nix environment automatically activates when we enter the folder 🎉
ℹ️ BTW
direnv
requires runningdirenv 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)"
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
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
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
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.