La gestion des dépendances#
Dès qu’un programme devient un peu plus complexe, deux phénomènes interviennent habituellement :
- Le programme doit être décomposé en plusieurs modules
- Certaines fonctionnalités du programme, pouvant être accomplies par des programmes (ou des bibliothèques) externes, doivent être détachées en des composantes ou des bibliothèques distinctes, qu’il est possible de réutiliser (c’est un peu le principe DRY que nous avons vu, appliqué dans le sens plus large d’un écosystème logiciel)
Dans le cas (2), on nomme parfois les bibliothèques externes des “dépendances”, ou des “paquets” (packages en anglais, plus communément).
Dans les débuts de l’ingénierie logicielle, ces dépendances étaient gérées à la main, en copiant et échangeant des fichiers.
Avec l’apparition de Linux au début des années 90, une innovation importante allait vite apparaître : les gestionnaires de paquets. Des exemples fameux sont APT pour les distributions Debian et Ubuntu de Linux, et RPM pour d’autres distributions comme Red Hat, Fedora, etc.
Ces paquets sont en général des programmes et des bibliothèques qui sont
compatibles avec une version particulière de Linux et des sous-systèmes qui sont
présents dans une distribution particulière. Par exemple si on utilise Ubuntu
24.04, une version particulière d’une distribution particulière de Linux basée
sur Debian, le gestionnaire de paquets apt ne permettra que d’installer des
programmes compatibles avec Ubuntu 24.04. Un aspect important des systèmes de
gestion de ce genre est que les dépendances ne sont pas des objets atomiques et
isolées : en général une dépendance est liée à une autre, ce qui forme un graphe
de dépendances, qu’on dit transitives, ou récursives (si X dépend de Y qui
dépend de Z, alors X dépend de Z, de manière transitive). Donc non
seulement la compatibilité des versions est gérée au niveau du système global,
mais aussi entre les paquets (programmes ou bibliothèques, entre eux). Si par
exemple l’installation de la version 7 de LibreOffice nécessite une version
particulière de Java, le gestionnaire de paquets gérera les dépendances
intelligemment, et automatiquement.
Par exemple on peut voir le graphe de dépendances du paquet sl sur Ubuntu Linux,
en utilisant l’utilitaire apt-rdepends :
$ apt-rdepends sl
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
sl
Depends: libc6 (>= 2.2.5)
Depends: libncurses6 (>= 6)
Depends: libtinfo6 (>= 6)
libc6
Depends: libgcc-s1
libgcc-s1
Depends: gcc-14-base (= 14.2.0-4ubuntu2~24.04)
Depends: libc6 (>= 2.35)
gcc-14-base
libncurses6
Depends: libc6 (>= 2.34)
Depends: libtinfo6 (= 6.4+20240113-1ubuntu2)
libtinfo6
Depends: libc6 (>= 2.34)Le versionnage sémantique (SemVer)#
Au fil du temps et de l’évolution de la culture du développement logiciel, le versionnage dit sémantique (SemVer) s’est imposé en tant que convention pour numéroter les versions d’un logiciel, que ce soit un programme ou une bibliothèque.
Bien que l’interprétation ne soit pas toujours identique, en général on s’accorde pour dire que la composante “majeure” du numéro de version correspond à un changement important, qui “brise” la compatibilité avec les versions précédentes (s’il y en a). S’il s’agit d’une bibliothèque logicielle par exemple, la mise à jour demandera probablement un travail assez important, non-trivial.
Le deuxième nombre, la version mineure, indique l’ajout de nouvelles fonctionnalités qui demeurent compatibles avec l’existant. Le logiciel s’enrichit, mais sans obliger les utilisateurs à modifier leur code. Enfin, le troisième nombre, appelé correctif ou patch, correspond à des réparations internes : corrections de bugs, améliorations de performance ou failles de sécurité qui ne modifient pas l’interface offerte aux autres programmes.
Grâce à cette structure simple, un numéro de version devient une véritable promesse. Il permet aux équipes de décider automatiquement quelles mises à jour peuvent être installées sans risque et lesquelles exigent une attention particulière. Les gestionnaires de dépendances modernes s’appuient largement sur cette logique pour éviter les surprises. Sans une convention partagée, une nouvelle version pourrait tout aussi bien contenir une amélioration bénigne qu’un changement radical rendant l’application inutilisable.
Adopter SemVer revient donc à instaurer une relation de confiance entre les mainteneurs d’un logiciel et celles et ceux qui l’intègrent dans leurs propres projets. Le numéro n’est plus simplement un identifiant : il devient une information sur la stabilité, la continuité et l’effort requis pour évoluer avec le produit.
Il est à noter que bien que SemVer soit le schéma le plus répandu, il en existe d’autres : Ubuntu par exemple utilise des numéros de version qui correspondent à l’année et au mois de livraison d’une certaine version.
Il y a aussi une notion assez populaire, qui consiste à considérer qu’un
logiciel ou une bibliothèque ne peut pas être considérée stable tant qu’elle n’a
pas atteint au moins la version 1.0.0. Certains projets restent d’ailleurs
indéfiniment dans un schéma de versions 0.x.y, afin d’échapper à une certaine
pression sociale, et ainsi pouvoir conserver une certaine liberté créative et
d’action.
Il est à noter que pour les langages de programmation (qui sont à la fois des programmes et des bibliothèques), la question du numéro de version est particulièrement cruciale et parfois même controversée. Un cas fameux est la transition de la version 2 du langage Python, à la version 3, qui était prévue devoir être relativement simple et rapide, mais qui a pris au moins 10 ans à intervenir, étant donné toute la complexité technique et sociale que cette transition a entraînée. En fait il est très probable que Python ne passe jamais à la version 4, étant donné son historique et sa position centrale dans de nombreux écosystèmes technologiques (web, IA, calcul scientifique, etc).
La notion de versionnage sémantique est tellement importante et omniprésente qu’elle a de l’influence à l’extérieur de la sphère du développement logiciel. On parlera ainsi de la Playstation 5, de la Mazda 3, etc. Même des choses qui pourraient à priori ressembler à du logiciel, comme le Web 2.0 et le Web3, n’en sont pas vraiment, ils sont plus des “évolutions culturelles et technologiques”.
Le gestionnaire uv pour Python#
Pour explorer concrètement ces idées, nous allons utiliser le gestionnaire de
bibliothèques uv, pour Python. uv est un outil très intéressant, car il est
apparu relativement tard dans l’histoire de Python (en 2024), à la suite d’une
longue lignée d’outils du même genre : pipenv, poetry, etc. Ces outils ne
doivent pas être confondus avec pip, qui est l’outil de base dans la
distribution Python, qui est moins puissant que les gestionnaires de
dépendances, dont il sera question ici. uv est apparu au bon moment, avec les
bonnes caractéristiques : extrêmement rapide (il est écrit lui-même dans le
langage Rust), avec une implémentation très complète et flexible des standards
de l’écosystème de packaging pour Python, qui ont pris un long moment avant de
maturer. Pendant de nombreuses années, la gestion du packaging en Python était
considérée un sujet pénible et beaucoup de controverse existait. L’apparition de
uv a introduit une certaine sérénité dans la culture de Python.
La bibliothèque my-lib#
Nous allons tout d’abord créer, avec uv, une petite bibliothèque simple, qui
n’offrira qu’une seule fonction :
$ uv init --lib my-lib
Initialized project `my-lib` at `.../uv-demo2/my-lib`La structure initiale de notre nouveau projet de bibliothèque devrait ressembler à ceci :
my-lib/
├── pyproject.toml
├── src/
│ └── my_lib/
│ └── __init__.pyuv a créé la structure de notre bibliothèque, mais il n’a évidemment pas fourni
le code qu’on veut y offrir, qu’il nous faut définir nous-même, dans le fichier
src/my_lib/secret.py (qui doit être créé) :
def get_secret_number():
return 42Le projet ressemble donc maintenant à ceci :
my-lib/
├── pyproject.toml
├── src/
│ └── my_lib/
│ ├── __init__.py
│ └── secret.py ← contient notre fonction get_secret_numberOn peut tout d’abord tester notre bibliothèque avec cette commande :
$ cd my-lib
$ uv run python -c 'from my_lib.secret import get_secret_number; print(get_secret_number())'
42On doit ensuite produire un artefact de type wheel, qui va contenir la totalité
de notre bibliothèque, en un seul fichier .whl (qu’il sera possible d’installer dans un
autre projet) :
$ cd my-lib
$ uv build --out-dir ../packages
Building source distribution (uv build backend)...
Building wheel from source distribution (uv build backend)...
Successfully built ../packages/my_lib-0.1.0.tar.gz
Successfully built ../packages/my_lib-0.1.0-py3-none-any.whlVoici maintenant où nous en sommes :
$ tree
.
├── my-lib
│ ├── pyproject.toml
│ ├── README.md
│ ├── src
│ │ └── my_lib
│ │ ├── __init__.py
│ │ ├── __pycache__
│ │ │ ├── __init__.cpython-313.pyc
│ │ │ └── secret.cpython-313.pyc
│ │ ├── py.typed
│ │ └── secret.py
│ └── uv.lock
└── packages
├── my_lib-0.1.0-py3-none-any.whl
└── my_lib-0.1.0.tar.gz
6 directories, 10 filesL’application my-app (qui utilise la bibliothèque my-lib)#
Créons maintenant un nouveau projet avec uv, d’une application cette fois, que
nous appellerons my-app :
$ cd .. # on doit être au même niveau que my-lib
$ uv init my-app
Initialized project `my-app` at `.../uv-demo2/my-app`Étant donné que my-app devra utiliser le code de la bibliothèque my-lib, nous
voudrons y ajouter une dépendance vers notre bibliothèque my-lib. Mais tout
d’abord, étant donné que ceci est un exercice d’apprentissage, nous devons
modifier notre configuration quelque peu, en ajoutant le bloc [tool.uv] qui
contient deux lignes supplémentaires, à notre fichier my-app/pyproject.toml :
[project]
name = "my-app"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = []
[tool.uv]
no-index = true
find-links = ["../packages"]Ces deux lignes sont très importantes dans notre contexte :
no-indexempêcheuvd’aller cherchermy-libsur registre de paquets public PyPI, qui contient déjà apparemment une bibliothèque avec ce nom. Nous voulons queuvutilise notre version locale demy-lib, que nous avons sur notre propre disquefind-linksindique le lieu oùuvdevra trouver les fichiers “wheels”, qui sont des sortes de “zip” d’une bibliothèque entière, optimisée pour un système particulier
Une fois cette configuration effectuée, on peut ajouter notre dépendance avec uv add :
$ cd my-app
$ uv add my-lib
Using CPython 3.13.5
Creating virtual environment at: .venv
Resolved 2 packages in 26ms
Prepared 1 package in 4ms
Installed 1 package in 3ms
+ my-lib==0.1.0On peut constater l’état du projet de notre application avec cette commande :
$ uv tree
Resolved 2 packages in 3ms
my-app v0.1.0
└── my-lib v0.1.0La notion de registre de paquets (PyPI)#
Nous avons brièvement mentionné ci-haut la notion de registre de paquets.
Étant donné que nous voulions utiliser notre propre bibliothèque my-lib, et non
celle qui se trouve en ligne, nous avons dû spécifier no-index = true dans la
configuration pyproject.toml de notre projet. Que se serait-il passé, sans
cette instruction particulière? uv est configuré, par défaut, pour fonctionner
avec le registre en ligne et officiel PyPI, qui est un
endroit public où il est possible de télécharger et publier des paquets.
Explorons cette notion en créant un nouveau projet avec uv :
$ cd .. # on ne doit pas être dans le répertoire d'un projet existant
$ uv init my-venv
Initialized project `my-venv` at `/Users/cjauvin/gh/inf1410-teluq/content/docs/module3/gestion-des-deps/uv-demo3/my-venv`Ajoutons une dépendance vers la bibliothèque requests, qui permet de faire des requêtes HTTP (web) en python :
$ cd my-venv
$ uv add requests
Using CPython 3.13.5
Creating virtual environment at: .venv
Resolved 6 packages in 183ms
Installed 5 packages in 4ms
+ certifi==2026.1.4
+ charset-normalizer==3.4.4
+ idna==3.11
+ requests==2.32.5
+ urllib3==2.6.3On constate trois choses :
uva téléchargé automatiquement la bibliothèquerequestsde PyPI, sans que l’ait rien configuré- La bibliothèque
requestselle-même nécessite quelques dépendances additionnelles (des dépendances transitives donc) :certifi,charset-normalizer, etc. - Apparemment un “environnement virtuel” a été créé, dans le répertoire
.venvde notre projet
Qu’est-ce qu’un environnement virtuel donc?
La notion d’environnement virtuel (virtual env, ou venv)#
En fait une question reliée serait : qu’est-ce qu’un projet uv, au juste?
Quand j’ajoute une dépendance dans mon projet, quel est l’effet concret, et
comment puis-le constater? Voyons voir le contenu du répertoire du projet
dans lequel nous venons d’installer une dépendance :
$ cd my-venv
$ ls -la
total 24
drwxr-xr-x@ 8 cjauvin staff 256 Feb 16 17:13 ./
drwxr-xr-x@ 5 cjauvin staff 160 Feb 16 17:14 ../
-rw-r--r--@ 1 cjauvin staff 5 Feb 12 14:55 .python-version
drwxr-xr-x@ 8 cjauvin staff 256 Feb 12 14:58 .venv/
-rw-r--r--@ 1 cjauvin staff 358 Feb 16 17:12 pyproject.toml
-rw-r--r--@ 1 cjauvin staff 0 Feb 12 14:55 README.md
drwxr-xr-x@ 3 cjauvin staff 96 Feb 12 14:55 src/
-rw-r--r--@ 1 cjauvin staff 254 Feb 16 17:13 uv.lockOn constate un répertoire particulier, appelé .venv : il s’agit d’un
environnement virtuel (souvent appelé venv), un endroit particulier sur le
disque qui contient :
- Une version particulière de l’interpréteur Python (par exemple la version 3.13, dans cet exemple), ainsi qu’une série d’utilitaires reliés
- Une série de paquets (packages) qui ont été installés dans le venv (dans le
contexte de ce projet, un seul a été installé :
requests)
Il est crucial de savoir et de comprendre que les paquets installés dans un venv particulier fonctionnent exclusivement dans le contexte de l’interpréteur python particulier, installé dans ce venv. On peut s’en convaincre avec cette commande :
$ ll .venv/lib/python3.13/site-packages/
total 24
-rw-r--r--@ 1 cjauvin staff 18B Feb 17 15:46 _virtualenv.pth
-rw-r--r--@ 1 cjauvin staff 4.2K Feb 17 15:46 _virtualenv.py
drwxr-xr-x@ 7 cjauvin staff 224B Feb 17 15:46 certifi/
drwxr-xr-x@ 9 cjauvin staff 288B Feb 17 15:46 certifi-2026.1.4.dist-info/
drwxr-xr-x@ 16 cjauvin staff 512B Feb 17 15:46 charset_normalizer/
drwxr-xr-x@ 10 cjauvin staff 320B Feb 17 15:46 charset_normalizer-3.4.4.dist-info/
drwxr-xr-x@ 11 cjauvin staff 352B Feb 17 15:46 idna/
drwxr-xr-x@ 8 cjauvin staff 256B Feb 17 15:46 idna-3.11.dist-info/
drwxr-xr-x@ 20 cjauvin staff 640B Feb 17 15:46 requests/
drwxr-xr-x@ 9 cjauvin staff 288B Feb 17 15:46 requests-2.32.5.dist-info/
drwxr-xr-x@ 18 cjauvin staff 576B Feb 17 15:46 urllib3/
drwxr-xr-x@ 8 cjauvin staff 256B Feb 17 15:46 urllib3-2.6.3.dist-info/Ceci permet un mécanisme d’isolation, qui permet d’éviter les conflits et les
problèmes de compatibilité entre les composantes. Par exemple, si un script de
traitement de données utilise la version 1 de numpy, on peut le rouler dans un
venv dont la version de numpy est gardée exclusivement à cette version. Si un
autre script utilise la version 2, il peut rouler dans son propre venv. Il est
en de même pour la version de l’interpréteur python utilisée, qui peut varier de
venv en venv, elle aussi.
Chaque fois qu’une commande
uvest exécutée, elle l’est dans le contexte d’un venv particulier. Si le venv n’existe pas, il sera créé pour l’occasion.
La notion de reproductibilité (uv.lock)#
Un autre fichier qu’il est intéressant de considérer dans notre projet est
uv.lock. Ce fichier (qu’on appelle parfois un “lockfile”) contient la liste
des dépendances du projet, sous la forme de métadonnées précises et exactes,
permettant de reconstruire de manière parfaite et exhaustive le contenu d’un
venv particulier (c’est une manière de le copier donc, en le reproduisant avec
une recette). Chaque dépendance y est figée dans le temps, à l’aide
d’identifiants et de urls non-ambigus, qui permettent de faire en sorte de
réinstaller exactement le même environnement, dans un endroit différent. Quand
le projet vient d’être créé, uv.lock est vide. Mais dans notre cas, étant
donné que nous avons ajouté requests au projet, on peut constater, en
l’examinant, qu’il contient une série de références précises vers les artefacts
en ligne des packages correspondant :
$ cd my-venv
$ cat uv.lock
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "certifi"
version = "2026.1.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
]
...Cette notion de reproductibilité est extrêmement importante dans les environnements de production, où certaines composantes logicielles sont parfois installées à répétition, de manière scriptée et automatisée, dans des environnements éphémères, comme des containers (docker ou autre), dans un système Kubernetes, ou un environnement pour effectuer des tests.
Mais que se passerait-il si, au lieu d’installer la bibliothèque requests,
nous tentions d’installer, par mégarde, request (sans s)?
$ uv add request
× No solution found when resolving dependencies:
╰─▶ Because request was not found in the package registry and your project depends on request, we can
conclude that your project's requirements are unsatisfiable.
help: If you want to add the package regardless of the failed resolution, provide the `--frozen` flag
to skip locking and syncing.Dans ce cas, le comportement souhaité est le bon : PyPI ne reconnaît pas le nom
de cette bibliothèque, et qui plus est, PyPI ne permettrait pas à quelqu’un de
publier une bibliothèque dont le nom serait trop proche d’une bibliothèque très
connue (requests est extrêmement populaire). Mais cette protection a ses
limites, et les registres de paquets restent vulnérables à plusieurs types
d’attaques. Nous y reviendrons en détail dans la section sur la sécurité, à la
fin de ce chapitre.
Après ces détours, revenons maintenant à notre application my-app : tout comme
nous l’avons fait pour la bibliothèque my-lib, c’est à nous qu’incombe la
responsabilité de fournir le code source pour l’application, qui sera très
simple. Dans le fichier my-app/main.py (qui a été créé automatiquement par
uv puisqu’il s’agissait d’une application), nous devons remplacer le contenu
par :
from my_lib.secret import get_secret_number
def main():
print(f"The secret number is: {get_secret_number()}")
if __name__ == "__main__":
main()On peut maintenant tester notre application :
$ cd my-app
$ uv run main.py
The secret number is: 42Maintenant regardons attentivement notre fichier my-app/pyproject.toml :
[project]
name = "my-app"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"my-lib>=0.1.0",
]
[tool.uv]
no-index = true
find-links = ["../packages"]On note que my-app définie sa dépendance à my-lib en utilisant une syntaxe
particulière : my-lib>=0.1.0, ce qui signifie évidemment : n’importe quelle
version de my-lib dont la version est plus grande ou égale à 0.1.0. Ceci peut
être dangereux, car que se passerait-il dans le cas où une nouvelle version de my-lib
serait publiée, et qu’elle contiendra des changements qui feraient en sorte de modifier
le comportement de my-app? Soyons plus conservateur en fixant plus précisément notre
limite de version pour my-lib. Pour ce faire, on va utiliser de nouveau cette syntaxe
particulière :
$ uv add "my-lib>=0.1.0,<0.2.0"
Resolved 2 packages in 7ms
Audited 1 package in 0.28msAvec
uv, les contraintes de version servent à indiquer quelles versions d’un paquet peuvent être installées. Les opérateurs classiques<,<=,>,>=permettent de fixer des bornes (par exemple>=1.2.0signifie « version 1.2.0 ou supérieure »), tandis que==impose une version exacte.uvsuit la spécification PEP 440 de l’écosystème Python : l’opérateur~=(compatible release) autorise les mises à jour compatibles selon la version indiquée (par exemple~=1.4accepte les versions1.xà partir de1.4, sans passer à2.0). Ces contraintes permettent de contrôler finement la stabilité d’un projet tout en autorisant, si souhaité, certaines mises à jour automatiques lors du verrouillage (uv lock).
La commande
uv add "my-lib~=0.1.0"serait ici équivalent àuv add "my-lib>=0.1.0,<0.2.0".
On peut maintenant constater l’effet de cette précision dans notre même fichier
my-app/pyproject.toml :
[project]
name = "my-app"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"my-lib>=0.1.0,<0.2.0",
]
[tool.uv]
no-index = true
find-links = ["../packages"]La version de my-lib pour my-app est “pinnée” (mot anglais) à 0.1.x, ce
qui veut donc dire que seules les versions avec des “patchs” différents (pour la
résolution de bogues, en général) seront tolérées : par exemple 0.1.1,
0.1.2, etc. La version 0.2.0, si elle en venait à exister, serait
“interdite” par le gestionnaire uv (remarquez bien l’usage de <0.2.0, et
non <=0.2.0).
La bibliothèque my-lib évolue#
Imaginons maintenant que la bibliothèque my-lib évolue, et que la fonction
offerte change : par exemple, au lieu d’être 42, le nombre magique devient 99.
Modifions tout d’abord le code de my-lib/src/secret.py :
def get_secret_number():
return 99Ensuite incrémentons la version de my-lib :
$ cd my-lib
$ uv version --bump minor # 0.1.0 -> 0.2.0
Resolved 1 package in 16ms
Built my-lib @ file:///Users/cjauvin/gh/inf1410-teluq/content/docs/module3/gestion-de
Prepared 1 package in 7ms
Uninstalled 1 package in 0.67ms
Installed 1 package in 1ms
- my-lib==0.1.0 (from file:///Users/cjauvin/gh/inf1410-teluq/content/docs/module3/gestion-des-deps/uv-demo3/my-lib)
+ my-lib==0.2.0 (from file:///Users/cjauvin/gh/inf1410-teluq/content/docs/module3/gestion-des-deps/uv-demo3/my-lib)
my-lib 0.1.0 => 0.2.0On peut constater dans notre fichier my-lib/pyproject.toml que le changement a
été fait :
[project]
name = "my-lib"
version = "0.2.0"
description = "Add your description here"
readme = "README.md"
authors = [
{ name = "Christian Jauvin", email = "cjauvin@gmail.com" }
]
requires-python = ">=3.13"
dependencies = []
[build-system]
requires = ["uv_build>=0.8.22,<0.9.0"]
build-backend = "uv_build"On peut maintenant “builder” notre bibliothèque afin d’en produire un artefact de type
“wheel” pour la nouvelle version 0.2.0, comme nous avons fait pour la précédente
version 0.1.0 :
$ uv build --out-dir ../packages
Building source distribution (uv build backend)...
Building wheel from source distribution (uv build backend)...
Successfully built ../packages/my_lib-0.2.0.tar.gz
Successfully built ../packages/my_lib-0.2.0-py3-none-any.whlOn constate donc la présence du fichier ../packages/my_lib-0.2.0-py3-none-any.whl, qui
contient la totalité de notre bibliothèque, prête à être installée dans le contexte de
n’importe quel projet, par uv.
L’application my-app devrait donc évoluer elle aussi !#
Retournons maintenant à my-app. Si on tente de la mettre à jour :
$ uv sync --upgrade
Resolved 2 packages in 17ms
Audited 1 package in 0.28msRien ne se passe, car bien que my-lib version 0.2.0 soit disponible, la
configuration de my-app ne lui permet pas d’utiliser une version aussi élevée.
Pour changer cela, on peut utiliser la commande :
uv add "my-lib>=0.1.0,<=0.2.0"
Resolved 2 packages in 17ms
Audited 1 package in 0.23msqui devrait maintenant permettre de faire en sorte de laisser passer la version 0.2.0
de my-lib :
$ uv sync --upgrade
Resolved 2 packages in 26ms
Prepared 1 package in 0.78ms
Uninstalled 1 package in 1ms
Installed 1 package in 1ms
- my-lib==0.1.0
+ my-lib==0.2.0On peut constater l’effet de cette mise à niveau de my-lib, sur notre
application elle-même :
$ uv run main.py
The secret number is: 99Finalement, étant donné ce changement de comportement important, et pour éviter
de confondre les utilisateurs, on va vouloir mettre à jour la version de
my-app elle-même :
$ uv version --bump major
Resolved 2 packages in 7ms
Audited 1 package in 0.25ms
my-app 0.1.0 => 1.0.0Dans ce cas étant donné qu’il s’agit d’une application, nous avons choisi de
changer la version majeure, qui passe donc à 1.0.0.
La sécurité de la chaîne d’approvisionnement#
Tout au long de cette section, nous avons vu comment un gestionnaire de
dépendances permet d’assembler rapidement un projet à partir de composantes
externes. Mais cette commodité repose sur un acte de confiance implicite :
lorsqu’on exécute uv add requests, on ne fait pas seulement confiance à la
bibliothèque requests elle-même, mais aussi à certifi, charset-normalizer,
idna et urllib3, à leurs propres mainteneurs, et aux infrastructures qui les
hébergent et les distribuent. Plus un projet accumule de dépendances, plus cet
arbre de confiance s’élargit, et plus la surface d’attaque potentielle grandit.
Cette réalité a donné naissance à une catégorie de menaces qu’on appelle les
attaques de la chaîne d’approvisionnement logicielle (supply chain attacks).
Le principe d’une supply chain attack est d’exploiter la confiance que les développeurs accordent à leur chaîne d’approvisionnement logicielle, plutôt que d’attaquer directement le système visé. Au lieu de chercher une faille dans l’application elle-même, l’attaquant compromet une de ses dépendances, souvent plusieurs niveaux en profondeur dans l’arbre de dépendances transitives. Le code malicieux est alors installé automatiquement par le gestionnaire de paquets, parfois sans que personne ne le remarque pendant des semaines ou des mois. Les vecteurs d’attaque les plus courants sont le typosquattage (publier un paquet dont le nom ressemble à un paquet populaire), la prise de contrôle du compte d’un mainteneur, et l’infiltration progressive d’un projet open source par un contributeur qui gagne la confiance de l’équipe avant d’y injecter du code hostile.
Même sans intention malicieuse, la dépendance excessive peut être dangereuse. En
2016, le retrait du registre npm d’un paquet appelé left-pad, une bibliothèque
de 11 lignes de JavaScript qui ajoutait des espaces à gauche d’une chaîne de
caractères, a instantanément cassé le build de milliers de projets à travers le
monde. Voici la totalité du code de cette bibliothèque :
module.exports = leftpad;
function leftpad(str, len, ch) {
str = String(str);
var i = -1;
ch || (ch = ' ');
len = len - str.length;
while (++i < len) {
str = ch + str;
}
return str;
}Le fait que des milliers de projets dépendaient d’une bibliothèque externe pour une fonction aussi triviale illustre bien les excès de la granularité des dépendances. Chaque dépendance ajoutée, aussi petite soit-elle, est un point de rupture potentiel. Nous reviendrons sur les dimensions sociales et économiques de cet épisode dans le module 6.
Et quand l’intention est malicieuse, les conséquences peuvent être bien pires.
Plusieurs incidents majeurs ont marqué l’histoire récente et illustrent la
diversité de ces attaques. En 2018, le paquet npm event-stream, téléchargé des
millions de fois par semaine, a été compromis d’une manière particulièrement
révélatrice. Le mainteneur original, épuisé par des années de travail bénévole
sur un projet qu’il n’utilisait même plus, a accepté de transférer le contrôle
du paquet à un inconnu qui s’était montré serviable. Ce nouveau mainteneur a
ensuite ajouté une dépendance vers un autre paquet, flatmap-stream, qui
contenait du code obfusqué conçu pour voler les portefeuilles de Bitcoin des
utilisateurs d’une application spécifique. L’attaque est restée invisible
pendant plusieurs semaines, car le code malicieux était caché dans un paquet
transitif et ne se déclenchait que dans un contexte très précis.
L’attaque la plus spectaculaire des dernières années est sans doute celle qui a visé xz Utils en 2024, un petit utilitaire de compression présent dans pratiquement toutes les distributions Linux. Un développeur utilisant le pseudonyme Jia Tan a commencé à contribuer au projet en 2021, de manière patiente et méthodique. Pendant plus de deux ans, il a soumis des correctifs légitimes, gagné la confiance du mainteneur principal (qui, comme dans le cas d’event-stream, était une personne seule et débordée), et obtenu progressivement les droits de publication. En février 2024, il a injecté une porte dérobée (backdoor) extrêmement sophistiquée, dissimulée dans les fichiers de test du projet, qui permettait à un attaquant distant de prendre le contrôle de n’importe quel serveur utilisant OpenSSH avec la bibliothèque compromise. La backdoor a été découverte par hasard, par un ingénieur de Microsoft qui avait remarqué que ses connexions SSH prenaient une demi-seconde de plus que d’habitude. Sans cette observation fortuite, la porte dérobée aurait pu se retrouver dans des millions de serveurs à travers le monde.
Un exemple encore plus récent est celui de LiteLLM, une bibliothèque Python très populaire (des millions de téléchargements par jour) servant de passerelle vers différents modèles d’IA. En mars 2026, un groupe appelé TeamPCP a réussi une attaque en cascade d’une sophistication remarquable. Ils ont d’abord compromis Trivy, un outil d’analyse de sécurité, qui était justement utilisé dans le pipeline CI/CD de LiteLLM. L’ironie est frappante : c’est un outil de sécurité qui a servi de vecteur d’attaque. En s’exécutant dans le pipeline de compilation de LiteLLM, le Trivy compromis a exfiltré le jeton de publication PyPI du projet, permettant aux attaquants de publier deux versions malicieuses de LiteLLM sur PyPI. Ces versions installaient un mécanisme qui, à chaque démarrage de Python, récoltait silencieusement les clés API, les secrets, les identifiants de bases de données et les clés SSH présents sur la machine. Les versions compromises n’ont été en ligne que pendant environ 40 minutes avant d’être retirées par PyPI, mais étant donné le volume de téléchargements, l’impact potentiel était considérable. Fait notable : les utilisateurs qui avaient des dépendances verrouillées dans un fichier de type lockfile n’ont pas été affectés.
Plusieurs des outils que nous avons vus dans ce chapitre jouent un rôle défensif
direct contre ces menaces : les lockfiles permettent de figer les versions
exactes des dépendances et d’éviter qu’une mise à jour malicieuse soit installée
automatiquement, les contraintes de version SemVer limitent l’exposition aux
changements inattendus, et les registres comme PyPI mettent en place des
protections contre le typosquattage et exigent de plus en plus l’authentification
à deux facteurs pour les mainteneurs de paquets populaires. Au-delà de ces
mécanismes, des pratiques comme l’audit régulier des dépendances (uv audit,
npm audit), la production d’inventaires de composants (SBOM, pour Software
Bill of Materials) et la vérification des signatures cryptographiques font
aujourd’hui partie des compétences attendues en génie logiciel.
Pas seulement pour Python !#
Tout ce que nous avons dit et expliqué à propos de uv et python s’applique
également à d’autres langages et leur écosystème :
- JavaScript avec Node et NPM
- Rust et Cargo
- Java avec Maven ou Gradle
- Go avec go mod
- .NET (C#, etc) et NuGet
- Perl et CPAN
- Ruby et Bundler
Pour votre culture personnelle, je vous suggère d’explorer une ou plusieurs de ces “paires”, et de tenter de répondre aux questions suivantes :
- Est-ce qu’il y a une notion d’environnement virtuel?
- Est-ce qu’il y a un dépôt centralisé de bibliothèques et de packages (comme PyPI)?
- Est-ce qu’il y a un mécanisme de lockfile (comme
uv.lock)? - Est-ce que SemVer y est utilisé de la même manière?
- Est-ce les contraintes au niveau des dépendances sont exprimées de la même manière (avec le même “mini-langage”)?
- Est-ce qu’on y retrouve la possibilité du même genre de vulnérabilité au niveau de la sécurité (typosquatting, etc)?