Nix pour les développeurs
Voir aussi : article linuxfr.org
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
= numpy.ndarray(42, int)
x 0::2] = 13
x[1::2] = 37
x[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 :
Recompiler OpenCV à partir du code source. Vous pourrez ensuite faire une installation système (mais cela peut impacter les autres logiciels) ou une installation locale (mais il faudra configurer les chemins vers votre OpenCV personnalisé quand vous en aurez besoin).
Utiliser un système d’environnements virtuels (chroot, flatpak, docker…). Pour cela, vous devrez mettre en place le système puis récupérer une image d’OpenCV compilé avec les bonnes options ou créer votre propre image.
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.
{ enableGtk2 = true; }; opencv3gtk = pkgs.opencv3.override
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 :
- les environnements sont reproductibles
- ils sont paramétrables et composables
- ils n’ont jamais besoin d’être dupliqués
- ils sont exécutés nativement
L’écosystème Nix comporte différents éléments :
- un langage permettant de décrire un environnement logiciel (appelé nix-expression)
- des outils (nix-build, nix-env, nix-shell…) permettant de construire, installer, exécuter… des nix-expressions
- un dépôt officiel (nixpkgs) de nix-expressions
- …
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
- voir les paquets installés :
nix-env -q
- voir les paquets disponibles contenant le motif “firefox” :
nix-env -qa 'firefox'
- installer le paquet “firefox” :
nix-env -i firefox
- désinstaller le paquet “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
= Extension('checkerboard_binding',
checkerboard_module = ['src/checkerboard/binding.cpp'],
sources = ['checkerboard', 'boost_python', 'opencv_core', 'opencv_highgui'])
libraries
= 'pycheckerboard',
setup(name = '0.1',
version = {'': 'src'},
package_dir = ['pycheckerboard'],
packages = '<3',
python_requires = [checkerboard_module]) ext_modules
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;
rec {
stdenv.mkDerivation
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.