Utiliser une propriété unique comme version Maven

This post is also available in: en

Quand je travaille sur un projet Maven multi-modules (plusieurs dizaines) sur plusieurs branches en même temps, je me retrouve souvent avec des conflits à résoudre pour chaque merge sur les versions définies un peu partout dans les pom.xml… Et c’est tout sauf marrant.
Tour d’horizon des solutions pour réduire le nombre de <version> déclarées dans tous ces pom.xml.

Notre projet exemple de départ est donc constitué de deux modules :

.
├── pom.xml
├── submodule-1
│   └── pom.xml
└── submodule-2
    └── pom.xml

Le pom.xml du projet parent ressemble à :

<project>
    <groupId>com.github.jcgay.example.version</groupId>
    <artifactId>parent-pom</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <modules>
        <module>submodule-1</module>
        <module>submodule-2</module>
    </modules>

</project>

et ceux des modules :

<project>
    <parent>
        <groupId>com.github.jcgay.example.version</groupId>
        <artifactId>parent-pom</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>submodule-1</artifactId>

</project>

La version du projet est répétée dans chaque pom.xml des modules, pour les changer ce n’est pas bien compliqué avec le plugin Versions. Par contre les merges entre branches sont vite pénibles, les conflits sont fréquents et généralement on ne peut pas se contenter de ne garder les changements que d’une des deux branches 😓.

Définir la version dans une propriété

Première tentative, extraire la version dans une propriété Maven. Le pom parent devient donc :

<project>
    <groupId>com.github.jcgay.example.version</groupId>
    <artifactId>parent-pom</artifactId>
    <version>${my-version}</version>
    <packaging>pom</packaging>

    <properties>
        <my-version>1.0-SNAPSHOT</my-version>
    </properties>

    <modules>
        <module>submodule-1</module>
        <module>submodule-2</module>
    </modules>

</project>

et un module utilise également la propriété ${my-version} pour référencer son parent :

<project>
    <parent>
        <groupId>com.github.jcgay.example.version</groupId>
        <artifactId>parent-pom</artifactId>
        <version>${my-version}</version>
    </parent>

    <artifactId>submodule-2</artifactId>

</project>

Etonnement ça fonctionne 😲
La seule arnaque provient des messages d’avertissements affichés par Maven.

Some problems were encountered while building the effective model for com.github.jcgay.example.version:submodule-1:jar:1.0-SNAPSHOT
'version' contains an expression but should be a constant. @ com.github.jcgay.example.version:parent-pom:${my-version}, /Users/jcgay/dev/toolbox-examples/unique-maven-version/pom.xml, line 7, column 14

Some problems were encountered while building the effective model for com.github.jcgay.example.version:submodule-2:jar:1.0-SNAPSHOT
'version' contains an expression but should be a constant. @ com.github.jcgay.example.version:parent-pom:${my-version}, /Users/jcgay/dev/toolbox-examples/unique-maven-version/pom.xml, line 7, column 14

Some problems were encountered while building the effective model for com.github.jcgay.example.version:parent-pom:pom:1.0-SNAPSHOT
'version' contains an expression but should be a constant. @ com.github.jcgay.example.version:parent-pom:${my-version}, /Users/jcgay/dev/toolbox-examples/unique-maven-version/pom.xml, line 7, column 14

It is highly recommended to fix these problems because they threaten the stability of your build.

For this reason, future Maven versions might no longer support building such malformed projects.

En l’état ce n’est pas vraiment sérieux, et puis il y a un autre problème avec cette solution mais on va la détailler dans la seconde.

Continuous delivery friendly version

Depuis Maven 3.2.1 il est possible d’utiliser des propriétés réservées pour éviter les messages d’avertissements précédents.

Si on remplace notre propriété my-version de la solution précédente par une autre nommée revision, plus de messages qui font peur 🎆.

Seulement utiliser des propriétés plutôt que des versions stables pose réellement un problème. Quand Maven installe dans le repository local ou déploie dans un repository distant nos artifacts, les pom.xml sont copiés en l’état. Si on a donc un projet qui dépend d’un de nos modules (comprendre qui n’est pas construit dans ce même projet multi-modules), la résolution des dépendances échouera car la propriété ${revision} n’existera pas dans ce projet extérieur.

Exemple avec ce projet tout simple qui n’est pas un module de notre parent-pom :

<project>
    <groupId>com.github.jcgay.example.version</groupId>
    <artifactId>independant-project</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>com.github.jcgay.example.version</groupId>
            <artifactId>submodule-1</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

Quand on lance un build :

$> mvn test

Scanning for projects...

Using the MultiThreadedBuilder implementation with a thread count of 4

------------------------------------------------------------------------
Building independant-project 1.0-SNAPSHOT
------------------------------------------------------------------------
Downloading: https://repo.maven.apache.org/maven2/com/github/jcgay/example/version/parent-pom/$%7Brevision%7D/parent-pom-$%7Brevision%7D.pom
------------------------------------------------------------------------
BUILD FAILURE
------------------------------------------------------------------------
Total time: 0.882 s (Wall Clock)
Finished at: 2016-08-09T15:26:09+02:00
Final Memory: 12M/193M
------------------------------------------------------------------------
Failed to execute goal on project independant-project: Could not resolve dependencies for project com.github.jcgay.example.version:independant-project:jar:1.0-SNAPSHOT: Failed to collect dependencies at com.github.jcgay.example.version:submodule-1:jar:1.0-SNAPSHOT: Failed to read artifact descriptor for com.github.jcgay.example.version:submodule-1:jar:1.0-SNAPSHOT: Could not find artifact com.github.jcgay.example.version:parent-pom:pom:${revision} in central (https://repo.maven.apache.org/maven2) -> [Help 1]

Et en effet le pom.xml présent dans mon repository local défini toujours ${revision} en tant que <version> du pom-parent.

Filtrer les pom à l’installation/déploiement

Ce problème de propriétés non remplacées m’a rappelé un vieil article dans lequel l’auteur s’était amusé à filtrer (dans le sens maven-resources-plugin filtering) ses fichiers pom.xml et les installer à la place des originaux.
J’ai donc recherché s’il était possible de faire de même mais avec une extension Maven (la dose de XML nécessaire et le côté gros hack de la solution décrite m’a découragé…). Et c’est effectivement faisable avec Aether (utilisé dans Maven depuis la version 3) via un org.eclipse.aether.impl.MetadataGenerator. On peut donc écrire un gros hack dans son langage préféré à la place 🤔.

Le code est disponible sur GitHub. Il prend la forme d’une extension Maven qui se charge de transformer les artifacts de type pom à l’installation et au déploiement. Toutes les occurrences de ${revision} sont remplacées par l’actuelle version de l’artifact traité (c’est à dire la valeur résolue de ${revision}). Le problème de résolution de dépendance est dans ce cas résolu !

On peut l’utiliser via le mécanisme d’extensions de Maven en créant (modifiant) le fichier ${maven.multiModuleProjectDirectory}/.mvn/extensions.xml :

<?xml version="1.0" encoding="UTF-8"?>
<extensions>
    <extension>
      <groupId>fr.jcgay.maven.extension</groupId>
      <artifactId>unique-revision-maven-filtering</artifactId>
      <version>[latest-version]</version>
    </extension>
</extensions>

ou bien en copiant directement le jar dans %M2_HOME%/lib/ext.

Conclusion

On a donc vu comment utiliser une propriété unique pour définir la version de tous les modules d’un projet. Clairement la fonctionnalité proposée par Maven (continuous delivery friendly version) ne semble pas prévue pour l’usage décrit ici, mais au moins je ne m’embête plus avec mes merge ! J’ai peut-être complètement raté une solution plus simple ? (dans ce cas je la veux bien 😉).

comments powered by Disqus