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:

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

  2. Exclude the group`rubygems`Maven repositories to avoid conflicts

  3. 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

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

  2. Docker missing + Java 17 → local execution

  3. 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 +asciidoctorGems

Gradle is looking for a`.jar`instead of a`.gem`

`[ext]`solves it`jar`by default

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

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

Receiver big elephant incompatible with the DSL Kotlin

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’s 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 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
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 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

This reveals all the 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: 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

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

2

jar tf jar.jar | grep NomClasse— check that the class exists

3

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

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.

Related articles