Migração de um plugin Asciidoctor RevealJS para buildSrc Kotlin : engenharia reversa de uma API Groovy
Publié le 16 April 2026
Contexto e objetivo
O ponto de partida
O projeto`slider-gradle`gera apresentações Reveal.js a partir de arquivos AsciiDoc via o plugin Gradle`org.asciidoctor.jvm.revealjs`. A configuração da tarefa principal`asciidoctorRevealJs`vivia diretamente no script de construção raiz`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
}
}
O objetivo
Mover toda essa configuração para`buildSrc/src/main/kotlin/slides/SlidesPlugin.kt`para que o buildscript do consumidor se reduza à:
apply<slides.SlidesPlugin>()
O plugin deve ser totalmente autônomo: ele aplica por si mesmo as dependências de plugins, configura seus repositórios, e gerencia suas gems Ruby.
O primeiro obstáculo: `ruby { gems() }
O que o açúcar sintático do Groovy esconde
A linha`repositories { ruby { gems() } }`é uma extensão DSL Groovy disponível apenas no contexto de execução do buildscript. Ela não existe como uma API Kotlin estática acessível a partir de buildSrc.
Ao tentar chamá-lo desde`SlidesPlugin.kt`, a compilação falha imediatamente.
Decomposição do mecanismo
Após análise do cache Gradle (~/.gradle/caches/modules-2/files-2.1/rubygems/), descobre-se no arquivo`ivy-3.1.0.xml`:
<artifact type='gem' url='https://rubygems.org/gems/asciidoctor-revealjs-3.1.0.gem' />
`ruby { gems() }`efetuava na realidadetrês operações distintas :
-
Salvar um repositório Ivy apontando para`https://rubygems.org/gems/`
-
Excluir o grupo`rubygems`repositórios Maven para evitar conflitos
-
Salvar a gema na configuração`asciidoctorGems`para que o JRuby a carregue em tempo de execução
Essas três responsabilidades devem ser reproduzidas separadamente em Kotlin.
A solução em três partes
Parte 1: o repositório 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 | A extensão`.gem`deve estar hardcodada`[ext]resolve por padrão em.jar`, que causa um erro`Resource missing`. |
| O repo Ivy é declarado diretamente em`project.repositories`e não em um bloco`repositories { }`porque o receiver deste bloco não é o`RepositoryHandler`padrão do Gradle mas uma API grolifant incompatível com a extensão`ivy`Kotlin DSL. |
Parte 2 : a dependência asciidoctorGems
O plugin`org.asciidoctor.jvm.gems`deve ser aplicado primeiro — ele cria a configuração`asciidoctorGems`e a tarefa`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 | A ordem de aplicação é importante:`gems`antes`revealjs`. <2> O qualificador`@gem`força a extensão correta na dependência. |
Parte 3 : settings.gradle.kts
`dependencyResolutionManagement`em`settings.gradle.kts`deve permitir que os projetos declarem seus próprios repositórios:
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
}
Sem essa linha, o Gradle ignora os repositórios declarados no plugin e a resolução das gems falha.
Introspecção da API por bytecodes
Por que javap?
O plugin`asciidoctor-gradle-jvm-slides`está em versão`4.0.0-alpha.1`. Sua documentação é inexistente ou incompleta. A única fonte confiável é a inspeção direta das classes compiladas.
Hierarquia da tarefa
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
Descoberta de `forkOptions
Ao inspecionar`AbstractAsciidoctorTask`:
javap -p -classpath asciidoctor-gradle-jvm-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask | grep -i "fork\|exec\|jvm"
Encontra-se:
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;
A API de JavaForkOptions (grolifant)
`javaLauncher`não existe nesta tarefa. A API real de`org.ysb33r.grolifant.api.v4.JavaForkOptions`exposição :
public void executable(java.lang.Object); (1)
public void setExecutable(java.lang.Object);
| 1 | O método`executable(Object)`substitui a atribuição`executable = …`que não compila (`val`não pode ser reatribuído). |
Extensão RevealJS
`revealjs { }`não é um método da tarefa mas umaextensão de projeto :
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.RevealJSExtension
É acessada via :
project.extensions.getByType<RevealJSExtension>().apply {
version = "3.1.0"
templateGitHub {
setOrganisation("hakimel")
setRepository("reveal.js")
setTag("3.9.1")
}
}
Configuração do toolchain Java
O problema de JavaToolchainService
`JavaToolchainService`não é uma extensão de projeto. A chamada seguinte falha :
// ERREUR : Extension of type 'JavaToolchainService' does not exist
project.extensions.getByType<JavaToolchainService>()
A boa API é`serviceOf`(There is no text to translate, so output is empty.)
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
)
}
}
Detecção automática Docker
Contexto
O plugin Asciidoctor/JRuby requer Java 17. Kotlin 2.0.x em buildSrc não suporta o Java 25 (o analisador de versão falha em`"25.0.2"`). O daemon Gradle deve portanto rodar no Java 17 ou o Docker deve ser usado.
Estratégia
-
Docker disponível → execução via contêiner`eclipse-temurin:17`(comportamento padrão)
-
Docker ausente + Java 17 → execução local
-
Docker ausente + Java > 17 → erro 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
O buildscript consumidor
apply<slides.SlidesPlugin>()
Isso é tudo. O plugin assume toda a responsabilidade.
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"
Resumo das armadilhas e soluções
| Problema | causa | Solução |
|---|---|---|
`ruby { gems() }`não disponível em Kotlin |
Extensão DSL Groovy apenas |
Três mecanismos separados: repositório Ivy + exclusão Maven + |
Gradle procura um`.jar`em vez de um`.gem` |
`[ext]`resolve em`jar`por padrão |
Hardcoder`.gem`no padrão Ivy + qualificador`@gem` |
|
Receiver grolifant incompatível com o DSL Kotlin |
Chamada direta`project.repositories.ivy { }` |
`javaLauncher`não resolvido |
Propriedade inexistente em`AsciidoctorJRevealJSTask` |
|
`executable = …`não compila |
propriedade`val`dentro`JavaForkOptions`elefante grande |
Método`executable(Object)`no lugar de |
|
Este é um serviço Gradle, não uma extensão |
|
`revealjs { }`não resolvido na tarefa |
Extensão do projeto, não método de tarefa |
|
Build falha com o Java 25 |
Kotlin 2.0.x não analisa as versões Java de dois dígitos |
Detecção Docker automática + fallback Java 17 |
Método de investigação : ler uma API desconhecida com javap
Princípio
Quando a documentação está ausente ou incompleta, os bytecodes são a fonte da verdade. javap`é a ferramenta padrão do JDK que descompila os arquivos.class`em assinaturas Java legíveis, sem precisar do código-fonte
Passo 1: localizar o jar no cache do Gradle
Gradle baixa todas as suas dependências em`~/.gradle/caches/modules-2/files-2.1/`. A primeira etapa é encontrar o JAR que contém a classe a ser inspecionada :
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
Etapa 2 : listar as classes do jar
Antes de inspecionar uma classe, verifica-se que ela realmente existe no jar:
jar tf asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar | grep -i "RevealJS\|revealjs"
Isso revela todas as classes disponíveis: AsciidoctorJRevealJSTask, RevealJSExtension, RevealJSOptions, etc.
Etapa 3: inspecionar uma classe
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask
A opção`-p`mostra todos os membros, inclusive os privados. O resultado mostra imediatamente a linha chave :
public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
Passo 4: subir na hierarquia
A tarefa estende`AbstractAsciidoctorTask`. Inspeciona-se à sua vez localizando primeiro o seu 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"
É aí que se descobre`forkOptions`, JAVA_EXEC, et javaForkOptions de tipo`org.ysb33r.grolifant.api.v4.JavaForkOptions`.
Passo 5: seguir os tipos desconhecidos
`JavaForkOptions`est é uma classe grolifant desconhecida. Localizamos o seu jar :
find ~/.gradle/caches -name "grolifant*.jar" 2>/dev/null
Então, inspeciona-se:
javap -p -classpath grolifant40-legacy-api-2.0.0-alpha.6.jar \
org.ysb33r.grolifant.api.v4.JavaForkOptions
Lá se encontra`executable(java.lang.Object)— o método correto a ser chamado, ao contrário de`executable = …`que não compila porque é uma propriedade`val.
Etapa 6 : verificar as extensões do projeto
para`revealjs { }`, a pergunta era : é um método da tarefa ou uma extensão de projeto? A inspeção de`AsciidoctorJRevealJSTask` não mostra nenhum método`revealjs`. Inspeciona-se então`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;
A presença de`NAME`confirma que é uma extensão registrada no projeto, acessível via`project.extensions.getByType<RevealJSExtension>()`.
Resumo do método
| Etapa | Ação |
|---|---|
1 |
|
2 |
|
3 |
|
4 |
Identificar`extends`e subir na hierarquia |
5 |
Seguir os tipos desconhecidos nos seus próprios jars |
6 |
Procurar`NAME`para identificar uma extensão de projeto |
Este método aplica-se a qualquer plugin Gradle cuja API não está documentada ou cuja versão alfa não corresponde mais à documentação existente.
próxima etapa
Este plugin buildSrc será extraído para um projeto independente publicado no Gradle Plugin Portal ou no Maven Local. O buildscript do consumidor passará a ser:
plugins { id("slides") version "1.0.0" }
Et settings.gradle.kts`será reduzido ao seu mínimo estrito sem referência a`foojay-resolver-convention, o provisionamento do JDK sendo gerenciado pelo próprio plugin ou documentado como pré-requisito.