# BEAM Languages (Erlang, Elixir & LFE) {#sec-beam} ## Introduction {#beam-introduction} In this document and related Nix expressions, we use the term, _BEAM_, to describe the environment. BEAM is the name of the Erlang Virtual Machine and, as far as we're concerned, from a packaging perspective, all languages that run on the BEAM are interchangeable. That which varies, like the build system, is transparent to users of any given BEAM package, so we make no distinction. ## Available versions and deprecations schedule {#available-versions-and-deprecations-schedule} ### Erlang OTP {#erlang} Nixpkgs follows upstream Erlang in their [support lifecycle](https://erlang.org/download/otp_versions_tree.html) and keeps up to the last 3 released versions of Erlang available. Due to upstream and NixOS release timings, this may mean removal of the oldest release prior to upstream fully dropping support. ### Elixir {#elixir} Nixpkgs follows the [official elixir deprecation schedule](https://hexdocs.pm/elixir/compatibility-and-deprecations.html) and keeps up to the last 5 released versions of Elixir available. ## Structure {#beam-structure} All BEAM-related expressions are available via top-level package sets. It is recommended to work with a single package set to ensure consistent versions. - `beamPackages` - default OTP version - `beamMinimalPackages` - default OTP version, without wxwidgets, which saves ~1GB in closure size There are also OTP version specific package sets, e.g. for OTP 28: - `beam28Packages` - `beamMinimal28Packages` Inside each package set are: - erlang itself (version comes from package set) - interpreters: elixir (multiple versions, e.g. elixir_1_18) and lfe - packages: rebar3, hex, etc - builders: mixRelease, buildRebar3, etc - hooks: for composing builders and packages To use a non-default Elixir it's important to keep the rest of the package set consistent, so it's recommended to use `.extend`. This ensures that builders like `mixRelease`, `fetchMixDeps`, and `buildMix` all pick up the overridden Elixir: ```nix let beamPackages = beam27Packages.extend (self: super: { elixir = self.elixir_1_18; }); in beamPackages.mixRelease { # ... } ``` ## Build Tools {#beam-build-tools} ### Rebar3 {#beam-build-tools-rebar3} We provide a version of Rebar3, under `beamPackages.rebar3`. We also provide a helper to fetch Rebar3 dependencies from a lockfile under `beamPackages.fetchRebar3Deps`. We also provide a version on Rebar3 with plugins included, under `beamPackages.rebar3WithPlugins`. This package is a function which takes two arguments: `plugins`, a list of nix derivations to include as plugins (loaded only when specified in `rebar.config`), and `globalPlugins`, which should always be loaded by rebar3. Example: `beamPackages.rebar3WithPlugins { globalPlugins = [beamPackages.pc]; }`. When adding a new plugin it is important that the `name` attribute is the same as the atom used by rebar3 to refer to the plugin. ### Erlang.mk {#beam-build-tools-erlangmk} Erlang.mk works exactly as expected. There is a bootstrap process that needs to be run, which is supported by the `buildErlangMk` derivation. ### Mix {#beam-build-tools-mix} For Elixir applications that use [mix release](https://hexdocs.pm/mix/Mix.Release.html), use the `mixRelease` builder to make a release. See examples for more details. There is also a `buildMix` helper, whose behavior is closer to that of `buildErlangMk` and `buildRebar3`. The primary difference is that `mixRelease` makes a release, while `buildMix` only builds the package, which is more useful for libraries and other dependencies. ## How to Install BEAM Packages {#how-to-install-beam-packages} To use any of these builders in your environment, refer to them by their attribute path under `beamPackages` (or another BEAM package set), e.g. `beamPackages.rebar3`: ::: {.example #ex-beam-ephemeral-shell} # Ephemeral shell ```ShellSession $ nix-shell -p beamPackages.rebar3 ``` ::: ::: {.example #ex-beam-declarative-shell} # Declarative shell ```nix let pkgs = import { config = { }; overlays = [ ]; }; in pkgs.mkShell { packages = [ pkgs.beamPackages.rebar3 ]; } ``` ::: ## Packaging BEAM Applications {#packaging-beam-applications} ### Erlang Applications {#packaging-erlang-applications} #### Rebar3 Packages {#rebar3-packages} The builder `beamPackages.buildRebar3` can be used to build a derivation that understands how to build a Rebar3 project. #### Erlang.mk Packages {#erlang-mk-packages} Erlang.mk functions similarly to Rebar3, except we use `beamPackages.buildErlangMk` instead of `beamPackages.buildRebar3`. If a package needs to compile native code via Erlang.mk's port compilation mechanism, add `compilePorts = true;` to the derivation. ### Elixir Applications {#packaging-elixir-applications} #### Mix Packages {#mix-packages} `beamPackages.mixRelease` is used to make a release in the mix sense. Dependencies will need to be fetched with `beamPackages.fetchMixDeps` and passed to it. #### mixRelease - Elixir Phoenix example {#mix-release-elixir-phoenix-example} There are 3 steps: frontend dependencies (javascript), backend dependencies (elixir), and the final derivation that puts both of those together. ##### mixRelease - Frontend dependencies (javascript) {#mix-release-javascript-deps} For phoenix projects, inside of Nixpkgs you can either use `fetchYarnDeps` or `buildNpmPackage`. An example with `buildNpmPackage` can be found [here](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/pl/plausible/package.nix), and an example with `fetchYarnDeps` can be found [here](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/pi/pinchflat/package.nix). ##### mixRelease - backend dependencies (mix) {#mix-release-mix-deps} There are 2 ways to package backend dependencies: either per-dependency mix2nix or with a fixed-output-derivation (FOD). When writing an elixir project targeting `mixRelease`, you can also consider using [deps_nix](https://github.com/code-supply/deps_nix) with `mixNixDeps`. `deps_nix` supports git dependencies, but is intended to be added to the project's `mix.exs` directly. ###### mix2nix {#mix2nix} `mix2nix` is a cli tool available in Nixpkgs. It will generate a Nix expression from a `mix.lock` file. It is quite standard in the 2nix tool series. Note that currently mix2nix can't handle git dependencies inside the mix.lock file. If you have git dependencies, you can either add them manually (see [example](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/pl/pleroma/package.nix)) or use the FOD method. The advantage of using mix2nix is that nix will know your whole dependency graph. On a dependency update, this won't trigger a full rebuild and download of all the dependencies, where FOD will do so. Practical steps: - run `mix2nix > mix_deps.nix` in the upstream repo. - pass `mixNixDeps = with pkgs; import ./mix_deps.nix { inherit lib beamPackages; };` as an argument to mixRelease. If there are git dependencies. - You'll need to fix the version artificially in mix.exs and regenerate the mix.lock with fixed version (on upstream). This will enable you to run `mix2nix > mix_deps.nix`. - From the mix_deps.nix file, remove the dependencies that had git versions and pass them as an override to the import function. ```nix { mixNixDeps = import ./mix.nix { inherit beamPackages lib; overrides = ( final: prev: { # mix2nix does not support git dependencies yet, # so we need to add them manually prometheus_ex = beamPackages.buildMix rec { name = "prometheus_ex"; version = "3.0.5"; # Change the argument src with the git src that you actually need src = fetchFromGitLab { domain = "git.pleroma.social"; group = "pleroma"; owner = "elixir-libraries"; repo = "prometheus.ex"; rev = "a4e9beb3c1c479d14b352fd9d6dd7b1f6d7deee5"; hash = "sha256-U17LlN6aGUKUFnT4XyYXppRN+TvUBIBRHEUsfeIiGOw="; }; # you can re-use the same beamDeps argument as generated beamDeps = with final; [ prometheus ]; }; } ); }; } ``` You will need to run the build process once to fix the hash to correspond to your new git src. ###### FOD {#fixed-output-derivation} A fixed output derivation will download mix dependencies from the internet. To ensure reproducibility, a hash will be supplied. Note that mix is relatively reproducible. An FOD generating a different hash on each run hasn't been observed (as opposed to npm where the chances are relatively high). See [akkoma](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/ak/akkoma/package.nix) for a usage example of FOD. Practical steps - start with the following argument to mixRelease ```nix { mixFodDeps = fetchMixDeps { pname = "mix-deps-${pname}"; inherit src version; hash = lib.fakeHash; }; } ``` The first build will complain about the hash value, you can replace with the suggested value after that. Note that if after you've replaced the value, nix suggests another hash, then mix is not fetching the dependencies reproducibly. An FOD will not work in that case and you will have to use mix2nix. ##### mixRelease - example {#mix-release-example} Here is how your `default.nix` file would look for a Phoenix project. ```nix { # beam27Packages or beam29Packages is available if you need a particular version beamPackages, }: let pname = "your_project"; version = "0.0.1"; src = builtins.fetchgit { url = "ssh://git@github.com/your_id/your_repo"; rev = "replace_with_your_commit"; }; # if using mix2nix you can use the mixNixDeps attribute mixFodDeps = beamPackages.fetchMixDeps { pname = "mix-deps-${pname}"; inherit src version; # nix will complain and tell you the right value to replace this with hash = lib.fakeHash; mixEnv = ""; # default is "prod", when empty includes all dependencies, such as "dev", "test". # if you have build time environment variables add them here MY_ENV_VAR = "my_value"; }; in beamPackages.mixRelease { inherit src pname version mixFodDeps ; # if you have build time environment variables add them here MY_ENV_VAR = "my_value"; postBuild = '' # for external task you need a workaround for the no deps check flag # https://github.com/phoenixframework/phoenix/issues/2690 mix do deps.loadpaths --no-deps-check, phx.digest mix phx.digest --no-deps-check ''; } ``` Setup will require the following steps: - Move your secrets to runtime environment variables. For more information refer to the [runtime.exs docs](https://hexdocs.pm/mix/Mix.Tasks.Release.html#module-runtime-configuration). On a fresh Phoenix build that would mean that both `DATABASE_URL` and `SECRET_KEY` need to be moved to `runtime.exs`. - Generate a Nix expression for your frontend dependencies using `fetchNpmDeps`/`buildNpmPackage` or `fetchYarnDeps`, depending on whether the project uses npm or yarn - commit and push those changes - you can now `nix-build .` - To run the release, set the `RELEASE_TMP` environment variable to a directory that your program has write access to. It will be used to store the BEAM settings. #### Example of creating a service for an Elixir - Phoenix project {#example-of-creating-a-service-for-an-elixir---phoenix-project} In order to create a service with your release, you could add a `service.nix` in your project with the following ```nix { config, pkgs, lib, ... }: let release = pkgs.callPackage ./default.nix { }; release_name = "app"; working_directory = "/home/app"; in { systemd.services.${release_name} = { wantedBy = [ "multi-user.target" ]; after = [ "network.target" "postgresql.target" ]; # note that if you are connecting to a postgres instance on a different host # postgresql.target should not be included in the requires. requires = [ "network-online.target" "postgresql.target" ]; description = "my app"; environment = { # RELEASE_TMP is used to write the state of the # VM configuration when the system is running # it needs to be a writable directory RELEASE_TMP = working_directory; # can be generated in an elixir console with # Base.encode32(:crypto.strong_rand_bytes(32)) RELEASE_COOKIE = "my_cookie"; MY_VAR = "my_var"; }; serviceConfig = { Type = "exec"; DynamicUser = true; WorkingDirectory = working_directory; # Implied by DynamicUser, but just to emphasize due to RELEASE_TMP PrivateTmp = true; ExecStart = '' ${release}/bin/${release_name} start ''; ExecStop = '' ${release}/bin/${release_name} stop ''; ExecReload = '' ${release}/bin/${release_name} restart ''; Restart = "on-failure"; RestartSec = 5; }; unitConfig = { StartLimitBurst = 3; StartLimitInterval = 10; }; # disksup requires bash path = [ pkgs.bash ]; }; # in case you have migration scripts or you want to use a remote shell environment.systemPackages = [ release ]; } ``` ## How to Develop {#how-to-develop} ### Creating a Shell {#creating-a-shell} Usually, we need to create a `shell.nix` file and do our development inside the environment specified therein. Just install your version of Erlang and any other interpreters, and then use your normal build tools. As an example, with Elixir: ```nix { pkgs ? import { }, }: with pkgs; let # pin OTP via beam27Packages/beam28Packages/... and Elixir via .extend beamPackages = beam27Packages.extend (self: super: { elixir = self.elixir_1_18; }); in mkShell { buildInputs = [ beamPackages.elixir ]; } ``` ### Using an overlay {#beam-using-overlays} If you need to use an overlay to change some attributes of a derivation, e.g. if you need a bugfix from a version that is not yet available in Nixpkgs, you can override attributes such as `version` (and the corresponding `hash`) and then use this overlay in your development environment: #### `shell.nix` {#beam-using-overlays-shell.nix} ```nix let elixir_1_18_1_overlay = ( self: super: { elixir_1_18 = super.elixir_1_18.override { version = "1.18.1"; hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; }; } ); pkgs = import { overlays = [ elixir_1_18_1_overlay ]; }; in with pkgs; mkShell { buildInputs = [ elixir_1_18 ]; } ``` #### Elixir - Phoenix project {#elixir---phoenix-project} Here is an example `shell.nix`. ```nix with import { }; let # pin OTP via beam27Packages/beam28Packages/... and Elixir via .extend beamPackages = beam27Packages.extend (self: super: { elixir = self.elixir_1_18; }); # define packages to install basePackages = [ git beamPackages.elixir nodejs postgresql_14 # formatting js file prettier ]; inputs = basePackages ++ lib.optionals stdenv.hostPlatform.isLinux [ inotify-tools ]; # define shell startup command hooks = '' # this allows mix to work on the local directory mkdir -p .nix-mix .nix-hex export MIX_HOME=$PWD/.nix-mix export HEX_HOME=$PWD/.nix-mix # make hex from Nixpkgs available # `mix local.hex` will install hex into MIX_HOME and should take precedence export MIX_PATH="${beamPackages.hex}/lib/erlang/lib/hex/ebin" export PATH=$MIX_HOME/bin:$HEX_HOME/bin:$PATH export LANG=C.UTF-8 # keep your shell history in iex export ERL_AFLAGS="-kernel shell_history enabled" # postgres related # keep all your db data in a folder inside the project export PGDATA="$PWD/db" # phoenix related env vars export POOL_SIZE=15 export DB_URL="postgresql://postgres:postgres@localhost:5432/db" export PORT=4000 export MIX_ENV=dev # add your project env vars here, word readable in the nix store. export ENV_VAR="your_env_var" ''; in mkShell { buildInputs = inputs; shellHook = hooks; } ``` Initializing the project will require the following steps: - create the db directory `initdb ./db` (inside your mix project folder) - create the postgres user `createuser postgres -ds` - create the db `createdb db` - start the postgres instance `pg_ctl -l "$PGDATA/server.log" start` - add the `/db` folder to your `.gitignore` - you can start your Phoenix server and get a shell with `iex -S mix phx.server`