Empaquetez (facilement) votre projet avec upt

Magazine
Marque
GNU/Linux Magazine
Numéro
222
Mois de parution
janvier 2019
Domaines


Résumé
Les logiciels que nous utilisons sur nos distributions GNU/Linux et *BSD proviennent généralement des dépôts associés à ces dernières. Ils y sont présents, car les mainteneurs de notre OS préféré les ont empaquetés : cela nous évite de devoir recompiler les sources nous-mêmes, de résoudre les problèmes de dépendances ou de parfaire l'intégration avec le reste de notre système. Nous verrons dans cet article comment les empaqueteurs ont réussi à automatiser une partie de leur travail. Nous proposerons ensuite une solution logicielle permettant à toutes les distributions d'empaqueter facilement le code disponible sur des plateformes telles que PyPI, RubyGems ou CPAN.

Body


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

upt

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




Articles qui pourraient vous intéresser...

Tendance actuelle : la conteneurisation d'applications

Magazine
Marque
GNU/Linux Magazine
HS n°
Numéro
110
Mois de parution
septembre 2020
Domaines
Résumé

Actuellement, il est fréquent de voir des applications proposées sous la forme d'un conteneur. Chaque mise en conteneur – ou conteneurisation – d'une application est particulière et j'ai justement dû récemment me plier à l'exercice : je partage avec vous mon expérience dans cet article.

Utilisez Terraform pour vos projets Docker

Magazine
Marque
GNU/Linux Magazine
Numéro
240
Mois de parution
septembre 2020
Domaines
Résumé

Terraform est un outil populaire pour déployer de l’infrastructure en particulier à destination des Clouds publics. Cependant, il possède de nombreux providers pour dialoguer avec différents hyperviseurs, bases de données ou solutions d’infrastructures en Software Defined. Voyons dans cet article son utilisation avec Docker.

Effectuer des sauvegardes avec rdiff-backup

Magazine
Marque
Linux Pratique
Numéro
121
Mois de parution
septembre 2020
Domaines
Résumé

Tous les jours, nous créons et manipulons des données. Certaines plus importantes que d’autres. Une chose que nous partageons tous c’est bien la peur de les perdre. Peut-être avez-vous déjà perdu des données suite à une panne de votre disque de stockage, l’attaque d’un virus ou le vol de votre ordinateur. Les personnes ayant déjà connu cette situation comprennent les tracas que cela peut causer. Vous allez découvrir dans ce tutoriel comment limiter le risque d’y faire face. La solution se trouve en un mot : « sauvegarde ». L’outil que nous allons vous présenter ici a été conçu pour vous aider à réaliser cette tâche de manière efficace et efficiente.

Assurez l’intégrité de vos fichiers avec fs-verity

Magazine
Marque
Linux Pratique
HS n°
Numéro
48
Mois de parution
septembre 2020
Domaines
Résumé

Vous êtes-vous déjà demandé comment faire pour protéger des fichiers importants ? Votre système d’exploitation vous a-t-il déjà informé que vos fichiers étaient corrompus ? Pensez-vous souvent à l’intégrité des informations contenues dans vos fichiers ? Vous êtes tombé au bon endroit, nous découvrirons ici comment protéger vos données avec fs-verity.

Gérer une base de données avec Adminer

Magazine
Marque
Linux Pratique
Numéro
121
Mois de parution
septembre 2020
Domaines
Résumé

La gestion des bases de données relationnelles est une technologie essentielle pour les entreprises. Sa complexité nécessite de disposer de logiciels pratiques et fiables pour manipuler les données avec efficacité et en toute sécurité. Adminer entre dans la catégorie poids plume de ces outils, mais ne manque pas d'arguments pour séduire les administrateurs de bases de données et les développeurs.