1. Introduction

Public cible : Développeurs Gradle ayant déjà rencontré des comportements de build "magiques" ou inattendus.

Dans notre aventure de création du plugin site-baker, nous avons suivi une approche TDD rigoureuse. Chaque fonctionnalité était testée, validée, et nous avancions avec confiance. Et puis, un jour, l’imprévu est arrivé. Les builds ont commencé à se comporter de manière erratique. Des modifications dans la logique du plugin ou dans les fichiers de configuration semblaient être ignorées, et nos tests fonctionnels, autrefois fiables, échouaient sans raison apparente.

Ce genre de problème peut être incroyablement frustrant. Il remet en question la fiabilité de l’outil et la validité de notre code. Après une session de débogage intense, le coupable a été identifié : le cache de configuration de Gradle.

2. Le Symptôme : Des Builds Fantômes

Le problème se manifestait de plusieurs manières :

  1. Je modifiais une chaîne de caractères dans une tâche println, mais l’ancienne chaîne continuait de s’afficher à l’exécution.

  2. Je changeais une valeur dans mon fichier managed-jbake-context.yml, mais le plugin agissait comme si le fichier n’avait pas été modifié.

  3. Les tests fonctionnels, qui créent des projets de test à la volée, échouaient parce que le plugin ne semblait pas détecter les fichiers de configuration fraîchement créés.

Tout se passait comme si Gradle exécutait une "version fantôme" de notre build, ignorant nos changements les plus récents.

3. L’Enquête : Qu’est-ce que le Cache de Configuration ?

Le cache de configuration est une fonctionnalité relativement moderne et extrêmement puissante de Gradle, activée par défaut dans les nouvelles versions. Son objectif est de rendre les builds plus rapides.

Le principe est simple :
  1. Lors de la première exécution, Gradle exécute la phase de Configuration (lecture des build.gradle.kts, création des tâches, résolution des dépendances) et construit un graphe de tâches.

  2. À la fin de cette phase, Gradle sérialise ce graphe de tâches et le met en cache.

  3. Lors des exécutions suivantes, si rien n’a changé (scripts de build, gradle.properties, etc.), Gradle saute complètement la phase de configuration et réutilise le graphe de tâches mis en cache.

Le gain de temps est spectaculaire sur les gros projets. Cependant, cette performance a un prix : elle impose des règles strictes sur la manière dont les plugins doivent être écrits.

config cache lifecycle
Figure 1. Le cycle de vie du cache de configuration

4. La Cause du Problème : Un Plugin Non Conforme

Notre plugin site-baker violait, sans le savoir, plusieurs règles du cache de configuration. Pour qu’un graphe de tâches soit sérialisable, les tâches ne doivent pas contenir de références à des objets complexes comme l’objet Project ou lire des fichiers de manière arbitraire pendant la phase d’exécution.

Notre erreur principale était de lire le contenu du fichier YAML directement à l’intérieur de la logique d’exécution de la tâche, en utilisant une référence au chemin stocké dans notre extension. Cette approche est incompatible avec le cache car Gradle ne peut pas savoir si le contenu du fichier a changé si cette lecture n’est pas modélisée comme une entrée de tâche (Task Input).

5. La Solution Temporaire : Désactiver le Cache

Pour nous débloquer et retrouver un comportement de build prévisible, la solution la plus rapide a été de désactiver le cache de configuration. Il suffit d’ajouter la ligne suivante dans le fichier gradle.properties du projet qui utilise le plugin (ou dans notre cas, le projet de test site-baker).

# site-baker/gradle.properties
org.gradle.configuration-cache=false

Instantanément, les builds ont retrouvé leur comportement normal. Chaque exécution relançait la phase de configuration et nos changements étaient pris en compte.

Cependant, c’est une solution de contournement, pas une solution durable. Elle sacrifie la performance et ne résout pas le problème de fond de notre plugin.

6. La Vraie Solution : Rendre le Plugin Compatible

Pour qu’un plugin soit un bon citoyen de l’écosystème Gradle moderne, il doit être compatible avec le cache de configuration. Cela implique de repenser la manière dont les données circulent vers nos tâches.

La clé est d’utiliser les Provider APIs de Gradle. Au lieu de passer des valeurs directes (comme un String ou un File) à nos tâches, nous devons passer des Property<T> ou des Provider<T>.

Voici le plan de refactoring :
  1. Déclarer les entrées de tâches : La tâche qui parse le fichier YAML doit déclarer ce fichier comme une entrée. On utilise pour cela l’annotation @InputFile. [source,kotlin] ---- @get:InputFile abstract val configFile: RegularFileProperty ----

  2. Utiliser les Property et Provider : La valeur de configFile sera connectée à la propriété configPath de notre extension DSL. Gradle est ainsi capable de tracer la provenance de la donnée. [source,kotlin] ---- // Dans le plugin tasks.register<MyTask>("myTask") { configFile.set(extension.configPath.flatMap { project.layout.projectDirectory.file(it) }) } ----

  3. Lire le contenu au bon moment : La lecture du fichier doit se faire à l’intérieur de l’action de la tâche (@TaskAction), en utilisant le Provider de l’entrée. [source,kotlin] ---- @TaskAction fun execute() { val content = configFile.get().asFile.readText() // …​ parser le contenu } ----

En suivant ce modèle, Gradle comprend que si le contenu de configFile change, le cache de configuration est invalide et la phase de configuration doit être ré-exécutée. De plus, il sait que la sortie de la tâche dépend du contenu de ce fichier, ce qui permet également d’optimiser le cache d’exécution (UP-TO-DATE).

7. Conclusion

Cette aventure de débogage a été une leçon précieuse. Le comportement "magique" du cache de configuration nous a forcés à mieux comprendre le cycle de vie de Gradle et les principes de la programmation déclarative et paresseuse (lazy).

Désactiver le cache de configuration est un outil de diagnostic utile, mais la véritable solution est de concevoir des plugins robustes et modernes. En modélisant correctement les entrées et sorties de nos tâches avec les APIs Provider, nous ne corrigeons pas seulement un bug, nous améliorons la performance, la fiabilité et la maintenabilité de notre plugin.

Dans le prochain article, nous mettrons en pratique ce refactoring pour rendre notre tâche de parsing YAML entièrement compatible avec le cache de configuration.