สารบัญ

บริบทและวัตถุประสงค์

จุดเริ่มต้น

โครงการ`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>()

ปลั๊กอินต้องพึ่งพาตนเองได้อย่างสมบูรณ์: มันจะใช้งาน dependency ของปลั๊กอินด้วยตัวเอง กำหนดค่า repos ของตัวเอง และจัดการ gems Ruby ของตัวเอง

อุปสรรคแรก: `ruby { gems() }

สิ่งที่ซ่อนอยู่ในน้ำตาลทางไวยากรณ์ของ Groovy

เส้น`repositories { ruby { gems() } }`เป็นส่วนขยาย Groovy DSL ที่มีให้ใช้เฉพาะในบริบทการทำงานของ 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() }`ในความเป็นจริงแล้วดำเนินการอยู่การดำเนินการที่แตกต่างกันสามประการ​

  1. บันทึก repo Ivy ที่ชี้ไปยัง`https://rubygems.org/gems/`

  2. ยกเว้นกลุ่ม`rubygems`ที่เก็บ Maven เพื่อหลีกเลี่ยงความขัดแย้ง

  3. บันทึก gem ลงในการกำหนดค่า`asciidoctorGems`เพื่อให้ JRuby โหลดมันตอนรันไทม์

ความรับผิดชอบทั้งสามนี้ต้องถูกทำซ้ำแยกจากกันใน Kotlin

โซลูชันสามส่วน

ส่วนที่ 1 : repo 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") }
}

ส่วนขยาย`.gem`ต้องถูกฮาร์ดโค้ด`[ext]แก้ไขโดยค่าเริ่มต้นเป็น.jar`, ซึ่งทำให้เกิดข้อผิดพลาด`Resource missing`.

หมายเหตุ: repo Ivy ถูกประกาศโดยตรงบน`project.repositories`และไม่อยู่ในบล็อก`repositories { }`เพราะรีซีฟเวอร์ของบล็อกนี้ไม่ใช่`RepositoryHandler`มาตรฐานของ Gradle แต่เป็น API grolifant ที่ไม่เข้ากันกับส่วนขยาย`ivy`Kotlin DSL

ส่วนที่ 2 : การพึ่งพา asciidoctorGems

ปลั๊กอิน`org.asciidoctor.jvm.gems`ควรใช้ก่อน — มันสร้างการกำหนดค่า`asciidoctorGems`และงาน`asciidoctorGemsPrepare`(No visible text to translate, so result is 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`บังคับให้ใช้ส่วนขยายที่ถูกต้องกับ dependency

ส่วนที่ 3 : settings.gradle.kts

`dependencyResolutionManagement`ใน`settings.gradle.kts`ต้องอนุญาตให้โปรเจกต์ประกาศรีโปของตนเอง:

@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
}

หากไม่มีบรรทัดนี้ Gradle จะเพิกเฉยต่อรีโพที่ประกาศไว้ในปลั๊กอิน และการ resolve 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`ไม่สามารถกำหนดค่าใหม่ได้)

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 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 daemon ต้องรันบน 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 เท่านั้น

สามกลไกที่แยกจากกัน: repo Ivy + การยกเว้น Maven +asciidoctorGems

Gradle กำลังมองหา`.jar`แทนที่จะเป็น`.gem`

`[ext]`แก้เป็น`jar`โดยค่าเริ่มต้น

ฮาร์ดโค้ด`.gem`ในแพตเทิร์น Ivy + ตัวระบุ`@gem`

ivy { }`ไม่คอมไพล์ใน`repositories { }

Receiver 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 ดาวน์โหลด dependency ทั้งหมดของมันไว้ใน`~/.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

find ~/.gradle/caches -name "*.jar"— ระบุตำแหน่ง jar

2

jar tf jar.jar | grep NomClasse— ตรวจสอบว่าคลาสมีอยู่

3

javap -p -classpath jar.jar NomCompletClasse— ตรวจสอบคลาส

4

ระบุ`extends`และไล่ขึ้นไปตามลำดับชั้น

5

ติดตามประเภทที่ไม่รู้จักใน jar ของตัวเอง

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 นั้นได้รับการจัดการโดยปลั๊กอินเอง หรือมีการจัดทำเอกสารไว้เป็นข้อกำหนดเบื้องต้น

บทความที่เกี่ยวข้อง