背景与目标

起点

项目`slider-gradle`生成来自 AsciiDoc 文件的 Reveal.js 演示文稿,通过 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() } }`这是一个仅在 buildscript 执行上下文中可用的 Groovy DSL 扩展。它不存在为可从 buildSrc 访问的静态 Kotlin API。

在尝试从这里调用它`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. 在配置中保存 gem`asciidoctorGems`为了让 JRuby 在运行时加载它

这三个责任必须在 Kotlin 中分别复制。

三部分的解决方案

第一部分: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 标准但一个怪异的 API 与扩展不兼容`ivy`Kotlin DSL。

第二部分: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 会忽略插件中声明的仓库,并且 gem 的解析会失败。

通过字节码对 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;

JavaForkOptions (grolifant) 的 API

`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`不能被重新分配)。

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工具链配置

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。

策略

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

最终结果

消费者的 buildscript

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

仅限 Groovy DSL 扩展

三个独立的机制: Ivy 仓库 + Maven 排除 +asciidoctorGems

Gradle 正在寻找一个`.jar`而不是一个`.gem`

`[ext]`解决`jar`默认

硬编码者`.gem`在 Ivy 模式 + 限定词`@gem`

ivy { }`在…​中不编译`repositories { }

grolifant 接收器与 Kotlin DSL 不兼容

直接调用`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 回退

调查方法:使用 javap 读取未知的 API

原则

当文档缺失或不完整时,字节码是事实的来源。 javap`是 JDK 的标准工具,用于反编译文件.class`在可读的 Java 签名中 无需源代码。

步骤1:在Gradle缓存中定位jar

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`. 然后对其进行检查 首先定位其 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`为了识别项目扩展

此方法适用于任何其 API 未被文档化的 Gradle 插件。 或者其 alpha 版本不再符合现有的文档。

下一步

此 buildSrc 插件将被提取到已发布在 Gradle Plugin Portal 或 Maven Local 的独立项目中。消费者的 buildscript 将变为:

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

Et settings.gradle.kts`将被减至其最低限度,且不作任何参考`foojay-resolver-convention, JDK 配置由插件本身管理或记录为前提条件。

相关文章