Asciidoctor RevealJS 플러그인을 buildSrc Kotlin으로 마이그레이션: Groovy API 역 엔지니어링
게시: 16 April 2026
배경 및 목적
시작점
프로젝트`slider-gradle`AsciiDoc 파일로부터 Gradle 플러그인을 통해 Reveal.js 발표를 생성합니다`org.asciidoctor.jvm.revealjs`. 주요 작업 설정`asciidoctorRevealJs`루트 빌드스크립트에 직접 있었다`build.gradle.kts`[No output]
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 { gems() }
그루비 구문 설탕이 숨기는 것
라인`repositories { ruby { gems() } }`이는 빌드스크립트 실행 컨텍스트에서만 사용할 수 있는 Groovy DSL 확장입니다. 이는 buildSrc에서 접근 가능한 정적 Kotlin API가 아닙니다.
그에게 전화를 걸려고 시도하면서`SlidesPlugin.kt`, 컴파일이 즉시 실패합니다.
메커니즘의 분해
그레이들 캐시 분석 후 (~/.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 저장소
-
구성에 gem을 저장`asciidoctorGems`JRuby가 런타임에 이를 로드하도록
이 세 가지 책임은 Kotlin에서 별도로 재현해야 합니다
세 부분으로 구성된 해결책
1부: rubygems를 위한 Ivy 리포지터리
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은 플러그인에 선언된 저장소를 무시하고, gem 의존성 해결이 실패합니다.
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;
JavaForkOptions의 API (grolifant)
`javaLauncher`이 작업에는 존재하지 않습니다. 실제 API의`org.ysb33r.grolifant.api.v4.JavaForkOptions`노출 :
public void executable(java.lang.Object); (1)
public void setExecutable(java.lang.Object);
| 1 | 방법`executable(Object)`할당을 대체한다`executable = …`컴파일되지 않는다 (`val`재할당할 수 없습니다). |
RevealJSExtension
`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")
}
}
자바 툴체인 구성
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이 필요합니다. buildSrc의 Kotlin 2.0.x는 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에서 사용할 수 없음 |
Groovy DSL 확장만 |
세 가지 별도의 메커니즘 : Ivy 저장소 + Maven 제외 + |
Gradle가 찾는`.jar`대신에`.gem` |
`[ext]`해결하는`jar`기본적으로 |
하드코더`.gem`Ivy 패턴 + 한정자 안에`@gem` |
|
수신자 grolifant는 Kotlin DSL과 호환되지 않습니다. |
직접 통화`project.repositories.ivy { }` |
`javaLauncher`해결되지 않은 |
존재하지 않는 속성`AsciidoctorJRevealJSTask` |
|
`executable = …`컴파일되지 않습니다 |
속성`val`안에`JavaForkOptions`그롤리팡 |
방법`executable(Object)`대신에 |
|
이것은 Gradle 서비스이지, 확장이 아닙니다. |
|
`revealjs { }`작업에서 해결되지 않음 |
프로젝트 확장, 작업 방법이 아님 |
|
Java 25에서 빌드가 충돌합니다 |
Kotlin 2.0.x는 두 자리 숫자 버전의 Java를 파싱하지 않습니다. |
자동 Docker 감지 + Java 17 폴백 |
조사 방법: javap을 사용해 알 수 없는 API 읽기
원칙
문서가 없거나 불완전할 때, 바이트코드가 진실의 원천입니다. javap`는 JDK의 표준 도구로 파일을 디컴파일하는 도구입니다..class`읽을 수 있는 Java 서명, 소스 코드가 필요 없이.
1단계: Gradle 캐시에서 JAR 파일 찾기
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"
단계 3: 클래스 검사
AsciidoctorJRevealJSTask, RevealJSExtension, RevealJSOptions, 등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 |
자신들의 jars에서 알려지지 않은 유형을 추적하다 |
6 |
검색하다`NAME`프로젝트 확장을 식별하기 위해 |
이 방법은 문서화되지 않은 API를 가진 모든 Gradle 플러그인에 적용됩니다. 또는 알파 버전이 기존 문서와 더 이상 일치하지 않는.
다음 단계
이 buildSrc 플러그인은 Gradle Plugin Portal 또는 Maven Local에 게시된 독립된 프로젝트로 추출됩니다. 소비자 buildscript는 다음과 같이 될 것입니다:
plugins { id("slides") version "1.0.0" }
Et settings.gradle.kts`최소한으로 축소될 것이며 참조를 갖지 않을`foojay-resolver-convention, 플러그인 자체에서 관리되거나 사전 요구 사항으로 문서화된 JDK 프로비저닝.
관련 기사
31 May 2026
14 May 2026