What's the current best-in-class approach to packaging Elixir/Erlang/BEAM applications using Nix+releases as of July 2020?

Hello,
I’m an enthusiastic Nix user whose day job is all in the Elixir ecosystem. I’d love to have the option to leverage Nix to package up our releases, especially to be able to capitalize on pkgs.dockerTools and other knock-on effects. We currently use asdf at development time and Docker at deploy-time and have to re-solve the same compatibility concerns in both layers.

I’m aware of these projects and find in practice that each of them is only part of the story:

  • hex2nix - last commit Aug '19, haven’t tried recently
  • mix2nix - last commit Apr '19, haven’t tried recently
  • mixnix - last commit Mar '19, incompatible with the current (undocumented) mix.lock schema
  • Nixpkgs documentation including buildHex and buildMix which don’t seem to get on all that well with a full-sized application, such as a Phoenix project

Is anyone familiar with ways to accentuate them to get to a happy answer today? Does anyone have a new project in this space that I can contribute to? I’m envious of the higher-level tooling available for Haskell users, Python users, even Node users, and especially of options like naersk and crate2nix for Rust applications.

I’d really like to be able to cite an example of taking a brand new mix phx.new project and spit out a release at the end with all of the downloads and compilation being done via nix-build instead.

In my spare time I’m plinking away at a standalone repo that I’ll probably turn into a flake/NUR/Cachix etc. that would provide the same catalog of historical versions that Elixir devs commonly get from asdf today, but it’s hard for me to keep up my motivation there when I get to “okay I have the exact version of Erlang and Elixir I wanted, now what?”

2 Likes

Sadly. This does seem to be a bit of a quagmire at present, with lots of the information out there being obsoleted.

We are heavy users of nix for our development environments, and generally interested in packaging Erlang applications with nix too. We do that today in one project by using rebar3 to build outside of nix, and then we have a nix derivation that uses the built asset as its source. That’s very much an expedient hack though, and not where we want to be.

I’d originally looked at using Announcing Rebar32nix, which, for Erlang, looks to me like the right approach, but wasn’t viable when I used it because most of the beam capabilities had already been removed from nixpkgs by that point.

There are also some prior discussions around this that are worth looking at, e.g. State of the BEAM Ecosystem in Nix - #6 by DianaOlympos

I’d be pretty interested in getting this conversation going again, and chiming in from the Erlang point of view. I don’t have any experience of Elixir, so I can’t comment about specifics there.

On another note, I maintain GitHub - id3as/nixpkgs-nixerl: A nix overlay for Erlang Releases, which I created as a sort-of replacement for kerl (which serves a similar function to asdf) in a nix world. It specifically makes most released versions of Erlang available for use.

1 Like

Hey @shanesveller happy to see you around :slight_smile:

So i have been in that state for quite a long time. Right now there are no easy solutions and all the X2nix afaik depends on reading the mix.lock which is… problematic as you pointed out.

The nix ecosystem in general has two ways to handle that.

  1. Reproduce the package manager into nix itself. This is what is happening for haskell or python as an example.
  2. Do a two stage derivation, one of them fetching the whole set of dependencies using the language package manager and the lock file to ensure that you get something reproducible out of it, then validating that with a hash to satisfy the sandbox. That is how buildGoModule work as an example.

The previous solution for BEAM in nix, that i tore down last year, is to use option 1. It simply did not work out because it means regularly mirroring all of hex into nixpkgs. This need a maintenance level that is really hard to produce. As a big user of the Python ecosystem in nix, it is probably the source of most of my pain because even the Python ecosystem cannot cope.

So my current goal would be to provide a solution based on option 2. It is not particularly hard to build imho, in pure nix, it is just that i had no time to do it yet. Following the path of buildGoModule and copying their code a lot should be enough to get us going. It will need documentation and imho should be a direct PR to nixpkgs. That should be definitely doable for rebar3 too @philipstears.

This approach has problems too, it seems to have generated quite some frictions with some part of the nix community. But the other 2 options, ie depending on reading a lock file that is opaque, reverse engineering it or reproducing hex on nixpkgs, both seems like too heavy in term of maintenance to be viable.

I am ofc really open to other ideas :smiley: I would particularly like if we could get this going because it would probably solve a lot of my problems with my own CI. I have just been a bit busy lately.

I would be happy to provide review and help if someone want to get on that ship though.

2 Likes

Hey @shanesveller, I don’t know if you’re on the Erlang slack (the link is at Community - Erlang/OTP if not), but a few folks (including @DianaOlympos and myself) have been talking about this on the “nix” channel there.

3 Likes

I originally wrote rebar32nix, but never really received any sort of feedback on it. So I just sort of let it go to waste. If there are people that are currently working with it, or want to, I am more than happy to continue work on it. I will also join the nix channel on the Erlang slack and maybe we can come up with something that works for everyone.

1 Like

Hey all, I have currently the same opinion as @DianaOlympos.

Reproducing the complete behavior of mix & rebar as a package manager within Nix seems a bit of too much work from my point of view (the .lock file is fine).
Even if I see the benefits this approach would bring - super super fast rebuilds in CI.
But in my work habit the dependence list in the .lock file is still countable and changes to my .lock file aren’t happening daily.
So a rebuild of + half a minute more is total acceptable occasionally.

But I have to explain my main usage or Nix with Elixir is the setup process for having a reproducible development environment.
So I’m aware in other main usage scenarios different things could be more valuable.

@shanesveller if you haven’t found my sample repo for showing off how I’m using Nix with Elixir currently → github.com/cw789/elixir_nix_seed

My future “goal” (not active working on it) for Nix & Elixir is cross compilation.
To have an easy setup where I can build releases and deploy on different architectures already including ERTS.

1 Like

As mix deps.get should provide (almost) constant output I have created the derivation that fetches all dependencies via mix dips.get:

{ stdenvNoCC, elixir, rebar, rebar3, git, cacert }:

let
  fetchMixDeps =
    { name ? null, src, sha256, env ? "prod" }:
    stdenvNoCC.mkDerivation {
      name = "mix-deps" + (if name != null then "-${name}" else "");

      nativeBuildInputs = [ elixir git cacert ];

      inherit src;

      MIX_ENV = env;
      MIX_REBAR = "${rebar}/bin/rebar";
      MIX_REBAR3 = "${rebar3}/bin/rebar3";

      configurePhase = ''
        export HEX_HOME="$PWD/hex"
        export MIX_HOME="$PWD/mix"
        export MIX_DEPS_PATH="$out"
        export REBAR_GLOBAL_CONFIG_DIR="$PWD/rebar3"
        export REBAR_CACHE_DIR="$PWD/rebar3.cache"

        mix local.hex --force
      '';

      buildPhase = ''
        mix deps.get
        find "$out" -path '*/.git/*' -a ! -name HEAD -exec rm -rf {} +
      '';

      dontInstall = true;

      outputHashAlgo = "sha256";
      outputHashMode = "recursive";
      outputHash = sha256;

      impureEnvVars = stdenvNoCC.lib.fetchers.proxyImpureEnvVars;
    };
in fetchMixDeps

Which is enough for my uses. It also do not rely on manually traversing mix.lock (which format is not guaranteed by the Mix in any way, so traversing it will always be “here be dragons”).

I use it for building ElixirLS in a form of:

{ stdenv, elixir, rebar3, hex, fetchFromGitHub, fetchMixDeps, git, cacert }:

let
  json = builtins.fromJSON (builtins.readFile ./elixir-ls.json);
in
stdenv.mkDerivation rec {
  name = "elixir-ls";
  version = json.rev;

  nativeBuildInputs = [ elixir hex git deps cacert ];

  deps = fetchMixDeps {
    name = "${name}-${version}";
    inherit src;

    sha256 = "1j7v56mfa087wi3x8kdcbqq0wsdiw284cwlccvxs1b60rypx5k55";
  };

  # refresh: nix-prefetch-git https://github.com/elixir-lsp/elixir-ls.git [--rev branchName | --rev sha]
  src = fetchFromGitHub json;

  dontStrip = true;

  configurePhase = ''
    runHook preConfigure
    export MIX_ENV=prod

    export HEX_OFFLINE=1
    export HEX_HOME="$PWD/hex"
    export MIX_HOME="$PWD"
    export MIX_REBAR3="${rebar3}/bin/rebar3"
    export REBAR_GLOBAL_CONFIG_DIR="$out/rebar3"
    export REBAR_CACHE_DIR="$out/rebar3.cache"

    cp --no-preserve=all -R ${deps} deps

    mix deps.compile --no-deps-check

    runHook postConfigure
  '';

  buildPhase = ''
    runHook preBuild

    mix do compile --no-deps-check, elixir_ls.release

    runHook postBuild
  '';

  installPhase = ''
    mkdir -p $out/bin
    cp -Rv release $out/lib

    # Prepare the wrapper script
    substitute release/language_server.sh $out/bin/elixir-ls \
      --replace 'exec "''${dir}/launch.sh"' "exec $out/lib/launch.sh"
    chmod +x $out/bin/elixir-ls

    # prepare the launcher
    substituteInPlace $out/lib/launch.sh \
      --replace "ERL_LIBS=\"\$SCRIPTPATH:\$ERL_LIBS\"" \
                "ERL_LIBS=$out/lib:\$ERL_LIBS" \
      --replace "elixir -e" "${elixir}/bin/elixir -e"
  '';
}

And so far it works mostly flawlessly.

6 Likes

Apologies for my absence since the first post, lots of work stuff leaching my attention this week. Thanks to everyone who responded so far and I’ll try to catch up this weekend. :wave:

1 Like

would you consider upstreaming at least fetchMixDeps and maybe elixir-ls as well? It looks great to me.

3 Likes

Linking the Status of lang2nix approaches discussion here because I think it’s pertinent.

Just to connect the dots:

1 Like