1. Le travail de l'empaqueteur
1.1 Convertir un paquet
Cet article concerne uniquement les logiciels disponibles sur des dépôts spécifiques à un langage particulier (tels que PyPI, RubyGems, CPAN) et généralement installables grâce aux gestionnaires de paquets associés (tels que pip, gem ou cpanm). Dans ce cas, le travail de l'empaqueteur consiste principalement à convertir un paquet upstream en un paquet downstream.
Prenons l'exemple de Python. La plateforme PyPI fournit, pour chaque projet qui s'y trouve, un tarball par version, ainsi que des métadonnées (numéro de version, description du paquet, etc.). Le mainteneur d'une distribution doit réorganiser le code et les métadonnées afin de se conformer au format utilisé par sa distribution. Ainsi, la bibliothèque requests nécessite, sous Fedora, la création des fichiers suivants :
python-requests/
├── dont-import-OrderedDict-from-urllib3.patch
├── Don-t-inject-pyopenssl-into-urllib3.patch
├── patch-requests-certs.py-to-use-the-system-CA-bundle.patch
├── python-requests.spec
├── Remove-tests-that-use-the-tarpit.patch
├── requests-2.12.4-tests_nonet.patch
└── sources
Fedora choisit de patcher le code, d'où la présence de nombreux patches. Les métadonnées, la liste des dépendances, les instructions spécifiques à l'installation, etc. sont présentes dans le fichier python-requests.spec. Enfin, le fichier sources contient le SHA512 du tarball utilisé.
Sous Debian, le format est différent, mais l'idée est similaire :
requests/debian/
├── changelog
├── clean
├── compat
├── control
├── copyright
├── docs
├── python3-requests.pyremove
├── python-requests.pyremove
├── rules
├── source
│ └── format
├── upstream
│ └── signing-key.asc
└── watch
Le code est ici aussi patché (les fichiers .pyremove listent des modules à supprimer), les dépendances sont spécifiées dans control, etc.
Un dernier exemple, avec les ports d'OpenBSD :
py-requests
├── distinfo
├── Makefile
├── pkg
│ ├── DESCR
│ └── PLIST
Ici, un simple Makefile décrit la procédure d'installation. On retrouve également des métadonnées (liste des fichiers dans PLIST, description du paquet dans DESCR, hachage cryptographique dans distinfo).
1.2 Difficultés
Empaqueter des logiciels est un travail plutôt long et quelque peu fastidieux. Voyons pourquoi.
1.2.1 De multiples sources
Il existe aujourd'hui de nombreux langages, généralement fournis avec un gestionnaire de paquets qui permet d'installer des paquets provenant d'une plateforme spécifique. Il est donc souvent nécessaire de connaître plusieurs environnements logiciels, similaires, mais différents.
1.2.2 De multiples cibles
Les développeurs procèdent rarement eux-mêmes à l'empaquetage de leur code : un développeur Python, par exemple, fera en sorte que sa production soit installable depuis PyPI, avec pip, mais déléguera l'empaquetage pour Debian/Fedora/OpenBSD à d'autres développeurs.
Un même logiciel est donc empaqueté de multiples fois, ce qui crée une grande quantité de travail. Il est toutefois à noter que certains paquets peuvent être partagés entre des distributions (un paquet Debian se porte relativement facilement sur Ubuntu, DragonflyBSD réutilise les ports FreeBSD…).
1.2.3 L'enfer des dépendances
La plupart des logiciels ne sont pas écrits « depuis zéro » : ils utilisent des bibliothèques. Empaqueter un logiciel nécessite donc d'empaqueter ses dépendances, et les dépendances de ses dépendances, etc. Un empaqueteur doit donc souvent créer plusieurs paquets d'un coup. Les bibliothèques requises peuvent déjà exister dans la distribution, mais dans une version trop ancienne : il faut alors les mettre à jour. On rencontre également des dépendances circulaires, qui sont un casse-tête.
1.2.4 Déverminage
Une fois que le paquet est écrit et installé, il se peut que le logiciel empaqueté ne se lance pas, ou présente des bugs. Il faut donc débugger le code, communiquer avec l'upstream et inclure des patches dans le paquet : il se peut qu'ils soient spécifiques à la distribution, ce qui veut dire qu'il faudra les maintenir à tout jamais, sans espoir de les voir inclus upstream.
2. Automatisation
De nombreuses étapes de la création d'un paquet semblent automatisables. Ainsi, créer un répertoire, créer des fichiers, noter la version du logiciel… ne sont pas des tâches passionnantes et sont souvent réalisées mécaniquement. Est-il possible pour les empaqueteurs d'automatiser ces étapes faciles et de se concentrer sur la gestion des dépendances et le débuggage ?
2.1 Disponibilité des métadonnées
Des plateformes telles que PyPI ou RubyGems montrent à leurs utilisateurs énormément de métadonnées sur des pages HTML. Mais elles sont également accessibles en JSON !
Pour les paquets Python, les informations sont disponibles à l'adresse https://pypi.org/pypi/<paquet>/json. Par exemple, pour la bibliothèque requests, on obtient cette sortie (tronquée par souci de lisibilité) :
{
"info" : {
"name" : "requests",
"version" : "2.19.1",
"summary" : "Python HTTP for Humans.",
"home_page" : "http://python-requests.org",
"license" : "Apache 2.0",
"requires_dist" : [
"PySocks (!=1.5.7,>=1.5.6); extra == 'socks'",
"idna (>=2.0.0); extra == 'security'",
"cryptography (>=1.3.4); extra == 'security'",
"pyOpenSSL (>=0.14); extra == 'security'",
"certifi (>=2017.4.17)",
"urllib3 (<1.24,>=1.21.1)",
"idna (<2.8,>=2.5)",
"chardet (<3.1.0,>=3.0.2)"
],
}
...
}
On voit ici qu'on retrouve des indications basiques sur le paquet (nom, version, description sommaire…) ainsi que des informations avancées : la liste des dépendances est donnée ! Certaines sont obligatoires (certifi, urllib3, idna et chardet), les autres sont optionnelles.
RubyGems fournit également du JSON (https://rubygems.org/api/v1/gems/<paquet>.json), ainsi que CPAN (https://fastapi.metacpan.org/v1/release/<paquet>). Le format est différent pour chaque outil, mais on y retrouve le même type de métadonnées.
On comprend alors qu'il est sans doute possible d'automatiser une partie non négligeable du travail d'un empaqueteur. Il n'est en effet pas très compliqué d'écrire un script qui prend en paramètre un nom de paquet, construit la bonne URL, récupère le JSON, le parcourt et réécrit les informations obtenues dans le format attendu par la distribution.
2.2 Les outils des distributions
De nombreux développeurs ont eu cette idée, et ils ont écrit plusieurs outils. En voici une liste non exhaustive :
Debian | Fedora | Guix | FreeBSD | OpenBSD | |
CPAN | dh-make-perl | cpan2rpm | guix import | ? | PortGen |
NPM | npm2deb | npm2rpm | N/A | ? | ? |
PyPI | pypi2deb | pyp2rpm | guix import | pytoport | PortGen |
Ruby | gem2deb | gem2rpm | guix import | ? | PortGen |
On remarque que Debian et Fedora ont chacun un outil par plateforme upstream. GNU Guix et OpenBSD ont chacun un outil unique, respectivement guix import et PortGen. Cette façon de faire a plusieurs avantages :
- factoriser une partie du code ;
- proposer à l'utilisateur une interface unifiée, alors que les outils Debian/Fedora ont tous des options différentes ;
- proposer à l'utilisateur un comportement unifié : certains outils vont se contenter de créer la source du paquet, d'autres vont procéder à la compilation… Un utilisateur voulant utiliser plusieurs de ces logiciels risque de s'emmêler les pinceaux.
2.3 Le vrai besoin
Ne pourrait-on pas écrire un seul outil qui remplacerait tous ceux vus précédemment ? Voyons comment cela pourrait constituer une amélioration de l'existant.
2.3.1 Interface unique
Un développeur d'une distribution (par exemple Debian) souhaitant empaqueter deux logiciels venant de deux plateformes différentes (par exemple PyPI et RubyGems) doit utiliser deux logiciels différents pour l'aider dans son travail (ici, pypi2deb et gem2deb).
De même, un développeur Python qui voudrait empaqueter lui-même son logiciel pour deux distributions différentes devrait utiliser deux outils différents (pypi2deb et pyp2rpm, par exemple).
Utiliser plusieurs outils différents implique de mémoriser plusieurs lignes de commandes différentes, d'écrire éventuellement un fichier de configuration pour chaque outil, de se souvenir des particularités des uns et des autres… Il serait beaucoup plus simple d'avoir une seule interface qui pourrait gérer toutes les plateformes de distribution de code et toutes les distributions.
2.3.2 Comportement unifié
De la même façon, devoir jongler entre plusieurs outils impose de connaître leurs comportements : pour Debian, il est possible qu'un outil crée le dossier debian/, et pas un autre ; un outil va lancer la construction du .deb, et pas un autre ; etc.
Avoir un seul outil, capable de gérer tous les dépôts upstream et toutes les distributions, permettrait d'avoir toujours le même comportement.
2.3.3 Outil modulaire
Tous ces outils réimplémentent souvent des fonctionnalités identiques. Ainsi, tous les logiciels travaillant avec des paquets Python vont lire le JSON présenté plus haut afin de récupérer des métadonnées intéressantes. Ils vont aussi essayer de trouver les dépendances d'un paquet donné. Et c'est souvent beaucoup plus compliqué qu'il n'y paraît. Elles peuvent être données dans le JSON, mais ce n'est pas toujours le cas. Elles peuvent être trouvées dans le fichier .whl distribué sur PyPI (un fichier zip pouvant être installé par pip), mais pas forcément. Il n'est pas toujours évident de savoir quelles dépendances sont uniquement requises pour les tests, lesquelles sont optionnelles, etc. Cela s'explique par le fait que les pratiques concernant l'empaquetage et les métadonnées des paquets Python ont beaucoup évolué au cours des dernières années. Le lecteur désireux d'en savoir plus pourra visionner la conférence donnée par Joachim Jablon et Stéphane Angel lors de la PyCon FR 2017 [1], dont les slides sont disponibles en ligne [2]. Même récupérer la licence du paquet demande plus d'efforts que simplement lire une ligne du fichier JSON contenant les métadonnées.
De même, tous les outils créant des paquets pour une distribution spécifique vont devoir créer les mêmes dossiers, les mêmes fichiers, et produiront les mêmes bugs.
On pourrait donc éviter de dupliquer énormément de code en écrivant un logiciel modulaire : chaque plateforme upstream aurait son propre module, chaque distribution downstream aurait son propre module, et tous ces modules communiqueraient entre eux.
3. Un outil unique : upt (the Universal Packaging Tool)
3.1 Concept
Fig. 1 : Architecture d'upt.
Comme on peut le voir en figure 1, les informations disponibles sur les plateformes upstream (PyPI, CPAN, RubyGems) sont lues par des modules frontend (upt-pypi, upt-cpan, upt-rubygems), qui les transmettent à upt. Ce dernier les fait ensuite parvenir aux modules backend (upt-fedora, upt-freebsd, upt-guix, upt-nix, upt-openbsd), qui génèrent du code downstream (un fichier .spec, un Makefile, du code Guile, ou un fichier default.nix).
Bien évidemment, il n'est pas nécessaire d'utiliser tous les modules : ainsi, pour empaqueter un logiciel écrit en Python pour Fedora, seuls upt-pypi, upt et upt-fedora sont nécessaires. De même, pour empaqueter un logiciel disponible sur CPAN pour OpenBSD, seuls upt-cpan, upt et upt-openbsd devront être installés.
Cette architecture n'est pas particulièrement originale : les compilateurs fonctionnent de la même façon. Leurs frontends savent parcourir le code d'un langage particulier (C ou C++ par exemple), le transformer en une représentation interne, qui est utilisée par leurs backends afin de générer du code assembleur pour une plateforme donnée (x86, x64, MIPS…). De même, pandoc [3] sait convertir des documents d'un langage de balisage à un autre grâce à une architecture similaire.
Terminons la présentation de l'architecture en notant que les modules upt ont chacun leur propre dépôt git. Il est ainsi possible de créer un nouveau module sans devoir le faire valider par l'upstream du projet upt.
3.2 Utilisation
Installer upt se fait facilement depuis PyPI :
$ pip install upt
On peut ensuite installer les modules dont nous avons besoin un par un :
$ pip install upt-pypi
$ pip install upt-fedora
...
On peut aussi installer upt et tous les frontends disponibles :
$ pip install upt[frontends]
Ou installer upt et tous les backends disponibles :
$ pip install upt[backends]
Ou encore upt et tous les modules disponibles :
$ pip install upt[frontends,backends]
Il est ensuite possible de vérifier quels modules sont disponibles :
$ upt list-frontends
cpan
pypi
rubygems
$ upt list-backends
fedora
freebsd
guix
nix
openbsd
Essayons maintenant d'empaqueter upt pour FreeBSD, depuis PyPI :
$ upt package --frontend pypi --backend freebsd -o /tmp/upt upt
[INFO ] [Backend] Creating /tmp/upt/py-upt
[INFO ] [Backend] Creating /tmp/upt/py-upt/Makefile
[INFO ] [Backend] Creating /tmp/upt/py-upt/pkg-descr
[INFO ] [Backend] Creating /tmp/upt/py-upt/distinfo
make: Entering directory '/tmp/upt/py-upt'
Makefile:20: *** missing separator. Stop.
make: Leaving directory '/tmp/upt/py-upt'
[WARNING ] [Backend] make makesum failed. Not generating /tmp/upt/py-upt/distinfo
$ tree /tmp/upt/py-upt/
/tmp/upt/py-upt/
├── Makefile
└── pkg-descr
0 directories, 2 files
On voit que le backend upt-freebsd a réussi à créer les fichiers Makefile et pkg-descr, mais qu'il n'a pas su générer distinfo. C'est normal : la création de ce fichier se fait via la commande make makesum, lancée depuis l'arbre des ports FreeBSD. Celle-ci échoue, car la commande est ici lancée sur Debian, mais fonctionnerait sur une machine FreeBSD.
$ cat /tmp/upt/py-upt/Makefile
# $FreeBSD$
PORTNAME= upt
DISTVERSION= 0.4.1
CATEGORIES= XXX python
MASTER_SITES= CHEESESHOP
PKGNAMEPREFIX= ${PYTHON_PKGNAMEPREFIX}
MAINTAINER= python@FreeBSD.org
COMMENT= Package software from any package manager to any distribution
LICENSE= BSD3CLAUSE
LICENSE_FILE= ${WRKSRC}/XXX
RUN_DEPENDS= ${PYTHON_PKGNAMEPREFIX}spdx-lookup>0:XXX/py-spdx-lookup@${FLAVOR}
USES= python
USE_PYTHON= autoplist distutils
.include <bsd.port.mk>
Le Makefile généré n'est pas prêt à être utilisé : certaines informations n'ont pas pu être obtenues par upt, qui a inséré la chaîne de caractères XXX afin de signifier au mainteneur ce qu'il doit corriger. Ainsi upt n'a pas su déterminer à quelle catégorie de ports ce paquet appartient, ni le fichier dans lequel est contenu la licence, ni la catégorie de l'unique dépendance de ce paquet.
Il reste donc un peu de travail au mainteneur, mais beaucoup moins que s'il avait dû tout écrire lui-même ! Les outils qu'upt entend remplacer ne sont en général pas non plus capables de générer un paquet complètement valide, qui ne nécessite aucune retouche.
3.3 Ajouter un module
Un des intérêts de l'architecture modulaire d'upt est de permettre d'ajouter (relativement) facilement de nouveaux frontends ou backends. Le projet met à disposition un modèle [4] au format cookiecutter [5]. Si nous voulions créer un backend pour Arch Linux, il serait recommandé de procéder comme suit :
$ cookiecutter https://framagit.org/upt/cookiecutter-upt
project_name []: upt-archlinux
project_slug [upt_archlinux]:
author []: John Doe
email []: john@doe.org
short_description []: Arch Linux backend for upt.
url [https://framagit.org/upt/upt-archlinux]:
Select license:
1 - BSD-3
2 - other
Choose from 1, 2 (1, 2) [1]: 1
Select kind:
1 - backend
2 - frontend
Choose from 1, 2 (1, 2) [1]: 1
entrypoint_name [archlinux]:
main_class_name [ArchlinuxBackend]:
Initialized empty Git repository in /tmp/upt-archlinux/.git/
[master (root-commit) 76b7a4f] Initial commit.
12 files changed, 192 insertions(+)
create mode 100644 .gitignore
create mode 100644 CHANGELOG
create mode 100644 LICENSE
create mode 100644 MANIFEST.in
create mode 100644 README.md
create mode 100644 setup.cfg
create mode 100644 setup.py
create mode 100644 tox.ini
create mode 100644 upt_archlinux/__init__.py
create mode 100644 upt_archlinux/templates/base.tmpl
create mode 100644 upt_archlinux/tests/__init__.py
create mode 100644 upt_archlinux/upt_archlinux.py
Il suffit de répondre à quelques questions afin de créer automatiquement un dépôt git contenant un squelette de module. Il n'y a ensuite plus qu'à compléter le code du module. Le README du projet upt [6] contient des informations au sujet de l'API d'upt, que les lecteurs intéressés pourront consulter.
4. Une autre approche : fpm
L'auteur d'upt (qui est aussi le rédacteur de cet article) a bien entendu fait un état de l'art, et a découvert fpm [7] dont le concept est similaire à celui d'upt.
Cet exemple tiré de la documentation officielle montre comment utiliser fpm :
% fpm -s python -t rpm pyramid
Trying to download pyramid (using easy_install)
Searching for pyramid
Reading http://pypi.python.org/simple/pyramid/
Reading http://docs.pylonshq.com
Reading http://docs.pylonsproject.org
Best match: pyramid 1.0
...
Created /home/jls/python-pyramid-1.0.noarch.rpm
Notons que fpm, écrit en Ruby, supporte énormément d'options pour chacun de ses modules, afin de permettre aux utilisateurs de créer des paquets aux petits oignons.
Comme upt, fpm a une approche modulaire et sait même gérer plus de sources et de cibles qu'upt ! La principale différence est qu'il génère un paquet binaire plutôt qu'un paquet source. Par exemple, dans le cas de Fedora, il pourra créer un fichier RPM, mais ne créera pas le fichier .spec associé [8]. C'est donc un outil très utile pour créer un paquet installable par le gestionnaire de votre distribution, mais il ne permet pas vraiment de réaliser le travail d'un empaqueteur.
Conclusion
Nous avons présenté dans cet article le travail des empaqueteurs, et les outils qu'ils utilisent actuellement pour automatiser une partie de ce travail. Nous avons ensuite défendu l'idée qu'un outil modulaire pourrait avantageusement remplacer la pléthore d'outils spécifiques actuellement utilisés. Enfin, nous avons décrit l'architecture d'un tel outil, et présenté quelques exemples d'utilisation d'upt. De nombreux modules (backends comme frontends) restent cependant encore à écrire avant que ce logiciel puisse vraiment être un outil d'empaquetage universel. Si l'absence d'un module vous empêche d'utiliser upt, n'hésitez pas à venir en discuter sur #upt-packaging, sur Freenode.
Références
[1] Programme de la PyConFR, « Les aventuriers du packaging perdu » : https://www.pycon.fr/2017/programme.html#les-aventuriers-du-packaging-perdu
[2] Diapositives - « Les aventuriers du packaging perdu » : https://twidi.github.io/python-packaging-talk/fr.html
[3] Site officiel de pandoc : https://pandoc.org/
[4] Dépôt git de cookiecutter-upt : https://framagit.org/upt/cookiecutter-upt
[5] Documentation de cookiecutter : https://cookiecutter.readthedocs.io/en/latest/
[6] Fichier README d'upt : https://framagit.org/upt/upt/blob/master/README.md
[7] Site officiel de fmp : https://fpm.readthedocs.io/en/latest/
[8] Discussion au sujet du comportement de fpm : https://github.com/jordansissel/fpm/issues/1507#issuecomment-411390171