Migratie van een Asciidoctor RevealJS-plugin naar buildSrc Kotlin: reverse engineering van een Groovy-API
Gepubliceerd op 16 April 2026
- Context en doel
- Het eerste obstakel : `ruby { gems() }
- De oplossing in drie delen
- De API-introspectie via bytecodes
- Configuratie van de Java toolchain
- Automatische detectie Docker
- Eindresultaat
- Samenvatting van de valkuilen en oplossingen
- Onderzoeksmethode : een onbekende API lezen met javap
- Volgende stap
Context en doel
Het startpunt
Het project`slider-gradle`genereert Reveal.js-presentaties van AsciiDoc-bestanden via de Gradle-plugin`org.asciidoctor.jvm.revealjs`. De configuratie van de hoofdtaak`asciidoctorRevealJs`leefde direct in het hoofd buildscript`build.gradle.kts`(Empty output)
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
}
}
Het doel
Verplaats deze hele configuratie naar`buildSrc/src/main/kotlin/slides/SlidesPlugin.kt`zodat het buildscript van de consument wordt teruggebracht tot:
apply<slides.SlidesPlugin>()
De plug‑in moet volledig autonoom zijn: hij past zijn plug‑in‑afhankelijkheden zelf toe, configureert zijn repositories, en beheert zijn Ruby‑gems.
Het eerste obstakel : `ruby { gems() }
Wat de Groovy syntactische suiker verbirgt
De lijn`repositories { ruby { gems() } }`is een Groovy DSL-extensie die alleen beschikbaar is in de uitvoeringscontext van het buildscript. Het bestaat niet als een statische Kotlin-API die toegankelijk is vanaf buildSrc.
Bij het proberen om hem/haar te bellen vanaf`SlidesPlugin.kt`, de compilatie mislukt onmiddellijk.
Ontbinding van het mechanisme
Na analyse van de Gradle-cache (~/.gradle/caches/modules-2/files-2.1/rubygems/), ontdekken we in het bestand`ivy-3.1.0.xml`:
<artifact type='gem' url='https://rubygems.org/gems/asciidoctor-revealjs-3.1.0.gem' />
`ruby { gems() }`voerde eigenlijk uitdrie afzonderlijke operaties:
-
Een Ivy-repo opslaan dat naar … verwijst`https://rubygems.org/gems/`
-
Groep uitsluiten`rubygems`Maven-repositories om conflicten te voorkomen
-
Sla de gem op in de configuratie`asciidoctorGems`zodaat JRuby het tijdens runtime laadt
Deze drie verantwoordelijkheden moeten apart in Kotlin gereproduceerd worden.
De oplossing in drie delen
Deel 1: de Ivy-repo voor 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 | De extensie`.gem`moet hard gecodeerd zijn.[ext]`lost standaard in.jar`, wat een fout veroorzaakt`Resource missing`. |
| De Ivy-repo wordt direct aangekondigd op`project.repositories`en niet in een blok`repositories { }`omdat de receiver van dit blok niet de`RepositoryHandler`standaard van Gradle maar een API grolifant onverenigbaar met de extensie`ivy`Kotlin DSL |
Deel 2: de afhankelijkheid asciidoctorGems
De plugin`org.asciidoctor.jvm.gems`moet als eerste worden toegepast — het creëert de configuratie`asciidoctorGems`en de taak`asciidoctorGemsPrepare`Please provide the text you would like me to translate.
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 | De volgorde van toepassing is belangrijk:`gems`voor`revealjs`. <2> De kwalificator`@gem`Forceer de juiste extensie op de afhankelijkheid. |
Deel 3 : `settings.gradle.kts
`dependencyResolutionManagement`in`settings.gradle.kts`moet de projecten toestaan hun eigen repositories te verklaren :
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
}
Zonder deze regel, Gradle negeert de in de plugin gedeclareerde repositories en mislukt de oplosser van de gems.
De API-introspectie via bytecodes
Waarom javap?
De plugin`asciidoctor-gradle-jvm-slides`is in versie`4.0.0-alpha.1`. De documentatie is niet bestaand of onvolledig. De enige betrouwbare bron is de directe inspectie van de gecompileerde klassen.
Hiërarchie van de taak
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask
Resultaat :
public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
implements org.asciidoctor.gradle.base.slides.SlidesToExportAware
Ontdekking van `forkOptions
Bij het inspecteren`AbstractAsciidoctorTask`[Translation of empty string: nothing]
javap -p -classpath asciidoctor-gradle-jvm-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask | grep -i "fork\|exec\|jvm"
Men vindt:
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;
De API van JavaForkOptions (grolifant)
`javaLauncher`bestaat niet op deze taak. De echte API van`org.ysb33r.grolifant.api.v4.JavaForkOptions`tentoonstelling :
public void executable(java.lang.Object); (1)
public void setExecutable(java.lang.Object);
| 1 | De methode`executable(Object)`vervang de toewijzing`executable = …`die niet compileert (`val`kan niet opnieuw worden toegewezen). |
RevealJSExtension
`revealjs { }`is geen methode van de taak maar eenuitbreiding van het project:
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.RevealJSExtension
Ze wordt via :
project.extensions.getByType<RevealJSExtension>().apply {
version = "3.1.0"
templateGitHub {
setOrganisation("hakimel")
setRepository("reveal.js")
setTag("3.9.1")
}
}
Configuratie van de Java toolchain
Het probleem van JavaToolchainService
`JavaToolchainService`Het is geen projectuitbreiding. De volgende aanroep mislukt :
// ERREUR : Extension of type 'JavaToolchainService' does not exist
project.extensions.getByType<JavaToolchainService>()
De goede API is`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
)
}
}
Automatische detectie Docker
Context
De Asciidoctor/JRuby-plugin vereist Java 17. Kotlin 2.0.x in buildSrc ondersteunt Java 25 niet (de versieparser crasht op`"25.0.2"`). De Gradle-daemon moet dus draaien op Java 17 of Docker moet worden gebruikt.
Strategie
-
Docker beschikbaar → uitvoering via container`eclipse-temurin:17`(standaardgedrag)
-
Docker afwezig + Java 17 → lokale uitvoering
-
Docker ontbreekt + Java > 17 → expliciete fout
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."
)
}
Eindresultaat
De consumenten buildscript
apply<slides.SlidesPlugin>()
Dat is alles. De plugin draagt de volledige verantwoordelijkheid.
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"
Samenvatting van de valkuilen en oplossingen
| probleem | Oorzaak | Oplossing |
|---|---|---|
`ruby { gems() }`niet beschikbaar in Kotlin |
Alleen Groovy DSL-extensie |
Drie gescheiden mechanismen : repo Ivy + Maven-uitsluiting + |
Gradle zoekt een`.jar`in plaats van een`.gem` |
`[ext]`lost op en`jar`standaard |
Hardcoder`.gem`in het patroon Ivy + kwalificatie`@gem` |
|
Receiver grolifant onverenigbaar met de Kotlin DSL |
Directe oproep`project.repositories.ivy { }` |
`javaLauncher`onopgelost |
Niet-bestaande eigenschap op`AsciidoctorJRevealJSTask` |
|
`executable = …`compileert niet |
Eigendom`val`in`JavaForkOptions`grolifant |
Methode`executable(Object)`in de plaats |
|
Dit is een Gradle-service, geen extensie |
|
`revealjs { }`niet opgelost in de taak |
Projectuitbreiding, geen taakmethode |
|
Build crasht met Java 25 |
Kotlin 2.0.x parseert geen Java-versies met twee cijfers |
Automatische Docker-detectie + fallback Java 17 |
Onderzoeksmethode : een onbekende API lezen met javap
Principe
Als de documentatie ontbreekt of incompleet is, zijn de bytecodes de bron van waarheid. javap`is het standaardhulpmiddel van de JDK die bestanden decompileert.class`in leesbare Java-handtekeningen, zonder de broncode nodig te hebben
Stap 1: zoek de jar in de Gradle-cache
Gradle downloadt al zijn afhankelijkheden in`~/.gradle/caches/modules-2/files-2.1/`. De eerste stap is om de jar te vinden die de te inspecteren klasse bevat :
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
Stap 2: de klassen van de jar opsommen
Voordat we een klasse inspecteren, controleren we of deze bestaat in de jar :
jar tf asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar | grep -i "RevealJS\|revealjs"
Stap 3 : een klasse inspecteren
AsciidoctorJRevealJSTask, RevealJSExtension, RevealJSOptions, etc.javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask
De optie`-p`toont alle leden inclusief de privé-leden. Het resultaat toont onmiddellijk de sleutelregel:
public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
Stap 4: de hiërarchie beklimmen
De taak strekt uit`AbstractAsciidoctorTask`. We inspecteren het op zijn beurt door eerst zijn jar te vinden :
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"
Daar ontdekken we`forkOptions`, JAVA_EXEC, et javaForkOptions van het type`org.ysb33r.grolifant.api.v4.JavaForkOptions`.
Stap 5: onbekende typen volgen
`JavaForkOptions`is een grolifant onbekende klasse. Zijn jar wordt gelokaliseerd:
find ~/.gradle/caches -name "grolifant*.jar" 2>/dev/null
Daarna inspecteren we het:
javap -p -classpath grolifant40-legacy-api-2.0.0-alpha.6.jar \
org.ysb33r.grolifant.api.v4.JavaForkOptions
Daar vind je`executable(java.lang.Object)— de juiste methode om aan te roepen, in tegenstelling tot`executable = …`die niet compileert omdat het een eigenschap is`val.
Stap 6: controleer de projectextensies
Voor`revealjs { }`, de vraag was: is dit een methode van de taak of een projectuitbreiding? De inspectie van`AsciidoctorJRevealJSTask` toont geen methode`revealjs`. Men inspecteert dan`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;
De aanwezigheid van`NAME`bevestigt dat het een geregistreerde extensie is op het project, toegankelijk via`project.extensions.getByType<RevealJSExtension>()`.
Samenvatting van de methode
| Stap | Actie |
|---|---|
1 |
|
2 |
|
3 |
|
4 |
Identificeren`extends`en de hiërarchie beklimmen |
5 |
Volg onbekende types in hun eigen jars |
6 |
zoeken`NAME`om een projectextensie te identificeren |
Deze methode is van toepassing op elke Gradle-plugin waarvan de API niet is gedocumenteerd. of waarvan de alpha-versie niet meer overeenkomt met de bestaande documentatie.
Volgende stap
Dit buildSrc-plugin wordt geëxtraheerd naar een apart project dat wordt gepubliceerd op de Gradle Plugin Portal of Maven Local. Het consumer-buildscript wordt dan:
plugins { id("slides") version "1.0.0" }
Et settings.gradle.kts`zal worden teruggebracht tot het strenge minimum zonder verwijzing naar`foojay-resolver-convention, het JDK-provisioning wordt beheerd door de plugin zelf of wordt gedocumenteerd als vereiste.