Développer en Haskell avec Nix, en 2023

Voir aussi : video youtube - code source

Dans l’article Développer en Haskell avec ghcup et vscode, en 2022, on a vu que ghcup permet désormais d’installer facilement un environnement pour développer en Haskell. Dans Comment utiliser Nix pour développer en Haskell, on avait vu que Nix fournit également un environnement intéressant. Cet environnement a évolué et ce nouvel article en fait une présentation plus à jour.

Rappels sur Nix

Nix est un système de paquets logiciels basé sur une approche déclarative et sans effet de bord. Concrètement, il s’agit à la fois d’un langage de programmation permettant d’écrire des expressions Nix (définir des paquets) et d’un outil permettant d’évaluer ces expressions (construire des paquets…). Basés sur ces deux outils, il existe aussi une logithèque (nixpkgs), un système d’exploitation (nixos)…

Nix apporte de nombreux avantages pour développer des logiciels, dans les principaux langages de programmation dont Haskell. Actuellement, il existe deux façons de configurer un projet Haskell avec Nix : Haskell4nix et Nix Flakes.

Haskell4nix

Haskell4nix est le support de Haskell dans l’infrastructure de nixpkgs. Concrètement, il fournit des compilateurs Haskell (ghc, ghcjs…) , des outils de développement (cabal, ghcid, hls…), des bibliothèques et des outils de packaging spécifiques.

En pratique, il s’agit d’utiliser la logithèque Nix classique. Par exemple, pour connaitre les compilateurs supportés, ou les bibliothèques Haskell disponibles :

$ nix-env -f "<nixpkgs>" -qaP -A haskell.compiler

haskell.compiler.ghc8107                 ghc-8.10.7
haskell.compiler.ghc884                  ghc-8.8.4
haskell.compiler.ghc902                  ghc-9.0.2
...

$ nix-env -f "<nixpkgs>" -qaP -A haskell.packages.ghc925
haskell.packages.ghc925.a50                                      a50-0.5
haskell.packages.ghc925.AAI                                      AAI-0.2.0.1
haskell.packages.ghc925.aasam                                    aasam-0.2.0.0
...

On peut également lancer un environnement Nix spécifique, par exemple via un fichier shell.nix. Mais généralement, on configure un projet Haskell avec l’outil Cabal, qui est compatible avec Haskell4nix. Il suffit donc d’écrire un fichier .cabal, classique en Haskell, puis d’écrire le fichier default.nix suivant pour avoir automatiquement une configuration Nix du projet :

{ pkgs ? import <nixpkgs> {} }:

let
  # ghc = pkgs.haskellPackages;         # version par défaut du compilateur
  ghc = pkgs.haskell.packages.ghc925;   # version 9.2.5

in ghc.developPackage {
  root = ./.;
  modifier = drv:
  pkgs.haskell.lib.addBuildTools drv (with ghc; [
    cabal-install
  ]);
}

Par exemple pour travailler sur le projet, on peut lancer un nix-shell (pour lancer un environnement avec toutes les dépendances nécessaires, déterminées d’après le fichier .cabal) puis utiliser les commandes Cabal habituelles :

$ nix-shell 
these 2 derivations will be built:
  /nix/store/0dlwbm8gll94aw22zb01v85h409bik82-hoogle-with-packages.drv
  /nix/store/9yb2mfx3a17hsv81xjnpdl8hl0cghz9j-ghc-9.0.2-with-packages.drv
...

[nix-shell]$ cabal build
Resolving dependencies...
Build profile: -w ghc-9.0.2 -O1
In order, the following will be built (use -v for more details):
 - myproject-0.1.0.0 (lib) (first run)
 - myproject-0.1.0.0 (exe:myproject) (first run)
...

[nix-shell]$ cabal run
<h1>Hello World!</h1>

On peut également contruire complètement le projet via Nix :

$ nix-build
this derivation will be built:
  /nix/store/rimxqr1avc7ycyzgqcd5lqdw0z0wgcqr-myproject-0.1.0.0.drv
...

$ ./result/bin/myproject 
<h1>Hello World!</h1>

Installer le projet sur la machine, via le système de paquet :

$ nix-env -if .
installing 'myproject-0.1.0.0'
this derivation will be built:
  /nix/store/8xf8mc0m1cvnfb3c0ybpy38rrxsa0gq2-myproject-0.1.0.0.drv
...

$ myproject 
<h1>Hello World!</h1>

Ou encore, intégrer le projet dans la logithèque, par exemple via un overlay Nix qui récupère le code du projet depuis un dépôt git :

# ~/.config/nixpkgs/overlays/myproject.nix

self: super: 

let 
  myproject-src = self.fetchFromGitLab {
    owner = "nokomprendo";
    repo = "nokomprendo.gitlab.io";
    rev = "0bb6f2e";
    sha256 = "sha256-4F4GXnMVWrE5BzJcewopr6xGmdPySeawwbvN66Cu9b0=";
  };

in {
  myproject = self.callPackage "${myproject-src}/posts/tuto_093/code/myproject-h4n/default.nix" {};
}

Enfin, on peut également fixer plus précisemment la version de la logithèque à utiliser, modifier des paquets (changer de version, patcher), etc.

Nix Flakes

Les Nix Flakes sont des fonctionnalités, encore expérimentales, disponibles via Nix. Celles-ci permettent de définir des unités de constructions d’un package, d’un déploiement, voire d’un système complet.

Les Flakes visent à rendre le packaging plus reproductible (meilleure gestion des fichiers de configuration, gel de versions…), composable et performant (meilleure mise en cache).

Ici, on s’intéresse uniquement à comment utiliser les Flakes pour configurer simplement un projet en Haskell. Pour cela, on peut écrire le fichier flake.nix suivant :

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    #flake-utils.lib.eachDefaultSystem (system:
    flake-utils.lib.eachSystem [ "x86_64-linux" ] (system:

      with nixpkgs.legacyPackages.${system};
      let
        t = lib.trivial;
        hl = haskell.lib;
        name = "myproject";

        # ghc = haskellPackages;                # version par défaut
        ghc = pkgs.haskell.packages.ghc925;     # version 9.2.5

        project = devTools:
          let addBuildTools = (t.flip hl.addBuildTools) devTools;
          in ghc.developPackage {
            root = lib.sourceFilesBySuffices ./. [ ".cabal" ".hs" ];
            name = name;
            returnShellEnv = !(devTools == [ ]);

            modifier = (t.flip t.pipe) [
              addBuildTools
              hl.dontHaddock
              hl.enableStaticLibraries
              hl.justStaticExecutables
              hl.disableLibraryProfiling
              hl.disableExecutableProfiling
            ];
          };

      in {
        packages.pkg = project [];

        defaultPackage = self.packages.${system}.pkg;

        devShell = project (with ghc; [
          cabal-install
        ]);
      });
    }

Grossièrement, ce fichier définit les entrées (ici : nixpkgs, pour récupérer les outils et bibliothèques Haskell nécessaires, et flake-utils, pour configurer le flake), et les sorties du flake (ici, un package et un environnement de développement correspondant).

On peut interroger la configuration du flake, par exemple pour connaitre ses sorties :

$ nix flake show --allow-import-from-derivation
path:/home/test/code/code/myproject-flake...
├───defaultPackage
   └───x86_64-linux: package 'myproject-0.1.0.0'
├───devShell
   └───x86_64-linux: development environment 'ghc-shell-for-myproject-0.1.0.0'
└───packages
    └───x86_64-linux
        └───pkg: package 'myproject-0.1.0.0'

On peut alors lancer un environnement de développement :

$ nix develop 

$ cabal build
Resolving dependencies...
Build profile: -w ghc-9.2.4 -O1
In order, the following will be built (use -v for more details):
 - myproject-0.1.0.0 (lib) (first run)
 - myproject-0.1.0.0 (exe:myproject) (first run)
...

$ cabal run
<h1>Hello World!</h1>

Ou lancer le package (si besoin après avoir été recompilé) :

$ nix run
<h1>Hello World!</h1>

Enfin, on peut également configurer le système avec des flakes et y inclure notre projet.

Voir aussi :

Conclusion

Pour développer en Haskell, il est généralement plus simple d’utiliser Ghcup. Si le projet est complexe (plusieurs langages de programmation, besoin de builds reproductibles, déploiements complexes…), alors il peut être intéressant d’utiliser Nix : soit l’environnement Haskell4nix (qui est plutôt complet et mature), soit l’environnement Flakes (qui apporte quelques améliorations concernant la reproductibilité, composabilité…).