Migración de un plugin Asciidoctor RevealJS a buildSrc Kotlin: ingeniería inversa de una API Groovy
Publié le 16 April 2026
- Contexto y objetivo
- El primer obstáculo: `ruby { gems() }
- La solución en tres partes
- Introspección de la API por bytecodes
- Configuración del toolchain Java
- Detección automática Docker
- Resultado final
- Resumen de las trampas y soluciones
- Método de investigación: leer una API desconocida con javap
- Próximo paso
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:
-
Registrar un repo Ivy apuntando a`https://rubygems.org/gems/`
-
Excluir el grupo`rubygems`repositorios Maven para evitar los conflictos
-
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
-
Docker disponible → ejecución mediante contenedor`eclipse-temurin:17`(comportamiento por defecto)
-
Docker ausente + Java 17 → ejecución local
-
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 + |
Gradle busca un`.jar`en lugar de un`.gem` |
`[ext]`resuelve en`jar`por defecto |
hardcodear`.gem`en el patrón Ivy + calificador`@gem` |
|
Receptor grolifant incompatible con el DSL Kotlin |
Llamada directa`project.repositories.ivy { }` |
`javaLauncher`no resuelto |
Propiedad inexistente en`AsciidoctorJRevealJSTask` |
|
`executable = …`no compila |
propiedad`val`en`JavaForkOptions`grolifant |
Método`executable(Object)`en el lugar |
|
Es un servicio Gradle, no una extensión |
|
`revealjs { }`no resuelto en la tarea |
Extensión de proyecto, no método de tarea |
|
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
/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 |
|
2 |
|
3 |
|
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.