Migrazione di un plugin Asciidoctor RevealJS verso buildSrc Kotlin : ingegneria inversa di un'API Groovy
Publié le 16 April 2026
Contesto e obiettivo
Il punto di partenza
Il progetto`slider-gradle`genera presentazioni Reveal.js da file AsciiDoc tramite il plugin Gradle`org.asciidoctor.jvm.revealjs`. La configurazione dell’attività principale`asciidoctorRevealJs`viveva direttamente nel buildscript radice`build.gradle.kts`(blank)
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
}
}
L’obiettivo
Sposta tutta questa configurazione in`buildSrc/src/main/kotlin/slides/SlidesPlugin.kt`affinché lo script di build del consumatore si riduca a :
apply<slides.SlidesPlugin>()
Il plug-in deve essere totalmente autonomo: applica da sé le sue dipendenze di plug-in, configura i suoi repository e gestisce i suoi gem Ruby.
Il primo ostacolo : `ruby { gems() }
Ciò che nasconde lo zucchero sintattico Groovy
La linea`repositories { ruby { gems() } }`è un’estensione DSL Groovy disponibile solo nel contesto di esecuzione del buildscript. Non esiste come API Kotlin statica accessibile da buildSrc.
Tentando di chiamarlo da`SlidesPlugin.kt`, la compilazione fallisce immediatamente.
Scomposizione del meccanismo
Dopo l’analisi della cache Gradle (~/.gradle/caches/modules-2/files-2.1/rubygems/), si scopre nel file`ivy-3.1.0.xml`:
<artifact type='gem' url='https://rubygems.org/gems/asciidoctor-revealjs-3.1.0.gem' />
`ruby { gems() }`effettuava realmentetre operazioni distinte :
-
Registrare un repo Ivy che punta a`https://rubygems.org/gems/`
-
Escludere il gruppo`rubygems`dei repository Maven per evitare i conflitti
-
Salvare la gem nella configurazione`asciidoctorGems`affinché JRuby lo carichi in fase di esecuzione
Queste tre responsabilità devono essere riprodotte separatamente in Kotlin.
La soluzione in tre parti
Parte 1 : il repo Ivy per 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’estensione`.gem`deve essere hardcodata.[ext]`risolve per impostazione predefinita in.jar`, il che provoca un errore`Resource missing`. |
Le repo Ivy est déclaré directement sur project.repositories`e non in un blocco`repositories { }`perché il ricevitore di questo blocco non è il`RepositoryHandler`standard di Gradle ma una API grolifant incompatibile con l’estensione.`ivy# Integrazione Gradle JBake può essere integrato nelle build Gradle utilizzando il plugin JBake per Gradle o chiamando direttamente la CLI JBake:
|
----
tasks.register<JavaExec>("bake") {
mainClass.set("org.jbake.launcher.Main")
classpath = configurations["jbake"]
args = listOf(projectDir.absolutePath, "$buildDir/jbake")
}
----
DSL Kotlin.
=== Parte 2: la dipendenza asciidoctorGems
Il plugin`org.asciidoctor.jvm.gems`deve essere applicato per primo — crea la configurazione`asciidoctorGems`e il compito`asciidoctorGemsPrepare`</think> :
[source,kotlin]
----
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'ordine di applicazione è importante :`gems`prima`revealjs`. <2> Il qualificatore`@gem`Forza l'estensione corretta sulla dipendenza.
=== Parte 3 : settings.gradle.kts
`dependencyResolutionManagement`in`settings.gradle.kts`deve consentire ai progetti di dichiarare i propri repo:
[source,kotlin]
----
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
}
----
Senza questa linea, Gradle ignora i repo dichiarati nel plugin e la risoluzione delle gemme fallisce.
== Introspezione dell'API tramite bytecode
=== Perché `javap`?
Il plugin`asciidoctor-gradle-jvm-slides`è nella versione`4.0.0-alpha.1`. La sua documentazione è inesistente o incompleta. L'unica fonte affidabile è l'ispezione diretta delle classi compilate.
=== Gerarchia del compito
[source,bash]
----
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask
----
Risultato :
[source]
----
public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
implements org.asciidoctor.gradle.base.slides.SlidesToExportAware
----
=== Scoperta di `forkOptions
Ispezionando`AbstractAsciidoctorTask`:
[source,bash]
----
javap -p -classpath asciidoctor-gradle-jvm-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask | grep -i "fork\|exec\|jvm"
----
Si trova :
[source]
----
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;
----
=== L'API di JavaForkOptions (grolifant)
`javaLauncher`non esiste per questa attivita. L'API reale di`org.ysb33r.grolifant.api.v4.JavaForkOptions`esposizione:
[source]
----
public void executable(java.lang.Object); (1)
public void setExecutable(java.lang.Object);
----
Il metodo`executable(Object)`sostituisce l'assegnazione`executable = ...`che non compila (`val`non può essere riassegnato).
=== RevealJSExtension
`revealjs { }`non è un metodo del compito ma una**estensione del progetto**:
[source,bash]
----
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.RevealJSExtension
----
Si accede via:
[source,kotlin]
----
project.extensions.getByType<RevealJSExtension>().apply {
version = "3.1.0"
templateGitHub {
setOrganisation("hakimel")
setRepository("reveal.js")
setTag("3.9.1")
}
}
----
== Configurazione del toolchain Java
=== Il problema di JavaToolchainService
`JavaToolchainService`non è un'estensione di progetto. La chiamata successiva fallisce :
[source,kotlin]
----
// ERREUR : Extension of type 'JavaToolchainService' does not exist
project.extensions.getByType<JavaToolchainService>()
----
L'API giusta è`serviceOf`:
[source,kotlin]
----
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
)
}
}
----
== Rilevamento automatico Docker
=== Contesto
Il plugin Asciidoctor/JRuby richiede Java 17. Kotlin 2.0.x in buildSrc non supporta Java 25 (il parser di versione si arresta su`"25.0.2"`Il daemon Gradle deve quindi essere eseguito su Java 17 o Docker deve essere utilizzato.
=== Strategia
1. Docker disponibile → esecuzione tramite container`eclipse-temurin:17`(comportamento predefinito)
2. Docker assente + Java 17 → esecuzione locale
3. Docker assente + Java > 17 → errore esplicito
[source,kotlin]
----
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."
)
}
----
== Risultato finale
=== Il buildscript consumatore
[source,kotlin]
----
apply<slides.SlidesPlugin>()
----
È tutto. Il plugin si assume l'intera responsabilità.
=== settings.gradle.kts
[source,kotlin]
----
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"
----
== Riepilogo delle trappole e delle soluzioni
[cols="2,3,3"]
|===
|problema |Causa |soluzione
|`ruby { gems() }`### Integrazione Gradle JBake può essere integrato nelle build Gradle utilizzando il plugin JBake Gradle o chiamando direttamente la CLI di JBake:
|Gradle cerca un`.jar`al posto di un`.gem` |`[ext]`ne risolve`jar`per impostazione predefinita |Hardcoder`.gem`nel pattern Ivy + qualificatore`@gem` |`ivy { }`python print(" non compila in", end='')`repositories { }` |Ricevitore grolifant incompatibile con il DSL Kotlin |Chiamata diretta`project.repositories.ivy { }` |`javaLauncher`non risolto |Proprietà inesistente su`AsciidoctorJRevealJSTask` |`setInProcess("JAVA_EXEC")`+`forkOptions { executable(...) }` |`executable = ...`non compila |Proprietà`val`in`JavaForkOptions`grolifant |Metodo`executable(Object)` à la place |`JavaToolchainService`introvabile via`extensions` |È un servizio Gradle, non un'estensione |`project.serviceOf<JavaToolchainService>()` |`revealjs { }`non risolto nel compito |Estensione del progetto, non metodo di attività |`project.extensions.getByType<RevealJSExtension>()` |Build si blocca con Java 25 |Kotlin 2.0.x non analizza le versioni Java a due cifre |Rilevamento Docker automatico + fallback Java 17
|===
== Metodo di indagine: leggere un'API sconosciuta con javap
=== principio
Quando la documentazione è assente o incompleta, i bytecode sono la fonte della verità. `javap`è lo strumento standard del JDK che decompila i file`.class`nelle firme Java leggibili, senza richiedere il codice sorgente.
=== Passo 1: individuare il jar nella cache Gradle
Gradle scarica tutte le sue dipendenze in`~/.gradle/caches/modules-2/files-2.1/`. Il primo passo è trovare il jar che contiene la classe da ispezionare :
[source,bash]
----
find ~/.gradle/caches -name "asciidoctor-gradle-jvm-slides*.jar" 2>/dev/null
----
.Risultato :
[source]
----
/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
----
=== Passo 2: elencare le classi del jar
Prima di ispezionare una classe, si verifica che essa esista effettivamente nel jar:
[source,bash]
----
jar tf asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar | grep -i "RevealJS\|revealjs"
----
.Questo rivela tutte le classi disponibili: `AsciidoctorJRevealJSTask`, `RevealJSExtension`, `RevealJSOptions`, etc.
=== Passo 3: ispezionare una classe
[source,bash]
----
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask
----
L'opzione`-p`mostra tutti i membri inclusi quelli privati. Il risultato mostra immediatamente la chiave :
[source]
----
public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
----
=== Passo 4: risalire la gerarchia
Il compito si estende`AbstractAsciidoctorTask`Lo si ispeziona a turno localizzando innanzitutto il suo jar :
[source,bash]
----
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"
----
È lì che si scopre`forkOptions`, `JAVA_EXEC`, et `javaForkOptions` di tipo`org.ysb33r.grolifant.api.v4.JavaForkOptions`.
=== Passo 5: seguire i tipi sconosciuti
`JavaForkOptions`è una classe grolifant sconosciuta. Si individua il suo jar :
[source,bash]
----
find ~/.gradle/caches -name "grolifant*.jar" 2>/dev/null
----
Poi lo si ispeziona:
[source,bash]
----
javap -p -classpath grolifant40-legacy-api-2.0.0-alpha.6.jar \
org.ysb33r.grolifant.api.v4.JavaForkOptions
----
Si trova`executable(java.lang.Object)`— il metodo corretto da chiamare, in opposizione a`executable = ...`che non compila perché è una proprietà`val`.
=== Passo 6: controllare le estensioni del progetto
Per`revealjs { }`, la domanda era: è un metodo del compito o un'estensione di progetto? L'ispezione di`AsciidoctorJRevealJSTask` non mostra nessun metodo`revealjs`. Si ispeziona poi`RevealJSExtension`:
[source,bash]
----
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.RevealJSExtension | head -5
----
[source]
----
public class RevealJSExtension implements groovy.lang.GroovyObject {
public static final java.lang.String NAME;
----
La presenza di`NAME`conferma che è un'estensione registrata sul progetto, accessibile tramite`project.extensions.getByType<RevealJSExtension>()`.
=== Riepilogo del metodo
[cols="1,3"]
|===
|Passo |azione
|1 |`find ~/.gradle/caches -name "*.jar"`— localizzare il jar |2 |`jar tf jar.jar \| grep NomClasse`— verificare che la classe esista |3 |`javap -p -classpath jar.jar NomCompletClasse`— ispezionare la classe |4 |Identificatore`extends`risalire la gerarchia |5 |Seguire i tipi sconosciuti nei loro jar |6 |cercare`NAME`per identificare un'estensione di progetto
|===
Questo metodo si applica a qualsiasi plugin Gradle la cui API non è documentata o di cui la versione alfa non corrisponde più alla documentazione esistente.
== Prossimo passo
Il plugin buildSrc sarà estratto in un progetto indipendente pubblicato su Gradle Plugin Portal o Maven Local. Il buildscript del consumatore diventerà quindi :
[source,kotlin]
----
plugins { id("slides") version "1.0.0" }
----
Et `settings.gradle.kts`sarà ridotto al minimo indispensabile senza riferimento a`foojay-resolver-convention`, il provisioning JDK gestito dal plugin stesso o documentato come prerequisito.
Articoli correlati
14 May 2026