1. Des packages et de l'eau gazeuse
NetBSD dispose depuis fort longtemps d'un système de packaging « source », le bien-nommé pkgsrc. pkgsrc permet, par un savant mélange de BSD make(1) et commandes shell de compiler, puis installer des logiciels sur une quinzaine d'architectures. De NetBSD, évidemment, en passant par GNU/Linux [2], SunOS, Dragonfly BSD (qui en a fait son système de packaging officiel [3]), FreeBSD, OpenBSD ou encore des architectures un peu plus vétustes telles que AIX ou HP-UX, pkgsrc permet de compiler tout ou partie de ses 10000 paquets sources sur ces plateformes, et, à l'image de NetBSD lui-même, assure ainsi une abstraction et une portabilité hors du commun.
J'aime pkgsrc, vraiment. J'y retrouve toute la rigueur des développeurs NetBSD. Son ingénieux système de dépendances (buildlink3) et une documentation très précise nous permettent de réaliser des paquets en un temps record, en minimisant les erreurs potentielles. Mais, je suis quelqu’un d'impatient, et la perspective d'attendre plusieurs heures avant d'utiliser un desktop NetBSD m'a toujours fait froid dans le dos. Ainsi, j'avais pour habitude de ne compiler que les « petits » logiciels, tels que screen ou sudo, puis d'installer les logiciels plus massifs (Firefox, des outils Gnome ou KDE) à l'aide du vénérable pkg_add(1).
Pour rappel, pkg_add(1) est l'ancestral outil issu des doigts magiques de Jordan Hubbard, cofondateur du projet FreeBSD, permettant l'installation de paquets binaires. Ce dernier est capable de trouver des packages dans un repository à l'aide de la variable PKG_PATH à laquelle on associe une adresse du type ftp://ftp.fr.NetBSD.org/pub/pkgsrc/packages/NetBSD/<architecture>/<version>/All. Il est alors possible d'installer un paquet en utilisant par exemple :
# pkg_add lighttpd
Si cette méthode fonctionne à peu près pour l'installation de packages, il n'en est pas de même lors d'une mise à jour. Évidemment, il existe une solution bâtarde consistant à utiliser pkgtools/pkg_chk, un script shell dont l'objectif est de gérer convenablement l'upgrade de paquets pkgsrc, binaires ou sources, mais l'expérience montre qu'en dehors d'une machinerie complexe mêlant pkgtools/pkg_comp et pkg_chk (voir GLMF HS 36), c'est-à-dire en gérant vous-même votre repository, il n'existe pas de moyen simple et rapide de tenir sa machine NetBSD à jour.
Depuis plusieurs années, j'attendais naïvement qu'un développeur en arrive au même constat et se fende d'un logiciel apt-like qui comblerait ce vide. L'année dernière, le projet est même apparu dans la page NetBSD du Google Summer of Code [5]. En vain. C'est ainsi que, en mars 2009, je me suis décidé à démarrer pkg_dry, qui deviendrait plus tard pkgin.
Habituellement, je me lance tête baissée dans un projet. J'arrondis les angles au fur et à mesure que le code avance. C'est ainsi que je travaille le mieux. Mais, cette fois, la donne était différente. D'abord, il y avait ce projet GSoC qui donnait une bonne partie des pistes à suivre, et puis il y avait ce fameux projet initié par deux grands manitous de l'univers NetBSD, Andrew Doran et Jared Mc Neill: Desktop NetBSD [6]. Ce dernier possède également ses impératifs, et notamment celui d'utiliser PackageKit comme surcouche graphique à un hypothétique package manager. De plus, les voix semblaient unanimes sur le fonctionnel de cet outil. Tout le monde décrivait peu ou prou la même interface homme-machine : apt.
Un point non négligeable dans le développement du logiciel réside dans la licence avec laquelle il sera distribué. En effet, s'il doit un jour intégrer le base system, il serait du meilleur effet qu'il soit publié sous licence BSD. Ceci, ajouté au fait que le format du repository est imposé par l'historique, impliquait qu'il était totalement hors de question de se lancer dans un portage d'apt. Il s'agirait d'une écriture from scratch. Pour être tout à fait honnête, je dois avouer que cela n'était pas pour me déplaire, car si la machinerie Debian a fait ses preuves, il m'était agréable de repenser tout le système sans plagier qui que ce soit, et au fur et à mesure que le projet avançait, j'était fier de comprendre des problématiques qui rendaient plus clairs certains choix dans l'implémentation d'apt.
Je me propose, dans la suite de cet article, d'expliquer les pièces maîtresses de ce programme à peine éclos en suivant les problématiques dans l'ordre où elles se sont présentées à moi.
2. I can has a database ?
L'élément central d'un outil de packaging est évidemment sa base de données. Après discussion avec les autres développeurs NetBSD, nous sommes arrivés à la conclusion que SQLite était un candidat idéal pour ce type de projet. Ainsi, arbre de dépendances, listes et informations diverses se retrouveraient dans une base SQLite (voir GLMF 116), ce qui permettrait d'obtenir des performances raisonnables sur des machines anciennes.
Pour nourrir cette base de donnée, c'est le format pkg_summary(5) qui a tout naturellement été choisi. En effet, dans l'ensemble des repositories pkgsrc, on trouve un fichier compressé, pkg_summary.{gz|bz2}, contenant entre autres :
- PKGNAME, le nom du paquet ;
- CONFLICTS, la liste des paquets avec lesquels ce dernier est en conflit ;
- DEPENDS, la liste des dépendances, au format glob [4] ;
- REQUIRES, la liste des fichiers requis par ce package ;
- PROVIDES, la liste des fichiers fournis par ce package.
Exemple :
PKGNAME=eterm-0.9.4nb1
DEPENDS=perl>=5.0
DEPENDS=libast>=0.6.1nb3
DEPENDS=imlib2>=1.2.0nb5
COMMENT=Enlightened terminal emulator for the X Window System
SIZE_PKG=9623503
BUILD_DATE=2009-04-17 23:04:31 +0000
CATEGORIES=x11
HOMEPAGE=http://www.eterm.org/
[...]
La bonne nouvelle, c'est que pkg_info(1), l'outil dont nous nous servirons pour obtenir les informations sur les paquets installés localement est également capable de fournir une liste au format pkg_summary(5) grâce à la commande :
$ pkg_info -Xa
Muni de ces informations, il est désormais possible de peupler une base de données avec les informations nécessaires à la résolution de dépendances.
3. La magie des SLIST
Il est un fait inéluctable : je suis une quiche interplanétaire en SQL. C'est comme ça, il y a des langages qui ne m'amusent pas et que je n'ai pas envie de creuser, SQL est de ceux-là. De fait, c'est Claude « zatmania » Charpentier qui m'a mis le pied à l'étrier, et m'a pondu ce qui allait servir de dorsale à la suite de mes balbutiements en SQL. Une chose était sûre cependant. Je n'allais pas écrire des requêtes SQL de 25 lignes pour réaliser des opérations récursives de détection de dépendances.
Afin de rendre l'utilisation de SQLite la plus abstraite possible, les fonctions relatives aux traitements de la base de données sont toutes localisées dans pkgindb.c. On y trouve notamment la fonction pkgindb_doquery(), la fonction par laquelle transitent toutes les requêtes SQL. On lui passe les paramètres suivants :
- la requête SQL à exécuter ;
- la fonction de callback à appeler pour chaque ligne de résultat ;
- un paramètre optionnel de type void *, très utile pour stocker un éventuel résultat.
int
pkgindb_doquery(const char *query,
int (*pkgindb_callback)(void *, int, char **, char **), void *param)
{
if (sqlite3_exec(pdb, query, pkgindb_callback, param, &pdberr)
!= SQLITE_OK) {
if (fp != NULL) {
if (pdberr != NULL)
fprintf(fp, "SQL error: %s\n", pdberr);
fprintf(fp, "SQL query: %s\n", query);
}
sqlite3_free(pdberr);
return PDB_ERR;
}
return PDB_OK;
}
Son rôle consiste essentiellement à appeler la fonction sqlite3_exec(), mais on se sert également de cette fonction-coquille pour écrire un éventuel log d'erreur.
Pour la suite des opérations, ainsi que dans à peu près l'ensemble de mes projets, j'utilise un header magique, queue.h. Il vous a été présenté à plusieurs reprises dans ces colonnes, aussi ne m'y attarderai-je que très peu. Le header queue.h est en fait une sorte de compilation de macros C rendant plus facile et plus sûre l'utilisation des listes chaînées. Ainsi, ai-je pris pour habitude de manipuler mes listes d'éléments sous forme de SLIST, liste simplement chaînée décrite dans queue.h, très aisée à utiliser et très rapide à parcourir.
Prenons comme exemple la liste des paquets installés. La requête SQL associée est relativement simple :
SELECT PKGNAME,COMMENT,FILE_SIZE,SIZE_PKG FROM LOCAL_PKG;
</code>
La fonction suivante permettra de peupler une SLIST :
<code>
Plisthead *
rec_pkglist(const char *pkgquery)
{
Plisthead *plisthead;
XMALLOC(plisthead, sizeof(Plisthead));
SLIST_INIT(plisthead);
if (pkgindb_doquery(pkgquery, pdb_rec_pkglist, plisthead) == 0)
return plisthead;
else
return NULL;
}
La structure Plisthead représente la tête d'une liste simplement chaînée dont les membres, de type Pkglist, sont composés des champs suivants :
typedef struct Pkglist {
char *pkgname; /* foo-1.0 */
char *comment; /* comment */
int64_t file_size; /* binary package size */
int64_t size_pkg; /* installed package size */
SLIST_ENTRY(Pkglist) next;
} Pkglist;
Après avoir alloué puis initialisé cette tête de liste, nous appelons la fonction pkgindb_doquery() en lui passant en paramètre la requête SQL, une fonction de callback qui remplira effectivement notre SLIST et la tête de liste plisthead :
static int
pdb_rec_pkglist(void *param, int argc, char **argv, char **colname)
{
Pkglist *plist;
Plisthead *plisthead = (Plisthead *)param;
if (argv == NULL)
return PDB_ERR;
/* PKGNAME was empty, probably a package installed
* from pkgsrc or wip that does not exist in
* pkg_summary(5), return
*/
if (argv[0] == NULL)
return PDB_OK;
XMALLOC(plist, sizeof(Pkglist));
XSTRDUP(plist->pkgname, argv[0]);
plist->size_pkg = 0;
plist->file_size = 0;
/* classic pkglist, has COMMENT and SIZEs */
if (argc > 1) {
if (argv[1] == NULL) {
/* COMMENT or SIZEs were empty
* not a valid pkg_summary(5) entry, return
*/
XFREE(plist->pkgname);
XFREE(plist);
return PDB_OK;
}
XSTRDUP(plist->comment, argv[1]);
if (argv[2] != NULL)
plist->file_size = strtol(argv[2], (char **)NULL, 10);
if (argv[3] != NULL)
plist->size_pkg = strtol(argv[3], (char **)NULL, 10);
} else
/* conflicts or requires list, only pkgname needed */
plist->comment = NULL;
SLIST_INSERT_HEAD(plisthead, plist, next);
return PDB_OK;
}
On remarquera l'utilisation du paramètre param que l'on caste allégrement en Plisthead afin de peupler notre liste chaînée.
Voici donc comment remplir puis parcourir l'ensemble des paquets installés :
#define LOCAL_PACKAGES "SELECT PKGNAME,COMMENT,FILE_SIZE,SIZE_PKG FROM LOCAL_PKG;"
[...]
Plisthead *plisthead;
Pkglist *plist;
if ((plisthead = rec_pkglist(LOCAL_PACKAGES)) == NULL)
return;
SLIST_FOREACH(plist, plisthead, next)
printf("nom du package: %s\n", plist->pkgname);
Finalement, rassemblant les deux mondes, SQL ne me sert qu'à balayer une masse importante de données le plus rapidement possible, et les SLIST de structurer une plus petite portion de ces données dans des listes simplement chaînées en vue d'un post traitement.
4. Dubo, Dubon, Dubonnet
À chaque fois, je me dis que j'y échapperai, que ce n'est pas une fatalité, qu'il doit bien y avoir une autre méthode. Et non, encore une fois, il a fallu que je me fasse des nœuds au cerveau avec une… fonction récursive (cri d'horreur).
Car, en effet, si enregistrer les dépendances directes d'un logiciel est chose aisée grâce aux informations extraites du fichier pkg_summary(5), obtenir l'ensemble de l'arbre des dépendances d'un logiciel est une toute autre affaire. Exemple :
- foo-1.2 est dépendant de libbar-1.0 et libfoo-0.5 ;
- libbar-1.0 dépend de libpwet-0.9 ;
- libpwet-0.9 dépend de bla-3.1 et libprout-2.4 ;
L'arbre de dépendance complet de foo-1.2 est donc :
- libbar-1.0
- libpwet-0.9
- bla-3.1
- libprout-2.4
- libfoo-0.5
Et là, on sent poindre la nécessité de se fendre d'une fonction récursive, mais également assez générique pour gérer de la même manière dépendances récursives et dépendances récursives inverse (+10 points « soirée entre amis »).
Nous n'étudierons ici que l'opération d'enregistrement des dépendances récursives, la différence avec les dépendances inverses ne résidant que dans la requête SQL. La toute première tâche consiste donc à écrire la requête SQL qui permet de récupérer les dépendances directes d'un paquet :
#define DIRECT_DEPS " \
SELECT REMOTE_DEPS.REMOTE_DEPS_PKGNAME \
FROM REMOTE_DEPS,REMOTE_PKG \
WHERE REMOTE_PKG.PKGNAME GLOB \'%s-[0-9]*\'\
AND REMOTE_DEPS.PKG_ID = REMOTE_PKG.PKG_ID;\
"
On vérifie simplement l'exactitude de cette requête à l'aide de l'interpréteur SQLite :
sqlite> SELECT REMOTE_DEPS.REMOTE_DEPS_PKGNAME FROM
REMOTE_DEPS,REMOTE_PKG
WHERE REMOTE_PKG.PKGNAME GLOB 'gnome-terminal-[0-9]*'
AND REMOTE_DEPS.PKG_ID = REMOTE_PKG.PKG_ID;
GConf>=2.14.0
dbus-glib>=0.71
glib2>=2.16.0
gnome2-dirs>=1.5
gtk2+>=2.13.6
rarian>=0.6.0
startup-notification>=0.8nb1
vte>=0.19.1
xdg-dirs>=1.1
Il incombe maintenant à la fonction full_dep_tree() de se servir au mieux de cette requête afin de fournir la liste complète des dépendances. Aussi paradoxal que cela puisse paraître, ce tour de passe-passe tient en à peine vingt lignes :
On exécute d'abord notre requête :
if (pkgindb_doquery(query, pdb_rec_direct_deps, pdphead) == 0) {
On enregistre le degré de dépendance de cette branche. Cela nous servira plus tard à ordonner l'installation ou la désinstallation :
/* record dependency level for installation */
SLIST_FOREACH(pdp, pdphead, next)
if (!pdp->level) {
pdp->level = level;
}
Puis, nous parcourons cette branche et rappelons la même fonction pour opérer de la même façon sur les branches filles issues de cette requête. On notera la condition if (!pdp->computed) qui nous permet de ne pas repasser plusieurs fois par le même chemin :
SLIST_FOREACH(pdp, pdphead, next) {
if (!pdp->computed) {
/* been parsed */
pdp->computed = 1;
full_dep_tree(pdp->matchname, depquery,
pdphead, __func__, level + 1);
}
} /* SLIST_FOREACH */
}
Et voilà, nous disposons maintenant d'une liste simplement chaînée dont la tête est pdphead et qui contient l'arbre complet de dépendances du logiciel à installer. Les membres de cette liste sont composés des éléments suivants
typedef struct Pkgdeptree {
char *depname; /* foo>=1.0 */
char *matchname; /* foo */
int computed; /* recursion memory */
int level; /* recursion level */
int pkgkeep; /* autoremovable package ? */
int64_t file_size; /* binary package size, used in order.c and actions.c */
SLIST_ENTRY(Pkgdeptree) next;
} Pkgdeptree;
où :
- depname représente le nom de la dépendance ;
- matchname le nom exact du logiciel, sans informations de versionning ;
- computed est une « mémoire » permettant de ne pas repasser plusieurs fois par le même chemin ;
- level, le niveau de dépendance dont nous reparlerons plus tard ;
- pkgkeep, une information précisant s'il s'agit d'un package volontairement installé ou d'une dépendance ;
- file_size, la taille du paquet.
Grâce à ces nouvelles listes, nous pouvons désormais envisager plus sérieusement de manipuler notre lot de packages.
5. Impact dans 3, 2, 1...
Je l'avais pressenti depuis le début du projet, la charnière d'un logiciel de packaging réside dans le calcul d'impact de l'installation et de la mise à jour, traduction : « que va-t-il se passer lorsque je vais installer/mettre à jour ce paquet ? »
La réponse à cette question est a priori la clé de la réussite du projet.
Installer ou mettre à jour un paquet, c'est d'abord :
- S'assurer de la disponibilité du paquet
count_samepkg() renvoie le nombre d'occurrences du logiciel dans la liste des packages disponibles, si le nombre d'occurrences est zéro, le logiciel n'est pas disponible.
if ((pkgcount = count_samepkg(remoteplisthead, *ppkgargs)) == 0) {
/* package is not available on the repository */
printf(MSG_PKG_NOT_AVAIL, *ppkgargs);
continue;
}
- Récupérer la liste des dépendances de ce paquet
*ppkgargs est le paquet en cours d'analyse, issu de la ligne de commande, pdphead, la tête de la liste de dépendances, encore vide à cet instant.
full_dep_tree(*ppkgargs, DIRECT_DEPS, &pdphead, __func__, 0);
Puis :
- Pour chaque dépendance, si elle n'est pas présente, planifier son installation
localplisthead est la liste des paquets installés, dep_present() vérifie l'existence d'un package dans l'arbre d'impact.
SLIST_FOREACH(plist, localplisthead, next) {
localmatch = end_expr(localplisthead, plist->pkgname); /* foo| */
/* match, package is installed */
if (strncmp(localmatch, matchname, strlen(localmatch)) == 0) {
...
}
}
if (!dep_present(impacthead, pdp->matchname)) {
pimpact->oldpkg = NULL;
pimpact->action = TOINSTALL;
pimpact->pkgname = remotepkg;
/* record package dependency deepness */
pimpact->level = pdp->level;
pimpact->file_size = mapplist->file_size;
pimpact->size_pkg = mapplist->size_pkg;
}
- Si elle est présente, vérifier que le versionning est à jour :
if (!pkg_match(pdp->depname, plist->pkgname)) {
/* local pkgname didn't match deps, remote pkg has a
* lesser version than local package.
*/
if (version_check(plist->pkgname, remotepkg) == 1) {
/*
* proposing a downgrade is definitely not useful,
*/
toupgrade = DONOTHING;
- Si le versionning n'est pas à jour, planifier l'effacement du paquet, puis l'installation de la version à jour. Dans ce cas, comme pour un effacement de package, il faudra détecter l'impact de cet effacement, et, en particulier, s'il va entraîner la désinstallation de paquets dépendants de ce dernier. De plus, s'il existe plusieurs packages à désinstaller, leur ordre devra être minutieusement noté afin de nous épargner des warnings lors de leur éviction (order.c). On vérifie également que l'installation ou la mise à jour de ce logiciel ne pose pas de conflits de dépendance avec des paquets déjà installés, auquel cas on ajoute ces derniers à la liste d'impact en les marquant « à effacer ».
/* insert as an upgrade */
/* oldpkg is used when building removal order list */
XSTRDUP(pimpact->oldpkg, plist->pkgname);
pimpact->action = toupgrade;
pimpact->pkgname = remotepkg;
/* record package dependency deepness */
pimpact->level = pdp->level;
/* record binary package size */
pimpact->file_size = mapplist->file_size;
/* record installed package size */
pimpact->size_pkg = mapplist->size_pkg;
/* does this upgrade break depedencies ? (php-4 -> php-5) */
break_depends(impacthead, pimpact);
Enfin :
- Si le package cible est présent mais pas à jour, programmez son effacement et notez son niveau de dépendance, comme précédemment.
- L'ajouter à la liste des paquets à installer, elle aussi affublée d'un niveau de dépendance.
À ce stade, deux vérifications supplémentaires sont nécessaires :
- Le ou les paquets devant être installés entrent-ils en conflit avec d'autres paquets déjà installés.
LOCAL_CONFLICTS est un #define de requête SQL permettant de lister les conflits pour un paquet donné :
/* conflicts list */
conflictshead = rec_pkglist(LOCAL_CONFLICTS);
...
/* check for conflicts */
if (pkg_has_conflicts(conflictshead, pimpact)) {
if (!check_yesno()) {
free_impact(impacthead);
XFREE(impacthead);
return rc;
}
}
- Les fichiers système requis par les paquets à installer, hors dépendances donc, sont-ils présents ? Pour ce dernier point, rappelons que, sur un système BSD, le système n'est pas issu d'un package, mais consiste en un tout fonctionnel, cohérent et indépendant. Il constitue ce qu'il est convenu d'appeler le base system.
pkg_met_reqs() est une fonction qui retourne 0 dans le cas où des fichiers nécessaires sont manquants, 1 autrement :
/* check for required files */
if (!pkg_met_reqs(impacthead)) {
printf(MSG_REQT_MISSING);
return rc;
}
Et, puisque nous devons pouvoir passer en paramètre plusieurs packages à installer ou mettre à jour, toutes ces actions devront boucler jusqu'à la fin des paramètres passés en ligne de commande.
À l'issue de cette classification, il nous reste mettre en ordre la liste grâce aux informations enregistrées lors du parcours des dépendances :
- les chaînons d'effacement : du moins impactant au plus impactant ;
- les chaînons d'installation : du plus impactant au moins impactant.
Cette machinerie, avec quelques actions et vérifications supplémentaires, peuple le fichier impact.c. Dans ce fichier source, les fonctions-clés sont pkg_impact() et deps_impact(). Ces fonctions ont pour rôle de remplir une liste simplement chaînée de ce que j'ai pompeusement appelé « l'arbre d'impact ». Les membres de cette liste sont constitués des champs suivants
typedef struct Pkgimpact {
char *depname; /* depencendy pattern: perl-[0-9]* */
char *pkgname; /* real dependency name: perl-5.10 */
char *oldpkg; /* package to upgrade: perl-5.8 */
int action; /* TOINSTALL or TOUPGRADE */
int level; /* dependency level, inherited
from full dependency list */
int64_t file_size; /* binary package size */
int64_t size_pkg; /* installed package size */
SLIST_ENTRY(Pkgimpact) next;
} Pkgimpact;
où :
- depname représente la dépendance au format glob ;
- pkgname est le package à installer ou mettre à jour ;
- oldpkg, le paquet à effacer ;
- action définit l'action à mener : installation, effacement simple ou upgrade ;
- level est le niveau de dépendance ;
- file_size est la taille du package, en octets ;
- size_pkg représente la taille du paquet une fois installé sur le filesystem.
Une fois cette liste obtenue, une boucle de type SLIST_FOREACH dirigera le chaînon vers l'opération spécifiée dans le champ action. Une dernière opération viendra marquer le ou les packages passés en ligne de commande avec un drapeau keep, signifiant que ce sont des paquets installés volontairement, et non des dépendances.
6. Keep keep it together
Revenons sur une notion dont nous avons brièvement parlé dans les sections précédentes : les paquets marqués keepables.
Les utilisateurs d'apt(8) connaissent bien entendu la fonction autoremove permettant de faire le ménage dans les paquets installés, mais dont plus aucun logiciel ne dépend. Ces paquets ont comme dénominateur commun qu'ils ont été installés automatiquement, en tant que dépendance, au contraire des logiciels que vous avez souhaité installer, et donc passés volontairement à la ligne de commande. On note ces derniers avec un drapeau particulier : keep, que j'ai également appelé « non-autoremovable packages » pour « paquets non effaçables automatiquement ».
Après moult tâtonnements, l'algorithme de nettoyage des dépendances orphelines n'est finalement pas si compliqué. Revoyons la scène au ralenti :
Avant tout, enregistrons les paquets marqués keep :
/* test if there's any keep package and record them */
if ((plisthead = rec_pkglist(KEEP_LOCAL_PKGS)) == NULL)
errx(EXIT_FAILURE, MSG_NO_PKGIN_PKGS, getprogname());
Afin d'obtenir leurs arbres de dépendances :
/* record keep packages deps */
SLIST_FOREACH(pkglist, plisthead, next) {
XSTRDUP(pkgname, pkglist->pkgname);
trunc_str(pkgname, '-', STR_BACKWARD);
full_dep_tree(pkgname, LOCAL_DIRECT_DEPS,
&keephead, __func__, 0);
XFREE(pkgname);
}
Nous obtenons alors la liste des paquets non-keepables de cette façon :
/* record unkeep packages */
if ((plisthead = rec_pkglist(NOKEEP_LOCAL_PKGS)) == NULL) {
free_deptree(&keephead);
printf(MSG_ALL_KEEP_PKGS);
return;
}
Puis la balayons :
/* parse non-keepables packages */
SLIST_FOREACH(pkglist, plisthead, next) {
XSTRDUP(pkgname, pkglist->pkgname);
trunc_str(pkgname, '-', STR_BACKWARD);
exists = 0;
Afin de vérifier quels paquets dans cette liste sont des dépendances de logiciels marqués keepables :
/* is it a dependence for keepable packages ? */
SLIST_FOREACH(pdp, &keephead, next) {
if (strncmp(pdp->depname, pkgname, strlen(pkgname)) == 0) {
exists = 1;
break;
}
}
Si aucun logiciel ne réclame cette dépendance, elle peut être ajoutée à la liste des paquets à effacer en toute sérénité :
/* package was not found, insert it on removelist */
XMALLOC(premove, sizeof(Pkgdeptree));
XSTRDUP(premove->depname, pkglist->pkgname);
premove->matchname = NULL; /* safety */
premove->level = 0;
SLIST_INSERT_HEAD(&removehead, premove, next);
removenb++;
Reste à ordonner cette liste afin d'effacer ces logiciels du moins impactant au plus impactant, évitant ainsi de déplaisants warnings lors de leur effacement (order.c) :
orderedhead = order_remove(&removehead);
Et nous voici avec une liste ordonnée des éléments dont plus aucun logiciel ne dépend et dont nous pouvons nous débarrasser sans regrets.
Il y a une dernière utilité au drapeau keep : l'upgrade. En effet, une mise à jour rapide n'a pas forcément besoin de remettre à niveau l'ensemble des dépendances d'un logiciel si ces dernières suffisent au fonctionnement de ce dernier. Ainsi, l'upgrade simple n'opèrera que sur les packages marqués keep, alors qu'un upgrade total, plus long, mettra à jour l'ensemble des paquets avec les versions les plus récentes disponibles sur le repository.
7. Il voyait des lutins partout
Comme je l'ai précisé au début de cet article, je n'ai pas détaillé, loin s'en faut, tous les méandres du développement de pkgin, mais plutôt orienté l'analyse autour des concepts clés du logiciel, le reste n'étant que de la cuisine. Je fais grâce au lecteur de tous les aspects liés à la vérification de l'espace disque disponible, le téléchargement des archives [7] ou encore l'effacement de paquets qui coule de source (ah-ah) si l'on a compris la mécanique d'impact précédemment exposée.
Si j'avais initialement prévu pkgin pour NetBSD 4.0 et supérieurs, des utilisateurs d'autres systèmes se sont manifestés assez rapidement après la première milestone [8]. Ce furent d'abord des utilisateurs de DragonFly BSD qui après quelques simples modifications de headers ont compilé et testé le logiciel sans soucis particuliers. Vinrent ensuite des demandes pour Darwin, GNU/Linux et SunOS 5.11.
De manière assez surprenante, ces portages n'ont pas été spécialement pénibles, seuls quelques #ifdef's et macros associées ont été nécessaires. Fort heureusement, BSD make(1) nous simplifie grandement la tâche. Voici les sections relatives à la portabilité issues du Makefile de pkgin.
On enregistre les informations relatives au système :
OPSYS!= uname
Puis, fonction de ce dernier, on inclut des fichiers sources supplémentaires, nécessaires à la compilation, ainsi que d'éventuels defines qui seront interprétés lors du preprocessing :
.if ${OPSYS} == "Darwin" || ${OPSYS} == "SunOS" || ${OPSYS} == Linux
SRCS+= humanize_number.c
.endif
.if ${OPSYS} == "SunOS"
SRCS+= strsep.c
CPPFLAGS+= -DNB_STRSEP
.endif
Dépendant du système, les bibliothèques nécessaires varient. Ainsi, on conditionne leur ajout de cette façon :
.if ${OPSYS} != "Linux"
LDADD+= -lssl
.endif
.if ${OPSYS} == "Darwin" || ${OPSYS} == "SunOS"
LDADD+= -lcrypto
.endif
.if ${OPSYS} == "DragonFly"
LDADD+= -lutil
.endif
.if ${OPSYS} == "SunOS" || ${OPSYS} == "Linux"
LDADD+= -lnbcompat
.endif
.if ${OPSYS} == "SunOS"
LDADD+= -lnsl -lsocket -lresolv
LDADD+= -Wl,-R -Wl,${LOCALBASE}/lib -L${LOCALBASE}/lib -lsqlite3
.else
LDADD+= -Wl,-rpath -Wl,${LOCALBASE}/lib -L${LOCALBASE}/lib -lsqlite3
.endif
Enfin, l'un des headers, pkgindb_create.h, étant généré à grands coups de sed(1) et ce dernier ne possédant pas les mêmes options sur tous les systèmes, on utilise la même méthode :
.if ${OPSYS} == "SunOS"
SEDCMD= "nbsed -E"
.elif ${OPSYS} == "Linux"
SEDCMD= "sed -r"
.else
SEDCMD= "sed -E"
.endif
pkgindb_create.h:
@SEDCMD=${SEDCMD} ./mkpkgindb.sh > pkgindb_create.h
Reste à s'assurer que l'ensemble des fonctions classiques que nous utilisons dans notre projet sont disponibles sur la plateforme cible. Je n'ai de cesse de le répéter, pkgsrc est une caverne aux trésors, et, parmi eux, l'on trouve pkgtools/libnbcompat. La libnbcompat, comme l'indique son nom, est une compilation de fonctions qui permettent à un développeur NetBSD de simplement porter ses applications. Par le simple ajout de la ligne suivante :
USE_FEATURES= nbcompat
Dans le Makefile pkgsrc du package, on spécifie que le logiciel devra être statiquement lié à cette bibliothèque. De nombreuses fonctions manquantes sur certaines plateformes ou des fonctions n'existant que sous NetBSD seront accessibles via la libnbcompat. Il s'agit là de la seule particularité du Makefile pkgsrc de pkgin.
8. Et sinon, ça marche ?
À l'issue de ce trépident récit, je nous propose de regarder de plus près ce qui concernera le plus grand nombre, le logiciel lui-même !
Après trois mois de développement, pkgin est aujourd'hui utilisable. Il reste certes quelques améliorations à apporter, et nous ne sommes tout de même pas encore au niveau d'un apt(8), mais les nombreux testeurs – que je remercie sincèrement – semblent satisfaits du fonctionnement. Voyons de quoi est capable notre fameux gestionnaire de paquets.
Comme on peut s'y attendre, l'appel au logiciel sans paramètres affiche un message d'aide :
Usage: pkgin [-fhvy] command [package ...]
Commands and shortcuts:
list (ls) - Lists installed packages.
avail (av) - Lists available packages.
install (in) - Performs packages installation or upgrade.
update (up) - Creates and populates the initial database.
remove (rm) - Remove packages and depending packages.
upgrade (ug) - Upgrade main packages to their newer versions.
full-upgrade (fug) - Upgrade all packages to their newer versions.
show-deps (sd) - Display direct dependencies.
show-full-deps (sfd) - Display dependencies recursively.
show-rev-deps (srd) - Display reverse dependencies recursively.
keep (ke) - Marks package as "non auto-removable".
unkeep (uk) - Marks package as "auto-removable".
show-keep (sk) - Display "non auto-removable" packages.
search (se) - Search for a package.
clean (cl) - Clean packages cache.
autoremove (ar) - Autoremove orphan dependencies.
La toute première opération consiste à renseigner ou modifier le fichier contenant la liste des repositories
# echo ftp://ftp.fr.netbsd.org/pub/pkgsrc/packages/NetBSD/i386/5.0/All > /usr/pkg/etc/pkgin/repositories.conf
puis d'initialiser la base de donnée des packages disponibles via la commande :
# pkgin update
downloading pkg_summary.bz2: 100%
processing remote summary (ftp://ftp.netbsd.org/pub/pkgsrc/packages/NetBSD/i386/5.0/All)...
updating database: 100%
À partir de cet instant, pkgin est utilisable. Il est possible de lister les paquets disponibles
# pkgin avail|head
0verkill-0.16 0verkill is bloody 2D action deathmatch-like game in ASCII-ART
2vcard-0.5 Convert an addressbook to the popular VCARD file format
3DKit-0.3.1r2nb14 Objective-C 3D graphics foundation class library
3ddesktop-0.2.9 3D Virtual Desktop Switcher
3proxy-0.5.3.11nb1 Multi-protocol proxy
4stAttack-2.1.4nb5 Game in which you have to try to out-smart your opponent
6tunnel-0.09 v4/v6 protocol translation
7plus-225nb1 Uuencode-like file coder for AMPR BBS S&F of binary files
855resolution-0.4nb2 BIOS VESA resolution utility for 855/865/915 Intel chips
915resolution-0.5.2 BIOS VESA resolution utility for 8XX/9XX Intel chips
lister les paquets installés
# pkgin ls|head
ImageMagick-6.4.8.3 Package for display and interactive manipulation of images
apache-2.2.11nb1 Apache HTTP (Web) server, version 2
apr-1.3.3 Apache Portable Runtime
apr-util-1.3.4nb1 Apache Portable Runtime utilities
autoconf-2.63 Generates automatic source code configuration scripts
automake-1.10.1 GNU Standards-compliant Makefile generator
bash-3.2.48 The GNU Bourne Again Shell
boehm-gc-7.1nb1 Garbage collection and memory leak detection for C and C++
cairo-1.8.6 Vector graphics library with cross-device output support
ou encore chercher, possiblement grâce à des expressions régulières, un paquet dans la liste des paquets disponibles :
[~/src/pkgin] ./pkgin search irssi
irssi-0.8.12nb2 Secure and modular IRC client with text mode user interface
irssi-icb-0.14nb12 Irssi plugin to access ICB networks
[~/src/pkgin] ./pkgin search ^p5-.*-XML
p5-Class-XML-0.06nb1 Perl 5 module providing a simple XML abstraction
p5-RPC-XML-0.64 XML-RPC client and server library for Perl
p5-WordPress-XMLRPC-1.18 = Perl 5 API to WordPress XML-RPC services
=: package is installed and up-to-date
<: package is installed but newer version is available
>: installed package has a greater version than available package
Mais, évidemment, ce que nous souhaitons utiliser par-dessus tout, ce sont les capacités d'installation et de mise à jour. Voici un petit scénario qui va nous permettre de constater ce fonctionnel.
Forçons tout d'abord pkgin à mettre à jour sa base de donnée sur un ancien repository en renseignant la variable PKG_REPOS, ce qui aura pour effet de supplanter le fichier repositories.conf :
# PKG_REPOS=ftp://ftp.netbsd.org/pub/pkgsrc/packages/NetBSD/i386/5.0_2008Q4/All pkgin up
cleaning database from ftp://ftp.netbsd.org/pub/pkgsrc/packages/NetBSD/i386/5.0/All entries...
downloading pkg_summary.bz2: 100%
processing remote summary (ftp://ftp.netbsd.org/pub/pkgsrc/packages/NetBSD/i386/5.0_2008Q4/All)...
updating database: 100%
Installons alors un paquet dont nous savons qu'il a évolué depuis :
# pkgin in pkg_select
calculating dependencies for pkg_select...
nothing to upgrade.
1 packages to be installed: pkg_select-20050817nb2 (38K to download, 66K to install)
proceed ? [y/N] y
downloading packages...
downloading pkg_select-20050817nb2.tgz: 100%
installing packages...
installing pkg_select-20050817nb2...
pkg_add: Warning: package `/var/db/pkgin/cache/pkg_select-20050817nb2.tgz' was built for a different version of the OS:
pkg_add: NetBSD/i386 5.0_BETA (pkg) vs. NetBSD/i386 5.0_RC4 (this host)
pkg_select-20050817nb2: copying /usr/pkg/share/examples/pkg_select/pkg_select.conf.example to /usr/pkg/etc/pkg_select.conf
processing local summary...
updating database: 100%
marking pkg_select-20050817nb2 as non auto-removeable
Puis, remettons notre base à jour en utilisant les informations présentes dans le fichier repositories.conf :
# pkgin up
cleaning database from ftp://ftp.fr.netbsd.org/pub/pkgsrc/packages/NetBSD/i386/5.0_2008Q4/All entries...
downloading pkg_summary.bz2: 100%
processing remote summary (ftp://ftp.fr.netbsd.org/pub/pkgsrc/packages/NetBSD/i386/5.0/All)...
updating database: 100%
Et lançons une mise à jour rapide :
# pkgin upgrade
calculating dependencies for sudo-1.7.0...
calculating dependencies for pkg_select-20090308nb1...
calculating dependencies for adobe-flash-plugin-10.0.0.525...
calculating dependencies for php4-mysql-4.4.9...
calculating dependencies for eterm-0.9.4nb1...
calculating dependencies for bash-4.0.10...
calculating dependencies for screen-4.0.3nb2...
calculating dependencies for sqlite3-3.6.11...
1 packages to be upgraded: pkg_select-20050817nb2
1 packages to be installed: pkg_select-20090308nb1 (41K to download, 74K to install)
proceed ? [y/N] y
downloading packages...
downloading pkg_select-20090308nb1.tgz: 100%
removing packages to be upgraded...
removing pkg_select-20050817nb2...
installing packages...
installing pkg_select-20090308nb1...
pkg_add: Warning: package `/var/db/pkgin/cache/pkg_select-20090308nb1.tgz' was built for a different version of the OS:
pkg_add: NetBSD/i386 5.0_RC3 (pkg) vs. NetBSD/i386 5.0_RC4 (this host)
pkg_select-20090308nb1: copying /usr/pkg/share/examples/pkg_select/pkg_select.conf.example to /usr/pkg/etc/pkg_select.conf
processing local summary...
updating database: 100%
marking sudo-1.7.0 as non auto-removeable
marking pkg_select-20090308nb1 as non auto-removeable
marking adobe-flash-plugin-10.0.0.525 as non auto-removeable
marking php4-mysql-4.4.9 as non auto-removeable
marking eterm-0.9.4nb1 as non auto-removeable
marking bash-4.0.10 as non auto-removeable
marking screen-4.0.3nb2 as non auto-removeable
marking sqlite3-3.6.11 as non auto-removeable
Comme prévu, une mise à jour avec la nouvelle version du logiciel pkg_select est proposée, l'ancienne est effacée et la plus récente, installée. Le lecteur averti aura noté le warning de pkg_add qui informe que la plateforme sur laquelle a été compilé le logiciel à installer est différente de la plateforme sur laquelle nous effectuons l'installation. Dans ce cas, il s'agit d'un paquet compilé sous NetBSD 5.0RC3 à installer sur une machine NetBSD 5.0RC4, aucun risque.
Il aurait également été possible d'utiliser la commande
# pkgin fug
qui, au lieu de ne mettre à jour que les paquets marqués keepables et les dépendances qui ne seraient plus suffisantes, parcourrait l'intégralité des paquets installés afin de les mettre à jour. Cette opération est évidemment un peu plus longue.
Un dernier scénario va nous permettre de constater le fonctionnement de la fonction autoremove. Installons un logiciel possédant une liste conséquente de dépendances :
# pkgin in claws-mail
calculating dependencies for claws-mail...
nothing to upgrade.
17 packages to be installed: ispell-base-3.3.02
hunspell-1.2.8 libgpg-error-1.6 opencdk-0.6.6
lzo-2.03 libtasn1-1.8 libgcrypt-1.4.4 libcfg+-0.6.2nb3
cyrus-sasl-2.1.23 startup-notification-0.9 libetpan-0.57nb1
hicolor-icon-theme-0.10nb1 gnutls-2.6.6 enchant-1.4.2nb1
db4-4.7.25.3 compface-1.5.2nb1
claws-mail-3.7.0 (21M to download, 75M to install)
proceed ? [y/N] y
[...]
processing local summary...
updating database: 100%
marking claws-mail-3.7.0 as non auto-removeable
Effaçons maintenant ce logiciel qui a comme propriété d'être keepable, donc installé volontairement :
# pkgin rm claws-mail
1 packages to delete: claws-mail-3.7.0
proceed ? [y/N] y
removing claws-mail-3.7.0...
processing local summary...
updating database: 100%
Puis, faisons le ménage grâce à la fonction autoremove :
# pkgin ar
in order to remove packages from the autoremove list, flag those with the -k modifier.
16 packages to be autoremoved: compface-1.5.2nb1 enchant-1.4.2nb1 gnutls-2.6.6
hicolor-icon-theme-0.10nb1 libetpan-0.57nb1 startup-notification-0.9
db4-4.7.25.3 cyrus-sasl-2.1.23 libcfg+-0.6.2nb3 libtasn1-1.8
lzo-2.03 opencdk-0.6.6 hunspell-1.2.8 ispell-base-3.3.02
libgcrypt-1.4.4 libgpg-error-1.6
proceed ? [y/N] y
removing compface-1.5.2nb1...
removing enchant-1.4.2nb1...
removing gnutls-2.6.6...
removing hicolor-icon-theme-0.10nb1...
removing libetpan-0.57nb1...
removing startup-notification-0.9...
removing db4-4.7.25.3...
removing cyrus-sasl-2.1.23...
=========================================
The following users are no longer being
used by cyrus-sasl-2.1.23,
and they can be removed if no other
software is using them:
cyrus
=========================================
=========================================
The following groups are no longer being
used by cyrus-sasl-2.1.23,
and they can be removed if no other
software is using them:
=========================================
removing libcfg+-0.6.2nb3...
removing libtasn1-1.8...
removing lzo-2.03...
removing opencdk-0.6.6...
removing hunspell-1.2.8...
removing ispell-base-3.3.02...
removing libgcrypt-1.4.4...
removing libgpg-error-1.6...
processing local summary...
updating database: 100%
Et voila ! 16 paquets effacés plus le logiciel cible. Nous retrouvons bien notre compte de 17 paquets précédemment installés.
Notons que cette fonction, couplée aux commandes keep et unkeep permet à l'utilisateur de nettoyer une machine assez simplement. En effet, il suffit de keeper quelques paquets-clés, unkeeper le reste, et demander un autoremove afin de faire le ménage.
Conclusion : J'achète !
Pkgin, à l'heure où j'écris cet article, n'est présent que sur pkgsrc-wip [9], mais je pense le commiter dans l'arbre pkgsrc officiel courant juin, ce qui en fait un candidat potentiel pour la release binaire pkgsrc-2009Q2. Des rumeurs laissent entendre qu'il pourrait faire son entrée dans le base system de NetBSD 6.0, mais cette information est à prendre avec précaution. Plusieurs éléments, notamment cosmétiques, restent à parfaire, et le jeune logiciel n'est sans aucun doute pas exempt de bugs. Aussi, si vous souhaitez participer à sa maturation, n'hésitez pas à me contacter par mail ou sur IRC, sur le réseau Freenode [10].
Liens
[2] http://www.dracolinux.org/
[3] http://www.dragonflybsd.org/docs/handbook/handbook-pkgsrc/
[4] http://en.wikipedia.org/wiki/Glob_(programming)
[5] http://www.netbsd.org/contrib/soc-projects.html#pkg-update-tool
[6] http://wiki.netbsd.se/Desktop_Project
[8] http://mail-index.netbsd.org/tech-pkg/2009/04/14/msg003070.html
[9] http://pkgsrc-wip.sourceforge.net/
[10] http://freenode.net/