مهاجرت افزونه Asciidoctor RevealJS به buildSrc Kotlin: معکوسسازی API Groovy
منتشر شده در 16 April 2026
زمینه و هدف
نقطه Anfangi
پروژه`slider-gradle`ارائههای Reveal.js را از فایلهای AsciiDoc توسط پلاگین Gradle تولید میدهد`org.asciidoctor.jvm.revealjs`. پیکربندی وظیفه اصلی`asciidoctorRevealJs`بهطور مستقیم در اسکریپت ساخت ریشه زندگی میکرد`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`تا اسکریپت ساخت مصرفکننده به : کاهش یابد
apply<slides.SlidesPlugin>()
پلاگین باید بهطور کامل خودکفا باشد: خود به خود وابستگیهای پلاگین خود را اعمال میکند، مخازنش را تنظیم میکند و gems Ruby خود را مدیریت میکند.
اولین مانع : `ruby { gems() }
چه چیزی شکر سینتکس گروویی پنهان دارد
خط`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 برای جلوگیری از اختلافات
-
ذخیره gem در تنظیمات`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]`حل mi-conduct به towardpishfarad در.jar`, که باعث خطا میشود`Resource missing`. |
| مخزن Ivy مستقیم بر اعلام شده است`project.repositories`و نه در یک بلوک`repositories { }`چون گیرنده این بلوک نیست`RepositoryHandler`معيار Gradle اما یک API grolifant ناسازگار با افزونه`ivy`Kotlin DSL. |
بخش ۲: وابستگی asciidoctorGems
پلاگین`org.asciidoctor.jvm.gems`باید بهعنوان اولین عمل انجام شود — آن پیکربندی را ایجاد میکند`asciidoctorGems`و وظیفه`asciidoctorGemsPrepare`(Empty)
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`. الواصف`@gem`افزونه صحیح را روی وابستگی اعمال کنید. |
بخش 3 : settings.gradle.kts
`dependencyResolutionManagement`در`settings.gradle.kts`باید به پروژهها اجازه دهد مخازن خود را اعلام کنند :
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
}
بدون این خط، Gradle مخازن اعلامشده در پلاگین را نادیده میگیرد و حل gems با شکست مواجه میشود.
انتروسپکشن 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 واقعی`org.ysb33r.grolifant.api.v4.JavaForkOptions`توضیح :
public void executable(java.lang.Object); (1)
public void setExecutable(java.lang.Object);
| 1 | روش`executable(Object)`جایگزین میشود`executable = …`که کامپایل نمیشود (`val`نمیتوانست reassigned شود). |
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")
}
}
پیکربندی زنجیر ابزار 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 را پشتیبانی نمیکند (پارسر نسخه crash بر`"25.0.2"`). دیمون Gradle بنابراین باید در Java 17 اجرا شود یا Docker باید به کار گرفته شود.
استراتژی
-
Docker در دسترس → اجرا از طریق کانتینر`eclipse-temurin:17`(رفتار پیشفرض)
-
داکر غایب + جاوا 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 فقط |
سه مکانیزم جداگانه: مخزن Ivy + حذف Maven + |
Gradle در حال جستجوی یک`.jar`به جای یک`.gem` |
`[ext]`حل میکند در`jar`به صورت پیشفرض |
Hardcoder`.gem`در الگوی Ivy + ممیز`@gem` |
|
دریافتکننده grolifant با DSL Kotlin ناسازگار است |
تماس مستقیم`project.repositories.ivy { }` |
`javaLauncher`حل نشده |
ویژگی موجود نیست روی`AsciidoctorJRevealJSTask` |
|
`executable = …`کامپایل نمیشود |
ملک`val`در`JavaForkOptions`فیل بزرگ |
روش`executable(Object)`به جای |
|
این یک سرویس Gradle است، نه یک افزونه |
|
`revealjs { }`حلنشده در وظیفه |
تمدید پروژه، نه روش کار |
|
Build crashe با Java 25 |
Kotlin 2.0.x نسخهای دو رقمی جاوا را پارس نمیکند |
تشخیص خودکار داکر + بازگشت به Java 17 |
روش تحقیق: خواندن یک API نامشخص با javap
مبدأ
زمانی که اسناد غایب یا ناقص هستند، بایتکد منبع حقیقت است. javap`ابزار استاندارد JDK است که فایلها را دیکامپایل میکند.class`در امضاهای Java قابلخواندن, بدون نیاز به کد منبع.
مرحله ۱: یافتن جار را در کش Gradle
Gradle تمام وابستگیهای خود را در`~/.gradle/caches/modules-2/files-2.1/`. مرحله اول این است که جار حاوی کلاس مورد بررسی را پیدا کنید :
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
مرحله ۲: لیست کردن کلاسهای جار
قبل از بررسی یک کلاس، مطمئن میشویم که در فایل JAR موجود است :
jar tf asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar | grep -i "RevealJS\|revealjs"
مرحله ۳: بررسی یک کلاس
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
مرحلة ۴: صعود در مراتب
وظیفه گسترش میدهد`AbstractAsciidoctorTask`. او نیز به نوبت آن مورد بررسی قرار میگیرد در ابتدا به وسیله locating his 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`برای شناسایی یک گسترش پروژه |
این روش به تمام پلاگینهای Gradleای که API آنها مستند نشده است، اعمال میشود. یا که نسخه آلفا بیشتر با اسناد موجود مطابقت ندارد.
مرحله بعدی
این پلاگین buildSrc در یک پروژه مستقل منتشر شده در Gradle Plugin Portal یا Maven Local استخراج خواهد شد. سپس buildscript مصرفکننده به این صورت خواهد شود:
plugins { id("slides") version "1.0.0" }
Et settings.gradle.kts`به حداقل ضروری کاهش داده خواهد شد بدون اشاره به`foojay-resolver-convention, تأمين JDK توسط خود پلاگین مدیریت میشود یا به عنوان پیشنیاز مستند شده است.