السياق والهدف

نقطة الانطلاق

المشروع`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() }`كان في الواقع يقوم بهثلاث عمليات منفصلة:

  1. احفظ مستودع Ivy يشير إلى`https://rubygems.org/gems/`

  2. استبعد المجموعة`rubygems`مستودعات Maven لتجنب النزاعات

  3. احفظ الجوهرة في الإعدادات`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.

استراتيجية

  1. Docker متاح → التنفيذ عبر الحاوية`eclipse-temurin:17`(السلوك الافتراضي)

  2. غياب Docker + Java 17 → تنفيذ محلي

  3. 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 +asciidoctorGems

Gradle يبحث عن`.jar`بدلاً من`.gem`

`[ext]`يُحلّ في`jar`افتراضيًا

مُبرمج ثابت`.gem`في النمط Ivy و المصّف`@gem`

ivy { } ne compile pas dans repositories { }

المستقبل grolifant غير متوافق مع DSL Kotlin

مكالمة مباشرة`project.repositories.ivy { }`

`javaLauncher`غير محلول

الخصية غير موجودة على`AsciidoctorJRevealJSTask`

setInProcess("JAVA_EXEC")+forkOptions { executable(…​) }

`executable = …​`لا يتم تجميعه

ملكية`val`في`JavaForkOptions`غروليفانت

طريقة`executable(Object)`بدلاً من

JavaToolchainService`غير موجود عبر`extensions

هذا خدمة Gradle، ليست امتدادًا

project.serviceOf<JavaToolchainService>()

`revealjs { }`غير محلل في المهمة

تمديد المشروع، ليس طريقة المهمة

project.extensions.getByType<RevealJSExtension>()

تعطل البناء مع 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
nتيجة :
/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

find ~/.gradle/caches -name "*.jar"— العثور على الجرة

2

jar tf jar.jar | grep NomClasse— التحقق من أن الفئة موجودة

3

javap -p -classpath jar.jar NomCompletClasse— افحص الصف

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 بواسطة البرنامج الإضافي نفسه أو توثيقه كمتطلب مسبق.

Articles connexes