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 main task configuration`asciidoctorRevealJs`lived directly in the 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
    }
}

The objective

Move all this configuration into`buildSrc/src/main/kotlin/slides/SlidesPlugin.kt`so that the consuming buildscript is reduced to:

apply<slides.SlidesPlugin>()

The plugin must be totally autonomous: it applies its own plugin dependencies, configures its repos, and manages its Ruby gems.

The first obstacle: ruby { gems() }

What the Groovy syntactic sugar hides

The line`repositories { ruby { gems() } }`is a Groovy DSL extension available only within the buildscript execution context. It does not exist as a static Kotlin API accessible from buildSrc.

When attempting to call it from`SlidesPlugin.kt`, compilation fails immediately.

Deconstructing the mechanism

After analyzing the Gradle cache (~/.gradle/caches/modules-2/files-2.1/rubygems/), we discover in the file`ivy-3.1.0.xml`:

<artifact type='gem' url='https://rubygems.org/gems/asciidoctor-revealjs-3.1.0.gem' />

`ruby { gems() }`actually performedthree distinct operations:

  1. Registering an Ivy repo pointing to`https://rubygems.org/gems/`

  2. Excluding the group`rubygems`from Maven repos to avoid conflicts

  3. Registering the gem in the configuration`asciidoctorGems`so that JRuby loads it at runtime

These three responsibilities must be reproduced separately in Kotlin.

The three-part solution

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 to.jar`, which causes an error`Resource missing`.
The Ivy repo is declared directly on`project.repositories`and not in a`repositories { }`block because the receiver of this block is not the`RepositoryHandler`standard Gradle one but a grolifant API incompatible with the`ivy`Kotlin DSL extension.

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 Application order is important:`gems`before`revealjs`. <2> The qualifier`@gem`forces 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 gem resolution fails.

API Introspection via bytecodes

Why javap?

The plugin`asciidoctor-gradle-jvm-slides`is version`4.0.0-alpha.1`. Its documentation is non-existent 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

By 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 JavaForkOptions (grolifant) API

`javaLauncher`does not exist on this task. The actual API of`org.ysb33r.grolifant.api.v4.JavaForkOptions`exposes:

public void executable(java.lang.Object);  (1)
public void setExecutable(java.lang.Object);
1 The method`executable(Object)`replaces the assignment`executable = …​`which does not compile (`val`cannot be reassigned).

RevealJSExtension

`revealjs { }`is not a task method 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")
    }
}

Java toolchain configuration

The JavaToolchainService problem

`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"`). Therefore, the Gradle daemon must run on Java 17 or Docker must be used.

Strategy

  1. Docker available → execution via container`eclipse-temurin:17`(default behavior)

  2. Docker absent + Java 17 → local execution

  3. Docker absent + 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 consuming buildscript

apply<slides.SlidesPlugin>()

That’s it. The plugin carries the entire 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 pitfalls and solutions

Problem Cause Solution

`ruby { gems() }`not available in Kotlin

Groovy DSL extension only

Three separate mechanisms: Ivy repo + Maven exclusion +asciidoctorGems

Gradle looks for a`.jar`instead of a`.gem`

`[ext]`resolves to`jar`by default

Hardcode`.gem`in the Ivy pattern + qualifier`@gem`

ivy { }`does not compile in`repositories { }

Grolifant receiver incompatible with Kotlin DSL

Direct call`project.repositories.ivy { }`

`javaLauncher`unresolved

Non-existent property on`AsciidoctorJRevealJSTask`

setInProcess("JAVA_EXEC")+forkOptions { executable(…​) }

`executable = …​`does not compile

Property`val`in`JavaForkOptions`grolifant

Method`executable(Object)`instead

JavaToolchainService`not found via`extensions

It is a Gradle service, not an extension

project.serviceOf<JavaToolchainService>()

`revealjs { }`unresolved in the task

Project extension, not task method

project.extensions.getByType<RevealJSExtension>()

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 documentation is absent or incomplete, bytecodes are the source of truth. javap`is the standard JDK tool that decompiles.class`files into readable Java signatures, without requiring source code.

Step 1: locate the jar in the Gradle cache

Gradle downloads all its dependencies into`~/.gradle/caches/modules-2/files-2.1/`. The first step is to find the jar containing the class to inspect:

find ~/.gradle/caches -name "asciidoctor-gradle-jvm-slides*.jar" 2>/dev/null
Result:
/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 of the jar

Before inspecting a class, verify 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

This reveals all available classes: 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: climb the hierarchy

The task extends`AbstractAsciidoctorTask`. We inspect 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"

This is where we discover`forkOptions`, JAVA_EXEC, et javaForkOptions of type`org.ysb33r.grolifant.api.v4.JavaForkOptions`.

Step 5: follow 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

We find`executable(java.lang.Object)`— the correct method to call, as opposed to`executable = …​`which does not compile because it is a`val`property.

Step 6: verify project extensions

For`revealjs { }`, the question was: is it a task method or a project extension? Inspection of`AsciidoctorJRevealJSTask` shows no`revealjs`method. 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 it is an extension registered on the project, accessible via`project.extensions.getByType<RevealJSExtension>()`.

Method Summary

Step Action

1

find ~/.gradle/caches -name "*.jar"— locate the jar

2

jar tf jar.jar | grep NomClasse— verify class exists

3

javap -p -classpath jar.jar NomCompletClasse— inspect the class

4

Identify`extends`and climb the hierarchy

5

Follow unknown types in their own jars

6

Search for`NAME`to identify a project extension

This method applies to any Gradle plugin whose API is undocumented or whose alpha version no longer matches the existing documentation.

Next step

This buildSrc plugin will be extracted into an independent project published on the Gradle Plugin Portal or Maven Local. The consuming buildscript will then become:

plugins { id("slides") version "1.0.0" }

Et settings.gradle.kts`will be reduced to its strict minimum without reference to`foojay-resolver-convention, with JDK provisioning handled by the plugin itself or documented as a prerequisite.

Related articles