Migrating an Asciidoctor RevealJS plugin to Kotlin buildSrc : reverse engineering a Groovy API
Published on 16 April 2026
Context and objective
The starting point
The project`slider-gradle`generates Reveal.js presentations from AsciiDoc files via the Gradle plugin`org.asciidoctor.jvm.revealjs`. The configuration of the main task`asciidoctorRevealJs`lived directly in the root buildscript`build.gradle.kts`(Note: Since no French text was provided, there is nothing to translate.)
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
}
}
The objective
Move all this configuration into`buildSrc/src/main/kotlin/slides/SlidesPlugin.kt`so that the consumer buildscript reduces to:
apply<slides.SlidesPlugin>()
The plugin must be completely self-contained: it applies its own plugin dependencies, configures its repos, and manages its Ruby gems.
The first obstacle: `ruby { gems() }
What Groovy syntactic sugar hides
The line`repositories { ruby { gems() } }`It is a Groovy DSL extension available only in the buildscript execution context. It does not exist as a static Kotlin API accessible from buildSrc.
Attempting to call it from`SlidesPlugin.kt`, the compilation fails immediately.
Decomposition of the mechanism
After analysis of the Gradle cache (~/.gradle/caches/modules-2/files-2.1/rubygems/), one discovers in the file`ivy-3.1.0.xml`:
<artifact type='gem' url='https://rubygems.org/gems/asciidoctor-revealjs-3.1.0.gem' />
`ruby { gems() }`was actually doingthree distinct operations:
-
Save an Ivy repo pointing to`https://rubygems.org/gems/`
-
Exclude the group`rubygems`Maven repositories to avoid conflicts
-
Save the gem in the configuration`asciidoctorGems`so that JRuby loads it at runtime
These three responsibilities must be reproduced separately in Kotlin.
The solution in three parts
Part 1: the Ivy repo for 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 | The extension`.gem`must be hardcoded.[ext]`resolves by default in.jar`, which causes an error`Resource missing`. |
| The Ivy repo is declared directly on`project.repositories`and not in a block`repositories { }`because the receiver of this block is not the`RepositoryHandler`Gradle standard but a grolifant API incompatible with the extension`ivy`Kotlin DSL |
Part 2: the asciidoctorGems dependency
The plugin`org.asciidoctor.jvm.gems`must be applied first — it creates the configuration`asciidoctorGems`and the task`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 | The application order is important:`gems`before`revealjs`. <2> The qualifier`@gem`force the correct extension on the dependency. |
Part 3 : settings.gradle.kts
`dependencyResolutionManagement`in`settings.gradle.kts`must allow projects to declare their own repos :
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
}
Without this line, Gradle ignores the repos declared in the plugin and the gem resolution fails.
API introspection by bytecode
Why javap ?
The plugin`asciidoctor-gradle-jvm-slides`is in version`4.0.0-alpha.1`. Its documentation is missing or incomplete. The only reliable source is direct inspection of the compiled classes.
Task hierarchy
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask
Result :
public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
implements org.asciidoctor.gradle.base.slides.SlidesToExportAware
Discovery of `forkOptions
When inspecting`AbstractAsciidoctorTask` :
javap -p -classpath asciidoctor-gradle-jvm-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask | grep -i "fork\|exec\|jvm"
We find:
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;
The API of JavaForkOptions (grolifant)
`javaLauncher`does not exist on this task. The real API of`org.ysb33r.grolifant.api.v4.JavaForkOptions`exhibit :
public void executable(java.lang.Object); (1)
public void setExecutable(java.lang.Object);
| 1 | The method`executable(Object)`replaces the assignment`executable = …`that does not compile (`val`cannot be reassigned). |
RevealJSExtension
`revealjs { }`is not a method of the task but aproject extension:
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.RevealJSExtension
It is accessed via:
project.extensions.getByType<RevealJSExtension>().apply {
version = "3.1.0"
templateGitHub {
setOrganisation("hakimel")
setRepository("reveal.js")
setTag("3.9.1")
}
}
Configuration of the Java toolchain
The problem of JavaToolchainService
`JavaToolchainService`is not a project extension. The following call fails:
// ERREUR : Extension of type 'JavaToolchainService' does not exist
project.extensions.getByType<JavaToolchainService>()
The correct 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
)
}
}
Automatic Docker detection
Context
The Asciidoctor/JRuby plugin requires Java 17. Kotlin 2.0.x in buildSrc does not support Java 25 (the version parser crashes on`"25.0.2"`). The Gradle daemon must therefore run on Java 17 or Docker must be used.
Strategy
-
Docker available → execution via container`eclipse-temurin:17`(default behavior)
-
Docker missing + Java 17 → local execution
-
Docker missing + Java > 17 → explicit error
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."
)
}
Final result
The consumer buildscript
apply<slides.SlidesPlugin>()
That’s it. The plugin bears full responsibility.
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"
Summary of traps and solutions
| Problem | Cause | Solution |
|---|---|---|
`ruby { gems() }`not available in Kotlin |
Groovy DSL Extension only |
Three separate mechanisms : Ivy repo + Maven exclusion + |
Gradle is looking for a`.jar`instead of a`.gem` |
`[ext]`solves it`jar`by default |
Hardcoder`.gem`in the Ivy pattern + qualifier`@gem` |
|
Receiver big elephant incompatible with the DSL Kotlin |
Direct call`project.repositories.ivy { }` |
`javaLauncher`unresolved |
Non-existent property on`AsciidoctorJRevealJSTask` |
|
`executable = …`does not compile |
Property`val`in`JavaForkOptions`grolifant |
Method`executable(Object)`instead |
|
It’s a Gradle service, not an extension |
|
`revealjs { }`unresolved in the task |
Project extension, not task method |
|
Build crashes with Java 25 |
Kotlin 2.0.x does not parse two-digit Java versions |
Automatic Docker detection + Java 17 fallback |
Investigation method: reading an unknown API with javap
Principle
When the documentation is absent or incomplete, the bytecodes are the source of truth. javap`is the standard JDK tool that decompiles files.class`in readable Java signatures, without requiring the source code.
Step 1: locate the jar in the Gradle cache
Gradle downloads all its dependencies in`~/.gradle/caches/modules-2/files-2.1/`. The first step is to find the jar that contains the class to inspect:
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
Step 2: list the classes in the jar
Before inspecting a class, we check that it actually exists in the jar:
jar tf asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar | grep -i "RevealJS\|revealjs"
Step 3: inspect a class
AsciidoctorJRevealJSTask, RevealJSExtension, RevealJSOptions, etc.javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask
The option`-p`displays all members including private ones The result immediately shows the key line:
public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
Step 4: go back up the hierarchy
The task extends`AbstractAsciidoctorTask`. One inspects it in turn by first locating its 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"
That’s where we discover`forkOptions`, JAVA_EXEC, et javaForkOptions of type`org.ysb33r.grolifant.api.v4.JavaForkOptions`.
Step 5: track unknown types
`JavaForkOptions`is an unknown grolifant class. We locate its jar :
find ~/.gradle/caches -name "grolifant*.jar" 2>/dev/null
Then we inspect it:
javap -p -classpath grolifant40-legacy-api-2.0.0-alpha.6.jar \
org.ysb33r.grolifant.api.v4.JavaForkOptions
One finds`executable(java.lang.Object)— the correct method to call, in contrast to`executable = …`which does not compile because it is a property`val.
Step 6: verify project extensions
For`revealjs { }`, the question was: is this a method of the task or a project extension? The inspection of`AsciidoctorJRevealJSTask` shows no method`revealjs`. We then inspect.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;
The presence of`NAME`confirms that it is an extension registered on the project, accessible via`project.extensions.getByType<RevealJSExtension>()`.
Summary of the method
| Step | Action |
|---|---|
1 |
|
2 |
|
3 |
|
4 |
Identifier`extends`and climb up the hierarchy |
5 |
Track unknown types in their own jars |
6 |
Search`NAME`to identify a project extension |
This method applies to any Gradle plugin whose API is not documented or whose alpha version no longer matches the existing documentation
Next step
This buildSrc plugin will be extracted into a separate project published on Gradle Plugin Portal or Maven Local. The consumer buildscript will then become:
plugins { id("slides") version "1.0.0" }
Et settings.gradle.kts`will be reduced to its bare minimum without reference to`foojay-resolver-convention, the JDK provisioning being managed by the plugin itself or documented as a prerequisite.