Petit traité empirique de l’empaquetage Debian

Vincent Bernat

Note

Ce guide est une mise à jour d’une version précédente. Si vous avez besoin de construire des paquets pour des distributions plus anciennes que Debian Stretch et Ubuntu Bionic, jetez un œil sur l’édition précédente.

Bien que la création de paquets Debian soit abondamment documentée, la plupart des tutoriaux ciblent les paquets respectueux de la charte Debian. De plus, leur création a longtemps eu la réputation d’être particulièrement difficile1 et beaucoup se sont tournés vers des outils moins contraignants2 tels que fpm ou CheckInstall.

Toutefois, la construction de paquets Debian en utilisant les outils officiels est plutôt simple en appliquant ces quelques concessions :

  1. Aucun paquet source ne sera généré. Les paquets seront construits directement depuis une copie propre issue du système de versions.

  2. Des dépendances supplémentaires peuvent être téléchargées pendant la construction. Empaqueter individuellement chaque dépendance est un travail ingrat, notamment avec certains environnements tels que Java, JavaScript et Go.

  3. Les paquets produits peuvent combiner et inclure des dépendances tierces. Cela peut lever certaines objections liées à la sécurité et à la maintenance à long terme, mais c’est une concession courante dans certains écosystèmes tels que Java, JavaScript et Go.

Ceinture blanche#

Deux types de paquets coexistent dans l’archive Debian : les paquets sources et les paquets binaires. Un paquet source produit un ou plusieurs paquets binaires. Chaque paquet doit porter un nom.

Comme indiqué lors de l’introduction, aucun paquet source ne sera construit. Nous allons travailler directement sur sa représentation décompressée : une arborescence de fichiers incluant le répertoire debian/. Les exemples qui suivent utilisent une arborescence composée uniquement du répertoire debian/, mais ce dernier peut être inclus dans n’importe quel project existant.

Comme point de départ, nous allons empaqueter memcached, un cache mémoire distribué. Il nous faut créer quatre fichiers :

  • debian/compat,
  • debian/changelog,
  • debian/control et
  • debian/rules.

Le premier contient uniquement 113 :

echo 11 > debian/compat

Le second contient ceci :

memcached (0.0-0) UNRELEASED; urgency=medium

  * Fake entry

 -- Happy Packager <happy@example.com>  Tue, 19 Apr 2016 22:27:05 +0200

La seule information d’importance est le nom du paquet source, memcached, sur la première ligne. Toutes les autres informations sont sans influence sur les paquets créés.

Le fichier de contrôle#

debian/control décrit les méta-données à propos du paquet source et des paquets binaires. Un bloc est dédié à chacun d’eux.

Source: memcached
Maintainer: Vincent Bernat <bernat@debian.org>

Package: memcached
Architecture: any
Description: high-performance memory object caching system

Le paquet source est memcached. Il faut utiliser le même nom que dans le fichier debian/changelog.

Un seul paquet binaire est créé : memcached. Par la suite, si vous voyez memcached, il s’agit du nom du paquet binaire. Le champ Architecture doit être soit any, soit all. Ce dernier est utilisé exclusivement si tous les fichiers sont indépendants de l’architecture matérielle. Dans le doute, il suffit de mettre any.

Le champ Description contient une courte description du paquet binaire.

La recette#

Le dernier fichier à rédiger est debian/rules. Il s’agit de la recette du paquet. Nous avons besoin de télécharger memcached, le construire et installer son arborescence dans debian/memcached/ :

#!/usr/bin/make -f

DISTRIBUTION = $(shell sed -n "s/^VERSION_CODENAME=//p" /etc/os-release)
VERSION = 1.6.6
PACKAGEVERSION = $(VERSION)-0~$(DISTRIBUTION)0
TARBALL = memcached-$(VERSION).tar.gz
URL = http://www.memcached.org/files/$(TARBALL)

%:
    dh $@

override_dh_auto_clean:
override_dh_auto_test:
override_dh_auto_build:
override_dh_auto_install:
    wget -N --progress=dot:mega $(URL)
    tar --strip-components=1 -xf $(TARBALL)
    ./configure --prefix=/usr
    make
    make install DESTDIR=debian/memcached

override_dh_gencontrol:
    dh_gencontrol -- -v$(PACKAGEVERSION)

Les cibles vides override_dh_auto_clean, override_dh_auto_test et override_dh_auto_build permettent de s’assurer que debhelper ne fera rien de « magique ». La cible override_dh_gencontrol permet de spécifier la version4 sans avoir à tenir à jour debian/changelog. Cette recette est très similaire à ce qui aurait été écrit pour fpm :

DISTRIBUTION=$(sed -n "s/^VERSION_CODENAME=//p" /etc/os-release)
VERSION=1.6.6
PACKAGEVERSION=${VERSION}-0~${DISTRIBUTION}0
TARBALL=memcached-${VERSION}.tar.gz
URL=http://www.memcached.org/files/${TARBALL}

wget -N --progress=dot:mega ${URL}
tar --strip-components=1 -xf ${TARBALL}
./configure --prefix=/usr
make
make install DESTDIR=/tmp/installdir

# Build the final package
fpm -s dir -t deb \
    -n memcached \
    -v ${PACKAGEVERSION} \
    -C /tmp/installdir \
    --description "high-performance memory object caching system"

Vous pouvez lire le résultat final sur GitHub et le construire avec la commande dpkg-buildpackage -us -uc -b ou avec GIT_PBUILDER_OPTIONS=--use-network=yes DIST=bionic git-pbuilder -us -uc -b si vous avez déjà mis en place un outil tel que pbuilder.

Ceinture jaune#

À partir de là, il est possible d’inclure quelques améliorations. Aucune n’est essentielle mais le gain est suffisamment intéressant pour justifier l’effort.

Les dépendances sources#

Notre recette initiale ne fonctionne que si nous disposons déjà de wget et de libevent-dev. Ces paquets ne sont pas présents sur tous les systèmes. Il est assez aisé de spécifier ces dépendances en ajoutant un champ Build-Depends dans debian/control :

Source: memcached
Build-Depends: debhelper (>= 11),
               wget, ca-certificates,
               libevent-dev

Il faut toujours spécifier debhelper (>= 11) car son utilisation est centrale dans debian/rules. Il n’y a pas besoin de dépendre de make ou d’un compilateur C car le paquet build-essential est considéré comme toujours présent et il les fournit indirectement. dpkg-buildpackage se plaindra si une des dépendances est manquante. Pour installer sans peine ces paquets, par exemple depuis un environnement d’intégration continue, vous pouvez utiliser la commande suivante5 :

mk-build-deps \
    -t 'apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends -qqy' \
    -i -r debian/control

Il est aussi intéressant de se pencher sur des outils tels que pbuilder, sbuild ou whalebuilder. Ils permettent de construire des paquets dans un environnement minimal et isolé.

Les dépendances binaires#

Si le paquet ainsi construit est installé sur une machine vierge, memcached refusera de démarrer en raison de l’absence de libevent. Il est possible d’exprimer cette dépendance en ajoutant un champ Depends dans le fichier debian/control. De plus, dans le cas des bibliothèques dynamiques, ces dépendances peuvent être générées automatiquement en utilisant des variables de substitution :

Package: memcached
Depends: ${misc:Depends}, ${shlibs:Depends}

Le paquet construit contiendra les informations suivantes :

$ dpkg -I ../memcached_1.6.6-0\~buster0_amd64.deb | grep Depends
 Depends: libc6 (>= 2.17), libevent-2.1-6 (>= 2.1.8-stable)

Intégration avec un système de démarrage#

La plupart des paquets fournissant un démon incluent une intégration avec le système de démarrage afin de démarrer le démon au boot ou de le redémarrer après une mise à jour. Bien que Debian assure la prise en charge de plusieurs systèmes de démarrage, je pense qu’il est préférable de se limiter à systemd.

Pour fournir un script pour systemd, il convient de placer son contenu dans debian/memcached.service. Par exemple :

[Unit]
Description=memcached daemon
After=network.target

[Service]
Type=forking
Environment=PORT=11211
Environment=MAXCONN=1024
Environment=CACHESIZE=64
Environment=OPTIONS=
ExecStart=/usr/bin/memcached -d -p $PORT -m $CACHESIZE -c $MAXCONN $OPTIONS
Restart=on-failure
User=_memcached
DynamicUser=yes

[Install]
WantedBy=multi-user.target

La directive Type est la plus importante. Nous avons utilisé forking car memcached est démarré à l’aide de l’option -d. Cela lui indique de se mettre en arrière-plan lorsqu’il est prêt à accepter des requêtes. Les autres possibilités sont notify pour un démon spécifiquement prévu pour fonctionner avec systemd ou simple pour un démon ne passant pas en arrière-plan.

Si un utilisateur veut personnaliser l’une des directives Environment, il peut utiliser systemctl edit memcached.service pour créer le fichier /etc/systemd/system/memcached.service.d/override.conf qui va surcharger les définitions du script fourni par le paquet. systemctl cat memcached.service permet d’obtenir la définition finale du service.

La directive DynamicUser est particulièrement intéressante. Elle indique à systemd de créer dynamiquement l’utilisateur _memcached6 qui sera utilisé pour faire tourner le démon. Cela nous libère de la gestion d’un utilisateur système !

Le résultat final est disponible sur GitHub et peut être testé avec la commande dpkg-buildpackage -us -uc -b.

Ceinture verte#

Il est possible d’exploiter certaines capacités de debhelper pour réduire la taille du fichier debian/rules et pour le rendre plus déclaratif. Cette section est totalement optionnelle : vous pouvez la sauter si besoin.

Banalités#

Il y a quatre étapes dans la construction d’un paquet Debian :

  1. debian/rules clean va nettoyer l’arborescence pour revenir dans son état initial.

  2. debian/rules build doit construire le logiciel. Pour quelque chose de basé sur autoconf, comme memcached, il s’agit essentiellement d’exécuter ./configure && make.

  3. debian/rules install doit installer l’arborescence de chaque paquet binaire dans le répertoire approprié. Pour des logiciels basés sur autoconf, il s’agit d’exécuter make install DESTDIR=debian/memcached.

  4. debian/rules binary doit empaqueter les différentes arborescences en paquets binaires.

Il ne faut pas écrire directement chacune de ces cibles. L’utilitaire dh, un composant de debhelper, va faire la majeure partie du boulot. Le fichier debian/rules minimaliste suivant suffit pour accomplir cette tâche pour de nombreux paquets sources :

#!/usr/bin/make -f
%:
    dh $@

Pour chacune des quatre cibles décrites ci-dessus, vous pouvez exécuter dh --no-act pour voir les utilitaires invoqués. Par exemple :

$ dh build --no-act
   dh_testdir
   dh_update_autotools_config
   dh_auto_configure
   dh_auto_build
   dh_auto_test

Chacun de ces utilitaires dispose d’une page de manuel. Ceux commençant par dh_auto sont un peu « magiques ». Par exemple, dh_auto_configure va tenter de configurer automatiquement le logiciel avant l’étape de construction. Selon les cas, il peut invoquer ./configure, cmake ou Makefile.PL.

Si un des utilitaires dh_ ne fait pas ce qu’il faut, il est possible de le remplacer en déclarant une cible nommée de manière adéquate :

override_dh_auto_configure:
    ./configure --with-some-grog

Chaque utilitaire est également configurable via des options. Ainsi, il est possible de modifier leurs comportements en définissant la cible correspondante et en invoquant l’utilitaire manuellement :

override_dh_auto_configure:
    dh_auto_configure -- --with-some-grog

Ainsi, ./configure sera appelé avec l’option --with-some-grog mais aussi avec des options par défaut telles que --prefix=/usr. Il y a une page de manuel pour chacun de ces outils.

Dans l’exemple initial de memcached, ces cibles « magiques » sont surchargées. dh_auto_clean, dh_auto_configure et dh_auto_build ont été neutralisées pour éviter tout comportement inattendu. dh_auto_install a été détournée pour exécuter l’intégralité de la construction de l’arborescence cible. De plus, le comportement de dh_gencontrol a été modifié en lui fournissant le numéro de version désiré plutôt que de le laisser regarder dans debian/changelog.

Construction automatique#

memcached utilisant autoconf, dh sait comment le construire : ./configure && make && make install. Il est donc possible de laisser dh faire la majeure partie du boulot avec le fichier debian/rules suivant :

#!/usr/bin/make -f

DISTRIBUTION = $(shell sed -n "s/^VERSION_CODENAME=//p" /etc/os-release)
VERSION = 1.6.6
PACKAGEVERSION = $(VERSION)-0~$(DISTRIBUTION)0
TARBALL = memcached-$(VERSION).tar.gz
URL = http://www.memcached.org/files/$(TARBALL)

%:
    dh $@ --with systemd

override_dh_update_autotools_config:
    wget -N --progress=dot:mega $(URL)
    tar --strip-components=1 -xf $(TARBALL)

override_dh_auto_test:
    # Don't run the whitespace test
    rm t/whitespace.t
    dh_auto_test

override_dh_gencontrol:
    dh_gencontrol -- -v$(PACKAGEVERSION)

La cible dh_update_autotools_config est détournée pour effectuer le téléchargement et la mise en place de l’arborescence. Ni dh_auto_configure, ni dh_auto_build ne sont modifiés. dh appellera ./configure avec les options appropriées puis make. dh_auto_test doit exécuter la suite de tests de memcached. Toutefois, un des tests échoue en raison d’un fichier dans le répertoire debian/. Nous supprimons ce test récalcitrant et invoquons manuellement dh_auto_test. dh_auto_install n’est pas surchargé et dh exécutera alors une variante de make install.

Afin de mieux apprécier la différence, la voici sous forme de rustine :

--- memcached-intermediate/debian/rules 2019-05-31 07:52:40.908868035 +0200
+++ memcached/debian/rules      2019-05-31 07:28:17.404380064 +0200
@@ -9,15 +9,14 @@
 %:
    dh $@

-override_dh_auto_clean:
-override_dh_auto_test:
-override_dh_auto_build:
-override_dh_auto_install:
+override_dh_update_autotools_config:
    wget -N --progress=dot:mega $(URL)
    tar --strip-components=1 -xf $(TARBALL)
-   ./configure --prefix=/usr
-   make
-   make install DESTDIR=debian/memcached
+
+override_dh_auto_test:
+   # Don't run the whitespace test
+   rm t/whitespace.t
+   dh_auto_test

 override_dh_gencontrol:
    dh_gencontrol -- -v$(PACKAGEVERSION)

Vous avez le choix de laisser dh faire une partie du travail ou non. Il est généralement possible de partir d’un debian/rules minimal et de surcharger uniquement quelques cibles.

Fichiers supplémentaires#

Bien que make install a installé les fichiers essentiels pour memcached, il est parfois nécessaire de copier quelques fichiers supplémentaires dans le paquet binaire. Pour se faire, il est possible d’utiliser cp ou encore de déclarer les fichiers à copier :

  • les fichiers listés dans debian/memcached.docs seront copiés dans /usr/share/doc/memcached par dh_installdocs,
  • les fichiers listés dans debian/memcached.examples seront copiés dans /usr/share/doc/memcached/examples par dh_installexamples,
  • les fichiers listés dans debian/memcached.manpages seront copiés dans le sous-répertoire approprié de /usr/share/man par dh_installman,

Voici un exemple pour debian/memcached.docs :

doc/*.txt

Si vous avez besoin de copier des fichiers à un endroit arbitraire, il est possible de lister ceux-ci ainsi que leur répertoire cible dans le fichier debian/memcached.install. dh_install se chargera de la copie. Par exemple :

scripts/memcached-tool usr/bin

L’utilisation de ces fichiers permet une description plus déclarative de la recette. Il s’agit d’une histoire de goût et vous pouvez tout à fait utiliser cp à la place. Le résultat final est visible sur GitHub.

Autres exemples#

Le dépôt Git comprend d’autres exemples. Ils suivent tous le même schéma et mettent en œuvre les techniques décrites dans les sections précédentes :

  • dh_update_autotools_config est surchargée pour télécharger et mettre en place les sources,
  • dh_gencontrol est modifiée pour fixer la version du paquet.

Il y a notamment des exemples de démons en Java, Go, Python et Node.js. Le but de ces exemples est de démontrer que l’utilisation des outils Debian est relativement simple. Mission accomplie ?

Mise à jour (05.2019)

Ce guide a été mis à jour pour utiliser override_dh_update_autotools_config pour télécharger des fichiers à la place de override_dh_auto_clean. Cela permet une intégration plus simple avec la plupart des outils de construction, tels que pbuilder et sbuild.


  1. La mémoire collective est toujours marquée par la glorieuse époque précédant l’introduction de debhelper 7.0.50 (circa 2009). La création du fichier debian/rules était alors particulièrement laborieuse. Toutefois, de nos jours, le squelette est devenu minimal. ↩︎

  2. La complexité n’est pas la seule raison de ce choix : les outils alternatifs proposent également la création de paquets RPM, ce que les outils Debian ne permettent pas. ↩︎

  3. Ce niveau de compatibilité est disponible à partir de Debian 9 et d’Ubuntu Bionic. Il couvre donc les distributions modernes. Cependant, il souffre d’un problème empêchant le démarrage d’un service lorsqu’un paquet est installé, désinstallé puis réinstallé de nouveau. Le niveau 10 augmente sensiblement la complexité. Si possible, vous pouvez opter pour la version 12, disponible depuis Debian 10 et Ubuntu Focal. ↩︎

  4. Il y a différentes façons de numéroter les versions d’un paquet. La façon proposée ici n’est pas plus mauvaise qu’une autre pour Ubuntu. Pour Debian, elle ne couvre pas les mises à jour entre deux versions de la distribution. De nos jours, il est cependant plutôt courant de réinstaller un système plutôt que de le mettre à jour. ↩︎

  5. Les paquets devscripts et equivs sont alors nécessaires. ↩︎

  6. La charte Debian ne se prononce pas sur la convention à utiliser. Il est courant de préfixer le nom du démon avec un tiret bas (comme dans les BSD). Un autre usage courant est d’utiliser Debian- comme préfixe. Cette dernière méthode a l’inconvénient de produire un nom d’utilisateur trop long pour être visible dans les utilitaires comme top et ps.

    Si la directive User n’est pas précisée, systemd utilise simplement le nom du service comme nom d’utilisateur. Si cela vous convient, vous pouvez alors omettre cette directive. ↩︎