Intermédiaire

Gradle, je t’aime : moi non plus.

[ɡʁadle]

Google a choisi Gradle comme système de référence pour aider les développeurs à construire leurs applications Android. Pourtant le standard dans l’écosystème Java était et reste toujours Maven. Même si toutes les applications Android sont aujourd’hui construites à partir de Gradle, la majorité des développeurs mobile subissent l’outil plus qu’ils n’exploitent son potentiel. Pourtant Gradle Inc., la société propriétaire de Gradle veut faire bouger les lignes et a développé les dernières versions dans ce sens.

Gradle Inc. oriente sa stratégie pour développer un outil de build pratique et puissant, riche en fonctionnalités innovantes. Quelles sont ces fonctionnalités ? En quoi peuvent-elles aider le développeur Android ? Gradle pourrait-il devenir le prochain outil de build de référence ?

Un outil de build ?

Pour déployer une nouvelle version en production, il suffit de copier les class modifiées sur le serveur.

Un projet doit construire un livrable à partir de différentes sources : code, assets, documentation… Le livrable peut être un jar, un apk ou un package plus complexe. Ce livrable peut même être constitué de multiples packages avec des variantes différentes (i.e. : version d’une application gratuite, payante, etc.).
Pour faciliter cette construction, il faut automatiser l’ensemble des opérations pouvant aboutir à notre livrable. C’est là qu’un outil de build intervient : il va être chargé de compiler les sources, transformer les assets, générer la documentation, construire le ou les livrables et les mettre à disposition du développeur.
Gradle n’impose pas de cycle de vie standard, mais plutôt un cycle d’exécution par défaut. Vous pouvez par la suite personnaliser ce cycle comme bon vous semble. Ici, ce n’est pas votre projet qui doit s’adapter à votre outil mais plutôt le contraire… C’est Gradle qui va s’adapter à votre projet.
Ce cycle d’exécution consiste en un graphe de tâches reliées les unes aux autres. Ce graphe est construit grâce aux tâches d’un projet et de ses sous projets. Comme pour Maven, des plugins peuvent être ajoutés, apportant ainsi de nouvelles fonctionnalités.
Ce sont majoritairement ces tâches et ces plugins que nous manipulerons dans Gradle.

Graph de dépendances

Les dépendances entre tâches peuvent être contrôlées via différentes méthodes. La plus commune est dependsOn. Une tâche a besoin d’un fichier : une dépendance de cette tâche sera alors la création de ce fichier. Grâce à dependsOn, notre tâche ne sera exécutée qu’après la tâche qui doit créer ce fichier. Cela permet de créer un graph simple mais suffisant dans de nombreux cas.

task root() {
	doLast {
    	println("...starting gradle task")
	}
}
 
task performing() {
	dependsOn "root"
	doLast {
    	println("...doing something")
	}
}
 
task ending() {
	dependsOn "performing"
	doLast {
    	println("...finishing build !")
	}
}

L’exécution de la tâche ending dépend de la tâche performing, qui elle-même dépend de la tâche root.

:ending
\--- :performing
    \--- :root

Si l’on invoque la tâche ending, naturellement, cela va déclencher la tâche root, performing puis finir avec ending. Ce qui correspond au graphe défini auparavant.

17:59:26: Executing external task 'ending'...
:root
...starting gradle task
:performing
...doing something
:ending
...finishing build !
 
BUILD SUCCESSFUL in 0s
3 actionable tasks: 3 executed

Il existe d’autres méthodes pour modifier son graphe, en termes de dépendances ou bien en terme d’ordre d’exécution. Par exemple, finalizedBy permet de forcer l’exécution d’une tâche après une autre tâche. Quant à mustRunAfter / shouldRunAfter, ses primitives permettent de mieux spécifier l’ordre d’exécution des tâches.

task ending() {
	dependsOn "performing"
	finalizeBy "anotherTask"
	doLast {
    	println("...finishing build !")
	}
}

L’exemple ci-dessus permet d’exécuter la tâche anotherTask, une fois que la tâche ending est terminée.

Gradle offre donc la possibilité de configurer finement l’ordre d’exécution des tâches qui vont constituer la construction de son application.

------------------------------------------------------------
Root project
------------------------------------------------------------

:build
+--- :assemble
|   \--- :jar
|       \--- :classes
|           +--- :compileJava
|           |   \--- :compileKotlin
|           \--- :processResources
\--- :check
    \--- :test
        +--- :classes
        |    +--- :compileJava
        |    |  \--- :compileKotlin
        |    \--- :processResources
        \--- :testClasses
            +--- :compileTestJava
            |   +--- :classes
            |   |    +--- :compileJava
            |   |    |  \--- :compileKotlin
            |   |    \--- :processResources
            |   \--- :compileTestKotlin
            |       \--- :classes
            |           +--- :compileJava
            |           |   \--- :compileKotlin
            |           \--- :processResources
            \--- :processTestResources

Exemple de graphe par défaut de Gradle.

Utiliser Gradle devient particulièrement intéressant dans le contexte des entrées/sorties des tâches :

task copySeed(type: Copy) {
	from 'SEED.md'
	into 'dist'
}
12:26:29: Executing external task 'copySeed'...
:copySeed
 
BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

L’exemple ci-dessus définit une tâche qui copie le fichier SEED.md dans le répertoire dist. Le fichier SEED.md va être considéré par Gradle comme entrée de la tâche et le répertoire dist comme sortie de la tâche. L’entrée de cette tâche peut également être la sortie d’une tâche, créant alors un lien entre ces tâches : pour copier le fichier généré par la tâche A, il faut invoquer la tâche A. Ce que Gradle se chargera de faire.

Grâce à ces différentes mécaniques, vous pouvez donc décrire la topologie de construction de votre projet selon vos besoins. Mais Gradle n’utilise pas cette topologie uniquement pour enchaîner les tâches, mais l’utilise également pour optimiser votre build.

Rapidité d’exécution

Optimisation de la topologie d’exécution

Comme expliqué précédemment, grâce à cette notion d’entrée/sortie, Gradle va construire une topologie de votre build.

Gradle va également reconnaître les fichiers qui doivent être générés. Si ces fichiers sont déjà générés, alors Gradle va considérer la tâche comme étant à jour (UP-TO-DATE) et va omettre volontairement l’exécution d’une tâche à jour. Au final, seules les tâches générant une sortie seront exécutées.

12:16:32: Executing external task 'copySeed'...
:copySeed UP-TO-DATE
 
BUILD SUCCESSFUL in 0s
1 actionable task: 1 up-to-date

Pour la copie d’un fichier, cela peut paraître négligeable, mais si votre tâche télécharge par exemple un fichier volumineux, compile des fichiers, assemble différents composants : ne pas ré exécuter des tâches inutilement est un gain de temps considérable !

Cette logique de dépendance est poussée jusqu’aux dépendances entre projets : le projet B utilise le projet A ? Lors de la compilation du projet B, Gradle va recompiler le projet A s’il a été modifié. Pour les utilisateurs de Maven qui déclenchent mvn clean install par réflexe, pour être sûr de ne pas oublier de compiler quelque chose, cela représente un gain de temps non négligeable. Le développeur est alors libéré de la réflexion liée à l’ordre des composants, devenue inutile.

Optimisation d’exécution du build

Outre l’optimisation du workflow d’exécution, Gradle essaie de gagner du temps en se lançant en tant que daemon. Sans cette fonctionnalité, la JVM (Java Virtual Machine) doit démarrer, charger les classes de Gradle… Cela peut être long.

Un développeur est amené à lancer Gradle plusieurs fois par jour. Plus il le lance, plus le temps perdu par le chargement s’accumule. Quand Gradle est lancé en tant que daemon, les invocations suivantes sont plus rapides : Gradle est chargé en mémoire et déjà prêt pour la prochaine exécution.

Gradle peut également vous faire gagner du temps sur votre premier build gradle. Vous n’avez pas Gradle sur votre poste ou la mauvaise version ? Utilisez le Gradle Wrapper ! C’est un jar que vous laissez avec votre projet et qui se charge de récupérer, pour vous, la bonne version de Gradle et de le configurer pour votre build. Ce même wrapper est configurable : vous pouvez lui faire télécharger la version de Gradle que votre entreprise utilise.

gradlew build
// gradle/wrapper/gradle.properties
[...]
distributionUrl=http\://internal.repository.corp/gradle-all-4.0.zip

Build Cache

Les dernières versions de l’outil de build proposent désormais le Build Cache : vous pouvez réutiliser ce qui a déjà été construit par d’autres build. Ce cache est local mais peut également être distant. Pourquoi ne pas réutiliser ce qui a déjà été compilé par votre intégration continue ?

On l’aura compris : une des ambitions de Gradle est de construire le plus rapidement possible un projet, que ce soit en simplifiant la configuration, en optimisant le build ou encore en réutilisant des éléments déjà existants. Et c’est un point critique pour les projets actuels : plus l’outil est rapide, plus vous pouvez construire rapidement des projets, tester des idées et ainsi prendre de vitesse vos concurrents.

Features

Composite Build

Votre application utilise un composant géré par une autre équipe. Malheureusement, ce composant contient un bug que vous avez détecté. Vous corrigez le problème sur le composant, vous le recompilez, vous repackagez votre application avec ce nouveau composant et testez votre modification. Malheureusement, cela ne marche toujours pas… Vous êtes alors dans l’obligation de refaire tout le cycle de modification.

Gradle vous aide à construire des applications, mais depuis la version 3.1 Gradle vous aide également à développer des composants d’applications. La fonctionnalité Composite Build permet d’inclure d’autres projets dans votre build et de laisser Gradle gérer la construction de ces autres projets. Ainsi, si vous modifiez un composant utilisé par votre application : démarrez votre application, Gradle va détecter que le composant a été modifié. Il suffit de le recompiler, recompiler par la suite votre application puis finir par démarrer votre application. Ici encore, Gradle peut vous faire gagner un temps précieux !

L’utilisation de Composite Build est simple à mettre en place : vous pouvez inclure un composant externe dans votre build. Si votre build génère des artéfacts qui sont directement utilisés par votre projet, cela est suffisant. Mais vous pouvez également configurer plus finement en jouant sur les substitutions : vous remplacerez alors une dépendance par votre composant. Avec Composite Build : modifiez le composant, redémarrez l’application : Gradle s’occupe du reste.

DSL

La vitesse n’est pas le seul facteur à prendre en compte sur un outil de build. Sa facilité de configuration est importante. Ce n’est pas quelque chose que l’on modifie régulièrement. On pourrait, à tort, penser qu’une fois le build configuré, il n’est plus nécessaire de le modifier et donc qu’importe si le build est simple ou complexe à configurer.

Pourtant, c’est justement pour cette même raison que la configuration du build doit être simple… Vous aurez à créer ou à modifier, même légèrement, votre configuration de build. Si cela est compliqué, une simple modification vous coûtera très cher : le temps de réapprendre l’outil, le temps de faire votre modification…

Sur ce point, Gradle est critiquable. Le DSL (Domain Specific Language) de Gradle est relativement souple, voire trop souple. Ce DSL repose sur le langage Groovy. Cela permet de faire du scripting de build assez facilement – pour peu que l’on ait des connaissances en Groovy. Ce n’est pas un problème en soi mais le côté scripting mélangé avec un DSL un peu trop souple peut perdre le développeur.

Par exemple, pour configurer une version de Java, vous devez assigner la variable sourceCompatibility. Mais où est définie cette variable ? D’où vient-elle ?

sourceCompatibility = JavaVersion.VERSION_1_8

Cette variable est en fait définie par le plugin java. C’est surement un choix pragmatique, mais lors des premières utilisations de Gradle, il est difficile de savoir ce qui est configurable ou non via ce fameux plugin Java. Il est dommage que la configuration ne soit pas dans un namespace spécifique. Cela permettrait de comprendre rapidement d’où vient cette option de configuration et quelles autres options sont effectivement disponibles.

java {
   sourceCompatibility = JavaVersion.VERSION_1_8
}

Dans certains cas, des configurations peuvent être paramétrées :

sourceSets {
	main {
   	// ...
	}
 
	toto {
  	// ...
	}
}

L’exemple ci-dessus décrit un sourceSets (grossièrement : un repository de code dans votre projet). Mais l’on peut décrire des sourceSets avec des noms différents qui peuvent ensuite être utilisés ailleurs dans notre projet. Il existe donc des options de configuration fixes et d’autres dynamiques… Ce qui peut complexifier la situation.

Toutefois, ce problème pourrait tendre à disparaître de lui-même. En effet, le langage Kotlin s’impose de plus en plus. Que ce soit sur Android ou côté Back end : ce langage s’immisce également dans Gradle. Kotlin devrait en effet devenir le langage par défaut de Gradle.
Le langage devrait simplifier l’utilisation du DSL. Même si l’utilisation de Kotlin dans Gradle est encore perfectible, les premiers exemples sont prometteurs, mêmes si d’autres sont encore un petit peu trop cryptique.

Le changement de langage est, malheureusement pour Groovy, probablement le meilleur choix. La nature dynamique du langage apporte une souplesse qui se trouve être néfaste dans la compréhension des scripts de build. Mais le vrai problème réside également dans le passage à Java 9 que Groovy risque de mal supporter.

Conserver Groovy comme unique langage pour construire des builds avec Gradle représenterait un vrai frein pour l’expansion de Gradle. Gradle continuera-t-il de fonctionner avec Java 9 ? Si oui, cela sera-t-il toujours le cas avec Java 10 ? Bref, Groovy sera-t-il toujours utilisable avec Gradle et les futures versions de Java ? Cette situation est loin d’être idéale pour plébisciter un outil qui se veut innovant !

Création de plugin pour son build

Que ce soit avec Groovy ou Kotlin, vous pouvez être amenés à ajouter du comportement dans vos scripts Gradle. Mais ce n’est pas forcément une bonne pratique. En effet, on finit vite par utiliser des println pour comprendre ce qui se passe quand un problème arrive.

Idéalement les scripts ne devraient contenir que de la configuration, voire du comportement considéré comme “simple”. Toute mécanique plus complexe devrait être exportée dans des tâches et/ou des plugins spécifiques.

Ecrire son plugin peut faire peur. Pourtant Gradle propose un mécanisme d’utilisation très simple. Avant de lancer votre build, Gradle va compiler le projet qui se trouve dans le répertoire buildSrc si celui-ci existe. Tout ce qui fait partie de ce projet se retrouve alors dans le classpath de votre build et donc dans votre script de build.

class WorldPlugin : Plugin<Project> {
	override fun apply(project: Project) {
    	project.logger.error("Hello from Gradle Plugin!")
	} 
}

Déporter la complexité de vos scripts vers un plugin, c’est également pouvoir choisir votre langage de programmation pour ce plugin, tant que celui-ci est capable d’exploiter l’API de Gradle.

Bref, si vous ne trouvez pas de plugin adapté à votre besoin : vous pouvez le créer vous-même, le tout sans devoir subir les problématiques liées à la publication du plugin. Enfin, un plugin se debugge plus facilement qu’un script de build, à condition de configurer correctement Gradle en ajoutant les options de debug et en désactivant le daemon le temps de l’opération.

Adoption majeure ?

Maven règne sur l’écosystème Java et n’a pour l’instant jamais tremblé. Pourtant, les mises à jour de Maven se font de plus en plus rares. Pendant ce temps, de nouvelles fonctionnalités sont régulièrement implémentées dans Gradle. Mais Gradle ne s’est pas encore imposé sur l’ensemble des projets Java et reste cantonné au monde Android.

Plusieurs freins peuvent expliquer cette tendance : complexité du DSL, manque de standard à certains niveaux (ex : release) ou tout simplement le manque de nouveaux projets. La majorité des projets actuels ne vont pas être migrés sur un outil non maîtrisé quand la chaîne de construction est déjà en place et parfaitement fonctionnelle.

Ces freins vont progressivement disparaître avec le temps. Les dernières nouveautés de Gradle liées à la rapidité d’exécution vont bénéficier aux développeurs. Kotlin devrait aider à simplifier le DSL même si cela reste, comme Groovy, un autre langage pour le développeur Java, la compréhension syntaxique reste simple. En parallèle, Gradle Inc., la société derrière Gradle, créé de plus en plus de services dédiés aux entreprises utilisant Gradle. Ce type d’initiative est parfait pour rassurer ceux qui hésitent à sauter le pas pour leurs nouveaux projets. Car il s’agit bien de cela pour Gradle : faire démarrer les nouveaux projets sur son outil, pour vendre à ceux et celles qui le souhaitent des services annexes.

À ce petit jeu, Gradle pourrait gagner la guerre que Maven n’a jamais réussi à mener…

Nombre de vue : 805

AJOUTER UN COMMENTAIRE