Lexpage c'est comme un cheval sans grenouille !    —  smikar

Discussions

Résolution des contraintes de dépendances pour pom.xml

Guybrush 8431 Bob
Bonjour à tous,

Vous le savez, je suis pas du tout un Java-iste, et mes connaissances dans le domaine sont plutôt limitées :-)
Suite à une collaboration avec des chercheurs d'une autre université, on se retrouve à étudier une thématique qui va nécessiter de résoudre les contraintes de dépendances. J'ai écrit, il y a quelques mois, une librairie en Python pour représenter ces contraintes à l'aide de disjonctions d'intervalles, et j'ai par la même occasion écrit quelques parseurs de contraintes pour NPM, Packagist, Cargo et Rubygems. Mais pas pour Java. Quelle erreur ! :-D

Du coup, avant de me lancer dans un tel parseur (parce que les cas spécifiques sont très très nombreux pour les contraintes exprimées dans les fichiers pom.xml), je me demandais s'il n'y avait pas un petit projet ou un plugin pour Maven qui permette, étant donnée une liste de versions et une contrainte de version, de lister quelles sont les versions qui satisfont la contrainte (ou mieux : quelle est la version installée par Maven si on avait exécuté le build associé !).

Je suppose qu'il doit y avoir du code dans Maven permettant de faire ça (bah oui : c'est *au moins* résolu en interne ce problème), mais le "hic" c'est que (1) bah je suis pas java-iste, et ma lecture du Java est assez limitée surtout dans des projets de l'envergure de Maven, et (2) Maven ne propose pas d'API permettant de manipuler ça directement. La seule API disponible permet de résoudre des contraintes, mais elles sont évaluées directement contre les versions disponibles sur Maven Repository, ce qui ne m'arrange pas vraiment vu que je souhaite m'en servir pour l'évolution des projets (et donc je dois pouvoir éliminer certaines versions pour me placer dans la situation d'il y a plusieurs mois, voire plusieurs années).
Fabe 610 Geek
Maven c'est un peu l'ancêtre de tout ça, il va beaucoup moins loin en termes d'expressions de contraintes (il ne supporte pas semver) et ne propose pas de "lock-file", notamment.

Je n'ai pas fait de Maven ces derniers temps donc mes connaissances peuvent être datées bien que le projet n'ait pas connu de révolution depuis un bon moment.

En gros en Maven tu dépends d'une "release" ou d'un "snapshot". le second ayant la particularité d'être updatable, c'est à dire que pour le même numéro de version tu peux récupérer une version plus récente de la dépendance à chaque build. (genre 2.0.1-SNAPSHOT)

A l'inverse une release est fixe, donc tu es plus ou moins assuré de récupérer toujours la même version.
Les dépendances transitives ne sont pas directement gérées: si tu demande une dépendance en 1.0.0 et qu'une de tes dépendances veut la même dépendance en 2.0.0, ben Maven va récupérer la 1.0.0, en causant probablement des soucis dans ta dépendance intermédiaire qui devra se démerder avec ça.

Derrière il y a la notion de scope, d'exclude et plein de trucs.

Bref, car je réalise que tu savais ptet déjà tout ça, du coup pour t'aider, je dirais bien de bricoler avec le plugin "dependency:tree" ou pourquoi pas du côté de ce genre de projet : github.com/ltearno/pom-e… mais je ne sais pas exactement ce que ça vaut.
Guybrush 8431 Bob
FabeMaven c'est un peu l'ancêtre de tout ça, il va beaucoup moins loin en termes d'expressions de contraintes (il ne supporte pas semver) et ne propose pas de "lock-file", notamment.
Je ne sais pas ce qui a poussé mes collègues à considérer Maven plutôt qu'un truc plus récent (j'ai en tête Gradle, je sais pas si c'est le "plus récent" ou pas ?). Par contre, on n'a pas directement ciblé "Maven" mais plutôt les "pom.xml" mais il me semble que c'est étroitement lié (Gradle utilise son propre mécanisme, même s'il peut gérer les pom.xml également).
FabeEn gros en Maven tu dépends d'une "release" ou d'un "snapshot". le second ayant la particularité d'être updatable, c'est à dire que pour le même numéro de version tu peux récupérer une version plus récente de la dépendance à chaque build. (genre 2.0.1-SNAPSHOT)
C'est ce que mes collègues pensaient aussi car en pratique, on retrouve presque toujours un numéro de version dans le champ correspondant, mais en réalité, la syntaxe autorise des contraintes de dépendances (que l'on exprime sous forme d'intervalles, un peu comme avec NuGet d'ailleurs).
FabeBref, car je réalise que tu savais ptet déjà tout ça, du coup pour t'aider, je dirais bien de bricoler avec le plugin "dependency:tree" ou pourquoi pas du côté de ce genre de projet : github.com/ltearno/pom-e… mais je ne sais pas exactement ce que ça vaut.
Dependency:tree utilise directement les données de Maven Central, et même si on peut lui indiquer un autre repository, ça veut dire qu'on va devoir générer un tel repository pour ne lister que les dépendances qui étaient disponibles "historiquement". Mais je vais jeter un oeil à pom-explorer, vu qu'on veut s'attaquer aux dépendances "internes" à la fondation Apache, ça a du sens d'utiliser quelque chose de multi-projects comme pom-explorer ! Merci !
Fabe 610 Geek
Guybrushla syntaxe autorise des contraintes de dépendances
Mouif, enfin comme il n'y a pas de lockfile, tu prends le risque de builder avec une autre version que celle avec laquelle tu as développé, et finalement la seule version qui compte vraiment est celle qui se trouve dans le classpath au moment du run. Spécifier une plage de version présente plus de risque que de bénéfices.
Guybrushj'ai en tête Gradle, je sais pas si c'est le "plus récent" ou pas ?
C'est bien plus récent, moderne, et développé avec beaucoup plus de moyens que Maven.
En revanche pour un éditeur de lib Java, il "faut" être disponible sous Maven pour exister. Analyser les pom ça a du sens je pense.

Gradle vise plutôt le marché du build d'applications "finales".


Ce message a été modifié 1 fois. Dernière modification : 10 juin 2018 à 21:25 par Fabe.

Guybrush 8431 Bob
FabeMouif, enfin comme il n'y a pas de lockfile, tu prends le risque de builder avec une autre version que celle avec laquelle tu as développé, et finalement la seule version qui compte vraiment est celle qui se trouve dans le classpath au moment du run. Spécifier une plage de version présente plus de risque que de bénéfices.
Ok, c'est donc relativement similaire à la plupart des autres "écosystèmes" (npm, PyPI, Rubygems, ...). Dans notre cas, on utilise surtout les contraintes (et pas les lock files) car cela représentent la "latitude" que le mainteneur donne à ses dépendances (et c'est une source de problèmes, effectivement, donc on étudie notamment ça pour trouver des solutions autre que les lock files).

Concernant le "présente plus de risques que de bénéfices", je ne partage pas tout à fait le point de vue. On a sorti récemment une étude à propos de npm et des vulnérabilités dans les packages distribués sur la plateforme, et c'est ahurissant de voir la durée nécessaire pour qu'un fix de sécurité soit "propagé" dans les paquets dépendants. Si on a trouvé qu'il faut en général moins de 3 mois pour qu'un fix soit déployé dans le paquet concerné, il faut près de 18 mois dans 50% des cas pour que le fix arrive dans les paquets dépendants (premier niveau de dépendances, on n'a même pas regardé transitivement ce que ça pouvait donner !).

Et pourtant, npm est supposé être un des bons candidats quant au respect de semver (ce qui signifie que tu peux normalement utiliser une contrainte qui accepte, les yeux fermés, les patchs voire les mineures).
FabeC'est bien plus récent, moderne, et développé avec beaucoup plus de moyens que Maven.
En revanche pour un éditeur de lib Java, il "faut" être disponible sous Maven pour exister. Analyser les pom ça a du sens je pense.
Ok, super ! Comme indiqué, ma connaissance du monde Java est vraiment faible, surtout que j'avais laissé Maven (pom.xml) de coté ces dernières années parce que ce type de manifest est vraiment ch... à analyser par rapport à d'autres écosystèmes (expansion des variables, héritage des pom.xml, etc. ça nécessite plus de travail que de simplement lire un package.json en npm :-D Et vu qu'on travaille à grande échelle (ex: 8 millions de versions à traiter sur npm !), le moindre travail supplémentaire à réaliser représente potentiellement des jours de calcul supplémentaires !). C'est aussi la raison pour laquelle j'avais laissé tombé PyPI (il faut grosso-modo exécuter setup.py pour espérer récupérer des informations fiables, l'analyse statique du manifest n'est pas suffisante :-/).
Fabe 610 Geek
Guybrushtrouver des solutions autre que les lock files
Il y a des hybrides, je connais des projets qui "gitignorent" le lock file pour avoir toujours les dépendances les plus récentes, et committent finalement le lock file au moment de la release afin de passer la chaîne de build de manière sécurisée uniquement au dernier moment.

Pas d'avis tranché, personnellement j'utilise les lock files et une pratique "éveillée" de son update.
Guybrushpremier niveau de dépendances, on n'a même pas regardé transitivement ce que ça pouvait donner !
Sur les ecosysteme que je connais (PHP, Go, Rust), le lockfile ne s'applique qu'à l'application "racine", c'est à dire celle qui est en train de builder. Une dépendance intermédiaire ne peut donc pas "freezer" la version d'une dépendance transitive, sauf à le faire dans l'expression de sa contrainte. Les boulets du semver, ça peut arriver...

----

Est-ce que tu t'es intéressé à Go dans ton étude ? Ces mecs sont vraiment des hippies de la gestion de dépendances, et s'appliquent à ne rien faire comme tout le monde.

Au début c'était "on a pas envie, ça a l'air chiant, donc ce sera github lol" suivi de plein d'itérations pour se rapprocher des autres ecosystèmes jusqu'à présenter une version stable de "dep", le package manager.

Et maintenant qu'ils ont un truc, ils projettent de tout péter pour gérer les breaking change dans l'import path (le projet github quoi) et appliquer ensuite un genre de "semver inversé" (on prend la plus petite version qui satisfait la contrainte du développeur).

Je sais pas si ça rentre dans ton champ, une telle obstination c'est à la limite de l'anthropologie :-D


Ce message a été modifié 1 fois. Dernière modification : 11 juin 2018 à 10:48 par Fabe.

Guybrush 8431 Bob
FabeIl y a des hybrides, je connais des projets qui "gitignorent" le lock file pour avoir toujours les dépendances les plus récentes, et committent finalement le lock file au moment de la release afin de passer la chaîne de build de manière sécurisée uniquement au dernier moment.
Pour mes librairies (= composants réutilisables), je ne distribue pas les lockfiles, mais juste des contraintes relativement souples. J'ai un CI qui lance les tests chaque semaine, donc je suis relativement "aware" en cas de souci (et mes tests sont en général très couvrants ;-)

Pour les projets, j'utilise exclusivement un lockfile (notamment pour des questions de réplications des résultats par d'autres équipes de chercheurs), mais je sais pertinement que ça ne marchera plus d'ici quelques mois/années :-D
FabeSur les ecosysteme que je connais (PHP, Go, Rust), le lockfile ne s'applique qu'à l'application "racine", c'est à dire celle qui est en train de builder. Une dépendance intermédiaire ne peut donc pas "freezer" la version d'une dépendance transitive, sauf à le faire dans l'expression de sa contrainte. Les boulets du semver, ça peut arriver...
Bon à savoir. Je pensais que ça fixait aussi les dépendances transitives (ce qui a du sens du point de vue réplicabilité). A ma connaissance, les outils dispo pour ça en Python fixent aussi les dépendances transitives, mais je ne sais pas pour les autres systèmes.
FabeEst-ce que tu t'es intéressé à Go dans ton étude ? Ces mecs sont vraiment des hippies de la gestion de dépendances, et s'appliquent à ne rien faire comme tout le monde.

Au début c'était "on a pas envie, ça a l'air chiant, donc ce sera github lol" suivi de plein d'itérations pour se rapprocher des autres ecosystèmes jusqu'à présenter une version stable de "dep", le package manager.

Et maintenant qu'ils ont un truc, ils projettent de tout péter pour gérer les breaking change dans l'import path (le projet github quoi) et appliquer ensuite un genre de "semver inversé" (on prend la plus petite version qui satisfait la contrainte du développeur).

Je sais pas si ça rentre dans ton champ, une telle obstination c'est à la limite de l'anthropologie :-D
J'ai éliminé Go assez rapidement pour deux raisons :
1) La quantité de packages disponibles est monstrueuse, car à peu près n'importe quel repository en Go peut être installé (on parle de plus d'un million de paquets, alors qu'on a déjà du mal à gérer les quelques 700k de npm).
2) Dans la même veine, il est très difficile de parler de "releases" pour un paquet Go, car à peu près n'importe quel commit sur le master est considéré ("considérable") comme une release. Avec npm, on a déjà plus de 8 millions de releases (et mes travaux concernent aussi d'autres systèmes, au total, on dépasse les 20 millions de releases). Ca devient difficile à gérer (sans forcément apporter une plus value).
3) Les versions "cassantes" (majeures) doivent être déployées dans un nouveau repository (par convention, mais est-ce vraiment respecté ?), ce qui rend impératif de faire un mapping entre les repository afin d'identifier les évolutions d'un même paquet. Vu la quantité de données, c'est couteux en temps de calcul.

Mais Go est définitivement quelque chose "à part" que j'aimerai étudier ! J'ai notamment été surpris par le système d'import (je ne connaissais rien à Go !). C'est à la fois sympa (facilité d'utilisation) et effrayant, et intéressant (pour les problèmes nouveaux que ça pose).


Par contre, quelque chose d'assez intéressant, c'est le fait de prendre "la plus petite version qui satisfait la contrainte". C'est contre-nature (parce que tu veux bénéficier des patchs de sécurité/bug fixes), et je serai bien curieux de connaître la motivation derrière ce choix (j'avais déjà été surpris d'apprendre que NuGet était justement passé il y a quelques années à cette sémantique).

Les versions de dépendances, c'est toujours un combat entre "bénéficier des correctifs" et "ne pas avoir tout qui pète". Il n'y a pas de solutions miracles, même si semver aide dans une certaine mesure. Le gros souci de semver, c'est l'impossibilité de pouvoir confirmer si c'est suivi ou non par un paquet, à moins de faire confiance aux claims des développeurs. Là, je bosse sur un nouveau papier où j'analyse les contraintes sur plusieurs systèmes, et je suggère l'idée qu'on pourrait se baser sur le "wisdom of the crowd" comme d'un proxy pour affirmer qu'un paquet respecte ou non semver. J'ai pratiquement fini les analyses (et le prototype d'outils pour vendre l'idée), et je suis pleinement convaincu que ça pourrait être utile d'avoir ce type d'indicateurs dans des services tels que gemnasium ou dependency-ci (monitoring de dépendances).

Répondre

Vous devez être inscrit et identifié.