Migracija plugina Asciidoctor RevealJS u buildSrc Kotlin : reverzni inženjerstvo Groovskog API-a
Објављено 16 April 2026
Контекст и циљ
početna tačka
Пројекат`slider-gradle`генерише Reveal.js презентације из AsciiDoc фајлова преко Gradle додатка`org.asciidoctor.jvm.revealjs`. Конфигурација главног задатка`asciidoctorRevealJs`živao je direktno u root buildscriptu`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
}
}
Циљ
Premesti celu ovu konfiguraciju u`buildSrc/src/main/kotlin/slides/SlidesPlugin.kt`da bi buildscript potrošač se smanji na :
apply<slides.SlidesPlugin>()
Додатак мора да буде потпуно самостал: сам примени swoje plugin-зависности, конфигурише своје репозиторијуме и управи својим Ruby gem-овима.
Prva prepreka : `ruby { gems() }
Šta sakriva Groovin sintaksički šećer
Linija`repositories { ruby { gems() } }`je Groovy DSL ekstenzija dostupna samo u kontekstu izvršavanja buildscript. Ne postoji kao statična Kotlin API dostupna iz buildSrc.
Pokusavajući da ga pozovem od`SlidesPlugin.kt`, kompajliranje ne uspe odmah
Raspadađ mehanizma
После анализе кеша Gradle (~/.gradle/caches/modules-2/files-2.1/rubygems/), откривамо у датотеци`ivy-3.1.0.xml`(empty)
<artifact type='gem' url='https://rubygems.org/gems/asciidoctor-revealjs-3.1.0.gem' />
`ruby { gems() }`De facto se obavljaloтри одвојене операције:
-
Sačuvati Ivy repo koji ukazuje na`https://rubygems.org/gems/`
-
Izuzeti grupu`rubygems`Maven repozitorijumi da se sprečavaju konflikti
-
Sačuvati gem u konfiguraciji`asciidoctorGems`da JRuby ga učitava tokom izvođenja
Ova tri odgovornosti moraju biti ponovo napravljene posebno u Kotlinu.
Trojedelno rešenje
Deo 1: repozitorijum Ivy za 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 | Ekstenzija`.gem`Треба да буде хардкодована.[ext]`решава по умолчанию у.jar`, što uzrokuje grešku`Resource missing`. |
OBAVEŠTENJE: Repo Ivy je deklarisano direktno na`project.repositories`i ne u bloku`repositories { }`jer пријемник овог блока није`RepositoryHandler`standard Gradle ali API grolifant nekompatibilan sa ekstenzijom`ivy`Kotlin DSL.
Deo 2: zavisnost asciidoctorGems
Додатак`org.asciidoctor.jvm.gems`треба да се примени прво — то прави конфигурацију`asciidoctorGems`и задатак`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 | Redosled primene je važan:`gems`пре`revealjs`. <2> Kvalifikator`@gem`prinuši ispravno proširenje na zavisnost. |
Део 3 : settings.gradle.kts
`dependencyResolutionManagement`у`settings.gradle.kts`mora dozvoliti projektima da prijave sopstvena repozitorijuma:
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
}
Bez ove linije, Gradle ignoriše repozitorijume deklarisane u plugin-u i rešavanje gemova ne uspe.
Introspekcija API-ja kroz bajtkodove
Zašto javap?
Плагин`asciidoctor-gradle-jvm-slides`je u verziji`4.0.0-alpha.1`. Njegova dokumentacija ne postoji ili je nepotpuna. Jedini pouzdani izvor je direktna inspekcija kompajliranih klasa.
Hierarhija zadatka
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask
Rezultat :
public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
implements org.asciidoctor.gradle.base.slides.SlidesToExportAware
Otkrivanje `forkOptions
Inspektujući`AbstractAsciidoctorTask` :
javap -p -classpath asciidoctor-gradle-jvm-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask | grep -i "fork\|exec\|jvm"
Pronađemo:
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;
API JavaForkOptions (grolifant)
`javaLauncher`ne postoji na ovom zadatku. Stvarni API od`org.ysb33r.grolifant.api.v4.JavaForkOptions`изложи :
public void executable(java.lang.Object); (1)
public void setExecutable(java.lang.Object);
| 1 | Metoda`executable(Object)`Zamjenjuje zadatak`executable = …`koje ne kompajlira (`val`не може да буде додело). |
RevealJSExtension
`revealjs { }`nije jedna metoda zadatka ali jednaпроширење пројекта[space]
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.RevealJSExtension
Pristupa se putem:
project.extensions.getByType<RevealJSExtension>().apply {
version = "3.1.0"
templateGitHub {
setOrganisation("hakimel")
setRepository("reveal.js")
setTag("3.9.1")
}
}
Konfiguracija Java toolchain
Problem JavaToolchainService
`JavaToolchainService`Није проширење пројекта. Следећи позив није успео :
// ERREUR : Extension of type 'JavaToolchainService' does not exist
project.extensions.getByType<JavaToolchainService>()
Dobra API je`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
)
}
}
Automatska detekcija Docker
Kontekst
Asciidoctor/JRuby plugin zahteva Java 17. Kotlin 2.0.x u buildSrc ne podržava Java 25 (parser verzije se svrće na`"25.0.2"`). Gradle daemon mora da radi na Java 17 ili Docker mora da se koristi.
Стратегија
-
Docker dostupan → izvršenje putem kontejnera`eclipse-temurin:17`(podrazumevano ponašanje)
-
Docker nedostupan + Java 17 → lokalno izvršenje
-
Docker nedostupan + Java > 17 → eksplicitna greška
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."
)
}
Konačni rezultat
potrošački buildscript
apply<slides.SlidesPlugin>()
To je sve. Plugin preuzima celokupnu odgovornost.
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"
Свод о пажња и решения
| Problem | Причина | Решение |
|---|---|---|
`ruby { gems() }`nije dostupan u Kotlinu |
Samo ekstenzija Groovog DSL |
Tri odvojenih mehanizma: repo Ivy + isključivanje Maven + |
Gradle traži jedan`.jar`умсте једног`.gem` |
`[ext]`решава у`jar`podrazumevano |
Hardcoder`.gem`у узору Ivy + kvalifikator`@gem` |
|
Пријемник grolifant није совместим са Kotlin DSL |
Позови директно`project.repositories.ivy { }` |
`javaLauncher`нерешено |
Nepostojeca svojstvo na`AsciidoctorJRevealJSTask` |
|
`executable = …`ne kompajlira |
svojstvo`val`у`JavaForkOptions` grolifant |
Metoda`executable(Object)`na mestu |
|
Ovo je Gradle usluga, a ne proširenje. |
|
`revealjs { }`nerešeno u zadatku |
Proširenje projekta, ne metoda zadatka |
|
Grad se ruši sa Javom 25 |
Kotlin 2.0.x ne parsira dvocifrne verzije Jave |
Automatsko otkrivanje Docker + fallback Java 17 |
Metod istraživanja : čitanje nepoznate API koristeći javap
Принцип
Kada dokumentacija nedostaje ili je nekompletna, bajtkodovi su izvor istine. javap`je standardni alat JDK koji dekompajlira datoteke.class`u čitljivim Java potpisima, bez potrebe od izvornog koda
Korak 1: pronaći JAR u Gradle kešu
Gradle preuzima sve svoje zavisnosti u`~/.gradle/caches/modules-2/files-2.1/`. Први корак је да нађете jar који садржи класу коју треба испитити :
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
Корак 2: исписати класе из jar-а
Пре него што проверавамо класу, проверавамо да ли постоји у jar :
jar tf asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar | grep -i "RevealJS\|revealjs"
Корак 3: проверити класу
AsciidoctorJRevealJSTask, RevealJSExtension, RevealJSOptions, itd.javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask
Opcija`-p`Prikazuje sve članove, uključujući i privatne. Rezultat odmah prikazuje ključnu liniju:
public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
Korak 4: povratak na hijerarhiju
Задатак простира`AbstractAsciidoctorTask`. Проверава се наред Prvo lokalizujući njegov/je 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"
Ovo je gde otkrivamo`forkOptions`, JAVA_EXEC, et javaForkOptions tip`org.ysb33r.grolifant.api.v4.JavaForkOptions`.
Етап 5: Следите непознате типове
`JavaForkOptions`Непозната класа гролифант. Локализујемо њену jar :
find ~/.gradle/caches -name "grolifant*.jar" 2>/dev/null
Zatim ga proveravamo:
javap -p -classpath grolifant40-legacy-api-2.0.0-alpha.6.jar \
org.ysb33r.grolifant.api.v4.JavaForkOptions
Nalazi se tu`executable(java.lang.Object)— правилна метода за позивање, u suprotnosti sa`executable = …`što ne kompajlira jer je svojstvo`val.
Корак 6 : проверити проширења пројекта
Za`revealjs { }`, pitanje je bilo : da li je to metoda zadatka ili proširjenje projekta? pregled`AsciidoctorJRevealJSTask` ne prikazuje nikakvu metodu`revealjs`. Затим се проверава`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;
Присутност`NAME`potvrđuje da je to registrovana ekstenzija na projektu, доступан преко`project.extensions.getByType<RevealJSExtension>()`.
Резюме метода
| Korak | akcija |
|---|---|
1 |
|
2 |
|
3 |
|
4 |
Identifikovati`extends`i podignuti se po hijerarhiji |
5 |
Праћење непохвативих типова у њиховим сопственим JAR-овима |
6 |
тражити`NAME`da identificiraš proširenje projekta |
Ova metoda se primenjuje na bilo koji Gradle plugin čiji API nije dokumentiran. čiji alfa verzija više ne podudara sa postojećom dokumentacijom.
Sledeći korak
Ovaj buildSrc plugin će biti izdvojen u nevisan projekat objavljen na Gradle Plugin Portal ili Maven Lokalno. Potrošački buildscript će tada biti :
plugins { id("slides") version "1.0.0" }
Et settings.gradle.kts`će se smanjiti na strogo minimum bez reference na`foojay-resolver-convention, provisioning JDK je rukovan od strane samog plugina ili dokumentisan kao preduvjet.