1# BEAM Languages (Erlang, Elixir & LFE) {#sec-beam} 2 3## Introduction {#beam-introduction} 4 5In 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. 6 7## Available versions and deprecations schedule {#available-versions-and-deprecations-schedule} 8 9### Elixir {#elixir} 10 11nixpkgs follows the [official elixir deprecation schedule](https://hexdocs.pm/elixir/compatibility-and-deprecations.html) and keeps the last 5 released versions of Elixir available. 12 13## Structure {#beam-structure} 14 15All BEAM-related expressions are available via the top-level `beam` attribute, which includes: 16 17- `interpreters`: a set of compilers running on the BEAM, including multiple Erlang/OTP versions (`beam.interpreters.erlang_22`, etc), Elixir (`beam.interpreters.elixir`) and LFE (Lisp Flavoured Erlang) (`beam.interpreters.lfe`). 18 19- `packages`: a set of package builders (Mix and rebar3), each compiled with a specific Erlang/OTP version, e.g. `beam.packages.erlang22`. 20 21The default Erlang compiler, defined by `beam.interpreters.erlang`, is aliased as `erlang`. The default BEAM package set is defined by `beam.packages.erlang` and aliased at the top level as `beamPackages`. 22 23To create a package builder built with a custom Erlang version, use the lambda, `beam.packagesWith`, which accepts an Erlang/OTP derivation and produces a package builder similar to `beam.packages.erlang`. 24 25Many Erlang/OTP distributions available in `beam.interpreters` have versions with ODBC and/or Java enabled or without wx (no observer support). For example, there's `beam.interpreters.erlang_22_odbc_javac`, which corresponds to `beam.interpreters.erlang_22` and `beam.interpreters.erlang_22_nox`, which corresponds to `beam.interpreters.erlang_22`. 26 27## Build Tools {#build-tools} 28 29### Rebar3 {#build-tools-rebar3} 30 31We provide a version of Rebar3, under `rebar3`. We also provide a helper to fetch Rebar3 dependencies from a lockfile under `fetchRebar3Deps`. 32 33We also provide a version on Rebar3 with plugins included, under `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: `rebar3WithPlugins { globalPlugins = [beamPackages.pc]; }`. 34 35When adding a new plugin it is important that the `packageName` attribute is the same as the atom used by rebar3 to refer to the plugin. 36 37### Mix & Erlang.mk {#build-tools-other} 38 39Erlang.mk works exactly as expected. There is a bootstrap process that needs to be run, which is supported by the `buildErlangMk` derivation. 40 41For Elixir applications use `mixRelease` to make a release. See examples for more details. 42 43There 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, making it useful for libraries and other dependencies. 44 45## How to Install BEAM Packages {#how-to-install-beam-packages} 46 47BEAM builders are not registered at the top level, because they are not relevant to the vast majority of Nix users. 48To use any of those builders into your environment, refer to them by their attribute path under `beamPackages`, e.g. `beamPackages.rebar3`: 49 50::: {.example #ex-beam-ephemeral-shell} 51# Ephemeral shell 52 53```ShellSession 54$ nix-shell -p beamPackages.rebar3 55``` 56::: 57 58::: {.example #ex-beam-declarative-shell} 59# Declarative shell 60 61```nix 62let 63 pkgs = import <nixpkgs> { config = {}; overlays = []; }; 64in 65pkgs.mkShell { 66 packages = [ pkgs.beamPackages.rebar3 ]; 67} 68``` 69::: 70 71## Packaging BEAM Applications {#packaging-beam-applications} 72 73### Erlang Applications {#packaging-erlang-applications} 74 75#### Rebar3 Packages {#rebar3-packages} 76 77The Nix function, `buildRebar3`, defined in `beam.packages.erlang.buildRebar3` and aliased at the top level, can be used to build a derivation that understands how to build a Rebar3 project. 78 79If a package needs to compile native code via Rebar3's port compilation mechanism, add `compilePort = true;` to the derivation. 80 81#### Erlang.mk Packages {#erlang-mk-packages} 82 83Erlang.mk functions similarly to Rebar3, except we use `buildErlangMk` instead of `buildRebar3`. 84 85#### Mix Packages {#mix-packages} 86 87`mixRelease` is used to make a release in the mix sense. Dependencies will need to be fetched with `fetchMixDeps` and passed to it. 88 89#### mixRelease - Elixir Phoenix example {#mix-release-elixir-phoenix-example} 90 91there are 3 steps, frontend dependencies (javascript), backend dependencies (elixir) and the final derivation that puts both of those together 92 93##### mixRelease - Frontend dependencies (javascript) {#mix-release-javascript-deps} 94 95For phoenix projects, inside of nixpkgs you can either use yarn2nix (mkYarnModule) or node2nix. An example with yarn2nix can be found [here](https://github.com/NixOS/nixpkgs/blob/master/pkgs/servers/web-apps/plausible/default.nix#L39). An example with node2nix will follow. To package something outside of nixpkgs, you have alternatives like [npmlock2nix](https://github.com/nix-community/npmlock2nix) or [nix-npm-buildpackage](https://github.com/serokell/nix-npm-buildpackage) 96 97##### mixRelease - backend dependencies (mix) {#mix-release-mix-deps} 98 99There are 2 ways to package backend dependencies. With mix2nix and with a fixed-output-derivation (FOD). 100 101###### mix2nix {#mix2nix} 102 103`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. 104 105Note 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/servers/pleroma/default.nix#L20)) or use the FOD method. 106 107The 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. 108 109Practical steps: 110 111- run `mix2nix > mix_deps.nix` in the upstream repo. 112- pass `mixNixDeps = with pkgs; import ./mix_deps.nix { inherit lib beamPackages; };` as an argument to mixRelease. 113 114If there are git dependencies. 115 116- 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`. 117- From the mix_deps.nix file, remove the dependencies that had git versions and pass them as an override to the import function. 118 119```nix 120 mixNixDeps = import ./mix.nix { 121 inherit beamPackages lib; 122 overrides = (final: prev: { 123 # mix2nix does not support git dependencies yet, 124 # so we need to add them manually 125 prometheus_ex = beamPackages.buildMix rec { 126 name = "prometheus_ex"; 127 version = "3.0.5"; 128 129 # Change the argument src with the git src that you actually need 130 src = fetchFromGitLab { 131 domain = "git.pleroma.social"; 132 group = "pleroma"; 133 owner = "elixir-libraries"; 134 repo = "prometheus.ex"; 135 rev = "a4e9beb3c1c479d14b352fd9d6dd7b1f6d7deee5"; 136 hash = "sha256-U17LlN6aGUKUFnT4XyYXppRN+TvUBIBRHEUsfeIiGOw="; 137 }; 138 # you can re-use the same beamDeps argument as generated 139 beamDeps = with final; [ prometheus ]; 140 }; 141 }); 142}; 143``` 144 145You will need to run the build process once to fix the hash to correspond to your new git src. 146 147###### FOD {#fixed-output-derivation} 148 149A 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 [elixir-ls](https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/beam-modules/elixir-ls/default.nix) for a usage example of FOD. 150 151Practical steps 152 153- start with the following argument to mixRelease 154 155```nix 156 mixFodDeps = fetchMixDeps { 157 pname = "mix-deps-${pname}"; 158 inherit src version; 159 hash = lib.fakeHash; 160 }; 161``` 162 163The first build will complain about the hash value, you can replace with the suggested value after that. 164 165Note 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. 166 167##### mixRelease - example {#mix-release-example} 168 169Here is how your `default.nix` file would look for a phoenix project. 170 171```nix 172with import <nixpkgs> { }; 173 174let 175 # beam.interpreters.erlang_26 is available if you need a particular version 176 packages = beam.packagesWith beam.interpreters.erlang; 177 178 pname = "your_project"; 179 version = "0.0.1"; 180 181 src = builtins.fetchgit { 182 url = "ssh://git@github.com/your_id/your_repo"; 183 rev = "replace_with_your_commit"; 184 }; 185 186 # if using mix2nix you can use the mixNixDeps attribute 187 mixFodDeps = packages.fetchMixDeps { 188 pname = "mix-deps-${pname}"; 189 inherit src version; 190 # nix will complain and tell you the right value to replace this with 191 hash = lib.fakeHash; 192 mixEnv = ""; # default is "prod", when empty includes all dependencies, such as "dev", "test". 193 # if you have build time environment variables add them here 194 MY_ENV_VAR="my_value"; 195 }; 196 197 nodeDependencies = (pkgs.callPackage ./assets/default.nix { }).shell.nodeDependencies; 198 199in packages.mixRelease { 200 inherit src pname version mixFodDeps; 201 # if you have build time environment variables add them here 202 MY_ENV_VAR="my_value"; 203 204 postBuild = '' 205 ln -sf ${nodeDependencies}/lib/node_modules assets/node_modules 206 npm run deploy --prefix ./assets 207 208 # for external task you need a workaround for the no deps check flag 209 # https://github.com/phoenixframework/phoenix/issues/2690 210 mix do deps.loadpaths --no-deps-check, phx.digest 211 mix phx.digest --no-deps-check 212 ''; 213} 214``` 215 216Setup will require the following steps: 217 218- 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`. 219- `cd assets` and `nix-shell -p node2nix --run node2nix --development` will generate a Nix expression containing your frontend dependencies 220- commit and push those changes 221- you can now `nix-build .` 222- 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. 223 224#### Example of creating a service for an Elixir - Phoenix project {#example-of-creating-a-service-for-an-elixir---phoenix-project} 225 226In order to create a service with your release, you could add a `service.nix` 227in your project with the following 228 229```nix 230{config, pkgs, lib, ...}: 231 232let 233 release = pkgs.callPackage ./default.nix; 234 release_name = "app"; 235 working_directory = "/home/app"; 236in 237{ 238 systemd.services.${release_name} = { 239 wantedBy = [ "multi-user.target" ]; 240 after = [ "network.target" "postgresql.service" ]; 241 # note that if you are connecting to a postgres instance on a different host 242 # postgresql.service should not be included in the requires. 243 requires = [ "network-online.target" "postgresql.service" ]; 244 description = "my app"; 245 environment = { 246 # RELEASE_TMP is used to write the state of the 247 # VM configuration when the system is running 248 # it needs to be a writable directory 249 RELEASE_TMP = working_directory; 250 # can be generated in an elixir console with 251 # Base.encode32(:crypto.strong_rand_bytes(32)) 252 RELEASE_COOKIE = "my_cookie"; 253 MY_VAR = "my_var"; 254 }; 255 serviceConfig = { 256 Type = "exec"; 257 DynamicUser = true; 258 WorkingDirectory = working_directory; 259 # Implied by DynamicUser, but just to emphasize due to RELEASE_TMP 260 PrivateTmp = true; 261 ExecStart = '' 262 ${release}/bin/${release_name} start 263 ''; 264 ExecStop = '' 265 ${release}/bin/${release_name} stop 266 ''; 267 ExecReload = '' 268 ${release}/bin/${release_name} restart 269 ''; 270 Restart = "on-failure"; 271 RestartSec = 5; 272 StartLimitBurst = 3; 273 StartLimitInterval = 10; 274 }; 275 # disksup requires bash 276 path = [ pkgs.bash ]; 277 }; 278 279 # in case you have migration scripts or you want to use a remote shell 280 environment.systemPackages = [ release ]; 281} 282``` 283 284## How to Develop {#how-to-develop} 285 286### Creating a Shell {#creating-a-shell} 287 288Usually, we need to create a `shell.nix` file and do our development inside of 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: 289 290```nix 291{ pkgs ? import <nixpkgs> {} }: 292 293with pkgs; 294let 295 elixir = beam.packages.erlang_24.elixir_1_12; 296in 297mkShell { 298 buildInputs = [ elixir ]; 299} 300``` 301 302### Using an overlay {#beam-using-overlays} 303 304If 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: 305 306#### `shell.nix` {#beam-using-overlays-shell.nix} 307 308```nix 309let 310 elixir_1_13_1_overlay = (self: super: { 311 elixir_1_13 = super.elixir_1_13.override { 312 version = "1.13.1"; 313 sha256 = "sha256-t0ic1LcC7EV3avWGdR7VbyX7pGDpnJSW1ZvwvQUPC3w="; 314 }; 315 }); 316 pkgs = import <nixpkgs> { overlays = [ elixir_1_13_1_overlay ]; }; 317in 318with pkgs; 319mkShell { 320 buildInputs = [ 321 elixir_1_13 322 ]; 323} 324``` 325 326#### Elixir - Phoenix project {#elixir---phoenix-project} 327 328Here is an example `shell.nix`. 329 330```nix 331with import <nixpkgs> { }; 332 333let 334 # define packages to install 335 basePackages = [ 336 git 337 # replace with beam.packages.erlang.elixir_1_13 if you need 338 beam.packages.erlang.elixir 339 nodejs 340 postgresql_14 341 # only used for frontend dependencies 342 # you are free to use yarn2nix as well 343 nodePackages.node2nix 344 # formatting js file 345 nodePackages.prettier 346 ]; 347 348 inputs = basePackages ++ lib.optionals stdenv.isLinux [ inotify-tools ] 349 ++ lib.optionals stdenv.isDarwin 350 (with darwin.apple_sdk.frameworks; [ CoreFoundation CoreServices ]); 351 352 # define shell startup command 353 hooks = '' 354 # this allows mix to work on the local directory 355 mkdir -p .nix-mix .nix-hex 356 export MIX_HOME=$PWD/.nix-mix 357 export HEX_HOME=$PWD/.nix-mix 358 # make hex from Nixpkgs available 359 # `mix local.hex` will install hex into MIX_HOME and should take precedence 360 export MIX_PATH="${beam.packages.erlang.hex}/lib/erlang/lib/hex/ebin" 361 export PATH=$MIX_HOME/bin:$HEX_HOME/bin:$PATH 362 export LANG=C.UTF-8 363 # keep your shell history in iex 364 export ERL_AFLAGS="-kernel shell_history enabled" 365 366 # postges related 367 # keep all your db data in a folder inside the project 368 export PGDATA="$PWD/db" 369 370 # phoenix related env vars 371 export POOL_SIZE=15 372 export DB_URL="postgresql://postgres:postgres@localhost:5432/db" 373 export PORT=4000 374 export MIX_ENV=dev 375 # add your project env vars here, word readable in the nix store. 376 export ENV_VAR="your_env_var" 377 ''; 378 379in mkShell { 380 buildInputs = inputs; 381 shellHook = hooks; 382} 383``` 384 385Initializing the project will require the following steps: 386 387- create the db directory `initdb ./db` (inside your mix project folder) 388- create the postgres user `createuser postgres -ds` 389- create the db `createdb db` 390- start the postgres instance `pg_ctl -l "$PGDATA/server.log" start` 391- add the `/db` folder to your `.gitignore` 392- you can start your phoenix server and get a shell with `iex -S mix phx.server`