Migration d'un plugin Asciidoctor RevealJS vers buildSrc Kotlin : reverse engineering d'une API Groovy
Publié le 06 March 2026
- 1. Contexte et objectif
- 2. Le premier obstacle :
ruby { gems() } - 3. La solution en trois parties
- 4. Introspection de l’API par bytecodes
- 5. Configuration du toolchain Java
- 6. Détection automatique Docker
- 7. Résultat final
- 8. Récapitulatif des pièges et solutions
- 9. Méthode d’investigation : lire une API inconnue avec javap
- 9.1. Principe
- 9.2. Étape 1 : localiser le jar dans le cache Gradle
- 9.3. Étape 2 : lister les classes du jar
- 9.4. Étape 3 : inspecter une classe
- 9.5. Étape 4 : remonter la hiérarchie
- 9.6. Étape 5 : suivre les types inconnus
- 9.7. Étape 6 : vérifier les extensions de projet
- 9.8. Récapitulatif de la méthode
- 10. Prochaine étape
1. Contexte et objectif
1.1. Le point de départ
Le projet slider-gradle génère des présentations Reveal.js à partir de fichiers AsciiDoc via le plugin Gradle org.asciidoctor.jvm.revealjs. La configuration de la tâche principale asciidoctorRevealJs vivait directement dans le buildscript racine build.gradle.kts :
plugins { id("org.asciidoctor.jvm.revealjs") }
apply<slides.SlidesPlugin>()
project.tasks.getByName<AsciidoctorJRevealJSTask>(TASK_ASCIIDOCTOR_REVEALJS) {
repositories { ruby { gems() } }
revealjs {
version = "3.1.0"
templateGitHub {
setOrganisation("hakimel")
setRepository("reveal.js")
setTag("3.9.1")
}
}
revealjsOptions {
// ... configuration complète
}
}
1.2. L’objectif
Déplacer toute cette configuration dans buildSrc/src/main/kotlin/slides/SlidesPlugin.kt pour que le buildscript consommateur se réduise à :
apply<slides.SlidesPlugin>()
Le plugin doit être totalement autonome : il applique lui-même ses dépendances de plugins, configure ses repos, et gère ses gems Ruby.
2. Le premier obstacle : ruby { gems() }
2.1. Ce que cache le sucre syntaxique Groovy
La ligne repositories { ruby { gems() } } est une extension DSL Groovy disponible uniquement dans le contexte d’exécution du buildscript. Elle n’existe pas en tant qu’API Kotlin statique accessible depuis buildSrc.
En tentant de l’appeler depuis SlidesPlugin.kt, la compilation échoue immédiatement.
2.2. Décomposition du mécanisme
Après analyse du cache Gradle (~/.gradle/caches/modules-2/files-2.1/rubygems/), on découvre dans le fichier ivy-3.1.0.xml :
<artifact type='gem' url='https://rubygems.org/gems/asciidoctor-revealjs-3.1.0.gem' />
ruby { gems() } effectuait en réalité trois opérations distinctes :
-
Enregistrer un repo Ivy pointant vers
https://rubygems.org/gems/ -
Exclure le groupe
rubygemsdes repos Maven pour éviter les conflits -
Enregistrer la gem dans la configuration
asciidoctorGemspour que JRuby la charge au runtime
Ces trois responsabilités doivent être reproduites séparément en Kotlin.
3. La solution en trois parties
3.1. Partie 1 : le repo Ivy pour rubygems
project.repositories.mavenCentral() {
content { excludeGroup("rubygems") }
}
project.repositories.ivy {
url = project.uri("https://rubygems.org/gems/")
patternLayout { artifact("[module]-[revision].gem") } (1)
metadataSources { artifact() }
content { includeGroup("rubygems") }
}
| 1 | L’extension .gem doit être hardcodée. [ext] résout par défaut en .jar, ce qui provoque une erreur Resource missing. |
Le repo Ivy est déclaré directement sur project.repositories et non dans un bloc repositories { } car le receiver de ce bloc n’est pas le RepositoryHandler standard de Gradle mais une API grolifant incompatible avec l’extension ivy Kotlin DSL.
|
3.2. Partie 2 : la dépendance asciidoctorGems
Le plugin org.asciidoctor.jvm.gems doit être appliqué en premier — il crée la configuration asciidoctorGems et la tâche asciidoctorGemsPrepare :
project.plugins.apply("org.asciidoctor.jvm.gems")
project.plugins.apply("org.asciidoctor.jvm.revealjs") (1)
project.dependencies {
add("asciidoctorGems", "rubygems:asciidoctor-revealjs:3.1.0@gem") (2)
}
| 1 | L’ordre d’application est important : gems avant revealjs. |
| 2 | Le qualificateur @gem force l’extension correcte sur la dépendance. |
3.3. Partie 3 : settings.gradle.kts
dependencyResolutionManagement dans settings.gradle.kts doit autoriser les projets à déclarer leurs propres repos :
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
}
Sans cette ligne, Gradle ignore les repos déclarés dans le plugin et la résolution des gems échoue.
4. Introspection de l’API par bytecodes
4.1. Pourquoi javap ?
Le plugin asciidoctor-gradle-jvm-slides est en version 4.0.0-alpha.1. Sa documentation est inexistante ou incomplète. La seule source fiable est l’inspection directe des classes compilées.
4.2. Hiérarchie de la tâche
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask
Résultat :
public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
implements org.asciidoctor.gradle.base.slides.SlidesToExportAware
4.3. Découverte de forkOptions
En inspectant AbstractAsciidoctorTask :
javap -p -classpath asciidoctor-gradle-jvm-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask | grep -i "fork\|exec\|jvm"
On trouve :
final org.ysb33r.grolifant.api.v4.JavaForkOptions javaForkOptions;
public void forkOptions(org.gradle.api.Action<org.ysb33r.grolifant.api.v4.JavaForkOptions>);
public static final org.asciidoctor.gradle.base.process.ProcessMode JAVA_EXEC;
4.4. L’API de JavaForkOptions (grolifant)
javaLauncher n’existe pas sur cette tâche. L’API réelle de org.ysb33r.grolifant.api.v4.JavaForkOptions expose :
public void executable(java.lang.Object); (1)
public void setExecutable(java.lang.Object);
| 1 | La méthode executable(Object) remplace l’assignation executable = … qui ne compile pas (val cannot be reassigned). |
4.5. RevealJSExtension
revealjs { } n’est pas une méthode de la tâche mais une extension de projet :
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.RevealJSExtension
Elle s’accède via :
project.extensions.getByType<RevealJSExtension>().apply {
version = "3.1.0"
templateGitHub {
setOrganisation("hakimel")
setRepository("reveal.js")
setTag("3.9.1")
}
}
5. Configuration du toolchain Java
5.1. Le problème de JavaToolchainService
JavaToolchainService n’est pas une extension de projet. L’appel suivant échoue :
// ERREUR : Extension of type 'JavaToolchainService' does not exist
project.extensions.getByType<JavaToolchainService>()
La bonne API est serviceOf :
import org.gradle.kotlin.dsl.support.serviceOf
project.tasks.getByName<AsciidoctorJRevealJSTask>(TASK_ASCIIDOCTOR_REVEALJS) {
setInProcess("JAVA_EXEC")
forkOptions {
executable(
project.serviceOf<JavaToolchainService>()
.launcherFor {
languageVersion.set(JavaLanguageVersion.of(17))
vendor.set(JvmVendorSpec.ADOPTIUM)
}
.get()
.executablePath
.asFile
.absolutePath
)
}
}
6. Détection automatique Docker
6.1. Contexte
Le plugin Asciidoctor/JRuby requiert Java 17. Kotlin 2.0.x dans buildSrc ne supporte pas Java 25 (le parser de version crashe sur "25.0.2"). Le daemon Gradle doit donc tourner sur Java 17 ou Docker doit être utilisé.
6.2. Stratégie
-
Docker disponible → exécution via container
eclipse-temurin:17(comportement par défaut) -
Docker absent + Java 17 → exécution locale
-
Docker absent + Java > 17 → erreur explicite
val isDockerAvailable = try {
Runtime.getRuntime().exec(arrayOf("docker", "info")).waitFor() == 0
} catch (e: Exception) {
false
}
val javaVersion = JavaVersion.current().majorVersion.toInt()
when {
isDockerAvailable -> project.tasks.register<Exec>(TASK_ASCIIDOCTOR_REVEALJS) {
group = GROUP_TASK_SLIDER
description = "Slider settings and generation (via Docker)"
dependsOn(TASK_CLEAN_SLIDES_BUILD)
finalizedBy(TASK_DASHBOARD_SLIDES_BUILD)
commandLine(
"docker", "run", "--rm",
"-v", "${project.rootDir.absolutePath}:/workspace",
"-v", "${System.getProperty("user.home")}/.gradle:/root/.gradle",
"-w", "/workspace",
"eclipse-temurin:17",
"./gradlew", TASK_ASCIIDOCTOR_REVEALJS
)
workingDir = project.rootDir
}
javaVersion == 17 -> {
project.repositories.mavenCentral() {
content { excludeGroup("rubygems") }
}
project.repositories.ivy {
url = project.uri("https://rubygems.org/gems/")
patternLayout { artifact("[module]-[revision].gem") }
metadataSources { artifact() }
content { includeGroup("rubygems") }
}
project.extensions.getByType<RevealJSExtension>().apply {
version = "3.1.0"
templateGitHub {
setOrganisation("hakimel")
setRepository("reveal.js")
setTag("3.9.1")
}
}
project.tasks.getByName<AsciidoctorJRevealJSTask>(TASK_ASCIIDOCTOR_REVEALJS) {
setInProcess("JAVA_EXEC")
forkOptions {
executable(
project.serviceOf<JavaToolchainService>()
.launcherFor {
languageVersion.set(JavaLanguageVersion.of(17))
vendor.set(JvmVendorSpec.ADOPTIUM)
}
.get()
.executablePath
.asFile
.absolutePath
)
}
// ... reste de la configuration
}
}
else -> error(
"Docker est requis pour exécuter $TASK_ASCIIDOCTOR_REVEALJS " +
"avec Java $javaVersion. Installez Docker ou utilisez Java 17."
)
}
7. Résultat final
7.1. Le buildscript consommateur
apply<slides.SlidesPlugin>()
C’est tout. Le plugin porte l’intégralité de la responsabilité.
7.2. settings.gradle.kts
pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
}
rootProject.name = "slider-gradle"
8. Récapitulatif des pièges et solutions
| Problème | Cause | Solution |
|---|---|---|
|
Extension DSL Groovy uniquement |
Trois mécanismes séparés : repo Ivy + exclusion Maven + |
Gradle cherche un |
|
Hardcoder |
|
Receiver grolifant incompatible avec le DSL Kotlin |
Appel direct |
|
Propriété inexistante sur |
|
|
Propriété |
Méthode |
|
C’est un service Gradle, pas une extension |
|
|
Extension de projet, pas méthode de tâche |
|
Build crashe avec Java 25 |
Kotlin 2.0.x ne parse pas les versions Java à deux chiffres |
Détection Docker automatique + fallback Java 17 |
9. Méthode d’investigation : lire une API inconnue avec javap
9.1. Principe
Quand la documentation est absente ou incomplète, les bytecodes sont la source de vérité.
javap est l’outil standard du JDK qui décompile les fichiers .class en signatures Java lisibles,
sans nécessiter le code source.
9.2. Étape 1 : localiser le jar dans le cache Gradle
Gradle télécharge toutes ses dépendances dans ~/.gradle/caches/modules-2/files-2.1/.
La première étape est de trouver le jar qui contient la classe à inspecter :
find ~/.gradle/caches -name "asciidoctor-gradle-jvm-slides*.jar" 2>/dev/null
/home/user/.gradle/caches/modules-2/files-2.1/org.asciidoctor/
asciidoctor-gradle-jvm-slides/4.0.0-alpha.1/.../
asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar
9.3. Étape 2 : lister les classes du jar
Avant d’inspecter une classe, on vérifie qu’elle existe bien dans le jar :
jar tf asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar | grep -i "RevealJS\|revealjs"
AsciidoctorJRevealJSTask, RevealJSExtension, RevealJSOptions, etc.
9.4. Étape 3 : inspecter une classe
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask
L’option -p affiche tous les membres y compris les privés.
Le résultat montre immédiatement la ligne clé :
public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
9.5. Étape 4 : remonter la hiérarchie
La tâche étend AbstractAsciidoctorTask. On l’inspecte à son tour
en localisant d’abord son jar :
find ~/.gradle/caches -name "asciidoctor-gradle-jvm-[0-9]*.jar" 2>/dev/null
javap -p -classpath asciidoctor-gradle-jvm-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask | grep -i "fork\|exec\|jvm\|java"
C’est là qu’on découvre forkOptions, JAVA_EXEC, et javaForkOptions
de type org.ysb33r.grolifant.api.v4.JavaForkOptions.
9.6. Étape 5 : suivre les types inconnus
JavaForkOptions est une classe grolifant inconnue. On localise son jar :
find ~/.gradle/caches -name "grolifant*.jar" 2>/dev/null
Puis on l’inspecte :
javap -p -classpath grolifant40-legacy-api-2.0.0-alpha.6.jar \
org.ysb33r.grolifant.api.v4.JavaForkOptions
On y trouve executable(java.lang.Object) — la méthode correcte à appeler,
par opposition à executable = … qui ne compile pas car c’est une propriété val.
9.7. Étape 6 : vérifier les extensions de projet
Pour revealjs { }, la question était : est-ce une méthode de la tâche
ou une extension de projet ? L’inspection de AsciidoctorJRevealJSTask
ne montre aucune méthode revealjs. On inspecte alors RevealJSExtension :
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.RevealJSExtension | head -5
public class RevealJSExtension implements groovy.lang.GroovyObject {
public static final java.lang.String NAME;
La présence de NAME confirme que c’est une extension enregistrée sur le projet,
accessible via project.extensions.getByType<RevealJSExtension>().
9.8. Récapitulatif de la méthode
| Étape | Action |
|---|---|
1 |
|
2 |
|
3 |
|
4 |
Identifier |
5 |
Suivre les types inconnus dans leurs propres jars |
6 |
Chercher |
Cette méthode s’applique à n’importe quel plugin Gradle dont l’API n’est pas documentée ou dont la version alpha ne correspond plus à la documentation existante.
10. Prochaine étape
Ce plugin buildSrc sera extrait dans un projet indépendant publié sur Gradle Plugin Portal ou Maven Local. Le buildscript consommateur deviendra alors :
plugins { id("slides") version "1.0.0" }
Et settings.gradle.kts sera réduit à son strict minimum sans référence à foojay-resolver-convention, le provisioning JDK étant géré par le plugin lui-même ou documenté comme prérequis.