Dépendances, version et Composer

Objectifs : Réfléchir à la gestion des dépendances dans un projet, et résoudre le problème de la dépendance incontrôlée sur Composer. Thèmes : Développement, PHP, Dépendances, Composer, Déploiement.

Dependency manager

L'utilisation de dépendances dans une stack logicielle est soumise à une règle d'or :

Pour chaque dépendance, mettre en place un contrôle des versions autorisées, en fonction de la confiance accordée à son mainteneur.

Dans l'univers JavaScript, on utiliserait Yarn, qui permet via un package.json de décrire un schéma des versions de dépendances acceptées.

Côté PHP, on a pris l'habitude d'utiliser Composer depuis quelques années pour gérer nos dépendances. Et ça donne :

{
    "require": {
        "monolog/monolog": "1.0.*"
    }
}

Exemple tiré de la documentation officielle de Composer

Dans cet exemple, on intègre le package monolog/monolog à notre projet, en explicitant la contrainte : version 1.0, ou tout patch sur cette version mineure.

Semantic versioning

Tout ça peut fonctionner car basé sur Semantic Versioning, une RFC qui définit un ensemble de règles pour la numérotation des versions (qui crée donc une cohésion sur la signification des numéros de version).

Dans l'exemple précédent, c'est Semantic Versioning qui nous permet de traduire la numérotation 1.0.* en version 1.0, ou tout patch sur cette version mineure.

Ensuite, c'est la confiance qu'on a envers les mainteneurs de la dépendance monolog/monolog qui nous permet d'être souple et d'accepter tous les patchs qui pourraient être publiés.

Dans le cas où l'on intègre la dépendance d'un mainteneur auquel on accorde moins de confiance, on peut remplacer par "monolog/monolog": "1.0.8", afin de laisser la place au moins de risque possible (la version choisie ayant été testée directement lors des développements, et étant donc fonctionnelle à un moment T).

Verrouillage

Le dernier mécanisme — nécessaire — apporté par les Dependency Manager modernes est le mécanisme de verrouillage des dépendances. Il permet de verrouiller les versions des dépendances de manière stricte lors de phases de release (jusque là, l'application des contraintes de version se fait lors des phases de développement).

Dans le cas de Composer, ce mécanisme est implémenté via le fichier composer.lock :

{
    "_readme": [
        "This file locks the dependencies of your project to a known state",
        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
        "This file is @generated automatically"
    ],
    "content-hash": "2bdb78875a3c225905ed078c9d9140f6",
    "packages": [
        {
            "name": "psr/cache",
            "version": "1.0.1",
            "source": {
                "type": "git",
                "url": "https://github.com/php-fig/cache.git",
                "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8",
                "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8",
                "shasum": ""
            },
            ...
        },
        ...
     ]
}

On voit que ce fichier (qui doit faire partie des sources à release) fixe la dépendance psr/cache à la référence de commit précise d11b50. On maîtrise donc entièrement quelle version de notre dépendance va être utilisée lors d'un déploiement.

Recommandation officielle

#!/bin/sh

EXPECTED_CHECKSUM="$(wget -q -O - https://composer.github.io/installer.sig)"
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', 'composer-setup.php');")"

if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]
then
    >&2 echo 'ERROR: Invalid installer checksum'
    rm composer-setup.php
    exit 1
fi

php composer-setup.php --quiet
RESULT=$?
rm composer-setup.php
exit $RESULT

Script d'installation automatique proposé par la documentation officielle de Composer

Composer propose un utilitaire d'installation, signé, sous la forme du script PHP composer-setup.php. La documentation indique qu'on peut choisir la version de l'utilitaire à utiliser. Cependant, il n'est pas indiqué qu'on DEVRAIT choisir la version de Composer à installer, et comment faire.

Cordonnier mal chaussé

Fin 2018, Composer sortait sa version 1.7.3 (un patch sur une version mineur, donc). Le script d'installation ci-dessus étant massivement utilisé, fleurirent sur Twitter et Github une pluie de commentaires de type :

Yesterday composer install works fine. Without any changes to composer.json & composer.lock today it fails

https://github.com/composer/composer/issues/7764 https://github.com/composer/composer/issues/7974 https://github.com/composer/composer/issues/7813

Et des réponses des mainteneurs du type :

This isn't a bug, it was a bug before which was fixed.

Avec l'arrivée de Composer 2 dans les prochains mois, les scripts de déploiement en production des applications PHP risquent de se mettre à planter. On commence à voir apparaître dans les logs de déploiement des Deprecation Notices qui n'existaient pas quelques mois auparavant.

Être strict AUSSI avec Composer

Les mainteneurs de Composer ont fait un travail admirable sur la stabilité du projet, et ont permis que puisse être utilisée quasiment n'importe quelle version de l'outil sans causer de problème. La confiance s'est donc installée, et la sécurité énormément relâchée. Afin de passer correctement le cap des prochains mois, il sera sans doute judicieux d'être à nouveau strict avec le Composer souhaité sur chaque projet.

Le script d'installation automatique pour Linux propose (voir les sources sur Github, bien que cela ne semble pas documenté) plusieurs options, dont --version qui nous intéresse particulièrement :

Composer Installer
------------------
Options
--help               this help
--check              for checking environment only
--force              forces the installation
--ansi               force ANSI color output
--no-ansi            disable ANSI color output
--quiet              do not output unimportant messages
--install-dir="..."  accepts a target installation directory
--preview            install the latest version from the preview (alpha/beta/rc) channel instead of stable
--snapshot           install the latest version from the snapshot (dev builds) channel instead of stable
--version="..."      accepts a specific version to install instead of the latest
--filename="..."     accepts a target filename (default: composer.phar)
--disable-tls        disable SSL/TLS security for file downloads
--cafile="..."       accepts a path to a Certificate Authority (CA) certificate file for SSL/TLS verification

L'option --version semble accepter en paramètre n'importe quel tag disponible au niveau du dépôt git Composer. Pour installer une version précise de Composer, on peut donc remplacer dans notre script :

php composer-setup.php --quiet --version="1.10.6"

Il ne semble malheureusement pas y avoir de possibilité d'utiliser une contrainte plus souple de version (de type 1.10.* ou ^1.10) à ce jour.

Liens