Romain Viallard

Mise en place d'un environnement de développement Haskell avec Nix

Pour ce premier article, nous allons voir comment rapidement mettre en place un environnement de développement pour nos projets Haskell grâce au langage Nix et à son infrastructure Nixpkgs. Je présenterai au passage quelques outils annexes permettant d’optimiser notre workflow encore davantage. Commençons !

Présentation de Nix

Comme expliqué sur le site du project, Nix est un gestionnaire de paquets pour Linux et autres systèmes Unix qui rend la gestion des paquets fiable et reproductible. Ce qui nous intéresse ici est que Nix va nous permettre de mettre en place des environnements isolés pour nos besoins de développement. C’est un peu comme Docker mais en plus léger et moins fastidieux (dans son utilisation basique en tout cas).

Commençons par l’installer et j’expliquerai ensuite comment ça fonctionne.

La procédure d’installation est on ne peut plus simple, coller juste ce qui suit dans votre terminal et vous serez opérationnel.

curl -L https://nixos.org/nix/install | sh

Vous aurez peut-être besoin de recharger votre profil pour avoir accès aux commandes nix.

Je vais maintenant présenter les commandes que nous allons utiliser dans le cas présent.

nix-channel

Cette commande devrait afficher ce qui suit lors de son invocation.

[romain@clevo-N141ZU:~]$ nix-channel --list
nixpkgs https://nixos.org/channels/nixpkgs-unstable

Ce qu’il faut retenir ici est que votre installation suit actuellement le channel nixpkgs-unstable. Une channel est reliée à une branche git du dépôt Github du projet Nixpkgs . Conceptuellement, une channel est un ensemble de paquets tels qu’ils sont définis dans le dépot Github au commit sur lequel pointe la branche, ce qui va nous permettre d’explorer facilement les sources d’un paquets une fois qu’on a compris comment est organisé le dépôt. Par exemple, vous pouvez voir quel est l’état de la channel nixpkgs-unstable en parcourant le dépôt à l’url suivant : https://github.com/NixOS/nixpkgs/tree/nixpkgs-unstable .
Attention toutefois, votre installation locale n’est pas automatiquement synchronisée avec Github, il faut régulièrement exécuter la commande nix-channel –update pour récupérer l’état le plus récent, de façon assez similaire à ce qu’un apt update apporte sur une distribution basée sur Debian.

Maintenant que vous avez compris le principe des channels, vous voudrez sûrement chercher des paquets de manière un peu plus pratique qu’en explorant les sources sur Github. C’est ce que la commande nix search permet de faire. Admettons que vous vouliez installer GHC, le Glasgow Haskell Compiler, sur votre machine. Pour rechercher le paquet, vous devez taper ce qui suit dans votre terminal:

[romain@clevo-N141ZU:~]$ nix search ghc
warning: using cached results; pass '-u' to update the cache
* nixpkgs.ghc (ghc)
  The Glasgow Haskell Compiler
* nixpkgs.ghcid (ghcid)
  GHCi based bare bones IDE
* nixpkgs.vimPlugins.ghc-mod-vim (vimplugin-ghcmod-vim)
* nixpkgs.vimPlugins.ghcid (vimplugin-ghcid)
* nixpkgs.vimPlugins.ghcmod (vimplugin-ghcmod-vim)
* nixpkgs.vimPlugins.ghcmod-vim (vimplugin-ghcmod-vim)
* nixpkgs.vimPlugins.neco-ghc (vimplugin-neco-ghc)
* nixpkgs.vimPlugins.necoGhc (vimplugin-neco-ghc)

Comme attendu, le premier résultat est celui qui nous intéresse.

Je vais maintenant vous montrer deux manières de rendre ce paquet disponible à l’utilisation.

nix-env

Vous pouvez installer GHC avec la commande nix-env -i.

[romain@clevo-N141ZU:~]$ nix-env -i ghc
installing 'ghc-8.8.3'
*** Elided output for brievity ***
building '/nix/store/b4p2sf432qyfr91ixz9xnl9hw6hqvg9p-user-environment.drv'...
created 52 symlinks in user environment

Comme nous pouvons le voir, le compilateur est bien disponible:

[romain@clevo-N141ZU:~]$ whereis ghc
ghc: /nix/store/f6j4lqvx3zmm05wqmpi3867srkghd4vv-user-environment/bin/ghc

[romain@clevo-N141ZU:~]$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 8.8.3

Vous vous demandez peut-être à quoi correspondent ces chemins de fichier un peu moches renvoyés par la commande whereis. Il est temps de faire une petite pause et de vous expliquer comment Nix fonctionne. Nix utilise un store centralisé qui se trouve dans /nix/store, vous y trouverez tous les paquets (et leurs dépendances) utilisés par votre installation courante. Les fichiers sont nommés sur le modèle 0ywc4dlk8159rj7q5fhkdvm2xrjkfxy3-ghc-8.8.30ywc4dlk8159rj7q5fhkdvm2xrjkfxy3 est un hash de tout ce qui a servi à construire le paquet.
Cela permet d’avoir plusieurs versions d’un même paquet installées sur une machine en même temps (également plusieurs versions de la même version d’un paquet en fonction des paramètres fournis lors du build). Ces paquets ne sont cependant pas disponibles par défaut. Il faut d’abord que Nix construise l’environnement de votre profil utilisateur en ayant recours à une utilisation massive de liens symboliques.

Par exemple, sur ma machine, vous pouvez voir que le fichier .nix-profile présent dans mon dossier utilisateur est un lien symbolique pointant vers un répertoire dans /nix/var. Nous allons suivre l’enchainement de liens symboliques pour voir où cela mène.

[romain@clevo-N141ZU:~]$ readlink .nix-profile
/nix/var/nix/profiles/per-user/romain/profile

L’entrée .nix-profile pointe vers un autre lien symbolique dans le répertoire où nix stocke les profils.

[romain@clevo-N141ZU:~]$ readlink /nix/var/nix/profiles/per-user/romain/profile
profile-71-link

Lui-même point vers profile-71-link, ainsi nommé car c’est la 71ème version de mon profil utilisateur. Une nouvelle version étant construite à chaque fois qu’on installe, met à jour ou supprime un paquet.

[romain@clevo-N141ZU:~]$ readlink /nix/var/nix/profiles/per-user/romain/profile-71-link
/nix/store/f6j4lqvx3zmm05wqmpi3867srkghd4vv-user-environment

On arrive ensuite à notre environnement dans le store de nix.

[romain@clevo-N141ZU:~]$ ll /nix/store/f6j4lqvx3zmm05wqmpi3867srkghd4vv-user-environment
total 20
dr-xr-xr-x 2 root root 4096  1 janv.  1970 bin
lrwxrwxrwx 1 root root   57  1 janv.  1970 lib -> /nix/store/yzndcg3yq2iaps4zjp8wc4fwwpi9b1pj-ghc-8.8.3/lib
lrwxrwxrwx 1 root root   66  1 janv.  1970 libexec -> /nix/store/yv86xmr68ssdq2ba0yy4zd05qxpvbcf1-yarn2nix-1.0.0/libexec
lrwxrwxrwx 1 root root   60  1 janv.  1970 manifest.nix -> /nix/store/lmsf8qj1zr4fi0aylk6jh9qv4q84x81i-env-manifest.nix
dr-xr-xr-x 6 root root 4096  1 janv.  1970 share
lrwxrwxrwx 1 root root   67  1 janv.  1970 tarballs -> /nix/store/yv86xmr68ssdq2ba0yy4zd05qxpvbcf1-yarn2nix-1.0.0/tarballs

Enfin, on peut voir où les binaires sont stockés.

[romain@clevo-N141ZU:~]$ ll /nix/store/f6j4lqvx3zmm05wqmpi3867srkghd4vv-user-environment/bin/
total 116
lrwxrwxrwx 1 root root 61  1 janv.  1970 ghc -> /nix/store/yzndcg3yq2iaps4zjp8wc4fwwpi9b1pj-ghc-8.8.3/bin/ghc
lrwxrwxrwx 1 root root 67  1 janv.  1970 ghc-8.8.3 -> /nix/store/yzndcg3yq2iaps4zjp8wc4fwwpi9b1pj-ghc-8.8.3/bin/ghc-8.8.3
lrwxrwxrwx 1 root root 62  1 janv.  1970 ghci -> /nix/store/yzndcg3yq2iaps4zjp8wc4fwwpi9b1pj-ghc-8.8.3/bin/ghci
lrwxrwxrwx 1 root root 68  1 janv.  1970 ghci-8.8.3 -> /nix/store/yzndcg3yq2iaps4zjp8wc4fwwpi9b1pj-ghc-8.8.3/bin/ghci-8.8.3
lrwxrwxrwx 1 root root 65  1 janv.  1970 ghc-pkg -> /nix/store/yzndcg3yq2iaps4zjp8wc4fwwpi9b1pj-ghc-8.8.3/bin/ghc-pkg
lrwxrwxrwx 1 root root 71  1 janv.  1970 ghc-pkg-8.8.3 -> /nix/store/yzndcg3yq2iaps4zjp8wc4fwwpi9b1pj-ghc-8.8.3/bin/ghc-pkg-8.8.3
lrwxrwxrwx 1 root root 65  1 janv.  1970 haddock -> /nix/store/yzndcg3yq2iaps4zjp8wc4fwwpi9b1pj-ghc-8.8.3/bin/haddock
lrwxrwxrwx 1 root root 75  1 janv.  1970 haddock-ghc-8.8.3 -> /nix/store/yzndcg3yq2iaps4zjp8wc4fwwpi9b1pj-ghc-8.8.3/bin/haddock-ghc-8.8.3
lrwxrwxrwx 1 root root 63  1 janv.  1970 hp2ps -> /nix/store/yzndcg3yq2iaps4zjp8wc4fwwpi9b1pj-ghc-8.8.3/bin/hp2ps
lrwxrwxrwx 1 root root 61  1 janv.  1970 hpc -> /nix/store/yzndcg3yq2iaps4zjp8wc4fwwpi9b1pj-ghc-8.8.3/bin/hpc
lrwxrwxrwx 1 root root 64  1 janv.  1970 hsc2hs -> /nix/store/yzndcg3yq2iaps4zjp8wc4fwwpi9b1pj-ghc-8.8.3/bin/hsc2hs
lrwxrwxrwx 1 root root 76  1 janv.  1970 markdown -> /nix/store/fngkm633j7ga5hg93amzbq1h058z4mxf-multimarkdown-4.7.1/bin/markdown
lrwxrwxrwx 1 root root 80  1 janv.  1970 markdown.bat -> /nix/store/fngkm633j7ga5hg93amzbq1h058z4mxf-multimarkdown-4.7.1/bin/markdown.bat
lrwxrwxrwx 1 root root 71  1 janv.  1970 mmd -> /nix/store/fngkm633j7ga5hg93amzbq1h058z4mxf-multimarkdown-4.7.1/bin/mmd
lrwxrwxrwx 1 root root 75  1 janv.  1970 mmd2all -> /nix/store/fngkm633j7ga5hg93amzbq1h058z4mxf-multimarkdown-4.7.1/bin/mmd2all
lrwxrwxrwx 1 root root 75  1 janv.  1970 mmd2odf -> /nix/store/fngkm633j7ga5hg93amzbq1h058z4mxf-multimarkdown-4.7.1/bin/mmd2odf
lrwxrwxrwx 1 root root 76  1 janv.  1970 mmd2opml -> /nix/store/fngkm633j7ga5hg93amzbq1h058z4mxf-multimarkdown-4.7.1/bin/mmd2opml
lrwxrwxrwx 1 root root 75  1 janv.  1970 mmd2pdf -> /nix/store/fngkm633j7ga5hg93amzbq1h058z4mxf-multimarkdown-4.7.1/bin/mmd2pdf
lrwxrwxrwx 1 root root 75  1 janv.  1970 mmd2rtf -> /nix/store/fngkm633j7ga5hg93amzbq1h058z4mxf-multimarkdown-4.7.1/bin/mmd2rtf
lrwxrwxrwx 1 root root 75  1 janv.  1970 mmd2tex -> /nix/store/fngkm633j7ga5hg93amzbq1h058z4mxf-multimarkdown-4.7.1/bin/mmd2tex
lrwxrwxrwx 1 root root 81  1 janv.  1970 multimarkdown -> /nix/store/fngkm633j7ga5hg93amzbq1h058z4mxf-multimarkdown-4.7.1/bin/multimarkdown
lrwxrwxrwx 1 root root 74  1 janv.  1970 ob -> /nix/store/gd99xxp41i5dllhpx341rxscgbyzlpqn-obelisk-command-0.8.0.0/bin/ob
lrwxrwxrwx 1 root root 69  1 janv.  1970 patchelf -> /nix/store/58vavyggrv0s48l5fshif4c8vswlhp5x-patchelf-0.9/bin/patchelf
lrwxrwxrwx 1 root root 64  1 janv.  1970 runghc -> /nix/store/yzndcg3yq2iaps4zjp8wc4fwwpi9b1pj-ghc-8.8.3/bin/runghc
lrwxrwxrwx 1 root root 70  1 janv.  1970 runghc-8.8.3 -> /nix/store/yzndcg3yq2iaps4zjp8wc4fwwpi9b1pj-ghc-8.8.3/bin/runghc-8.8.3
lrwxrwxrwx 1 root root 68  1 janv.  1970 runhaskell -> /nix/store/yzndcg3yq2iaps4zjp8wc4fwwpi9b1pj-ghc-8.8.3/bin/runhaskell
lrwxrwxrwx 1 root root 61  1 janv.  1970 xev -> /nix/store/lzjswkiibs9yr6b47ir6h2vgbrnzp9sv-xev-1.2.3/bin/xev
lrwxrwxrwx 1 root root 71  1 janv.  1970 yarn2nix -> /nix/store/yv86xmr68ssdq2ba0yy4zd05qxpvbcf1-yarn2nix-1.0.0/bin/yarn2nix
lrwxrwxrwx 1 root root 64  1 janv.  1970 zola -> /nix/store/2m7llhrfgjqk064fz7pqhdcaz9x1rb59-zola-0.10.1/bin/zola

Les autres binaires que vous voyez sont des paquets que j’ai installés dans mon environnement en utilisant nix-env -i. Vous pouvez en afficher la liste grâce à la commande nix-env -q.

[romain@clevo-N141ZU:~]$ nix-env -q
ghc-8.8.3
multimarkdown-4.7.1
obelisk-command-0.8.0.0
patchelf-0.9
xev-1.2.3
yarn2nix-1.0.0
zola-0.10.1

Pour désinstaller un paquet, on utilise nix-env -e, nous allons d’ailleurs le faire pour montrer la seconde manière de rendre GHC disponible.

[romain@clevo-N141ZU:~]$ nix-env -e ghc
uninstalling 'ghc-8.8.3'

A noter, si j’installais GHC à nouveau maintenant en utilisant nix-env -i ghc, cela s’effectuerait quasi instantanément car tous les fichiers téléchargés lors de l’installation précédente sont toujours présents dans le store. Nix a juste à reconstruire l’environnement correspondant en reconstruisant les liens symboliques adéquats.

Quand vous voulez nettoyer le store pour libérer un peu d’espace disque sur votre machine, vous pouvez lancer nix-collect-garbage -d, ce qui aura pour effet de supprimer tous les fichiers du store qui ne font pas partie d’une garbage collection root. En somme, Nix va supprimer les paquets du store qui ne sont plus référencés directement ou indirectement par des liens symboliques quelquepart sur votre machine.

nix-shell

Le shell Nix utilise les mêmes mécaniques mais est davantage destiné à mettre en place des environnements dédiés à des projets ou pour tester un paquet sans l’installer. C’est ce que nous allons utiliser pour notre projet tout à l’heure.

Tout d’abord un exemple d’utilisation de GHC en utilisant la commande nix-shell -p cette fois, ce qui signifie grosso modo je veux un shell avec les paquets suivants en scope.

[romain@clevo-N141ZU:~]$ ghc
The program ‘ghc’ is currently not installed. You can install it by typing:
  nix-env -iA nixos.ghc

[romain@clevo-N141ZU:~]$ nix-shell -p ghc

[nix-shell:~]$ ghc
ghc: no input files
Usage: For basic information, try the `--help' option.

[nix-shell:~]$ exit

[romain@clevo-N141ZU:~]$ ghc
The program ‘ghc’ is currently not installed. You can install it by typing:
  nix-env -iA nixos.ghc

Assez simple à comprendre. Le shell nix permet également de construire un environnement à partir d’un fichier .nix. C’est ce qu’on utilise en général lors du développement d’un projet. Lorsqu’on invoque nix-shell, Nix va chercher un fichier shell.nix ou default.nix dans cet ordre dans le répertoire courant.

L’exemple classique dans la communauté est la mise en place d’un shell pour le paquet gnu hello, voici ce que ça donne:

[romain@clevo-N141ZU:~]$ mkdir nix-shell-example && cd nix-shell-example

Nous allons créer un fichier shell.nix avec le contenu suivant:

let
  pkgs = import <nixpkgs> {};
in
pkgs.mkShell {
  buildInputs = [
    pkgs.hello
  ];
}

Expliquons ce que nous avons là.
D’abord on importe , ce qui signifie que nous aurons accès à la channel en cours d’utilisation actuellement et à tous les paquets qui la composent, ici nous avons donc nixpkgs-unstable disponible via la variable pkgs. On invoque ensuite mkShell qui va construire l’environnement avec les paquets renseignés dans buildInputs. C’est l’équivalent de nix-shell -p hello mais de façon déclarative dans un fichier. Ici pkgs.hello correspond à ce qu’on aurait trouvé en utilisant nix search hello.
Nous testons ensuite avec nix-shell, ce qui nous donne le résultat escompté.

[nix-shell:~/nix-shell-example]$ ll
total 4
-rw-r--r-- 1 romain users 92 juin   2 18:28 shell.nix

[romain@clevo-N141ZU:~/nix-shell-example]$ nix-shell

[nix-shell:~/nix-shell-example]$ hello
Bonjour, le monde !

Il y a cependant un problème avec notre setup actuel, c’est que nous dépendons de , ce n’est pas ce qu’on pourrait vraiment appeler un environnement reproductible car la channel dont nous utilisons les paquets est globale à notre machine. Effectuer une mise à jour sur notre machine impacterait donc tous les projets qui se servent de . Nous allons voir comment corriger ce problème avec notre exemple de projet Haskell.

Un environnement Haskell simplifié

Vous devriez désormais avoir une compréhension basique du fonctionnement de Nix, il est temps de la mettre en oeuvre pour un usage de développement Haskell.

Nous allons procéder avec le projet basique suivant.

[romain@clevo-N141ZU:~/Code/haskell-nix]$ tree .
.
├── default.nix
├── .gitignore
├── haskell-nix.cabal
├── shell.nix
└── src
    └── Main.hs

1 directory, 4 files

haskell-nix.cabal

cabal-version: >= 1.10
name: haskell-nix
version: 0.1.0
build-type: Simple

executable haskell-nix
  hs-source-dirs: src
  main-is: Main.hs
  default-language: Haskell2010
  build-depends: base

Main.hs

1
2
3
4
module Main where

main :: IO ()
main = print "hello, world!"

Rien de transcendant ici.

shell.nix

(import ./default.nix).shell

Notre shell.nix est branché sur l’attribut shell exposé par le fichier default.nix que nous allons maintenant détailler.

default.nix

let

  nixpkgsRev = "0f5ce2fac0c7";
  compilerVersion = "ghc865";
  compilerSet = pkgs.haskell.packages."${compilerVersion}";

  githubTarball = owner: repo: rev:
    builtins.fetchTarball { url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; };

  pkgs = import (githubTarball "NixOS" "nixpkgs" nixpkgsRev) { inherit config; };
  gitIgnore = pkgs.nix-gitignore.gitignoreSourcePure;
  
  config = {
    packageOverrides = super: let self = super.pkgs; in rec {
      haskell = super.haskell // {
        packageOverrides = self: super: {
          haskell-nix = super.callCabal2nix "haskell-nix" (gitIgnore [./.gitignore] ./.) {};
        };
      };
    };
  };
  
in {
  inherit pkgs;
  shell = compilerSet.shellFor {
    packages = p: [p.haskell-nix];
    buildInputs = with pkgs; [
      compilerSet.cabal-install
    ];
  };
}

C’est ici que tout se passe, je vais donc vous expliquer les principales nouveautés.

nixpkgsRev = "0f5ce2fac0c7";
compilerVersion = "ghc865";
compilerSet = pkgs.haskell.packages."${compilerVersion}";

Nous déclarons 3 variables complètement arbitraires.

githubTarball = owner: repo: rev:
  builtins.fetchTarball { url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; };

Nous avons ensuite un exemple de fonction, tel que vous pourriez en définir dans votre langage favori. Ici githubTarball est une fonction qui, lorsqu’on lui donne un utilisateur et un repo github ainsi qu’un commit, récupère les sources depuis Github.

pkgs = import (githubTarball "NixOS" "nixpkgs" nixpkgsRev) { inherit config; };
gitIgnore = pkgs.nix-gitignore.gitignoreSourcePure;

config = {
  packageOverrides = super: let self = super.pkgs; in rec {
    haskell = super.haskell // {
      packageOverrides = self: super: {
        haskell-nix = super.callCabal2nix "haskell-nix" (gitIgnore [./.gitignore] ./.) {};
      };
    };
  };
};

Vous pouvez voir ce qui diffère de notre exemple de shell précédent. Ici nous n’utilisons pas la variable “magique” mais nous spécifions la version spécifique que nous souhaitons.

Vous devez également remarquer que nous fournissons une config particulière lors de notre instantiation de pkgs. C’est comme cela que nous pouvons surcharger les sources. Ici nous ajoutons à la liste des paquets Haskell disponibles notre paquet haskell-nix, comme s’il en faisait initialement partie.
Nous allons également utiliser la fonction gitignoreSourcePure du paquet nix-gitignore. Cela va nous permettre de spécifier à Nix la liste des fichiers à ne pas prendre en compte lors de la compilation à partir du contenu de notre .gitignore.

Nous avons ensuite notre déclaration de config, c’est le même config que celui passé à l’import de nixpkgs quelques lignes au-dessus. La définition de packageOverrides est un peu verbeuse mais c’est la manière recommandée de surcharger la liste de paquets pour chaque version de GHC disponible.
Pour ajouter notre paquet haskell-nix à cette liste, nous nous servons de callCabal2nix qui est un utilitaire faisant partie de l’infrastructure Nixpkgs dédiée à Haskell. Celui-ci va transformer automatiquement notre fichier cabal en dérivation nix sans que nous n’ayons à intervenir.
Dans sa forme la plus simple montrée ici, vous avez juste à fournir le nom de votre paquet tel que défini dans le fichier cabal ainsi que le chemin vers le fichier cabal. Ici nous donnons le chemin transformée par la fonction gitIgnore mentionnée précédemment.

in {
  inherit pkgs;
  shell = compilerSet.shellFor {
    packages = p: [p.haskell-nix];
    buildInputs = with pkgs; [
      compilerSet.cabal-install
    ];
  };
}

Nous arrivons ensuite à la liste des attribus exposés par notre fichier default.nix, il y en a deux:

Vérifions si ça fonctionne.

[romain@clevo-N141ZU:~/Code/haskell-nix]$ nix-shell
building '/nix/store/w0z0lil4lmhiiab2iiywi97v4j9gv5cm-cabal2nix-haskell-nix.drv'...
installing

[nix-shell:~/Code/haskell-nix]$ cabal run haskell-nix
Warning: The package list for 'hackage.haskell.org' is 245 days old.
Run 'cabal update' to get the latest list of available packages.
Resolving dependencies...
Build profile: -w ghc-8.6.5 -O1
In order, the following will be built (use -v for more details):
 - haskell-nix-0.1.0 (exe:haskell-nix) (first run)
Configuring executable 'haskell-nix' for haskell-nix-0.1.0..
Preprocessing executable 'haskell-nix' for haskell-nix-0.1.0..
Building executable 'haskell-nix' for haskell-nix-0.1.0..
[1 of 1] Compiling Main             ( src/Main.hs, /home/romain/Code/haskell-nix/dist-newstyle/build/x86_64-linux/ghc-8.6.5/haskell-nix-0.1.0/x/haskell-nix/build/haskell-nix/haskell-nix-tmp/Main.o )
Linking /home/romain/Code/haskell-nix/dist-newstyle/build/x86_64-linux/ghc-8.6.5/haskell-nix-0.1.0/x/haskell-nix/build/haskell-nix/haskell-nix ...
"hello, world!"

Comme vous pouvez le voir, cabal nous avertit que notre liste de paquets n’est pas du tout à jour. Vous ne devriez pas y faire attention car Nix détermine et récupère les dépendances Haskell avec ses propres mécanismes. Nous ne nous servirons ici de Cabal que pour ses capacités de build et non de gestion de dépendances.

Pour compiler notre paquet avec Nix, je vais vous présenter la commande nix-build. Si vous ne passez pas d’argument -Q, Nix va chercher un fichier default.nix dans le répertoire courant. Nous voulons compiler haskell-nix qui est désormais exposé via le nixpkgs surchargé, nous allons donc l’indiquer à nix-build (vous n’avez pas besoin d’être dans un shell nix pour utiliser nix-build).

[romain@clevo-N141ZU:~/Code/haskell-nix]$ nix-build -A pkgs.haskell.packages.ghc865.haskell-nix
these derivations will be built:
  /nix/store/kpasmmscjr2hzqrj5zizkkgdrn3r6jj1-haskell-nix-0.1.0.drv
building '/nix/store/kpasmmscjr2hzqrj5zizkkgdrn3r6jj1-haskell-nix-0.1.0.drv'...
setupCompilerEnvironmentPhase
Build with /nix/store/89ln27rjz9xisxcfvvcbm43myd92y280-ghc-8.6.5.
unpacking sources
unpacking source archive /nix/store/a6wsbh4pp0nvhq2bribmm6p0yam0800a-haskell-nix
source root is haskell-nix
*** Elided output for brievity ***
/nix/store/b9h0kd4907n790lig06cdvaawbkr0vl1-haskell-nix-0.1.0

La dernière ligne en sortie correspond au chemin dans le store où a été stocké notre paquet haskell-nix. Nous avons désormais également dans le répertoire courant un symlink result qui pointe vers le store, ce qui nous permet de le tester facilement.

[romain@clevo-N141ZU:~/Code/haskell-nix]$ ./result/bin/haskell-nix
"hello, world!"

C’est tout ce qu’il y a besoin de savoir pour un usage basique. Nous allons maintenant voir comment optimiser notre setup. Pour l’instant, si vous ajoutez une dépendance à votre fichier cabal, vous devez sortir du shell et y réentrer. Si vous ne le faites pas, cabal essaiera de récupérer les nouvelles dépendances par ses propres moyens et ça, vous ne voulez pas que ça se produise.

Présentation de Direnv et Lorri

Ces deux outils vont nous apporter un peu de confort, surtout lorsque nous essaierons d’intégrer les divers IDEs dans notre workflow.

Direnv

Comme expliqué sur le site, “Direnv est une extension de votre shell. Il ajoute une fonctionnalité qui permet de charger et décharger des variables d’environnement en fonction du répertoire actuel.". Pour l’installer, vous pouvez vous servir de votre nouveau gestionnaire de paquets favori et exécuter:

nix-env -i direnv

Vous devez ensuite hook Direnv dans votre shell. Les instructions diffèrent selon celui-ci, je vous renvoie donc à la documentation de Direnv . Pour l’utiliser dans notre projet, nous allons devoir ajouter un fichier .envrc à la racine du projet. Chaque fois que vous changez de répertoire, Direnv va vérifier si celui-ci comporter un fichier .envrc et, le cas échéant, charger l’environnement décrit par ce dernier. Cré Nous allons donc créer ce fichier et utiliser l’intégration nix fournie.

-- .envrc
use_nix

Pour que ça fonctionne, vous devrez d’abord exécuter direnv allow dans le répertoire (et ce, à chaque fois que le fichier .envrc est modifié), il s’agit d’une sécurité afin d’éviter des scripts arbitraires que vous n’auriez pas explicitement validés d’être exécutés. C’est tout, Direnv va désormais utliser nix-shell pour construire l’environnement comme vous l’avez fait manuellement jusqu’ici. Vous n’avez plus besoin d’entrer dans le shell explicitement mais vous devrez néanmoins toujours exécuter direnv reload à chaque fois que vous modifiez votre fichier cabal pour récupérer les nouvelles dépendances. C’est toujours un peu contraignant, nous allons donc ajouter à nos outils..

Lorri

Comme expliqué sur son dépôt Github , “Lorri est un remplaçant de nix-shell dédié au développement de projets, basé sur une intégration rapide de Direnv, pour une intégration robuste avec les editeurs et la ligne de commande”. Pour l’installer, il suffit comme d’habitude d’exécuter:

nix-env -i lorri

Nous remplaçons ensuite le contenu de notre fichier .envrc par:

-- .envrc
eval "$(lorri direnv)"

Pour que lorri fonctionne, vous devez démarrer son daemon en invoquant lorri daemon dans un shell. Vous n’avez pas besoin de le faire dans le répertoire du projet spécifiquement car c’est un process unique pour tout le système. Vous pouvez voir ici si vous souhaitez démarrer automatiquement le daemon avec systemd. Lorri va continuellement surveiller les changements de fichier référencés par votre shell.nix et recharger l’environnement au besoin. Un des avantages est que lorri créera automatiquement des garbage collection roots dans votre dossier utilisateur ~/.cache/lorri/gc_roots. Cela va vous éviter de perdre l’environnement d’un projet lors de l’utilisation de nix-collect-garbage, vous évitant par la même d’avoir à retélécharger de nombreux fichiers avant de pouvoir vous remettre sur un projet. Vous devrez par contre régulièrement purger ce dossier si vous ne voulez pas finir avec une machine saturée si vous travaillez sur de nombreux projets.

Bonus: Installation de Ghcide

Ghcide est, avec haskell-ide-engine , un des IDEs les plus avancés actuellement, bien qu’encore un peu jeune. Ces deux projets seront fusionnés dans haskell-language-server dans un futur proche. Nous allons utiliser ghcide-nix . D’abord, nous installons cachix, cela va nous permettre d’ajouter facilement des caches de binaires précompilées. Un cache de binaires est un serveur distant depuis lequel Nix récupère les fichiers déjà compilés. Nix détermine s’il peut utiliser un fichier précompilé plutôt que de le compiler localement en se basant sur le hash présent dans le nom des fichiers du store, si celui-ci est identique, le paquet est identique et peut donc être récupéré. Cela nous permet entre autre d’éviter de compiler l’univers entier sur notre machine, ce qui a quand même de bons côtés. Poursuivons.

$ nix-env -iA cachix -f https://cachix.org/api/v1/install
$ cachix use ghcide-nix

C’est tout, Nix va désormais utiliser le cache de ghcide-nix en plus du cache par défaut de nix.

Modifions maintenant notre fichier default.nix.

Dans la section let, nous allons ajouter:

ghcide = (import (githubTarball "cachix" "ghcide-nix" "master") {})."ghcide-${compilerVersion}";

Et dans les buildInputs du shell, on ajoute ghcide:

shell = compilerSet.shellFor {
  packages = p: [p.haskell-nix];
  buildInputs = with pkgs; [
    compilerSet.cabal-install
    ghcide
  ];
};

C’est tout, vous devez maintenant recharger l’environnement. Cela peut prendre un petit moment suivant la vitesse de votre connexion.

Bien, voyons désormais si tout fonctionne encore.

[nix-shell:~/Code/haskell-nix]$ ghcide
ghcide version: 0.1.0 (GHC: 8.6.5) (PATH: /nix/store/8cn9219qr3iq479qvcrn6wh5qr8x635l-ghcide-exe-ghcide-0.1.0/bin/ghcide)
Ghcide setup tester in /home/romain/Code/haskell-nix.
Report bugs at https://github.com/digital-asset/ghcide/issues

Step 1/6: Finding files to test in /home/romain/Code/haskell-nix
Found 1 files

Step 2/6: Looking for hie.yaml files that control setup
Found 1 cradle

Step 3/6, Cradle 1/1: Implicit cradle for /home/romain/Code/haskell-nix
Cradle {cradleRootDir = "/home/romain/Code/haskell-nix", cradleOptsProg = CradleAction: Default}

Step 4/6, Cradle 1/1: Loading GHC Session
Interface files cache dir: /home/romain/.cache/ghcide/da39a3ee5e6b4b0d3255bfef95601890afd80709

Step 5/6: Initializing the IDE

Step 6/6: Type checking the files

Completed (1 file worked, 0 files failed)

Vous allez devoir maintenant configurer la partie client du language server. Ceci dépend de l’ide que vous utilisez. J’utilise personnellement Emacs avec lsp-haskell qui fonctionne plutôt bien et n’est pas trop complexe à installer.

Bien, voilà qui conclue cette introduction à l’utilisation combinée de Haskell et Nix.
Si vous avez des questions, n’hésitez pas à me les poser en commentaire ci-dessous.

Need some help with your Haskell or Nix projects ? I can help !
Ping me on GitHub or shoot me an email at romain[at]romainviallard.dev