ترحيل إضافة Asciidoctor RevealJS إلى buildSrc Kotlin: الهندسة العكسية لواجهة برمجة تطبيقات Groovy
نُشر في 16 April 2026
السياق والهدف
نقطة البداية
المشروع`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>()
يجب أن يكون البرنامج المساعد مستقلاً تمامًا: هو نفسه يطبق اعتماداته من الإضافات، ويكوّن مستودعاته، ويَدير جواهر Ruby.
العقبة الأولى : `ruby { gems() }
ما يخفيه سكر الصياغة Groovy
السطر`repositories { ruby { gems() } }`هو إضافة DSL Groovy متوفرة فقط في سياق تنفيذ buildscript. لا توجد كواجهة برمجة تطبيقات Kotlin ثابتة يمكن الوصول إليها من buildSrc.
عند محاولة استدعائه من`SlidesPlugin.kt`, يفشل التجميع فورًا.
تحليل الآلية
بعد تحليل ذاكرة التخزين المؤقت لـ Gradle ( ~/.gradle/caches/modules-2/files-2.1/rubygems/), Najd…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 غير متوافقة مع الإضافة`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)
}
ترتيب التطبيق مهم :`gems`قبل`revealjs`. <2> الوصف`@gem`فرض الامتداد الصحيح على التبعية.
الجزء 3 : settings.gradle.kts
`dependencyResolutionManagement`في`settings.gradle.kts`يجب أن يسمح للمشاريع بالإعلان عن مستودعاتها الخاصة :
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
}
بدون هذا السطر، يتجاهل Gradle المخازن المعلنة في الإضافة ويفشل حل الجواهر.
تفحص واجهة برمجة التطبيقات عن طريق بايت كود
لماذا 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 (grolifant)
`javaLauncher`لا يوجد على هذه المهمة. الواجهة البرمجية الحقيقية لـ`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 { }`ليست طريقة من طرق المهمة بل هيتوسيع المشروعيرجى إدخال `name و email و password في الحقول المخصصة.
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.
استراتيجية
-
دُوكر متوفر → التنفيذ عبر الحاوية`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`افتراضيًا |
Hardcoder`.gem`في نمط Ivy + المؤهل`@gem` |
|
المستقبل جروليفان غير متوافق مع DSL Kotlin |
مكالمة مباشرة`project.repositories.ivy { }` |
`javaLauncher`غير محلول |
الخاصية غير موجودة على`AsciidoctorJRevealJSTask` |
|
`executable = …`لا يتم تجميعه |
ملكية`val`في`JavaForkOptions`غروليفانت |
طريقة`executable(Object)`بدلاً |
|
هذه خدمة Gradle، ليست امتدادًا |
|
`revealjs { }`غير محلول في المهمة |
تمديد المشروع، وليس طريقة مهمة |
|
تعطل البناء مع Java 25 |
Kotlin 2.0.x لا يحلل إصدارات Java ذات رقمين |
كشف Docker تلقائي + الاحتياطي لجافا 17 |
طريقة التحقيق: قراءة واجهة برمجة تطبيقات غير معروفة باستخدام javap
مبدأ
عندما تكون الوثائق غائبة أو غير مكتملة، يكون الـ bytecode هو مصدر الحقيقة. javap`هو الأداة القياسية في JDK التي تفكك الملفات.class`في توقيعات جافا قابلة للقراءة, بدون الحاجة إلى الكود المصدر.
الخطوة 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, etc.
الخطوة 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 |
تتبع الأنواع غير المعروفة في ملفات JAR الخاصة بهم |
6 |
يبحث`NAME`لتحديد امتداد مشروع |
هذه الطريقة تنطبق على أي مكوّن Gradle لا تكون واجهته البرمجية موثّقة أو التي لم يعد إصدار ألفا الخاص بها يتوافق مع الوثائق الحالية.
الخطوة التالية
سيتم استخراج هذا البرنامج الإضافي buildSrc في مشروع مستقل تم نشره على Gradle Plugin Portal أو Maven Local. سيصبح buildscript المستهلك بعد ذلك :
plugins { id("slides") version "1.0.0" }
Et settings.gradle.kts`سيتم تقليله إلى الحد الأدنى دون إشارة إلى`foojay-resolver-convention, إعداد JDK يتم إدارته بواسطة المكوّن الإضافي نفسه أو يُوثّق كمتطلب مسبق.