Environnement de développement Elixir avec Nix (flakes)
Comme beaucoup, je travaille sur de nombreux projets, pour le Groupe BAO, pour des clients ou encore pour tester le nouveau framework à la mode. L'une des difficultés lorsqu'on arrive sur un nouveau projet est de mettre en place son environnement de développement. J'ai longtemps utilisé Docker et Docker Compose pour essayer de formaliser et standardiser mes environnements, mais je finis toujours par n'utiliser que les services externes dans des conteneurs et j'installe directement sur mon poste les outils de développement (langage, outils de construction, ...). Depuis mon passage sur NixOS, j'essaie d'utiliser nix pour résoudre ce problème en ajoutant un flake avec un shell de développement pour chacun de mes projets.
Voici comment je m'y prends pour mes projets Elixir.
Prerequis
Pour suivre cet article, vous devez avoir installé Nix et Flake sur Linux ou Mac, ou encore WSL, ou encore avoir activé Flake sur NixOS.
Vous devez aussi avoir installé Direnv (pour ma part, j'utilise Home-Manager pour le faire).
C'est quoi un shell nix ?
Nix permet de définir des environnements de développement de manière déclarative et reproductible.
Ces environnements sont définis par :
- les paquets que l'on souhaite installer.
- les commandes jouées au démarrage de l'environnement.
Pour déclarer un shell, Nix fournit la fonction mkShell dans ses paquets standards (nixpkgs).
{
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [ nodejs_22 ];
};
}
Lorsque l'on démarre ce shell avec la commande nix develop
, nodejs en version 22 est disponible.
Qu'est-ce qu'un flake ?
Le fichier flake.nix
est l'équivalent du package.json
ou du mix.exs
(en très gros ...), il permet de spécifier les packages et autres artefacts dont les shells. Le fichier contient 4 parties :
description
: une chaîne de caractères qui décrit le flakeinputs
: un hashset décrivant les dépendances du flakeoutputs
: une fonction qui prend en argument les inputs (sortOf) et qui permet de définir les artefacts du projetnixConfig
: un hashset qui va servir pour surcharger le comportement de nix.
Combiné avec flake.lock
, il permet de geler les versions des dépendances.
Plus d'infos sur Nix et les flakes:
Initialiser un flake pour une application Elixir
Pour l'exemple, je vais créer une application Elixir toute simple nix-env-elixir
mkdir nix-env-elixir
cd nix-env-elixir
nix flake init
Cette commande va créer le fichier flake.nix
dans le répertoire nix-env-elixir
.
{
description = "A very basic flake";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
};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;};
}
Inputs
Le fichier généré contient les inputs suivants:
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
};
Il s'agit de la dépendance vers les paquets standards nix. Ici, je suis en version unstable de nixos.
J'ajoute systématiquement la dépendance flake-utils
aux inputs, cette librairie propose pas mal de petits utilitaires pour se simplifier la vie dans les flakes.
J'ai donc les inputs suivants:
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
Outputs
La prochaine étape est de définir un shell, qui contiendra tout ce dont j'ai besoin pour développer mon application Elixir.
L'outputs minimal ressemble donc à :
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
inherit (pkgs.lib) optional optionals; pkgs = import nixpkgs {
inherit system;
};
in with pkgs; {
devShells.default = pkgs.mkShell {
buildInputs = [
elixir
glibcLocales
] ++ optional stdenv.isLinux inotify-tools
++ optional stdenv.isDarwin terminal-notifier
++ optionals stdenv.isDarwin (with darwin.apple_sdk
.frameworks; [
CoreFoundation
CoreServices
]);
};
}
);
Ici, j'utilise flake-utils
pour boucler sur les systèmes par défaut ["x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin"]
et je déclare un shell pour chacun d'entre eux.
Les shells créés seront assignés aux shell par défaut de chaque système et ils sont créés via l'appel à la fonction mkShell
.
Ensuite, il suffit de lancer la commande nix develop
pour lancer le shell par défaut qui correspond à votre système.
Et voilà, nous sommes dans un shell avec Elixir disponible, nous pouvons utiliser la commande suivante pour initialiser le projet :
mix new . --app nix_elixir_env
Ici, j'utilise .
, pour spécifier le répertoire courant et --app pour spécifier le nom de l'application, cela me permet d'avoir l'application au même niveau que le flake et non dans un répertoire.
Ajout de Direnv
Pour me faciliter la vie, j'utilise Direnv pour lancer le shell lorsque j'entre dans le répertoire du projet.
Pour que ça fonctionne, il faut ajouter un fichier .envrc
à la racine du projet et lancer la commande direnv allow
pour autoriser Direnv à charger le shell automatiquement.
Le contenu du fichier est très simple:
use flake
Conclusion
Avec cette première approche, nous obtenons un environnement déclaratif et reproductible, qui fonctionne sur les principaux systèmes supportés par Nix.
Il y a encore beaucoup de choses qui peuvent être améliorées, comme définir une version particulière d'Elixir, ou ajouter Postgresql et Phoenix.
On peut aussi constituer un ensemble de déclarations d'environnement dans un repo git et les appeler depuis direnv.