article linuxfr.org

Nix pour les développeurs

Nix est un gestionnaire de paquets «fonctionnel» (c’est-à-dire basé sur des fonctions, sans effet de bord). Cette caractéristique apporte des avantages indéniables, notamment de pouvoir mettre en place des environnements logiciels isolés, reproductibles et composables. Ceci peut être très utile à un administrateur système mais également à un développeur.

On trouve pas mal d’informations sur l’écosystème Nix, et son utilisation, ainsi que des retour d’expériences des utilisateurs. En revanche, les documents à destination des développeurs sont moins nombreux et se limitent souvent à l’utilisation ou à la mise en place d’environnements de développement simples.

Cet article a pour objectif d’illustrer l’intérêt de Nix pour un développeur dans des cas simples et «un peu moins simples». Pour cela, il se base sur un projet d’exemple en C++ et en Python mais Nix peut également être utilisé pour d’autres langages. Je ne suis pas un expert en Nix donc n’hésitez pas à proposer vos remarques ou améliorations dans les commentaires ou sur le dépôt git du projet d’exemple.

Introduction

Exemple 1 : créer un environnement de développement de test

Scénario

Vous avez codé un script Python myscript.py et vous voulez le tester dans un environnement vierge.

# myscript.py
import numpy
x = numpy.ndarray(42, int)
x[0::2] = 13
x[1::2] = 37
print(x)

Avec les outils classiques (Python, virtualenv, pip)

virtualenv -p /usr/bin/python3 --no-site-packages ~/myvenv
source ~/myvenv/bin/activate
pip install numpy
python myscript.py
deactivate
rm -rf ~/myvenv

Avec Nix

nix-shell --pure -p python35Packages.numpy --run "python myscript.py"

Exemple 2 : reproduire un environnement de développement

Scénario

Vous développez un logiciel myprog en C++, compilé via cmake et utilisant la bibliothèque Boost. Vous avez récupéré votre projet sur une nouvelle machine et voulez le compiler.

Avec les outils classiques (par exemple sous Arch)

sudo pacman -S gcc cmake boost
mkdir build
cd build
cmake ..
make

Avec Nix

Au cours du projet, un fichier default.nix est tenu à jour (il indique notamment les dépendances à cmake et à Boost). Il suffit alors de lancer la commande :

nix-build

Exemple 3 : packager un projet

Scénario

Le projet myprog de l’exemple précédent vient d’aboutir à une release 0.1 dont le code source est disponible en ligne. Vous voulez l’installer proprement sur votre système.

Avec les outils classiques (par exemple sous Arch)

sudo pacman -S base-devel
mkdir myprog
cd myprog
# écrire un fichier PKGBUILD (avec l'url de la release, les dépendances, les instructions de compilation, etc...)
makepkg
sudo pacman -U myprog-0.1-1-any.pkg.tar.xz

Cette solution fonctionne pour Arch uniquement. Si vous voulez une solution pour Debian ou Fedora, il faut créer les paquets deb ou rpm correspondants.

Avec Nix

cp default.nix release.nix
# dans le fichier release.nix, changer la valeur de la variable src par l'url de la release
nix-env -f release.nix -i myprog

Ici, la solution devrait fonctionner automatiquement pour tout système compatible avec Nix (Arch, Debian, Fedora…).

Exemple 4 : personnaliser des dépendances

Scénario

Vous développez des logiciels de traitement d’images utilisant la bibliothèque OpenCV. Pour cela, vous utilisez le paquet OpenCV fournit par la logithèque système. Un de vos logiciels doit utiliser gtk malheureusement le paquet OpenCV a été compilé avec l’option -DWITH_GTK=OFF.

Avec les outils classiques

Bienvenue en enfer… Quelques «solutions» classiques :

Avec Nix

Les paquets Nix sont paramétrables. Ainsi pour activer l’option gtk2 du paquet OpenCV, il suffit (presque) d’ajouter la ligne suivante dans le fichier default.nix. La recompilation et la gestion des différentes versions est automatique.

opencv3gtk = pkgs.opencv3.override { enableGtk2 = true; };

Quelques rappels sur Nix

Présentation

Nix est un gestionnaire de paquets fonctionnel. Le terme «fonctionnel» est à prendre au sens mathématique : une fonction prend des entrées et produit une sortie, sans réaliser d’effet de bord. Ceci permet de créer des environnements logiciels (compilation, installation et configuration) avec les avantages suivants :

L’écosystème Nix comporte différents éléments :

Il existe une distribution Linux (NixOS) directement basée sur ces éléments mais le système Nix peut être installé sur un OS quelconque (Linux, BSD, OSX) pour y servir de logithèque et de système d’environnement virtuel.

Enfin, Nix a inspiré un système concurrent, nommé GNU Guix. Tout comme Nix, Guix peut être utilisé sur un OS classique ou via une distribution dédiée, GuixSD. À la différence de Nix, Guix est basé sur un langage existant (Guile Scheme) et accorde une plus grande importance à l’aspect “logiciel libre”.

Quelques commandes Nix

nix-env -q
nix-env -qa 'firefox'
nix-env -i firefox
nix-env -e firefox

Toutes ces commandes sont utilisables avec les droits utilisateurs et dans l’environnement de l’utilisateur. Les paquets sont gérés par un service (nix-daemon) qui les installe dans un répertoire commun /nix/store et les rend disponibles aux différents utilisateurs.

Projet d’exemple (C++/Python)

Pour illustrer l’utilisation de Nix, on considère un projet type (the_checkerboard_project) qui calcule et affiche des images de damier.

Ce projet est composé d’une bibliothèque C++ (checkerboard) et d’une interface Python (pycheckerboard) contenant le binding proprement dit et un script Python additionnel.

the_checkerboard_project/
├── checkerboard
│   ├── CMakeLists.txt
│   ├── checkerboard.cpp
│   ├── checkerboard.hpp
│   └── test_checkerboard.cpp
└── pycheckerboard
    ├── setup.py
    └── src
        ├── checkerboard
        │   └── binding.cpp
        └── pycheckerboard
            ├── __init__.py
            └── test1.py

La bibliothèque C++ (checkerboard) fournit des fonctions pour calculer un damier et pour afficher une image, en utilisant la bibliothèque OpenCV. La compilation est réalisée via cmake, qui fait le lien avec OpenCV et qui construit la bibliothèque checkerboard et un exécutable de test.

# checkerboard/CMakeLists.txt
cmake_minimum_required( VERSION 3.0 )
project( checkerboard )

# lien avec OpenCV
find_package( PkgConfig REQUIRED )
pkg_check_modules( MYPKG REQUIRED opencv )
include_directories( ${MYPKG_INCLUDE_DIRS} )

# bibliothèque checkerboard
add_library( checkerboard SHARED checkerboard.cpp ) 
target_link_libraries( checkerboard ${MYPKG_LIBRARIES} )
install( TARGETS checkerboard DESTINATION lib )
install( FILES checkerboard.hpp DESTINATION "include" )

# exécutable de test
add_executable( test_checkerboard test_checkerboard.cpp )
target_link_libraries( test_checkerboard checkerboard ${MYPKG_LIBRARIES} )
install( TARGETS test_checkerboard DESTINATION bin )

L’interface Python est faite avec Boost Python. Le binding (binding.cpp) expose simplement les deux fonctions de la bibliothèque checkerboard. Un script additionnel (test1.py) fournit une fonction et un programme de test. Le tout est compilée dans un package Python en utilisant un script setuptools/pip très classique.

# pycheckerboard/setup.py
from setuptools import setup, Extension

checkerboard_module = Extension('checkerboard_binding',
    sources = ['src/checkerboard/binding.cpp'],
    libraries = ['checkerboard', 'boost_python', 'opencv_core', 'opencv_highgui'])

setup(name = 'pycheckerboard',
    version = '0.1',
    package_dir = {'': 'src'},
    packages = ['pycheckerboard'],
    python_requires = '<3',
    ext_modules = [checkerboard_module])

Configuration Nix basique

Classiquement (sans Nix), on exécuterait les commandes suivantes pour compiler et installer la bibliothèque checkerboard :

mkdir checkerboard/build
cd checkerboard/build
cmake ..
make
sudo make install

Nix permet d’exécuter ces commandes automatiquement. Pour cela, il suffit d’écrire un fichier de configuration default.nix indiquant le nom du package, le chemin vers le code source et les dépendances (voir cette dépêche sur l’anatomie d’une dérivation nix).

# checkerboard/default.nix
with import <nixpkgs> {};
with pkgs; 
stdenv.mkDerivation {
    name = "checkerboard";
    src = ./.;
    buildInputs = [ cmake pkgconfig opencv3 ];
}

Les dépendances spécifiée ici seront installées automatiquement par Nix, si besoin. Ici la dépendance à cmake implique que la compilation sera réalisée avec cmake (cf les commandes précédentes). La compilation et l’installation de la bibliothèque checkerboard peuvent alors être lancées avec la commande :

nix-env -f . -i checkerboard

La bibliothèque (ainsi que l’exécutable de test) est alors disponible dans les chemins systèmes, ou plus exactement dans les chemins systèmes vus par l’utilisateur. Cependant, il n’est pas obligatoire d’installer le package checkerboard. On peut simplement lancer un shell dans un environnement virtuel contenant le package :

nix-shell 

Ce mécanisme de shell virtuel propose des fonctionnalités très utiles; par exemple, partir d’un environnement vierge et exécuter juste une commande dans cet environnement :

nix-shell --pure --run test_checkerboard

Configuration Nix modulaire

Au lieu de proposer un package complet, on peut découper notre nix-expression (default.nix) en plusieurs modules, appelés dérivations, qui pourront alors être réutilisés ou reparamétrés. Par exemple, pour créer une dérivation “opencv3gtk” et une dérivation “checkerboard” :

# checkerboard/default.nix
{ system ? builtins.currentSystem }:
let
    pkgs = import <nixpkgs> { inherit system; };
in
with pkgs; 
stdenv.mkDerivation rec {

    opencv3gtk = import ./opencv3gtk.nix { inherit (pkgs) opencv3; };

    checkerboard = import ./checkerboard.nix { 
        inherit opencv3gtk;
        inherit (pkgs) cmake pkgconfig stdenv;
    };
}

Ces deux dérivations sont implémentées dans des fichiers spécifiques, pour faciliter leur réutilisation. Par exemple, pour checkerboard :

# checkerboard/checkerboard.nix 
{ cmake, opencv3gtk, pkgconfig, stdenv }:
stdenv.mkDerivation {
    name = "checkerboard";
    src = ./.;
    buildInputs = [ cmake opencv3gtk pkgconfig ];
}

Ici, la deuxième ligne indique les paramètres du package (c’est-à-dire les dépendances à utiliser pour cmake, pkgconfig, etc…). Ce mécanisme permet de composer les packages de façon très puissante. Par exemple, on peut reparamétrer le package OpenCV en activant le support gtk (qui n’est pas activé par défaut) et composer ce nouveau package à notre package checkerboard, qui disposera alors des fonctionnalités gtk. On peut même modifier finement les options de compilation du package OpenCV (par exemple, désactiver les entêtes précompilés qui consomment beaucoup de RAM) :

# checkerboard/opencv3gtk.nix 
{ opencv3 }:
let
    opencv3gtk = opencv3.override { enableGtk2 = true; };
in 
opencv3gtk.overrideDerivation (
    attrs: { cmakeFlags = [attrs.cmakeFlags "-DENABLE_PRECOMPILED_HEADERS=OFF"]; }
)

Bien entendu, reparamétrer un package nécessite une recompilation si le package n’a pas déjà été compilé pour ce jeu de paramètres.

Notez que le nouveau default.nix ne contient pas de dérivation par défaut. Il faut donc préciser la dérivation à utiliser ou à installer :

nix-shell -A checkerboard
nix-env -f . -iA checkerboard

Configuration Nix pour un package Python

De nombreux langages proposent leur propre système de gestion de paquets (pip pour Python, gem pour Ruby, npm pour JavaScript…). Nix fournit des fonctionnalités pour créer ce genre de packages.

Par exemple, pour créer un package Python de notre projet, on peut écrire un fichier default.nix, qui va réutiliser les dérivations opencv3gtk et checkerboard précédentes. Nix fournit une fonction buildPythonPackage qui permet de créer simplement un package Python en utilisant le script setuptools/pip :

# pycheckerboard/default.nix
{ system ? builtins.currentSystem }:
let
    pkgs = import <nixpkgs> { inherit system; };
    opencv3gtk = import ../checkerboard/opencv3gtk.nix { inherit (pkgs) opencv3; };
    checkerboard = import ../checkerboard/checkerboard.nix { 
        inherit opencv3gtk;
        inherit (pkgs) cmake pkgconfig stdenv; 
    };
in
with pkgs;
pythonPackages.buildPythonPackage {
    name = "pycheckerboard";
    src = ./.;
    buildInputs = [ checkerboard python27Packages.boost opencv3gtk ];
}

Comme pour la bibliothèque, on peut alors installer la dérivation ou la tester interactivement dans un shell virtuel. Les dépendances opencv3gtk et checkerboard correspondent aux dérivations de la section précédente et ne seront pas recompilées ni dupliquées.

$ cd pycheckerboard
$ nix-shell --pure --run python
Obtaining file:///home/nokomprendo/the_checkerboard_project/pycheckerboard
Installing collected packages: pycheckerboard
...
Python 2.7.13 (default, Dec 17 2016, 20:05:07) 
>>> import pycheckerboard.test1 as pt
>>> pt.test1()
running test1.py...

Conclusion

Nix permet de définir des environnements logiciels reproductibles, paramétrables et composables. Pour cela, il suffit d’écrire quelques fichiers «.nix» qui viennent compléter les outils de compilation classiques du projet. Les paquets ainsi créés peuvent ensuite être installés ou exécutés, éventuellement dans un environnement isolé. Nix gère automatiquement la compilation, les dépendances et les différentes versions de paquets. Ces fonctionnalités sont intéressantes pour un développeur car elles permettent non seulement de simplifier le déploiement mais également d’offrir un environnement de développement multi-langage léger et reproductible.