Миграция плагина Asciidoctor RevealJS на buildSrc Kotlin: реверс-инжиниринг Groovy API
Опубликовано 16 April 2026
Контекст и цель
Начальная точка
Проект`slider-gradle`генерирует презентации Reveal.js из файлов AsciiDoc с помощью плагина Gradle`org.asciidoctor.jvm.revealjs`. Конфигурация основной задачи`asciidoctorRevealJs`жил непосредственно в корневом 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
}
}
Цель
Переместить всю эту конфигурацию в`buildSrc/src/main/kotlin/slides/SlidesPlugin.kt`Чтобы потребительский buildscript сократился до :
apply<slides.SlidesPlugin>()
Плагин должен быть полностью автономным: он сам применяет свои зависимости от плагинов, настраивает свои репозитории и управляет своими гемами Ruby.
Первое препятствие: `ruby { gems() }
Что скрывает синтаксический сахар Groovy
Линия`repositories { ruby { gems() } }`является расширением DSL Groovy, доступным только в контексте выполнения buildscript. Она не существует как статический API Kotlin, доступный из buildSrc.
Пытаясь позвонить ему из`SlidesPlugin.kt`, компиляция не удаётся сразу
Разложение механизма
После анализа кэша Gradle (~/.gradle/caches/modules-2/files-2.1/rubygems/), в файле обнаруживается`ivy-3.1.0.xml`Пожалуйста, предоставьте текст для перевода.
<artifact type='gem' url='https://rubygems.org/gems/asciidoctor-revealjs-3.1.0.gem' />
`ruby { gems() }`осуществлял на самом делетри отдельные операции:
-
Сохранить репозиторий Ivy, указывающий на`https://rubygems.org/gems/`
-
Исключить группу`rubygems`депозитории Maven для избежания конфликтов
-
Сохранить гем в конфигурации`asciidoctorGems`чтобы JRuby загрузил её во время выполнения
Эти три обязанности должны быть воспроизведены отдельно в Kotlin.
Решение в трёх частях
Часть 1: репозиторий Ivy для 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 | Расширение`.gem`должна быть жестко закодирована.[ext]`Решает по умолчанию в.jar`, что приводит к ошибке`Resource missing`. |
ПРИМЕЧАНИЕ: репозиторий Ivy объявлен непосредственно на`project.repositories`и не в блоке`repositories { }`потому что приемник этого блока не является`RepositoryHandler`стандарт Gradle но grolifant несовместимая API с расширением`ivy`Kotlin DSL.
Часть 2: зависимость asciidoctorGems
Плагин`org.asciidoctor.jvm.gems`должен быть применен первым — он создает конфигурацию`asciidoctorGems`и задача`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 | Порядок применения важен:`gems`перед`revealjs`. <2> Квалификатор`@gem`принудительно задать правильное расширение для зависимости. |
Часть 3 : settings.gradle.kts
`dependencyResolutionManagement`в`settings.gradle.kts`должен разрешить проектам объявлять свои собственные репозитории :
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
}
Без этой строки Gradle игнорирует репозитории, объявленные в плагине, и разрешение гемов не удаётся.
Интроспекция API посредством байткода
Почему javap ?
Плагин`asciidoctor-gradle-jvm-slides`является в версии`4.0.0-alpha.1`. Её документация отсутствует или неполная. Единственным надежным источником является непосредственный осмотр скомпилированных классов.
Иерархия задачи
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask
Результат:
public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
implements org.asciidoctor.gradle.base.slides.SlidesToExportAware
Обнаружение `forkOptions
Проверяя`AbstractAsciidoctorTask`:
javap -p -classpath asciidoctor-gradle-jvm-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask | grep -i "fork\|exec\|jvm"
Находим:
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;
API JavaForkOptions (grolifant)
`javaLauncher`не существует на этой задаче. Реальная API de`org.ysb33r.grolifant.api.v4.JavaForkOptions`Экспозиция :
public void executable(java.lang.Object); (1)
public void setExecutable(java.lang.Object);
| 1 | Метод`executable(Object)`заменяет присвоение`executable = …`который не компилируется (`val`нельзя переназначить). |
Расширение RevealJS
`revealjs { }`не является методом задачи, норасширение проекта:
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.RevealJSExtension
Доступ к нему через :
project.extensions.getByType<RevealJSExtension>().apply {
version = "3.1.0"
templateGitHub {
setOrganisation("hakimel")
setRepository("reveal.js")
setTag("3.9.1")
}
}
Настройка набора инструментов Java
Проблема JavaToolchainService
`JavaToolchainService`не является расширением проекта. Следующий вызов завершится ошибкой :
// ERREUR : Extension of type 'JavaToolchainService' does not exist
project.extensions.getByType<JavaToolchainService>()
Правильный API — это`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
)
}
}
Автоматическое обнаружение Docker
Контекст
Плагин Asciidoctor/JRuby требует Java 17. Kotlin 2.0.x в buildSrc не поддерживает Java 25 (анализатор версии падает на`"25.0.2"`). демон Gradle должен работать на Java 17 или Docker должен использоваться.
Стратегия
-
Docker доступен → выполнение через контейнер`eclipse-temurin:17`(поведение по умолчанию)
-
Docker отсутствует + Java 17 → локальное выполнение
-
Docker отсутствует + Java > 17 → явная ошибка
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."
)
}
Итоговый результат
Потребительский buildscript
apply<slides.SlidesPlugin>()
Это всё. Плагин несёт полную ответственность.
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"
Сводка ловушек и решений
| Проблема | Причина | Решение |
|---|---|---|
`ruby { gems() }`недоступно в Kotlin |
Расширение DSL только для Groovy |
Три отдельных механизма: repo Ivy + исключение Maven + |
Gradle ищет один`.jar`вместо одного`.gem` |
`[ext]`решает в`jar`по умолчанию |
Hardcoder`.gem`в паттерне Ivy + квалификатор`@gem` |
|
Ресивер |
Прямой вызов`project.repositories.ivy { }` |
`javaLauncher`не решено |
Свойство отсутствует в`AsciidoctorJRevealJSTask` |
|
`executable = …`не компилируется |
Свойство`val`в`JavaForkOptions`grolifant |
Метод`executable(Object)`вместо этого |
|
Это сервис Gradle, а не расширение |
|
`revealjs { }`не решено в задаче |
Расширение проекта, а не метод задачи |
|
Сборка падает с Java 25 |
Kotlin 2.0.x не парсит двухзначные версии Java |
Автоматическое обнаружение Docker + fallback на Java 17 |
Метод исследования : читать неизвестный API с javap
Принцип
Когда документация отсутствует или неполна, байткоды являются источником истины. javap`это стандартный инструмент JDK, который декомпилирует файлы.class`в читаемых подписях Java, без необходимости исходного кода.
Шаг 1: найти jar в кэше Gradle
Gradle загружает все свои зависимости в`~/.gradle/caches/modules-2/files-2.1/`. Первый шаг — найти jar, содержащий класс, который нужно проинспектировать :
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
Шаг 2 : перечислить классы jar
Перед проверкой класса проверяют, что он действительно существует в jar:
jar tf asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar | grep -i "RevealJS\|revealjs"
Это раскрывает все доступные классы : AsciidoctorJRevealJSTask, RevealJSExtension, RevealJSOptions, и т.д.
Шаг 3: осмотреть класс
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask
Опция`-p`Показывает всех членов, включая приватные. Результат сразу показывает ключевую строку:
public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
Шаг 4: подняться по иерархии
Задача расширяет`AbstractAsciidoctorTask`. Его проверяют в свою очередь при первой локализации его 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"
Вот где мы обнаруживаем`forkOptions`, JAVA_EXEC, et javaForkOptions типа`org.ysb33r.grolifant.api.v4.JavaForkOptions`.
Шаг 5: отслеживать неизвестные типы
`JavaForkOptions`Это неизвестный класс grolifant. Мы находим его jar :
find ~/.gradle/caches -name "grolifant*.jar" 2>/dev/null
Затем его проверяют:
javap -p -classpath grolifant40-legacy-api-2.0.0-alpha.6.jar \
org.ysb33r.grolifant.api.v4.JavaForkOptions
Там есть`executable(java.lang.Object)— правильный метод для вызова, в отличие от`executable = …`который не компилируется потому что это свойство`val.
Шаг 6 : проверить расширения проекта
Для`revealjs { }`, вопрос был : это метод задачи или расширение проекта ? Инспекция`AsciidoctorJRevealJSTask` не показывает ни одного метода`revealjs`. Тогда осматривают`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;
Присутствие`NAME`подтверждает, что это зарегистрированное расширение в проекте, доступно через`project.extensions.getByType<RevealJSExtension>()`.
Сводка метода
| Этап | действие |
|---|---|
1 |
|
2 |
|
3 |
|
4 |
Идентификатор`extends`и подняться по иерархии |
5 |
Следить за неизвестными типами в их собственных банках |
6 |
Искать`NAME`pour идентификации расширения проекта |
Этот метод применим к любому плагину Gradle, API которого не документирована. или чья альфа-версия больше не соответствует существующей документации.
Следующий шаг
Этот плагин buildSrc будет извлечён в отдельный проект, опубликованный на Gradle Plugin Portal или в Maven Local. Потребительский buildscript будет выглядеть так:
plugins { id("slides") version "1.0.0" }
Et settings.gradle.kts`будет сокращён до своего строгого минимума без ссылки на`foojay-resolver-convention, подготовка JDK управляется самим плагином или документируется как предварительное условие.