Migration eines Asciidoctor RevealJS-Plugins nach buildSrc Kotlin: Reverse-Engineering einer Groovy-API
Publié le 16 April 2026
- Kontext und Ziel
- Das erste Hindernis: `ruby { gems() }
- Die Lösung in drei Teilen
- Introspection der API durch Bytecodes
- Konfiguration des Java-Toolchains
- Automatische Docker-Erkennung
- Endresultat
- Zusammenfassung der Fallen und Lösungen
- Untersuchungsmethode: Eine unbekannte API mit javap lesen
- Nächster Schritt
Kontext und Ziel
Der Ausgangspunkt
Das Projekt`slider-gradle`Erstellt Reveal.js-Präsentationen aus AsciiDoc-Dateien über das Gradle-Plugin.org.asciidoctor.jvm.revealjs. Die Konfiguration der Hauptaufgabe`asciidoctorRevealJs`lebte direkt im Root-Buildscript`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
}
}
Das Ziel
Verschieben Sie diese gesamte Konfiguration in`buildSrc/src/main/kotlin/slides/SlidesPlugin.kt`damit das Consumer-Buildscript auf: reduziert wird
apply<slides.SlidesPlugin>()
Das Plugin muss völlig autonom sein: Es wendet seine Plugin-Abhängigkeiten selbst an, konfiguriert seine Repositorys und verwaltet seine Ruby-Gems.
Das erste Hindernis: `ruby { gems() }
Was der Groovy-syntaktische Zucker verbirgt
Die Zeile`repositories { ruby { gems() } }`Es ist eine Groovy-DSL-Erweiterung, die ausschließlich im Buildskript-Ausführungskontext verfügbar ist. Sie existiert nicht als statische Kotlin-API, die von buildSrc aus zugänglich ist.
Beim Versuch, ihn von … aus anzurufen`SlidesPlugin.kt`, die Kompilierung schlägt sofort fehl.
Zerlegung des Mechanismus
Nach Analyse des Gradle-Cache (~/.gradle/caches/modules-2/files-2.1/rubygems/), man entdeckt in der Datei`ivy-3.1.0.xml`:
<artifact type='gem' url='https://rubygems.org/gems/asciidoctor-revealjs-3.1.0.gem' />
`ruby { gems() }`führte tatsächlich durchdrei verschiedene Operationen:
-
Ein Ivy-Repo speichernd, das auf`https://rubygems.org/gems/`
-
Gruppe ausschließen`rubygems`Maven-Repositorien zur Vermeidung von Konflikten
-
Speichere das Gem in der Konfiguration`asciidoctorGems`damit JRuby es zur Laufzeit lädt
Diese drei Verantwortlichkeiten müssen separat in Kotlin reproduziert werden.
Die Lösung in drei Teilen
Teil 1: das Ivy-Repository für 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 | Die Erweiterung`.gem`muss hardkodiert sein.[ext]`löst standardmäßig in.jar`, was einen Fehler verursacht`Resource missing`. |
| Das Ivy-Repo wird direkt darauf erklärt`project.repositories`und nicht in einem Block`repositories { }`weil der Empfänger dieses Blocks nicht das`RepositoryHandler`Gradle-Standard aber eine grolifante API, die mit der Erweiterung inkompatibel ist`ivy`Kotlin DSL. |
Teil 2 : die Abhängigkeit asciidoctorGems
Das Plugin`org.asciidoctor.jvm.gems`muss zuerst angewendet werden — es erstellt die Konfiguration`asciidoctorGems`und die Aufgabe`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 | Die Anwendungsreihenfolge ist wichtig:`gems`vor`revealjs`. <2> Der Qualifikator`@gem`zwingt die korrekte Erweiterung auf die Abhängigkeit. |
Teil 3: settings.gradle.kts
`dependencyResolutionManagement`in`settings.gradle.kts`muss den Projekten erlauben, ihre eigenen Repos zu deklarieren:
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
}
Ohne diese Zeile ignoriert Gradle die im Plugin deklarierten Repositorys und die Auflösung der Gems schlägt fehl.
Introspection der API durch Bytecodes
Warum javap ?
Das Plugin`asciidoctor-gradle-jvm-slides`ist in Version`4.0.0-alpha.1`. Seine Dokumentation existiert nicht oder ist unvollständig. Die einzige zuverlässige Quelle ist die direkte Inspektion der kompilierten Klassen.
Hierarchie der Aufgabe
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask
Ergebnis:
public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
implements org.asciidoctor.gradle.base.slides.SlidesToExportAware
Entdeckung von `forkOptions
Bei der Überprüfung`AbstractAsciidoctorTask`:
javap -p -classpath asciidoctor-gradle-jvm-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask | grep -i "fork\|exec\|jvm"
Es gibt:
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;
Die API von JavaForkOptions (grolifant)
`javaLauncher`existiert nicht bei dieser Aufgabe. Die reale API von
(Note: The output preserves the leading space, the space after the period, and the trailing space as in the original.)`org.ysb33r.grolifant.api.v4.JavaForkOptions`Ausstellung :
public void executable(java.lang.Object); (1)
public void setExecutable(java.lang.Object);
Die Methode`executable(Object)`ersetzt die Zuweisung`executable = …`das nicht kompiliert (`val`kann nicht neu zugewiesen werden).
RevealJSExtension
`revealjs { }`ist keine Methode der Aufgabe, sondern eineProjektverlängerung:
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.RevealJSExtension
Es wird über: zugegriffen
project.extensions.getByType<RevealJSExtension>().apply {
version = "3.1.0"
templateGitHub {
setOrganisation("hakimel")
setRepository("reveal.js")
setTag("3.9.1")
}
}
Konfiguration des Java-Toolchains
Das Problem von JavaToolchainService
`JavaToolchainService`Ist keine Projekt-Erweiterung. Der folgende Aufruf schlägt fehl:
// ERREUR : Extension of type 'JavaToolchainService' does not exist
project.extensions.getByType<JavaToolchainService>()
Die gute API ist`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 Docker-Erkennung
Kontext
Das Plugin Asciidoctor/JRuby erfordert Java 17. Kotlin 2.0.x in buildSrc unterstützt Java 25 nicht (der Versionsparser stürzt bei`"25.0.2"`). Der Gradle-Daemon muss daher auf Java 17 laufen oder Docker muss verwendet werden.
Strategie
-
Docker verfügbar → Ausführung über Container`eclipse-temurin:17`(Standardverhalten)
-
Docker fehlt + Java 17 → lokale Ausführung
-
Docker fehlt + Java > 17 → expliziter Fehler
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."
)
}
Endresultat
Der Konsument-buildscript
apply<slides.SlidesPlugin>()
Das ist alles. Das Plugin trägt die gesamte Verantwortung.
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"
Zusammenfassung der Fallen und Lösungen
| Problem | Ursache | Lösung |
|---|---|---|
`ruby { gems() }`nicht verfügbar in Kotlin |
Nur Groovy-DSL-Erweiterung |
Drei getrennte Mechanismen: Ivy-Repository + Maven-Ausschluss + |
Gradle sucht ein`.jar`statt eines`.gem` |
`[ext]`löst in`jar`standardmäßig |
Hardcoder`.gem`im Muster Ivy + Qualifikator`@gem` |
|
Receiver grolifant ist nicht mit dem DSL Kotlin kompatibel |
Direktaufruf`project.repositories.ivy { }` |
`javaLauncher`nicht gelöst |
Nicht vorhandene Eigenschaft auf`AsciidoctorJRevealJSTask` |
|
`executable = …`kompiliert nicht |
Eigenschaft`val`in`JavaForkOptions`grolifant |
Methode`executable(Object)`stattdessen |
|
Es ist ein Gradle-Service, keine Erweiterung |
|
`revealjs { }`nicht gelöst in der Aufgabe |
Erweiterung des Projekts, keine Methode der Aufgabe |
|
Build crasht mit Java 25 |
Kotlin 2.0.x analysiert keine zweistelligen Java-Versionen |
Automatische Docker-Erkennung + fallback Java 17 |
Untersuchungsmethode: Eine unbekannte API mit javap lesen
Prinzip
Wenn die Dokumentation fehlt oder unvollständig ist, sind die Bytecodes die Quelle der Wahrheit. javap`ist das Standard-Werkzeug des JDK, das die Dateien dekompiliert..class`in lesbaren Java-Signaturen, ohne den Quellcode zu benötigen.
Schritt 1: die JAR im Gradle-Cache lokalisieren
Gradle lädt alle seine Abhängigkeiten herunter in`~/.gradle/caches/modules-2/files-2.1/`. Der erste Schritt besteht darin, die JAR-Datei zu finden, die die zu inspizierende Klasse enthält:
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
Schritt 2: Klassen im JAR auflisten
Bevor eine Klasse inspiziert wird, überprüft man, ob sie tatsächlich im JAR enthalten ist :
jar tf asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar | grep -i "RevealJS\|revealjs"
Schritt 3: eine Klasse überprüfen
AsciidoctorJRevealJSTask, RevealJSExtension, RevealJSOptions, usw.javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask
Die Option`-p`zeigt alle Mitglieder einschließlich der privaten Das Ergebnis zeigt sofort die Schlüsselzeile:
public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
Schritt 4: die Hierarchie hinaufsteigen
Die Aufgabe erweitert`AbstractAsciidoctorTask`. Es wird nacheinander inspiziert. Indem du zunächst sein JAR lokalisierst :
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"
Das ist, wo man es entdeckt.forkOptions, JAVA_EXEC, et javaForkOptions vom Typ`org.ysb33r.grolifant.api.v4.JavaForkOptions`.
Schritt 5: unbekannte Typen verfolgen
`JavaForkOptions`Es ist eine unbekannte grolifant-Klasse. Wir lokalisieren sein JAR:
find ~/.gradle/caches -name "grolifant*.jar" 2>/dev/null
Dann inspiziert man es:
javap -p -classpath grolifant40-legacy-api-2.0.0-alpha.6.jar \
org.ysb33r.grolifant.api.v4.JavaForkOptions
Man findet dort`executable(java.lang.Object)— die richtige Methode aufzurufen, im Gegensatz zu`executable = …`der nicht kompiliert, weil es eine Eigenschaft ist`val.
Schritt 6: Projekt-Erweiterungen überprüfen
Für`revealjs { }`, die Frage war: ist das eine Methode der Aufgabe? oder eine Projektverlängerung? Die Inspektion von`AsciidoctorJRevealJSTask` zeigt keine Methode`revealjs`. Man überprüft dann`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;
Die Anwesenheit von`NAME`bestätigt, dass es eine auf dem Projekt registrierte Erweiterung ist, zugänglich über`project.extensions.getByType<RevealJSExtension>()`.
Zusammenfassung der Methode
| Schritt | Aktion |
|---|---|
1 |
|
2 |
|
3 |
|
4 |
identifizieren`extends`und die Hierarchie erklimmen |
5 |
Die unbekannten Typen in ihren eigenen JARs verfolgen |
6 |
Suchen`NAME`Um eine Projekterweiterung zu identifizieren |
Diese Methode gilt für jedes Gradle-Plugin, dessen API nicht dokumentiert ist oder dessen Alpha-Version nicht mehr zur vorhandenen Dokumentation passt.
Nächster Schritt
Dieses buildSrc-Plugin wird in ein unabhängiges Projekt extrahiert, das auf dem Gradle Plugin Portal oder Maven Local veröffentlicht wird. Der konsumierende Buildscript wird dann:
plugins { id("slides") version "1.0.0" }
Et settings.gradle.kts`wird auf das notwendigste Minimum reduziert ohne Bezug auf`foojay-resolver-convention, das JDK-Provisioning wird vom Plugin selbst verwaltet oder als Voraussetzung dokumentiert.