Installing different versions of Elixir and Erlang

I’m trying to install Elixir 1.15 compiled against latest available Erlang, e.g. 26.1.2.

How do I do that? Is it possible to customise the unstablePkgs.elixir line to say “Please, make sure to install a version of erlang that’s unstablePkgs.erlang_26”?

My flake.nix looks like this:

{
  inputs = {
    nixpkgs = {
      url = "nixpkgs/nixos-23.05";
    };

    unstable = {
      url = "nixpkgs/nixos-unstable";
    };
  };

  outputs = { self, nixpkgs, unstable }: {
    packages."x86_64-darwin".default = let
      pkgs = nixpkgs.legacyPackages."x86_64-darwin";
      unstablePkgs = unstable.legacyPackages."x86_64-darwin";
    in pkgs.buildEnv {
      name = "my-packages";
      paths = with pkgs; [
        curl
        git
        unstablePkgs.elixir
        unstablePkgs.erlang_26
      ];
    };
  };
}

If I run:

nix --extra-experimental-features 'nix-command flakes' build

I get this output when running interpreters - note the different erts versions - 14.1.1 corresponds to OTP 26:

$ result/bin/elixir --version
Erlang/OTP 25 [erts-13.2.2.4] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1]

Elixir 1.15.7 (compiled with Erlang/OTP 25)
$ result/bin/erl -version
Erlang (SMP,ASYNC_THREADS) (BEAM) emulator version 14.1.1

I’m using an Intel-based MacBook. This is my second attempt to get into Nix world. This time my motivation aligns with that of Julia Evan’s blog post, e.g. replacing Homebrew, a package manager I’ve used for many years, with a simple Nix flake.

I’m a professional Elixir developer for years, who needs to cooperate with organizations and OSS repos that still primarily use asdf or rtx to manage their runtime versions of the BEAM toolchain. I have little to no intention (or authority) asking my employers or those other projects to adopt Nix officially.

I’ve created a repository nix-beam-flakes with additional Erlang/Elixir tooling beyond what’s in nixpkgs, which you can consume as a flake input to create devShells. It also exposes poorly-documented library functions that let you do this in a slightly more ad-hoc way if you need better flexibility, and it also has support for reading .tool-versions files from asdf. The documentation is also rendered in an mdbook here. It offers a binary cache for the matrix of compatible versions between the latest 3 Erlang and latest 3 Elixir releases, populated via the GitHub Actions runs you can observe on the repo.

If you don’t like my approach, or don’t like flakes, or the flake dependencies I’ve adopted, I can point you to some of the internal implementation code that is not specific to any of those things and you can adapt to your liking - the pattern itself is pretty portable. Just let me know.

For learning purposes you can also observe the stable manual to learn how the BEAM-specific attributes are structured in nixpkgs:

It’ll point you to attributes such as:

  • beam.packages.erlangR26.elixir_1_15

These are not specific to any given patch version, modulo your use of a flake.lock to freeze it that way. That tradeoff was what led me to create my repo, since I wanted the higher specificity that was independent of my nixpkgs inputs’ specific contents.

pkgs.elixir as a simple selector still defaults to OTP 25, as you’ve found, so your devShell probably has both 25 and 26 in the PATH.

1 Like

I would like to try your approach, as well as the default one. But as I mentioned in the post, I am a nix newbie, and barely capable of reading nix code.

Would you mind editing my example to show me how both approaches would look like in practice, e.g. yours vs the one from manual?

No problem.

Here’s your original version modified near the end to be specific about which major version of Erlang to use. That may be enough to get you started, if you like.

{
  inputs = {
    nixpkgs = {
      url = "nixpkgs/nixos-23.05";
    };

    unstable = {
      url = "nixpkgs/nixos-unstable";
    };
  };

  outputs = { self, nixpkgs, unstable }: {
    packages."x86_64-darwin".default = let
      pkgs = nixpkgs.legacyPackages."x86_64-darwin";
      unstablePkgs = unstable.legacyPackages."x86_64-darwin";
    in pkgs.buildEnv {
      name = "my-packages";
      paths = with pkgs; [
        curl
        git
        unstablePkgs.beam.packages.erlangR26.elixir_1_15
        unstablePkgs.beam.packages.erlangR26.erlang
      ];
    };
  };
}

Here’s a version that uses my flake module via flake-parts and is based loosely on my non-Phoenix flake template:

{
  inputs = {
    beam-flakes = {
      url = "github:shanesveller/nix-beam-flakes";
      inputs.flake-parts.follows = "flake-parts";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    flake-parts.url = "github:hercules-ci/flake-parts";
    nixpkgs.url = "nixpkgs/nixos-unstable";
  };

  outputs = inputs @ {
    beam-flakes,
    flake-parts,
    ...
  }:
    flake-parts.lib.mkFlake {inherit inputs;} {
      imports = [beam-flakes.flakeModule];

      systems = ["aarch64-darwin" "x86_64-darwin"];

      perSystem = {pkgs, ...}: {
        # options are documented at https://nix-beam-flakes.sveller.codes/options
        beamWorkspace = {
          enable = true;
          devShell.extraPackages = with pkgs; [curl git];
          versions = {
            elixir = "1.15.7";
            erlang = "26.1.2";
          };
        };
      };
    };
}

This uses a flake devShell rather than a package output, so you use nix develop .# to enter that context.

Extra learning topics below, feel free to skip but I’m leaving them as further jumping off points if you decide you have appetite.


The project templates can be used with these commands, which per the Nix CLI behavior should abort if they would overwrite any existing file in your project.

nix flake init -t github:shanesveller/nix-beam-flakes#default
# or
nix flake init -t github:shanesveller/nix-beam-flakes#phoenix

They assume use of direnv + nix-direnv, so if you don’t use those you can erase the .envrc file.


If any x86 Linux users wanted to use my binary cache, please do your own homework about trust, auditability, supply chain attacks, etc. but you can introduce this as a top-level key in your flake, as a sibling to inputs and outputs. Notably in your case, OP, it would be pointless, because I don’t build or push any Darwin artifacts due to GHA limitations (and I don’t have any Intel hardware left).

  nixConfig = {
    extra-substituters = ["https://shanesveller-nix-beam-flakes.cachix.org"];
    extra-trusted-public-keys = [
      "shanesveller-nix-beam-flakes.cachix.org-1:DMWI87/57GNone8wN7dXcqlgdIk36qHfvhXJ/esq5hk="
    ];
  };
1 Like

Thanks a lot @shanesveller for amazing and very-very descriptive messages about what’s going on here, that’s exactly what I needed! Your modification of my code worked flawlessly. I’ll make sure to study your flake sources when I get to it!

Oh my god, I cannot express how grateful I am for you poiting out that beam.packages.erlangR26.elixir_1_15 is possible. I’ve been stuck on this problem multiple times and just gave up.