Python : Stocker la description des dépendances vérifiables et les détails de l'environnement de build comme parties distinctes d'un contrat de version reproductible
Stockez les dépendances d'exécution dans [project.dependencies] et les exigences de build dans [build-system.requires] de pyproject.toml, puis fixez les versions exactes avec des hachages sha256 dans un fichier requirements séparé pour créer un contrat de version vérifiable au niveau octet.
Dans ce guide
Idée principale
Définissez les dépendances d'exécution dans [project.dependencies] et les exigences de build dans [build-system.requires] à l'intérieur de pyproject.toml. Fixez les versions exactes et ajoutez des entrées --hash=sha256 dans un fichier requirements distinct pour créer un contrat de version vérifiable et reproductible au niveau de l'octet.
Séparer les dépendances d'exécution des exigences du système de build dans pyproject.toml
La spécification pyproject.toml définit deux couches de dépendances distinctes. La table [build-system] déclare les dépendances au niveau Python nécessaires pour exécuter le système de build du projet, telles que setuptools ou flit. La clé requires est obligatoire pour cette table et liste les paquets nécessaires au moment du build. La table [project], introduite par la PEP 621, contient la clé dependencies, qui liste les paquets dont les consommateurs ont besoin lors de l'installation (dépendances d'exécution). Celles-ci sont mappées vers des entrées Requires-Dist dans les métadonnées du paquet.
Le maintien de la séparation de ces couches est essentiel pour un contrat de version reproductible. L'environnement de build doit être reproductible pour produire les mêmes artefacts de distribution, tandis que l'environnement d'exécution doit être reproductible pour garantir un comportement cohérent après l'installation. En déclarant les deux dans pyproject.toml, vous rendez le contrat explicite et vérifiable : n'importe qui peut inspecter quels outils sont nécessaires pour construire le projet et quelles bibliothèques sont nécessaires pour l'exécuter. La spécification note que les outils ne devraient pas exiger l'existence de la table [build-system] ; si elle est absente, des valeurs par défaut sont utilisées, lesquelles peuvent varier. Par conséquent, l'énoncé explicite des deux couches élimine toute ambiguïté.
Pourquoi le blocage des versions et la vérification des hachages répondent à des limites de confiance différentes
Le blocage des versions de paquets avec == dans un fichier requirements protège contre les changements de version en amont qui pourraient introduire des bugs ou des incompatibilités. Cependant, cela fait toujours confiance à l'index de paquets (comme PyPI) et à la chaîne de certificats TLS. Un attaquant qui compromettrait l'index ou effectuerait une attaque de l'homme du milieu pourrait servir un paquet malveillant portant le même numéro de version.
Le mode de vérification par hachage, activé avec --require-hashes et des entrées --hash=sha256:..., vérifie l'artefact téléchargé octet par octet par rapport à un condensat connu. Cela protège contre la compromission de l'index, les attaques sur la chaîne de certificats et les re-téléchargements silencieux d'un paquet sans changement de version (sur les index qui le permettent). La documentation de pip indique que la vérification par hachage est une alternative permettant de gagner du temps par rapport à l'exécution d'un serveur d'index privé : elle élimine la nécessité de télécharger des paquets, de maintenir des ACL et de tenir un journal d'audit, tandis qu'un VCS fournit ce journal pour le fichier requirements. Le compromis réside dans le coût de maintenance : chaque fois qu'une version de dépendance change, les hachages doivent être régénérés et examinés. Cette approche est parfaitement adaptée aux déploiements automatisés sur serveur où l'environnement est contrôlé.
Exemple concret : un contrat de version avec les deux couches
Un fichier pyproject.toml minimal déclare build-system.requires (par exemple, setuptools et wheel) et project.dependencies (par exemple, requests et pydantic). Un fichier compagnon requirements-pinned.txt, généré par pip-compile ou pip freeze, fixe chaque dépendance transitive avec des versions exactes et des hachages sha256. Ces deux fichiers forment ensemble le contrat vérifiable.
Pour installer, exécutez pip install --require-hashes -r requirements-pinned.txt. Cela impose que chaque paquet téléchargé corresponde au hachage enregistré. Les exigences du système de build sont utilisées lors de la construction du projet à partir des sources ; si seul un wheel pré-construit est installé, elles ne sont pas nécessaires lors de l'installation, mais elles font toujours partie du contrat pour quiconque doit reconstruire le projet.
La liste build-system.requires garantit que l'outillage de build est reproductible ; project.dependencies déclare ce dont l'application installée a besoin. Le fichier requirements compagnon fixe les versions exactes et fournit des hachages pour toutes les dépendances transitives, formant le contrat de version vérifiable lorsqu'il est utilisé avec pip install --require-hashes -r requirements-pinned.txt. Les hachages affichés sont des espaces réservés tronqués ; les hachages réels doivent être générés avec pip-compile ou pip freeze, puis vérifiés manuellement ou commités dans le contrôle de version. La séparation des préoccupations signifie que la mise à jour d'une dépendance d'exécution ne nécessite pas de modifier la spécification du système de build, et vice versa. Le fichier pyproject.toml seul n'impose pas la reproductibilité ; c'est la combinaison avec le fichier requirements fixé et haché qui crée le contrat. La table build-system est obligatoire pour la reproductibilité car elle fixe les outils de build ; l'omettre reviendrait à s'appuyer sur des valeurs par défaut spécifiques aux outils qui peuvent différer selon les environnements, brisant ainsi le contrat.
# pyproject.toml
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "myapp"
version = "1.0.0"
dependencies = ["requests>=2.31", "pydantic>=2.5"]
# requirements-pinned.txt (generated by pip-compile or pip freeze)
requests==2.31.0 --hash=sha256:58cd2187c8b806c9970b3f8a8e7c1d2e4f6a8b9c0d1e2f3a4b5c6d7e8f9a0b1
pydantic==2.5.0 --hash=sha256:7f4f1b1b0d8d4e8c9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2
urllib3==2.1.0 --hash=sha256:a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2
annotated-types==0.6.0 --hash=sha256:b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3
pydantic-core==2.14.1 --hash=sha256:c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4
typing-extensions==4.9.0 --hash=sha256:d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5
# Install command enforcing hash verification:
pip install --require-hashes -r requirements-pinned.txtLimites de cette approche
Les bundles Wheelhouse contenant des wheels compilés sont spécifiques au système d'exploitation et à l'architecture, ils ne sont donc pas portables entre les machines. La vérification par hachage ne garantit pas la disponibilité des paquets comme le ferait un index privé ou une bibliothèque vendored ; si un paquet est supprimé de l'index, l'installation échoue même si le hachage est connu. La table [build-system] est optionnelle ; en son absence, les outils reviennent à des valeurs par défaut, qui peuvent différer selon les environnements, brisant la reproductibilité. Le blocage avec hachages fait toujours confiance à la résolution initiale et ne protège pas contre le code malveillant à l'intérieur de la version bloquée elle-même. Enfin, la maintenance des hachages ajoute une surcharge : chaque mise à jour de dépendance nécessite la régénération et l'examen des entrées de hachage.
Conditions d’application
- Votre projet utilise-t-il pyproject.toml avec les tables [build-system] et [project] ?
- Êtes-vous prêt à régénérer les entrées de hachage chaque fois qu'une version de dépendance change ?
- L'environnement cible est-il suffisamment homogène pour que les wheels compilés (s'il y en a) soient portables, ou pouvez-vous les reconstruire sur chaque machine ?
- Avez-vous un processus pour générer et examiner le fichier requirements fixé (par exemple, pip-compile ou pip freeze) ?
- Pouvez-vous accepter le coût de maintenance de la mise à jour des hachages pour les correctifs de sécurité et les mises à niveau ?
- L'absence d'index privé ou de bibliothèque vendored est-elle acceptable pour vos exigences de disponibilité ?
Champ d’application
Les bundles Wheelhouse contenant des wheels compilés sont spécifiques à l'OS et à l'architecture, ils ne sont donc pas portables. La vérification par hachage ne garantit pas la disponibilité des paquets contrairement à un index privé. La table [build-system] est optionnelle ; si elle est absente, les outils utilisent des valeurs par défaut variables. Le blocage avec hachages fait confiance à la résolution initiale et ne protège pas contre le code malveillant déjà présent dans la version bloquée.