Bối cảnh và mục tiêu

Điểm khởi đầu

Dự án`slider-gradle`tạo các bản thuyết trình Reveal.js từ các tệp AsciiDoc thông qua plugin Gradle`org.asciidoctor.jvm.revealjs`. Cấu hình của tác vụ chính`asciidoctorRevealJs`nằm trực tiếp trong buildscript gốc`build.gradle.kts`Vui lòng cung cấp văn bản cần dịch.

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
    }
}

Mục tiêu

Di chuyển toàn bộ cấu hình này vào`buildSrc/src/main/kotlin/slides/SlidesPlugin.kt`để buildscript tiêu thụ chỉ còn lại:

apply<slides.SlidesPlugin>()

Plugin phải hoàn toàn độc lập: nó tự áp dụng các plugin phụ thuộc, cấu hình các repo và quản lý các Ruby gem của chính mình.

Trở ngại đầu tiên: `ruby { gems() }

Những gì ẩn sau cú pháp đường (syntactic sugar) của Groovy

Dòng`repositories { ruby { gems() } }`là một phần mở rộng DSL Groovy chỉ khả dụng trong ngữ cảnh thực thi của buildscript. Nó không tồn tại dưới dạng API Kotlin tĩnh có thể truy cập được từ buildSrc.

Khi cố gắng gọi nó từ`SlidesPlugin.kt`, quá trình biên dịch sẽ thất bại ngay lập tức.

Phân tích cơ chế

Sau khi phân tích bộ nhớ đệm Gradle (~/.gradle/caches/modules-2/files-2.1/rubygems/), chúng ta tìm thấy trong tệp`ivy-3.1.0.xml`Vui lòng cung cấp văn bản cần dịch.

<artifact type='gem' url='https://rubygems.org/gems/asciidoctor-revealjs-3.1.0.gem' />

ruby { gems() }`thực chất đã thực hiệnba thao tác riêng biệtHãy nhập `name của bạn:

  1. Lưu một repo Ivy trỏ đến`https://rubygems.org/gems/`

  2. Loại bỏ nhóm`rubygems`các repo Maven để tránh xung đột

  3. Lưu gem vào cấu hình`asciidoctorGems`để JRuby tải nó vào runtime

Ba trách nhiệm này phải được tái hiện riêng biệt trong Kotlin.

Giải pháp gồm ba phần

Phần 1: repo Ivy cho 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 Tiện ích mở rộng`.gem`phải được hardcoded.[ext]`được giải quyết theo mặc định bằng.jar`, điều này gây ra lỗi`Resource missing`Vui lòng cung cấp văn bản cần dịch.

LƯU Ý: Repo Ivy được khai báo trực tiếp trên`project.repositories`và không nằm trong một khối`repositories { }`vì receiver của khối này không phải là`RepositoryHandler`tiêu chuẩn của Gradle nhưng là một API grolifant không tương thích với extension`ivy`Kotlin DSL.

Phần 2: sự phụ thuộc asciidoctorGems

Plugin`org.asciidoctor.jvm.gems`phải được áp dụng trước tiên — nó tạo ra cấu hình`asciidoctorGems`và tác vụ`asciidoctorGemsPrepare`Vui lòng cung cấp văn bản cần dịch.

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 Thứ tự áp dụng là rất quan trọng:`gems`trước`revealjs`Vui lòng cung cấp nội dung cần dịch. <2> Bộ định tính`@gem`buộc extension chính xác trên dependency.

Phần 3: `settings.gradle.kts

`dependencyResolutionManagement`trong`settings.gradle.kts`phải cho phép các dự án khai báo các repo của riêng chúng:

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

Nếu không có dòng này, Gradle sẽ bỏ qua các repo được khai báo trong plugin và việc giải quyết các gems sẽ thất bại.

Truy xuất nội soi API bằng bytecodes

Tại sao lại là javap?

Plugin`asciidoctor-gradle-jvm-slides`đang ở phiên bản`4.0.0-alpha.1`. Tài liệu của nó không tồn tại hoặc không đầy đủ. Nguồn đáng tin cậy duy nhất là kiểm tra trực tiếp các lớp đã biên dịch.

Hệ thống phân cấp tác vụ

javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
  org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask

Kết quả:

public class AsciidoctorJRevealJSTask
  extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
  implements org.asciidoctor.gradle.base.slides.SlidesToExportAware

Khám phá `forkOptions

Khi kiểm tra`AbstractAsciidoctorTask`Vui lòng cung cấp văn bản tiếng Pháp cần dịch.

javap -p -classpath asciidoctor-gradle-jvm-4.0.0-alpha.1.jar \
  org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask | grep -i "fork\|exec\|jvm"

Chúng ta tìm thấy:

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 của JavaForkOptions (grolifant)

`javaLauncher`không tồn tại trong tác vụ này. API thực tế của`org.ysb33r.grolifant.api.v4.JavaForkOptions`phơi bày :

public void executable(java.lang.Object);  (1)
public void setExecutable(java.lang.Object);
1 Phương pháp`executable(Object)`thay thế phép gán`executable = …​`không biên dịch được (`val`không thể gán lại).

RevealJSExtension

`revealjs { }`không phải là một phương thức của tác vụ mà là mộtmở rộng dự ánVui lòng cung cấp văn bản cần dịch.

javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
  org.asciidoctor.gradle.jvm.slides.RevealJSExtension

Truy cập thông qua:

project.extensions.getByType<RevealJSExtension>().apply {
    version = "3.1.0"
    templateGitHub {
        setOrganisation("hakimel")
        setRepository("reveal.js")
        setTag("3.9.1")
    }
}

Cấu hình toolchain Java

Vấn đề của JavaToolchainService

`JavaToolchainService`không phải là một phần mở rộng của dự án. Lời gọi sau đây bị lỗi:

// ERREUR : Extension of type 'JavaToolchainService' does not exist
project.extensions.getByType<JavaToolchainService>()

API đúng là`serviceOf`Vui lòng cung cấp văn bản cần dịch.

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
        )
    }
}

Tự động phát hiện Docker

Ngữ cảnh

Plugin Asciidoctor/JRuby yêu cầu Java 17. Kotlin 2.0.x trong buildSrc không hỗ trợ Java 25 (trình phân tích cú pháp phiên bản bị treo tại`"25.0.2"`). Do đó, Gradle daemon phải chạy trên Java 17 hoặc phải sử dụng Docker.

Chiến lược

  1. Docker có sẵn → thực thi qua container`eclipse-temurin:17`(hành vi mặc định)

  2. Thiếu Docker + Java 17 → thực thi cục bộ

  3. Thiếu Docker + Java > 17 → lỗi rõ ràng

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."
    )
}

Kết quả cuối cùng

Buildscript tiêu thụ

apply<slides.SlidesPlugin>()

Vậy là xong. Plugin chịu toàn bộ trách nhiệm.

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"

Tóm tắt các cạm bẫy và giải pháp

Vấn đề Nguyên nhân Giải pháp

`ruby { gems() }`không có sẵn trong Kotlin

Tiện ích mở rộng chỉ dành cho DSL Groovy

Ba cơ chế riêng biệt: repo Ivy + loại trừ Maven +asciidoctorGems

Gradle đang tìm một`.jar`thay vì một`.gem`

`[ext]`giải quyết bằng`jar`mặc định

Hardcoder`.gem`trong pattern Ivy + qualifier`@gem`

ivy { }`không biên dịch trong`repositories { }

Bộ thu grolifant không tương thích với DSL Kotlin

Cuộc gọi trực tiếp`project.repositories.ivy { }`

`javaLauncher`chưa giải quyết

Thuộc tính không tồn tại trên`AsciidoctorJRevealJSTask`

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

`executable = …​`không biên dịch được

Thuộc tính`val`trong`JavaForkOptions`grolifant

Phương pháp`executable(Object)`thay vào đó

JavaToolchainService`không tìm thấy qua`extensions

Đây là một Gradle service, không phải là một extension

project.serviceOf<JavaToolchainService>()

`revealjs { }`chưa được giải quyết trong tác vụ

Mở rộng dự án, không phải phương pháp tác vụ

project.extensions.getByType<RevealJSExtension>()

Build bị crash với Java 25

Kotlin 2.0.x không phân tích cú pháp các phiên bản Java có hai chữ số

Tự động phát hiện Docker + fallback Java 17

Phương pháp điều tra: đọc một API chưa biết bằng `javap

Nguyên tắc

Khi tài liệu thiếu hoặc không đầy đủ, các bytecode là nguồn sự thật. javap`là công cụ tiêu chuẩn của JDK dùng để dịch ngược các tệp.class`thành các chữ ký Java dễ đọc, mà không cần mã nguồn.

Bước 1: xác định vị trí tệp jar trong cache Gradle

Gradle tải xuống tất cả các phụ thuộc của mình vào`~/.gradle/caches/modules-2/files-2.1/`Vui lòng cung cấp văn bản cần dịch. Bước đầu tiên là tìm file jar chứa class cần kiểm tra:

find ~/.gradle/caches -name "asciidoctor-gradle-jvm-slides*.jar" 2>/dev/null
Kết quả:
/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

Bước 2: liệt kê các lớp của jar

Trước khi kiểm tra một lớp, chúng ta xác minh rằng nó thực sự tồn tại trong file jar:

jar tf asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar | grep -i "RevealJS\|revealjs"

Điều này hiển thị tất cả các lớp có sẵn: AsciidoctorJRevealJSTask, RevealJSExtension, RevealJSOptions, v.v.

Bước 3: kiểm tra một lớp

javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask

Tùy chọn`-p`hiển thị tất cả thành viên bao gồm cả những thành viên riêng tư. Kết quả hiển thị ngay lập tức dòng chính:

public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask

Bước 4: đi ngược lên cấp bậc phân cấp

Nhiệm vụ mở rộng`AbstractAsciidoctorTask`. Lần lượt kiểm tra nó bằng cách định vị tệp jar của nó trước:

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"

Đó là nơi chúng ta khám phá ra`forkOptions`, JAVA_EXEC, et javaForkOptions loại`org.ysb33r.grolifant.api.v4.JavaForkOptions`Vui lòng cung cấp nội dung cần dịch.

Bước 5: theo dõi các kiểu không xác định

`JavaForkOptions`là một lớp grolifant không xác định. Chúng ta xác định vị trí file jar của nó:

find ~/.gradle/caches -name "grolifant*.jar" 2>/dev/null

Sau đó chúng ta kiểm tra nó:

javap -p -classpath grolifant40-legacy-api-2.0.0-alpha.6.jar \
org.ysb33r.grolifant.api.v4.JavaForkOptions

Ở đó có`executable(java.lang.Object)`— phương thức chính xác cần gọi, trái ngược với`executable = …​`không biên dịch được vì đó là một thuộc tính`val`Vui lòng cung cấp văn bản tiếng Pháp cần dịch.

Bước 6: kiểm tra các phần mở rộng của dự án

Cho`revealjs { }`, câu hỏi là: đây có phải là một phương pháp của tác vụ hay một phần mở rộng của dự án? Việc kiểm tra`AsciidoctorJRevealJSTask` không hiển thị phương thức nào`revealjs`. Sau đó chúng ta kiểm tra`RevealJSExtension`Vui lòng cung cấp văn bản tiếng Pháp cần dịch.

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;

Sự hiện diện của`NAME`xác nhận rằng đây là một phần mở rộng được đăng ký trên dự án, có thể truy cập thông qua`project.extensions.getByType<RevealJSExtension>()`Vui lòng cung cấp văn bản cần dịch.

Tóm tắt phương pháp

Bước Hành động

1

find ~/.gradle/caches -name "*.jar"— định vị file `jar

2

jar tf jar.jar | grep NomClasse— kiểm tra xem lớp có tồn tại không

3

javap -p -classpath jar.jar NomCompletClasse— kiểm tra class

4

Định danh`extends`và đi lên cấp bậc cao hơn

5

Theo dõi các kiểu không xác định trong các file jar của riêng chúng

6

Tìm kiếm`NAME`để xác định một phần mở rộng của dự án

Phương pháp này áp dụng cho bất kỳ plugin Gradle nào có API không được tài liệu hóa hoặc phiên bản alpha của nó không còn khớp với tài liệu hiện có.

Bước tiếp theo

Plugin buildSrc này sẽ được trích xuất thành một dự án độc lập được xuất bản trên Gradle Plugin Portal hoặc Maven Local. Khi đó, buildscript tiêu thụ sẽ trở thành:

plugins { id("slides") version "1.0.0" }

Et settings.gradle.kts`sẽ được giảm xuống mức tối thiểu tuyệt đối mà không tham chiếu đến`foojay-resolver-convention, việc cấp phát (provisioning) JDK được quản lý bởi chính plugin hoặc được ghi chú là điều kiện tiên quyết.

Bài viết liên quan