- Le système de gestion des dépendances de Rust facilite le développement, mais le nombre de dépendances et les problèmes de qualité deviennent préoccupants
- Même un crate bien utilisé peut ne plus être à jour, au point qu’il vaut parfois mieux l’implémenter soi-même
- Après avoir ajouté des crates populaires comme Axum et Tokio, le nombre total de lignes de code, dépendances incluses, atteint 3,6 millions, ce qui devient difficile à gérer
- Le code que j’ai réellement écrit ne représente qu’environ 1 000 lignes, mais il est en pratique impossible de revoir et d’auditer tout le code autour
- Il n’existe pas de solution claire concernant une éventuelle extension de la bibliothèque standard de Rust ni sur la manière d’implémenter les infrastructures essentielles, et toute la communauté doit réfléchir ensemble à l’équilibre entre performances, sécurité et maintenabilité
Aperçu du problème des dépendances dans Rust
- Rust est mon langage préféré, et sa communauté ainsi que son ergonomie sont excellentes
- La productivité de développement est élevée, mais j’ai récemment commencé à m’inquiéter de la gestion des dépendances
Les avantages des crates Rust et de Cargo
- Cargo permet la gestion des paquets et l’automatisation du build, ce qui améliore fortement la productivité
- Il est facile de passer d’un système d’exploitation ou d’une architecture à l’autre, sans devoir gérer soi-même les fichiers ni la configuration des outils de build
- On peut se mettre à écrire du code immédiatement, sans se soucier d’une gestion de paquets séparée
Les inconvénients de la gestion des crates Rust
- En prêtant moins attention à la gestion des paquets, on devient aussi moins vigilant sur la stabilité
- Par exemple, j’utilisais le crate dotenv, avant d’apprendre via une Security Advisory que sa maintenance avait été abandonnée
- En envisageant un crate de remplacement (
dotenvy), j’ai finalement choisi d’implémenter directement uniquement la partie nécessaire, en une trentaine de lignes
- Comme les problèmes de paquets non maintenus sont fréquents dans de nombreux langages, le vrai fond du problème est une dépendance inévitable aux dépendances externes
L’explosion du volume de code provoquée par les dépendances
- J’utilise des paquets importants et de bonne qualité de l’écosystème Rust, comme Tokio et Axum
- J’ai ajouté comme dépendances Axum, Reqwest, ripunzip, serde, serde_json, tokio, tower-http, tracing et tracing-subscriber
- Le but principal est d’avoir un serveur web, la décompression de fichiers et des fonctions de log, donc le projet lui-même reste simple
- J’ai utilisé la fonctionnalité Cargo vendor pour télécharger localement tous les crates dépendants
- En analysant le nombre de lignes avec tokei, j’arrive à environ 3,6 millions de lignes dépendances incluses (environ 11 136 lignes sans les crates vendorisés)
- À titre de comparaison, on dit que le noyau Linux entier fait environ 27,8 millions de lignes, ce qui signifie que mon petit projet représente déjà un septième de ce volume
- Le code que j’ai réellement écrit ne fait qu’environ 1 000 lignes
- Surveiller et auditer un tel volume de code dépendant est en pratique impossible
Réflexions sur les solutions possibles
- Pour l’instant, il n’y a pas de solution évidente
- Certains proposent, comme en Go, d’étendre la bibliothèque standard, mais cela crée aussi de nouveaux problèmes comme la charge de maintenance
- Rust vise la haute performance, la sécurité et la modularité, avec l’ambition de convenir à l’embarqué et de rivaliser avec le C++, donc l’élargissement de la bibliothèque standard doit être envisagé avec prudence
- Par exemple, même un runtime avancé comme Tokio est géré de manière très active sur Github et Discord
- En pratique, implémenter soi-même des infrastructures essentielles comme un runtime asynchrone ou un serveur web dépasse les capacités d’un développeur individuel
- Même un grand service comme Cloudflare utilise directement tokio et les dépendances de crates.io, sans que l’on sache à quelle fréquence les audits sont réalisés
- Clickhouse a lui aussi évoqué les problèmes liés à la taille des binaires et au nombre de crates
- Avec Cargo, il est difficile d’identifier précisément les lignes de code réellement incluses dans le binaire final, et il subsiste aussi du code inutile selon les plateformes
- Au final, il ne reste qu’à poser la question à l’ensemble de la communauté
3 commentaires
Quand on passe Trivy, il y a bien moins de vulnérabilités high ou critical qu’avec js NPM ou Java Maven, donc de quel argument sur Rust cet article essaie-t-il de se servir ?
Avis Hacker News
import foolib, et personne ne se soucie de ce qu’il y a dedans. À chaque étape, on n’a besoin que d’environ 5 % des fonctionnalités, mais plus l’arbre est profond, plus le code inutile s’accumule. Au final, un simple binaire finit par faire 500 MiB, et on importe une dépendance juste pour formater un nombre. Go et Rust encouragent à tout mettre dans un seul fichier, ce qui devient gênant quand on ne veut utiliser qu’une partie. À long terme, la seule vraie solution serait un suivi ultra-fin des symboles et des dépendances, où chaque fonction/type déclare exactement ce dont il a besoin, pour ne garder que le code strictement nécessaire et jeter le reste. Personnellement, je n’aime pas trop cette idée, mais je ne vois pas d’autre moyen de résoudre le système actuel qui aspire tout l’univers depuis l’arbre de dépendancesvulkan, décodage PNG, unicode shaping, etc.). Les dépendances inutiles étaient surtout de toutes petites choses, et je n’ai retiré queserde_jsonavec une modification mineure. Les plus grosses dépendances (winit/wgpu, etc.) demanderaient des changements d’architecture, donc on ne peut pas les retirer facilement.opar fonction, puis on les regroupait dans une archive.a, et l’éditeur de liens ne prenait que les fonctions nécessaires. Le namespacing se faisait avec des formes commefoolib_do_thing(). Aujourd’hui, c’est plutôt le pattern du god object, avec toutes les fonctions sur un objet de haut niveau, donc un simpleimport foolibattire tout. Dans cet état, il est difficile pour l’éditeur de liens de savoir quelles fonctions sont vraiment nécessaires. En revanche, Go est excellent pour l’élimination du code mort, donc ce qui n’est pas utilisé est bien retiré du binairemin-sized-rustisEven,isOdd,leftpaddans npm, une grosse bibliothèque générique maintenue par une équipe fédérée offre bien plus de garanties d’avenir et de continuité--gc-sectionsglobdevrait juste fournir une fonction de globbing simple, mais son auteur y ajoute aussi un outil en ligne de commande et donc une grosse dépendance de parsing. Cela provoque des alertes fréquentes de type « dependency out-of-date ». Il y a aussi débat sur la responsabilité de la bibliothèqueglob. Faire uniquement du matching de motifs sur des chaînes est une conception plus souple (plus simple à tester, à abstraire du système de fichiers). Beaucoup d’utilisateurs veulent toutefois une bibliothèque omnipotente qui « fait tout », mais plus c’est le cas, plus les effets secondaires sont importants. Je pense que Rust n’est pas si différentstdlib::data_structures::automata::weighted_finite_state_transducer— avec des espaces de noms bien ordonnés et un ensemble « batteries included ». Le langage intègre déjà la gestion des versions et la compatibilité descendante, donc on peut espérer des améliorationsglobde POSIX parcourt réellement le système de fichiers. Pour le simple matching sur chaîne, il y afnmatch. L’idéal serait quefnmatchsoit dans un module séparé et soit une dépendance deglob. Si on essaie d’implémenterglobsoi-même, c’est en réalité assez difficile : structure de répertoires,brace expansion, etc. Il faut donc une bonne composition de fonctions bien conçues#![deny(unsafe_code)], on peut provoquer une erreur de compilation si du codeunsafeest utilisé, et informer l’utilisateur de ce fait. Ce n’est toutefois pas une contrainte absolue, puisqu’on peut toujours autoriser explicitement du codeunsafe. On peut imaginer l’introduction d’un capability system permettant d’ajuster transitivement les fonctionnalités de la bibliothèque standard, un peu comme des feature flagspanic, et il faudrait produire et distribuer un profil de capabilities par bibliothèque. Quelque chose de proche a déjà été démontré dans l’écosystème TypeScriptweb,tls,x509,base64, etc.), le choix et la gestion des bibliothèques restent péniblesunsafe, etc.)dom0, chaque bibliothèque dans une VM modèle distincte, avec communication via des espaces de noms réseau. Dans des secteurs sensibles, cela pourrait être pratiqueblessed.rsrecommande une liste de bibliothèques utiles difficiles à intégrer à la bibliothèque standard. J’aime ce système, car il permet de garder la plupart des paquets limités à des usages précis et donc gérablescargo-vetmérite aussi d’être recommandé. Il permet de suivre et de définir les paquets de confiance, depuis ceux qui doivent être audités par des experts avant import jusqu’à des politiques semi-YOLO du genre « les paquets maintenus par les mainteneurs detokio, on leur fait confiance ». C’est un peu plus formel queblessed.rset c’est un bon moyen de partager en équipe une liste officielle ou quasi officielle de référencesleftpad, il reste une mauvaise image des gestionnaires de paquets. Quelque chose commetokioest en pratique une fonctionnalité de niveau langage, donc si l’OP estime qu’il faudrait auditer soi-même l’ensemble de Go ou même le V8 de Node, ce n’est pas réalistetokioaussi est audité en continu par quelqu’un. Ce n’est pas fait par tout le monde, mais quelqu’un s’en charge malgré toutcargoinclue les deux versions lorsqu’il y a deux dépendances vers des versions différentes est un support assez particulier à cargocargosont vraiment excellents. J’ouvre souvent des PR pour cacher des dépendances inutiles derrière ces flags. Aveccargo tree, on peut voir très facilement l’arbre de dépendances. Une vue des lignes de code qui finissent réellement dans le binaire n’a pas beaucoup de sens, car avec l’inlining des fonctions, la plupart finissent absorbées dansmainCe n’est pas un problème propre à Rust.
C’est à la fois un avantage commun et un problème potentiel partagé par tous les langages qui ont un gestionnaire de paquets avec un dépôt public de paquets et la prise en charge des dépendances transitives.
Au final, tout dépend de la façon dont ceux qui les utilisent s’en servent…
Malgré l’affaire leftpad de Node&npm, rien n’a changé.