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 :

  1. Enregistrer un repo Ivy pointant vers https://rubygems.org/gems/

  2. Exclure le groupe rubygems des repos Maven pour éviter les conflits

  3. Enregistrer la gem dans la configuration asciidoctorGems pour 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

  1. Docker disponible → exécution via container eclipse-temurin:17 (comportement par défaut)

  2. Docker absent + Java 17 → exécution locale

  3. 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

ruby { gems() } non disponible en Kotlin

Extension DSL Groovy uniquement

Trois mécanismes séparés : repo Ivy + exclusion Maven + asciidoctorGems

Gradle cherche un .jar au lieu d’un .gem

[ext] résout en jar par défaut

Hardcoder .gem dans le pattern Ivy + qualificateur @gem

ivy { } ne compile pas dans repositories { }

Receiver grolifant incompatible avec le DSL Kotlin

Appel direct project.repositories.ivy { }

javaLauncher non résolu

Propriété inexistante sur AsciidoctorJRevealJSTask

setInProcess("JAVA_EXEC") + forkOptions { executable(…​) }

executable = …​ ne compile pas

Propriété val dans JavaForkOptions grolifant

Méthode executable(Object) à la place

JavaToolchainService introuvable via extensions

C’est un service Gradle, pas une extension

project.serviceOf<JavaToolchainService>()

revealjs { } non résolu dans la tâche

Extension de projet, pas méthode de tâche

project.extensions.getByType<RevealJSExtension>()

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
Résultat :
/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"
Cela révèle toutes les classes disponibles :

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

find ~/.gradle/caches -name "*.jar" — localiser le jar

2

jar tf jar.jar | grep NomClasse — vérifier que la classe existe

3

javap -p -classpath jar.jar NomCompletClasse — inspecter la classe

4

Identifier extends et remonter la hiérarchie

5

Suivre les types inconnus dans leurs propres jars

6

Chercher NAME pour identifier une extension de projet

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.