Πίνακας περιεχομένων

Περιβάλλον και στόχος

Η αρχή

Το έργο`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>()

Το πρόσθετο πρέπει να είναι πλήρως αυτόνομο: εφαρμόζει αυτόματα τις εξαρτήσεις του, ρυθμίζει τα αποθετήρια του, και διαχειρίζεται τα Ruby gems του.

Το πρώτο εμπόδιο : `ruby { gems() }

Τι κρύβει το συντακτικό ζάχαρο του Groovy

Η γραμμή`repositories { ruby { gems() } }`είναι μια επέκταση DSL Groovy που είναι διαθέσιμη μόνο στον περιβάλλον εκτέλεσης του buildscript. Δεν υπάρχει ως στατική API Kotlin που είναι προσβάσιμη από το buildSrc.

Προσπαθώντας να τον καλέσω από`SlidesPlugin.kt`, η σύνθεση αποτύχει αμέσως.

Ανάλυση του μηχανισμού

Μετά από ανάλυση του cache του 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. Αποθηκεύστε το gem στη διαμόρφωση`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") }
}

Η επέκταση`.gem`Πρέπει να είναι σκληροκωδικοποιημένη.[ext]`λύει από προεπιλογή σε.jar`, το προκαλεί ένα σφάλμα`Resource missing`.

Το αποθετήριο Ivy δηλώνεται άμεσα σε`project.repositories`και όχι σε ένα μπλοκ`repositories { }`επειδή ο δέκτης αυτού του μπλοκ δεν είναι ο`RepositoryHandler`τυπικό Gradle αλλά μια API grolifant που δεν είναι συμβατή με την επέκταση`ivy`Kotlin DSL.

Μέρος 2: η εξάρτηση asciidoctorGems

Το πρόσθετο`org.asciidoctor.jvm.gems`Πρέπει να εφαρμοστεί πρώτο — δημιουργεί τη διαμόρφωση`asciidoctorGems`και η εργασία`asciidoctorGemsPrepare`(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`. <2> Ο προσδιοριστής`@gem`Ανάγκασε τη σωστή επέκταση στην εξάρτηση.

Μέρος 3 : settings.gradle.kts

`dependencyResolutionManagement`σε`settings.gradle.kts`πρέπει να επιτρέπει στα έργα να δηλώνουν τα δικά τους αποθετήρια :

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

Χωρίς αυτή τη γραμμή, το Gradle αγνοεί τα αποθετήρια που δηλώνονται στο πρόσθετο και η επίλυση των gems αποτυγχάνει.

Introspection της API μέσω bytecodes

Γιατί 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);

Η μέθοδος`executable(Object)`αντικαθιστά την ανάθεση`executable = …​`που δεν μεταγλωττίζει`val`den μπορεί να ανατεθεί ξανά).

επέκταση RevealJS

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

Διαμόρφωση του Java 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 απαιτεί Java 17. Το Kotlin 2.0.x στον buildSrc δεν υποστηρίζει το Java 25 (ο αναλυτής έκδοσης σπάει σε`"25.0.2"`). πρέπει να τρέχει σε Java 17 ή ο Docker πρέπει να χρησιμοποιηθεί.

Στρατηγική

  1. Docker διαθέσιμο → εκτέλεση μέσω container`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."
    )
}

Τελικό αποτέλεσμα

Ο buildscript του καταναλωτή

apply<slides.SlidesPlugin>()

Αυτό είναι όλο. Το plugin φέρει όλη την ευθύνη.

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`με προεπιλογή

Hardcoder`.gem`στο πρότυπο Ivy + προσδιοριστικό`@gem`

ivy { }`Δεν μεταγλωττίζει σε`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>()

Το build έπεσε με το Java 25

Kotlin 2.0.x δεν αναλύει τις εκδόσεις Java με δύο ψηφία

Αυτόματη ανίχνευση Docker + επαναφορά Java 17

Μέθοδος έρευνας : ανάγνωση μιας άγνωστης API με javap

αρχή

Όταν η τεκμηρίωση λείπει ή είναι ανεπαρκής, τα bytecode είναι η πηγή αλήθειας. javap`είναι το τυπικό εργαλείο του JDK που αποδιαμεταγλωττοποιεί τα αρχεία.class`σε υπογραφές Java αναγνωσιμες, χωρίς να απαιτείται ο πηγαίος κώδικας.

Βήμα 1 : εντοπίστε το jar στον cache 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"

Βήμα 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`. Το ελέγχουμε με σειρά εντοπίζontan πρώτοτα το 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`είναι μιά κλάση άγνωστη. Βρίσκουμε το 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

Ακολουθήστε τους άγνωστους τύπους στα δικά τους jars

6

Αναζήτηση`NAME`για να εντοπίσαι μια επέκταση του έργου

Αυτή η μέθοδος εφαρμόζεται σε οποιοδήποτε πρόσθετο Gradle της οποίας η API δεν είναι τεκμηριωμένη ή της οποίας η έκδοση α δεν ταιριάζει πλέον με την υπάρχουσα τεκμηρίωση.

Επόμενο βήμα

Αυτό το plugin buildSrc θα εξαχθεί σε ένα ανεξάρτητο έργο που θα δημοσιευτεί στο Gradle Plugin Portal ή στο Maven Local. Ο consumer buildscript θα γίνει:

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

Et settings.gradle.kts`θα μειωθεί στο ελάχιστό του χωρίς αναφορά σε`foojay-resolver-convention, το provisioning του JDK διαχειρίζεται από το plugin αυτό ή τεκμηριωθεί ως προϋποθεσία.

Σχετικά άρθρα