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, we must find a solution that installs exactly the right versions just for our project, across all developer machines and environments. Let's go explore some tools that are designed to do 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 sogit clone
it is…
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 how each plugin requires their own set of Homebrew-installed system dependencies, because that seems to throw 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 quite enough: Developers will have to manually run asdf install
to stay in sync with the specified versions, it's not taken care of automatically. Can we automate this part?
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 🎉
We can compare tools later, for now let's move on to try the next tool to see how it's different.
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)
🖥️ Terminal
ℹ️ 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, it sure feels like a lot just to install some system tools…
Nix uses a custom pseudo programming language for specifying dependencies. I've struggled greatly to understand any available 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.
We then edit flake.nix
to specify our (Mac) 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!
direnv
But just as with #asdf we would like the tools to automatically be available 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 pkgx
$ pkgx dev integrate
Create a .pkgx.yml
file to specify dependencies:
$ cat .pkgx.yml
dependencies:
erlang.org: =27.3.2
elixir-lang.org: =1.18.3
postgresql.org: =17.2.0
Activate the dependencies:
$ dev
+erlang.org=27.3.2 +elixir-lang.org=1.18.3 +postgresql.org=17.2.0
$ which erl
/Users/cloud/.pkgx/erlang.org/v27.3.2/bin/erl
And… huh, that’s it?! The tools are automatically only available when inside the folder:
$ cd ..
-erlang.org=27.3.2 -elixir-lang.org=1.18.3 -postgresql.org=17.2.0
$ which erl
erl not found
$ cd perfect-elixir
+erlang.org=27.3.2 +elixir-lang.org=1.18.3 +postgresql.org=17.2.0
$ which erl
/Users/cloud/.pkgx/erlang.org/v27.3.2/bin/erl
Hard to get any simpler than that! And although we didn't show it here, pkgx
also supports all kinds of core system tools such as bash
, grep
, etc., which is super useful.
In Conclusion
asdf seems to be a popular choice judging from an abundance of articles mentioning it, but I found it quite cumbersome and old-fashioned to use. I don't mean to be rude, and I'm sure asdf has helped developers for decades, but I think asdf is probably popular more for historical reasons than for how it compares to its present-day peers.
I would not recommend using asdf for a new project.
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 worth mentioning: Mise does not support system tools such as bash
, grep
, etc., and having different versions of those are very common sources of errors 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 are highly likely to end up also maintaining a list of Homebrew dependencies that developers must install, which causes the issues of lack of versioning we saw in the Just... Install the Dependencies section.
Ultimately Mise provides a nice experience, but cannot offer versioning of a wide enough set of tools.
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 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, and it ends up being really intrusive compared to the task we're looking to solve.
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 a huge overkill for that purpose.
You should carefully consider its learning curve and reach into everyone's systems before adopting it, including how it will impact every person who will ever work on this project, including future hires.
pkgx
is impressively simple, and so very easy to use: Easy to install, easy to configure, and easy to use. It's crazy simple all the way: No need for sudo
, the installer automatically handles integrating with your preferred shell, and its dev
command is enormously convenient in how everything just works out of the box.
Furthermore, the pkgx
registry includes all manner of packages, including system-tools such as bash
and grep
. That very valuable because projects tend to accumulate Bash scripts and it's a common source of errors when a team does not have the same system-tools available.
Ultimately the best tool must depend on your specific needs and preferences; only you can choose based on the requirements you face. To me pkgx is by far the most user-friendly and comprehensive option available so it's an easy recommendation, and it is the tool I'll use going forward in this article series.
Top comments (4)
Great breakdown of the contemporary options. I use asdf with direnv and know of nix, but its configuration seemed pretty daunting and just another rabbit hole to go down. Thanks for introducing pkgx, looks very sleek and will definitely try it out.
I wonder what the journey is like with nix and pkgx to install erlang and elixir with compiled source docs?
In asdf, erlang docs are installed by setting some env variables, and the elixir plugin will compile any github version tag or sha ref from scratch with e.g.
asdf install elixir ref:v1.17.0-dev
. This way you can navigate to any erlang or elixir function's source from your own codebase in an IDE.Thanks for the feedback, and yeah I also wonder about that journey. We can see the pkgx Erlang package page here which leads to the actual pkgx package.yml definition here, and in that we can spot the familiar environment flags of
--without-javac
& friends. I guess we could make a PR that adds a configuration variant?Very interesting to hear about the ability to navigate to built-in source, I can't quite find a link that describes this so if you have any resources (or can write a blog about it 😅) I'd be very happy to receive.
Yes absolutely, you're very welcome to write about it! Wish I had the time... 🫠
I posted a reply on the Elixir forum about this and as luck would have it the link was in my notifications as a recent popular link.
Please link your blog post to the form thread when you're done, I'm sure people would want to read it, too.