Contexto y objetivo

El punto de partida

El proyecto`slider-gradle`genera presentaciones Reveal.js a partir de archivos AsciiDoc mediante el plugin Gradle`org.asciidoctor.jvm.revealjs`. La configuración de la tarea principal`asciidoctorRevealJs`vivía directamente en el buildscript raíz`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
    }
}

El objetivo

Mover toda esta configuración a`buildSrc/src/main/kotlin/slides/SlidesPlugin.kt`para que el buildscript del consumidor se reduzca a :

apply<slides.SlidesPlugin>()

El plugin debe ser totalmente autónomo: aplica él mismo sus dependencias de plugins, configura sus repos, y gestiona sus gemas Ruby.

El primer obstáculo: `ruby { gems() }

Lo que esconde el azúcar sintáctico Groovy

La línea`repositories { ruby { gems() } }`es una extensión DSL Groovy disponible únicamente en el contexto de ejecución del buildscript. No existe como una API Kotlin estática accesible desde buildSrc.

Intentando llamarlo desde`SlidesPlugin.kt`, la compilación falla inmediatamente.

Descomposición del mecanismo

Después del análisis del caché Gradle (~/.gradle/caches/modules-2/files-2.1/rubygems/), se descubre en el archivo`ivy-3.1.0.xml`(Note: As the provided French text to translate was empty beyond the colon and formatting instructions, no translation output is generated per the requirement to output only the translated text of the given fragment.)

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

`ruby { gems() }`efectuaba en realidadtres operaciones distintas:

  1. Registrar un repo Ivy apuntando a`https://rubygems.org/gems/`

  2. Excluir el grupo`rubygems`repositorios Maven para evitar los conflictos

  3. Guardar la gema en la configuración`asciidoctorGems`para que JRuby lo cargue en tiempo de ejecución

Estas tres responsabilidades deben reproducirse por separado en Kotlin.

La solución en tres partes

Parte 1: el repo Ivy para 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 La extensión`.gem`debe estar hardcodeada.[ext]`resuelve por defecto en.jar`, lo que provoca un error`Resource missing`.

NOTA: El repo Ivy se declara directamente en`project.repositories`y no en un bloque`repositories { }`pues el receiver de este bloque no es el`RepositoryHandler`estándar de Gradle pero una API grolifant incompatible con la extensión`ivy`Kotlin DSL.

Parte 2 : la dependencia asciidoctorGems

El plugin`org.asciidoctor.jvm.gems`debe aplicarse primero — crea la configuración`asciidoctorGems`y la tarea`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 El orden de aplicación es importante :`gems`antes`revealjs`. <2> El calificador`@gem`fuerza la extensión correcta sobre la dependencia.

Parte 3: settings.gradle.kts

`dependencyResolutionManagement`dentro`settings.gradle.kts`debe permitir que los proyectos declaren sus propios repos:

@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
}

Sin esta línea, Gradle ignora los repos declarados en el plugin y la resolución de las gems falla.

Introspección de la API por bytecodes

¿Por qué javap?

El plugin`asciidoctor-gradle-jvm-slides`está en versión`4.0.0-alpha.1`. Su documentación es inexistente o incompleta. La única fuente fiable es la inspección directa de las clases compiladas.

Jerarquía de la tarea

javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
  org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask

Resultado :

public class AsciidoctorJRevealJSTask
  extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
  implements org.asciidoctor.gradle.base.slides.SlidesToExportAware

Descubrimiento de `forkOptions

Al inspeccionar`AbstractAsciidoctorTask`:

javap -p -classpath asciidoctor-gradle-jvm-4.0.0-alpha.1.jar \
  org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask | grep -i "fork\|exec\|jvm"

Se encuentra:

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;

La API de JavaForkOptions (grolifant)

`javaLauncher`no existe en esta tarea. La API real de`org.ysb33r.grolifant.api.v4.JavaForkOptions`python import sys

def main(): data = sys.stdin.read() if not data: return parts = data.split('’) for i in range(len(parts)): if i % 2 == 0: parts[i] = parts[i].replace(" :", ":") parts[i] = parts[i].replace("expose", "expone") sys.stdout.write(''.join(parts))

if name == 'main': main()

public void executable(java.lang.Object);  (1)
public void setExecutable(java.lang.Object);
1 El método`executable(Object)`reemplaza la asignación`executable = …​`que no compila (`val`no puede ser reasignado).

RevealJSExtension

`revealjs { }`no es un método de la tarea pero unaextensión de proyecto:

javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
  org.asciidoctor.gradle.jvm.slides.RevealJSExtension

Se accede vía :

project.extensions.getByType<RevealJSExtension>().apply {
    version = "3.1.0"
    templateGitHub {
        setOrganisation("hakimel")
        setRepository("reveal.js")
        setTag("3.9.1")
    }
}

Configuración del toolchain Java

El problema de JavaToolchainService

`JavaToolchainService`no es una extensión de proyecto. La siguiente llamada falla :

// ERREUR : Extension of type 'JavaToolchainService' does not exist
project.extensions.getByType<JavaToolchainService>()

La buena API es`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
        )
    }
}

Detección automática Docker

Contexto

El plugin Asciidoctor/JRuby requiere Java 17. Kotlin 2.0.x en buildSrc no admite Java 25 (el analizador de versión falla al`"25.0.2"`). El daemon Gradle debe ejecutarse por tanto en Java 17 o debe usarse Docker.

Estrategia

  1. Docker disponible → ejecución mediante contenedor`eclipse-temurin:17`(comportamiento por defecto)

  2. Docker ausente + Java 17 → ejecución local

  3. Docker ausente + Java > 17 → error explícito

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."
    )
}

Resultado final

El buildscript consumidor

apply<slides.SlidesPlugin>()

Es todo. El plugin lleva toda la responsabilidad.

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"

Resumen de las trampas y soluciones

problema causa Solución

`ruby { gems() }`no disponible en Kotlin

Extensión DSL Groovy únicamente

Tres mecanismos separados : repo Ivy + exclusión Maven +asciidoctorGems

Gradle busca un`.jar`en lugar de un`.gem`

`[ext]`resuelve en`jar`por defecto

hardcodear`.gem`en el patrón Ivy + calificador`@gem`

ivy { }`no compila en`repositories { }

Receptor grolifant incompatible con el DSL Kotlin

Llamada directa`project.repositories.ivy { }`

`javaLauncher`no resuelto

Propiedad inexistente en`AsciidoctorJRevealJSTask`

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

`executable = …​`no compila

propiedad`val`en`JavaForkOptions`grolifant

Método`executable(Object)`en el lugar

JavaToolchainService`no disponible vía`extensions

Es un servicio Gradle, no una extensión

project.serviceOf<JavaToolchainService>()

`revealjs { }`no resuelto en la tarea

Extensión de proyecto, no método de tarea

project.extensions.getByType<RevealJSExtension>()

El build falla con Java 25

Kotlin 2.0.x no procesa las versiones Java de dos dígitos

Detección Docker automática + fallback Java 17

Método de investigación: leer una API desconocida con javap

Principio

Cuando la documentación está ausente o incompleta, los bytecodes son la fuente de la verdad. javap`es la herramienta estándar del JDK que descompila los archivos.class`firmas Java legibles, sin requerir el código fuente.

Paso 1 : localizar el jar en el caché de Gradle

Gradle descarga todas sus dependencias en`~/.gradle/caches/modules-2/files-2.1/`. La primera etapa consiste en encontrar el jar que contiene la clase a inspeccionar:

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

Paso 2: listar las clases del jar

Antes de inspeccionar una clase, se verifica que existe realmente en el jar :

jar tf asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar | grep -i "RevealJS\|revealjs"

Esto revela todas las clases disponibles : AsciidoctorJRevealJSTask, RevealJSExtension, RevealJSOptions, etc.

Paso 3: inspeccionar una clase

javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask

La opción`-p`muestra todos los miembros, incluidos los privados. El resultado muestra inmediatamente la línea clave:

public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask

Paso 4 : subir la jerarquía

La tarea extiende`AbstractAsciidoctorTask`. Lo inspeccionamos a su vez localizando primero su 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"

Es allí donde se descubre`forkOptions`, JAVA_EXEC, et javaForkOptions de tipo`org.ysb33r.grolifant.api.v4.JavaForkOptions`.

Paso 5: seguir los tipos desconocidos

`JavaForkOptions`es una clase grolifant desconocida. Localizamos su jar :

find ~/.gradle/caches -name "grolifant*.jar" 2>/dev/null

Entonces lo inspeccionamos:

javap -p -classpath grolifant40-legacy-api-2.0.0-alpha.6.jar \
org.ysb33r.grolifant.api.v4.JavaForkOptions

Se encuentra`executable(java.lang.Object)— el método correcto para llamar, opuesto a`executable = …​`que no compila porque es una propiedad`val.

Paso 6: verificar las extensiones del proyecto

Para`revealjs { }`, la pregunta era: ¿es un método de la tarea? o una extensión de proyecto? La inspección de`AsciidoctorJRevealJSTask` no muestra ningún método`revealjs`. Entonces se inspecciona`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;

La presencia de`NAME`confirma que es una extensión registrada en el proyecto, accesible mediante`project.extensions.getByType<RevealJSExtension>()`.

Resumen del método

Etapa acción

1

find ~/.gradle/caches -name "*.jar"— localizar el jar

2

jar tf jar.jar | grep NomClasse— comprobar que la clase existe

3

javap -p -classpath jar.jar NomCompletClasse— inspeccionar la clase

4

Identificar`extends`y subir la jerarquía

5

Seguir los tipos desconocidos en sus propios jar

6

Buscar`NAME`para identificar una extensión de proyecto

Este método se aplica a cualquier plugin Gradle cuyo API no está documentada o cuya versión alfa ya no corresponde a la documentación existente.

Próximo paso

Este plugin buildSrc se extraerá en un proyecto independiente publicado en el Portal de Plugins de Gradle o en Maven Local. El buildscript del consumidor será entonces:

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

Et settings.gradle.kts`se reducirá al mínimo estricto sin referencia a`foojay-resolver-convention, la provisión del JDK siendo gestionada por el propio plugin o documentada como requisito previo.

Articles connexes