ترحيل إضافة Asciidoctor RevealJS إلى buildSrc Kotlin : الهندسة العكسية لواجهة برمجة Groovy
Publié le 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>()
يجب أن يكون البرنامج المساعد مستقلًا تمامًا: فهو يطبق تبعيات البرنامج المساعد بنفسه، ويضبط مستودعاته، ويتعامل مع gems الخاصة بـ Ruby.
العقبة الأولى : `ruby { gems() }
ما يخفيه سكر Groovy النحوي
الخط`repositories { ruby { gems() } }`إنها امتداد DSL غروفي متوفر فقط في سياق تنفيذ buildscript. لا توجد كواجهة 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 لروبيجيمز
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`ولا في khối`repositories { }`لأن المستقبل من هذا البلوك ليس هو`RepositoryHandler`معيار Gradle لكن واجهة برمجة تطبيقات grolifant غير متوافق مع الامتداد`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 réelle de`org.ysb33r.grolifant.api.v4.JavaForkOptions`عرض :
public void executable(java.lang.Object); (1)
public void setExecutable(java.lang.Object);
الطريقة`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")
}
}
تكوين toolchain جافا
مشكلة 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 جافا 17. Kotlin 2.0.x في buildSrc لا يدعم جافا 25 (محلل الإصدار يتعطل على`"25.0.2"`). يجب أن يعمل Daemon 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."
)
}
النتيجة النهائية
نصّب البناء للمستهلك
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`افتراضيًا |
مُبرمج ثابت`.gem`في النمط Ivy و المصّف`@gem` |
|
المستقبل grolifant غير متوافق مع DSL Kotlin |
مكالمة مباشرة`project.repositories.ivy { }` |
`javaLauncher`غير محلول |
الخصية غير موجودة على`AsciidoctorJRevealJSTask` |
|
`executable = …`لا يتم تجميعه |
ملكية`val`في`JavaForkOptions`غروليفانت |
طريقة`executable(Object)`بدلاً من |
|
هذا خدمة Gradle، ليست امتدادًا |
|
`revealjs { }`غير محلل في المهمة |
تمديد المشروع، ليس طريقة المهمة |
|
تعطل البناء مع Java 25 |
Kotlin 2.0.x لا يحلل إصدارات Java ذات الرقمين |
اكتشاف Docker التلقائي + احتياطي 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 est une classe grolifant inconnue. On localise son 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 أو Maven محلية. سيصبح نص بناء المستهلك بعد ذلك:
plugins { id("slides") version "1.0.0" }
Et settings.gradle.kts`سيُخفض إلى الحد الأدنى الصارم بدون إشارة إلى`foojay-resolver-convention, يتم إدارة توفير JDK بواسطة البرنامج الإضافي نفسه أو توثيقه كمتطلب مسبق.