Pour avoir plusieurs fois rencontré la situation et avoir téléchargé les dépendances une à une sur CPAN et les copier sur le serveur au fur et à mesure que l'installation d'un module plante, je me suis dit qu'il doit y avoir plus intelligent...
Il me semblait donc utile de pouvoir récupérer un package de toutes les dépendances de mon appli, avec toutes les dépendances des dépendances et encore les dépendances de ces dernières. CPAN est là pour ça me diriez vous... Effectivement, mais la tâche n'est pas si simple, car sur CPAN les dépendances n'apparaissent pas. Enfin, n'apparaissaient pas car depuis peu CPAN donne accès à un listing des mais qui reste fastidieux car il faut encore aller chercher chaque module à la main.
Il existe plusieurs modules qui pourraient faire l'affaire sur CPAN (CPAN::Dependency ou Module::ScanDeps pour ne citer qu'eux) mais c'est l'occasion d'aller mettre les mains dans le cambouis pour comprendre les coulisses de ce superbe repository, et de créer une archive de tous les packages d'un coup dans la foulée.
L'objectif de ce post est donc de détailler les différentes étapes pour :
- Récupérer récursivement toutes les dépendances d'un module
- Stocker quelque part toutes les archives des packages à installer
- Générer un listing ordonné des packages pour dérouler l'installation
- comparer le listing avec les packages déjà installé
- installer tous les packages manquants
La solution CPAN::Mini
Une petite parenthèse sur la solution CPAN::Mini : elle ne permet pas de faire exactement ce que propose ce post, mais est beaucoup plus simple à mettre en oeuvre dans certains cas.Le CPAN::Mini, ou minicpan, permet d'installer en local une version miniature (uniquement les dernières versions et seulement les fichiers utiles à la commande cpan) du repository CPAN. Le minicpan se comporte alors via à vis de lui même ou d'une autre machine comme un référentiel CPAN et peut ainsi permettre d'installer simplement des modules sur une machine qui n'a pas accès à Internet.
Schématiquement :
![]() |
Schéma minicpan |
Pour faire le parallèle avec le monde RedHat & dérivés, elle doit pouvoir être comparable à un référentiel de rpm interne interrogé directement par yum, et bien utilisé elle doit permettre aussi bien la mise à dispo des paquets que la gestion des versions et l'inventaire des paquets installés sur une machine.
A creuser, je ne m'attarde pas plus là dessus dans ce post, l'objectif étant également de comprendre les mécanismes basiques sous-jacent de CPAN.
Petit détour sur la structure de CPAN
Avant de commencer à naviguer programmatiquement dans CPAN, attardons nous un peu sur la structure des infos qu'on peut y trouver. N'ayant jamais utilisé CPAN au delà de cpan install mon_module, j'ai passé pas mal de temps à comprendre cette structure, somme toute assez simple.Modules & distributions
La recherche sur www.cpan.org se fait via module : par exemple XML::Twig, CPAN::Distribution ou DBD::Oracle.Les modules sont mis à disposition dans des distributions qui contiennent un ou plusieurs modules : DBD-Oracle-1.44 par exemple pour le module DBD::Oracle. Cette distribution contient d'autres modules, notamment DBD::Oracle::GetInfo ou DBD::Oracle::Object. La distribution est identifié par le fichier archive qui la contient, P/PY/PYTHIAN/DBD-Oracle-1.44.tar.gz pour notre exemple.
Ce sont donc les distributions qui vont nous intéresser puisque installer une distribution revient à installer tous les modules qu'il contient. Nous allons donc chercher à établir la liste de toutes les distributions, dans le bon ordre, à installer pour satisfaire toutes les dépendances du module qui nous intéresse.
Le shell CPAN permet d'interroger les modules et les distributions avec les commandes m (comme module) et d (comme distribution) :
cpan> m DBD::OracleModule id = DBD::Oracle
DESCRIPTION Oracle Driver for DBI
CPAN_USERID DBIML (DBI Mailing Lists <dbi-users@perl.org>)
CPAN_VERSION 1.44
CPAN_FILE P/PY/PYTHIAN/DBD-Oracle-1.44.tar.gz
DSLI_STATUS MmcO (mature,mailing-list,C,object-oriented)
MANPAGE DBD::Oracle - Oracle database driver for the DBI module
INST_FILE /usr/lib/perl5/site_perl/5.8.5/i386-linux-thread-multi/DBD/Oracle.pm
INST_VERSION 1.21
cpan> d P/PY/PYTHIAN/DBD-Oracle-1.44.tar.gzOn voit ici qu'installer DBD::Oracle revient à installer la distribution P/PY/PYTHIAN/DBD-Oracle-1.44.tar.gz qui contient les modules listés sur la ligne CONTAINSMODS.
Distribution id = P/PY/PYTHIAN/DBD-Oracle-1.44.tar.gz
CPAN_USERID DBIML (DBI Mailing Lists <dbi-users@perl.org>)
CONTAINSMODS DBD::Oracle::db DBD::Oracle::GetInfo DBD::Oracle::st DBD::Oracle::Object DBD::Oracle DBD::Oracle::dr DBD::Oracle::Troubleshooting
Installation
L'installation d'une distribution (et non pas d'un module puisque c'est la distribution qui est téléchargée, compilée et installée) récupérée sur CPAN suit toujours le même schéma :# perl Makefile.PLPuis compilation et test du module :
# make && make testSi le test est OK, il ne reste plus qu'à l'installer :
# make installSi des dépendances sont manquantes, soit la compilation échoue, soit le test échoue.
Dépendances
Où sont les dépendances ? C'est bien ce qui nous intéresse mais les commandes m ou d ne les affichent pas. Malheureusement, elle ne semblent pas accessible directement via CPAN et il faut chercher un peu.Dans la plupart des cas, la distribution contient un fichier META.yml qui décrit le package et contient des 3 sections de dépendance :
- build_requires
- configure_requires
- requires
Intéragir avec CPAN en perl
C'est parti pour les choses intéressantes. Le but de la manœuvre est d'avoir un script qui va :- Se connecter à CPAN
- Récupérer la distribution du module qui nous intéresse
- Extraire les dépendances
- Recommencer sur chacune des dépendances
- Enregistrer toutes les archives tar.gz récupérée de CPAN dans un coin
- Générer une liste des distributions dans l'ordre des dépendances pour pouvoir les installer
La documentation de l'inteface Perl du shell cpan n'est pas très fournie, l'essentiel se trouve sur la page du module CPAN : http://search.cpan.org/~andk/CPAN-1.9800/lib/CPAN.pm#Methods_in_the_other_Classes
Un petit exemple sera plus clair qu'un long discours. Reprenons l'exemple ci dessous : récupérer le nom de la distribution contenant DBD::Oracle et récupérer l'objet CPAN::Distribution qui correspond :
Il ne nous reste plus qu'à télécharger et extraire la distribution à l'aide de la méthode CPAN::Distribution::get(), et analyser le fichier META.yml qui se trouvera dans le dossier CPAN::Distribution::dir() (là où la distribution a été extraite) pour télécharger l'archive, l'extraire, analyser le fichier META.yml et recommencer !# Récupération de l'objet CPAN::Module pour DBD::Oraclemy $mod = CPAN::Shell->expand("Module","DBD::Oracle");# Récupération du nom de la distribution ($dist_file contient P/PY/PYTHIAN/DBD-Oracle-1.44.tar.gz)my $dist_file = $mod->cpan_file();# Récupération de l'objet CPAN::Distributionmy $dist = CPAN::Shell->expand("Distribution",$dist_file);
# Téléchargement et extraction de l'archive$dist->get();
# Répertoire où est extraite l'archivemy $directory = $dist->dir();
my $meta_file = "$directory/META.yml"
Exemple pour copier l'archive .tar.gz dans un dossier $dossier_sortie :
copy($dist->{localfile}, $dossier_sortie);
Putting it all together : lister les dépendances
L'algorithme est somme toute assez simple : on récupère la distribution, on récupère la liste des dépendances et on exécute le même algo récursivement sur chaque dépendance, en construisant l'arbre de dépendance au fur et à mesure. Pour éviter de boucler indéfiniment, on enregistre simplement les modules déjà récupérés dans une table.Arbre de dépendence : structure
L'arbre de dépendence va servir à ordonner la liste des dépendence dans l'ordre d'installation. La structure sera un simple pointeur vers une table, chaque élément étant lui même un pointeur vers une table de hashage des sous-dépendences. A chaque appel récursif, la variable $dependency_tree va pointer sur la table de dépendence du père (donc celui qui dépend du module analyser dans l'appel en cours). L'enregistrement du fils se fait simplement par :# Ajout de la distribution a l'arbre de dependenceLa table de dépendence de la distribution en cours d'installation est donc $dependency_tree->{$dist_file}. C'est cette valeur qui sera donc passé en paramètre de l'appel récursif de toutes les dépendences trouvées dans le META.ymlde cette distribution.
$dependency_tree->{$dist_file} = {};
Pour éviter de boucler indéfiniement, les distributions déjà rencontrées sont enregistrées dans la table %downloaded_dists, en y stockant au passage le nom du module demandé (il nous servira à faire le ménage sur le serveur cible pour n'installer que les distributions qui ne sont pas déjà présentes).
On ne stocke dans l'arbre que les dépendences qui n'ont pas déjà été rencontrée par les autres distributions. Ne pas les stocker permettera de sortir la liste dans l'ordre d'installation en un parcours en profondeur du graph : si une distribution dépend d'un module qui a déjà été rencontré par une autre distribution, ce module aura sera rencontré avant dans le parcours en profondeur.
Une petite exception à celà : le module ExtUtils::MakeMaker est une dépendence de tous les modules, puisqu'il est utilisé par cpan pour l'installation, y compris les modules dont il dépend comme Data::Dumper... On va l'éliminer de l'arbre de dépendence car il crée des dépendences récursives.
Voici par exemple l'arbre de dépendence du module DBD::Oracle :
![]() |
Graph de dépendence de DBD::Oracle |
Seuls les noeuds gris sont enregistrés dans l'arbre de dépendence. On voit sur le cas de Test-Simple que le parcours en profondeur permettera de lister le noeud gris avant le noeud jaune.
Récupération des dépendences et appel récursif
Nous avons déjà vu comment récupérer la distribution associée à un module, l'enregistrer dans un coin pour constituer notre package global et la décompresser pour pouvoir accéder au META.yml.Parser le META.yml est très simple gràce au module YAML :
my $file = "$directory/META.yml";
if( -f $file) {
my $meta = YAML::LoadFile($file);
my $requires = $meta->{'build_requires'};
my %reqs = %$requires;
foreach my $required_mod (keys(%reqs)) {
get_dist_and_dependencies($required_mod,$dependency_tree->{$dist_file},$level+1);
}
}
La function get_dist_and_dependencies correspond à l'appel récursif.
Code global
Le code global est publié sur GitHub à l'adresse suivante : https://github.com/...Il peut bien sur être amélioré pour mieux gérer les erreurs et la sortie. Il a dans sa forme l'avantage d'être simple et facilement comprehensible avec les explications de ce post.
Seul un passage est légèrement obscur :
open STDOUT_OLD,">&STDOUT";Ce drôle de code sert à éliminer la sortie standard avant l'appel à get() pour éviter d'avoir toute la sortie du log cpan s'afficher et mieux voir l'arbre de dépendence se construire au fur et à mesure des appels récursifs.
open STDOUT,">/dev/null";
$dist->get();
close STDOUT;
open STDOUT, ">&STDOUT_OLD";
close STDOUT_OLD;
Installer le tout sur le serveur cible
Nous avons maintenant toutes nos distribution sous la main, et l'ordre dans lequel les installer.Certaines sont peut être déjà installée sur le serveur, autant aller jusqu'au bout de la démarche et n'installer que ce qui manque.
Rappelez vous le petit détail des paragraphes précédents que nous n'avons pas encore exploité : nous avons associé à chaque distribution le nom du module requit qui a ammené à l'inclure.
Cela parce qu'il est beaucoup plus simple de lister les modules installées que les distributions.
ExtUtils::Installed est là pour ça et propose tout simplement une méthode modules() qui liste les modules installés. Et pour faire encore plus simple, le script instmodsh fournit une interface à ExtUtils::Installed, et à modules() avec la commande l. Même pas de Perl à écrire ;)
Donc c'est parti : on extrait la liste des modules installées avec instmodsh, et avant d'installer chaque module on vérifie si il n'est pas présent dans cette liste.
En shell, si le fichier install.txt est la sortie de notre script (au format "distribution;module") et installed le fichier qui contient la liste des modules installés, ça donne :
for i in `cat install.txt `; doOn pourrait faire plus propre avec un petit script qui appelle ExtUtils::Installed et qui déroule lui même l'installation (cpan le fait bien !), mais ce sera pour une autre fois ;)
dist=`echo $i | cut -d";" -f1`;
module=`echo $i | cut -d";" -f2`;
if grep -q "^$module$" installed; then
echo "$dist: SKIP";
else
echo "$dist: INSTALL";
# Decommentez cette ligne pour lancer l'installation dans la foulé
#(cd `echo $dist | sed -e 's/.tar.gz//'`; perl Makefile.PL && make && make test && make install) || break ;
fi;
done