Virtualenv est un logiciel très populaire chez les développeurs Python : il leur permet de définir des environnements virtuels au sein desquels ils peuvent installer des paquets Python sans modifier l'état de leur système. Nous verrons dans cet article que cette approche présente quelques problèmes et montrerons comment les résoudre en utilisant GNU Guix.
De nombreux développeurs Python utilisent aujourd'hui virtualenv, un outil dédié à la création d'environnements virtuels isolés. Sa principale utilité est de permettre d'installer plusieurs versions d'un même paquet Python (par exemple pour tester une application Web avec Django 1.8 et 1.9), ce qui est généralement difficile, sinon impossible, avec le gestionnaire de paquets fourni par une distribution GNU/Linux classique. De plus, ces paquets ne sont pas installés dans le site-packages global, évitant ainsi de « polluer » le système. Malheureusement, virtualenv souffre de quelques défauts que nous commencerons par lister avant de montrer comment, à l'aide de GNU Guix[1], un gestionnaire de paquets fonctionnel, il est possible de construire un environnement de développement réellement isolé du reste du système.
1. Virtualenv : un environnement isolé ?
Voyons tout d’abord comment, actuellement, la plupart des développeurs Python créent un environnement de développement « isolé ».
1.1 Utilisation de virtualenv
On peut facilement créer un environnement « virtuel » embarquant Python 3.4 grâce à la commande suivante :
$ virtualenv -p python3.4 py34
Il suffit ensuite de l'activer, ce qui permet d'y gérer ses paquets sans être root :
$ source py34/bin/activate
(py34)$ pip list
pip (1.5.6)
setuptools (18.8)
(py34)$ python -c "import six"
Traceback (most recent call last):
File "<string>", line 1, in <module>
ImportError: No module named 'six'
(py34)$ pip install six
Downloading/unpacking six
Downloading six-1.10.0-py2.py3-none-any.whl
Installing collected packages: six
Successfully installed six
Cleaning up...
(py34)$ python -c "import six"
(py34)$ deactivate
$
On peut remarquer que la bibliothèque six, bien qu'installée sur le système (c'est le paquet python3-six sous Debian) n'est pas disponible au sein de l'environnement virtuel et doit y être installée avec pip.
Il est possible de créer de nombreux environnements virtuels et d'y installer des paquets différents. On peut ainsi lancer les tests d'un logiciel dans plusieurs environnements, fournissant des versions différentes de Python et des dépendances, afin de s'assurer que l'application fonctionnera correctement dans diverses configurations. Malheureusement, l'isolation n'est pas parfaite.
1.2 Dépendances
La gestion des dépendances peut parfois laisser à désirer : le problème principal est que pip, qui est utilisé pour installer des paquets au sein d'un environnement virtuel, ne peut gérer que des paquets Python. Voyons quelques exemples.
1.2.1 Erreur à l’installation
(py34)$ pip install cffi
...
c/_cffi_backend.c:15:17: fatal error: ffi.h: No such file or directory
compilation terminated
error: command 'x86_64-linux-gnu-gcc' failed with exit status 1
Une erreur survient : l'installation du paquet cffi comporte en effet une phase de compilation qui requiert la présence de certains fichiers d'en-tête, dont ffi.h. Il convient donc d'installer libffi-dev, qui fournit ffi.h, (sous Debian) avant de chercher à installer cffi, ce qui est problématique pour deux raisons :
- une partie des dépendances sera gérée par apt : l'environnement n'est donc plus vraiment isolé ;
- la commande pip install cffi donnera des résultats différents selon la machine sur laquelle elle sera exécutée, selon que le fichier d'en-tête ffi.h s'y trouve ou non : on ne peut donc pas reproduire de manière fiable un environnement.
1.2.2 Erreur à l’exécution
Le projet OpenStack utilise pbr (Python Build Reasonableness), une bibliothèque dont l'objectif est de simplifier l'écriture du setup.py. Elle nécessite la présence du binaire Git lors de l'exécution ; que se passe-t-il s'il n'est pas disponible ?
(py34)$ git clone https://github.com/openstack/python-keystoneclient
(py34)$ sudo apt-get remove git
(py34)$ cd python-keystoneclient
(py34)$ python setup.py install
...Are you sure that git is installed?
...
(py34)$ echo $?
1
Certes, cet exemple est un peu tiré par les cheveux : pbr est conçu pour être utilisé dans un dépôt Git, il est relativement normal qu'il ne fonctionne pas lorsque Git n'est pas installé. Toutefois, nous voyons ici que le couple pip/virtualenv n'ayant pas connaissance des dépendances à l'exécution et ces dernières pouvant être installées en dehors de l'environnement virtuel, il est possible de supprimer l'une d'entre elles par erreur.
1.2.3 Impact sur les performances : le cas de PyYAML
Essayons maintenant d'exécuter le code suivant, qui charge un grand nombre de fichiers YAML et affiche à l'écran le temps d'exécution :
import os
import timeit
import yaml
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
def test():
for f in os.listdir('/tmp/yaml-files'):
yaml.load(f, Loader=SafeLoader)
if __name__ == '__main__':
print(timeit.timeit("test()",
setup="from __main__ import test",
number=1000))
Le paquet yaml permet de charger un fichier grâce à la méthode load en utilisant un module écrit en Python (SafeLoader) ou en C (CSafeLoader) si la bibliothèque libyaml (le paquet libyaml-dev sous Debian) est présente sur le système. Les performances peuvent être radicalement différentes : sur ma machine, le temps d'exécution est passé de 3,5 secondes à 1 seconde en utilisant la version écrite en C !
Des problèmes de performance pourront donc apparaître sur certaines machines et pas d'autres, quand bien même les environnements virtuels seraient similaires : l'état du reste du système a en effet un impact sur le module utilisé pour charger nos fichiers YAML.
1.3 Variables d’environnement
Un des avantages d'un environnement dit « virtuel » est de permettre de reproduire le comportement d'un programme sur de nombreuses machines différentes, notamment afin que tous les développeurs puissent lancer les tests unitaires dans des conditions similaires. Il est toutefois extrêmement facile d'introduire des variations, même en utilisant virtualenv. Ainsi, les variables d'environnement ne sont pas modifiées lors de l'activation d'un environnement virtuel, or elles peuvent changer le fonctionnement d'un programme du tout au tout :
$ LANG=C python3 -c "print('é')"
Unable to decode the command from the command line:
UnicodeEncodeError: 'utf-8' codec can't encode character '\udcc3' in position 7:
surrogates not allowed
$ LANG=en_US.UTF-8 python3 -c "print('é')"
é
Deux développeurs ayant deux valeurs différentes pour la variable LANG verront donc deux comportements très différents pour ce programme Python pourtant très simple.
2. Construire son environnement avec Guix
Idéalement, nous aimerions conserver les avantages de virtualenv (construire des environnements virtuels, ne pas polluer notre système en y installant des paquets, gérer plusieurs versions d'une bibliothèque) sans rencontrer les problèmes que nous venons de lister. C'est chose possible grâce à GNU Guix !
2.1 Manuellement
GNU Guix est un gestionnaire de paquets fonctionnel dont les avantages et l'architecture sont détaillés dans GNU/Linux Magazine n° 194 pages 46 à 50. Il peut également être utilisé pour offrir des fonctionnalités similaires à celles proposées par virtualenv, grâce à la commande guix environment. Elle prend en argument un ou plusieurs noms de paquets disponibles dans GNU Guix :
$ guix environment python-keystoneclient
Dans GNU Guix, les paquets Python portent les mêmes noms que sur PyPI, mais sont préfixés par python-, pour les paquets Python 3 (la version par défaut), ou par python2-. La commande précédente crée un environnement dans lequel sont disponibles les dépendances du paquet python-keystoneclient. Nous pouvons vérifier que le paquet pytest, requis pour lancer les tests de python-keystoneclient, est bien disponible et a été construit par Guix :
$ python
>>> import pytest
>>> pytest.__file__
'/gnu/store/n7pr4jnw9p51vmyblw41ppimhpg5f9js-profile/lib/python3.4/site-packages/pytest-2.6.1-py3.4.egg/pytest.py'
Nous nous éloignons toutefois légèrement de l'approche de virtualenv, qui permet d'installer une liste de paquets plutôt que leurs dépendances ; on peut retrouver ce comportement en utilisant l'option --ad-hoc :
$ guix environment --ad-hoc python python-keystoneclient
$ python
>>> import keystoneclient
>>>
2.2 Une bien meilleure isolation
Nous pouvons donc reproduire le comportement de virtualenv et retrouver ses avantages en utilisant GNU Guix : nous pouvons en effet utiliser des paquets Python sans les installer réellement sur notre système et nous pouvons en installer plusieurs versions différentes si elles sont empaquetées. Mais qu'avons-nous vraiment à y gagner ?
2.2.1 Gestion complète des dépendances
L'isolation du reste du système est bien meilleure qu'avec virtualenv : en effet, toutes les dépendances sont spécifiées dans un paquet GNU Guix. Si l'on revient rapidement à la première partie de l'article, cela signifie que :
- l'installation de cffi ne peut pas échouer à cause de l'absence du fichier d'en-tête ffi.h : libffi est en effet une dépendance de cffi ;
- les dépendances à l'exécution sont spécifiées, même si ce ne sont pas des paquets Python : le paquet git est une dépendance du paquet python-pbr ;
- le paquet python-pyyaml sera toujours installé avec les extensions C, puisque libyaml est une de ses dépendances.
Nous pouvons vérifier tout cela en étudiant les dépendances des paquets grâce à la commande guix package :
$ guix package -s python-cffi | recsel -p dependencies
dependencies: ... python-pytest-2.6.1 ...
$ guix package -s python-pbr | recsel -p dependencies
dependencies: git-2.6.3 ...
$ guix package -s python-pyyaml | recsel -p dependencies
dependencies: libyaml-0.1.5 ...
2.2.2 Variables d'environnement sous contrôle
Nous avons vu que virtualenv ne modifiait pas les variables d'environnement et que cela pouvait être un problème pour reproduire un environnement correctement. GNU Guix permet de supprimer ces variables grâce à l'option --pure :
$ guix environment --ad-hoc --pure python python-keystoneclient
$ echo $PATH
/gnu/store/36106pz9mjkbp0ikb6f3cqsxfl0mc5ay-profile/bin
$ echo $PYTHONPATH
/gnu/store/36106pz9mjkbp0ikb6f3cqsxfl0mc5ay-profile/lib/python3.4/site-packages
Les variables PATH et PYTHONPATH ne contiennent plus qu'un seul chemin, qui mène aux binaires et aux bibliothèques d'un profil GNU Guix temporaire, dans lequel ne sont installés que les paquets python et python-keystoneclient. Les autres variables d'environnement sont supprimées, à l'exception de HOME, USER, LOGNAME, DISPLAY, TERM, TZ et PAGER.
2.2.3 La cerise sur le gâteau : un conteneur !
Nous venons de voir que la variable PATH était « purifiée » grâce à l'option --pure, ce qui devrait nous empêcher d'appeler une commande installée en dehors de notre environnement virtuel. Toutefois, il est encore possible d'utiliser un chemin absolu :
$ guix environment --ad-hoc --pure python
[env]$ make -v
bash: make: command not found
[env]$ /usr/bin/make -v
GNU Make 4.0
...
Pour pallier ce problème, la commande guix environment fournit une option --container, qui, comme son nom l'indique, permet de placer l'environnement virtuel dans un conteneur :
$ guix environment --ad-hoc --pure --container python
[env]sh-4.3# /usr/bin/make -v
sh: /usr/bin/make: No such file or directory
Nous avons donc désormais un environnement particulièrement bien isolé de notre système.
2.3 Aller plus loin avec guix-tox
De nombreux projets utilisent Tox [2], un outil en ligne de commande permettant de gérer des environnements virtuels, notamment pour lancer les tests unitaires :
$ cd monprojet/
$ tox -l # Listons les environnements définis dans tox.ini
py26
py27
py33
py34
pep8
La commande tox -epy27 utilisera virtualenv afin de créer un environnement virtuel, puis y lancera les tests en utilisant Python 2.7. Le but de cet article n'est pas de faire une présentation complète de Tox, mais il convient d'indiquer que des configurations complexes peuvent être définies dans le fichier tox.ini, ce qui en fait un outil de gestion d'environnements virtuels très pratique.
Malheureusement, Tox utilise virtualenv, ce qui nous expose aux problèmes définis précédemment. Une preuve de concept non officielle remplace virtualenv par tox dans Guix [3]. Un exemple d'utilisation :
$ GUIX_TOX_EXTRA=openssl guix-tox --env=guix -epy34
...
Ran 1133 (+1132) tests in 30.764s
PASSED (id=65, skips=4)
Nul besoin de modifier la configuration. De même, les commandes sont les mêmes,à deux exceptions près :
- l'option --env qui permet de sélectionner le gestionnaire d'environnements virtuels souhaité (guix ou virtualenv);
- la variable d'environnement GUIX_TOX_EXTRA qui permet de spécifier des dépendances non disponibles sur PyPI : ici le binaire openssl, utilisé dans les tests.
Conclusion
Nous avons vu les défauts de virtualenv qui nous empêchent d'obtenir des environnements virtuels de développement réellement isolés du reste de notre machine et parfaitement reproductibles, et nous savons désormais comment corriger ces problèmes grâce à GNU Guix. Toutefois, ce dernier n'est pas un remplacement miraculeux pour le couple pip/virtualenv : en effet, tous les paquets disponibles sur PyPI ne sont pas présents dans GNU Guix. Toutefois, l'approche fonctionnelle nous semble plus robuste que les solutions existantes.
Références
[1] Site officiel de GNU Guix :http://www.gnu.org/s/guix
[2] Site officiel de Tox : https://tox.readthedocs.org/
[3] Dépôt Git de guix-tox : https://git.framasoft.org/Steap/guix-tox