为 Gradle 子项目配置 Kotlin 扩展

我正在为 JVM 建立一个基于 Kotlin 的多模块 Gradle 项目。由于根项目不包含任何代码,因此 Kotlin 插件应仅应用于子项目。

build.gradle.kts (根项目)

plugins {
    kotlin("jvm") version "1.6.20" apply false
}

subprojects {
    apply(plugin = "kotlin")

    group = "com.example"

    repositories {
        mavenCentral()
    }

    dependencies {}

    kotlin {
        jvmToolchain {
            check(this is JavaToolchainSpec)
            languageVersion.set(JavaLanguageVersion.of(11))
        }
    }
}

尝试设置工具链会导致构建在 kotlin {...} 扩展时失败:

Unresolved reference. None of the following candidates is applicable because of receiver type mismatch: 
public fun DependencyHandler.kotlin(module: String, version: String? = ...): Any defined in org.gradle.kotlin.dsl
public fun PluginDependenciesSpec.kotlin(module: String): PluginDependencySpec defined in org.gradle.kotlin.dsl

如果我将扩展定义复制到每个子项目构建脚本,它工作正常,但为什么它在主脚本中不可用?

stack overflow Configure Kotlin extension for Gradle subprojects
原文答案
author avatar

接受的答案

这是我最喜欢在 Gradle 中解决的问题之一,并且真正展示了可能的灵活性(以及演示为什么 Gradle 可能很复杂!)

首先,我将提供一些有关 subprojects {} DSL 的背景信息,然后我将展示如何修复您的脚本,最后我将展示与 buildSrc 约定插件共享构建逻辑的最佳方式。 (即使它是最后一个,我真的建议使用 buildSrc!)

组合与继承

使用 allprojects {}subprojects {} 真的很常见,我经常看到。它更类似于 Maven 的工作方式,所有配置都在“父”构建文件中定义。但是 Gradle 不建议这样做。

[A], discouraged, way to share build logic between subproject is cross project configuration via the subprojects {} and allprojects {} DSL constructs.

Gradle Docs: Sharing Build Logic between Subprojects

(这可能很常见,因为它易于理解 - 它使 Gradle 更像 Maven,因此每个项目都从一个父级继承。但 Gradle 是为组合而设计的。进一步阅读: Composition over inheritance: Gradle vs Maven

快速修复:“未解决的参考”

您看到的错误基本上是因为您没有应用 Kotlin 插件。

plugins {
    kotlin("jvm") version "1.6.20" apply false // <- Kotlin DSL won't be loaded
}

kotlin { } 配置块是一个非常有用的扩展函数,在应用 Kotlin 插件时加载。这是它的样子:

/**
 * Configures the [kotlin][org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension] extension.
 */
fun org.gradle.api.Project.`kotlin`(configure: Action<org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension>): Unit =
    (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("kotlin", configure)
// (note: this is generated code)

所以如果我们没有扩展功能,我们可以直接调用 configure ,从而配置 Kotlin 扩展。

subprojects {
  // this is the traditional Gradle way of configuring extensions, 
  // and what the `kotlin { }` helper function will call.
  configure<org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension> {
    jvmToolchain {
      check(this is JavaToolchainSpec)
      languageVersion.set(JavaLanguageVersion.of(11))
    }
  }

  // without the Kotlin Gradle plugin, this helper function isn't available
  // kotlin {
  //   jvmToolchain {
  //    check(this is JavaToolchainSpec)
  //     languageVersion.set(JavaLanguageVersion.of(11))
  //   }
  // }
}

然而,即使这样可行,使用 subprojects {} 也有问题。有更好的方法...

buildSrc 和约定插件

buildSrc 基本上是一个独立的 Gradle 项目,我们可以在主项目的构建脚本中使用它的输出。所以我们可以编写自己的自定义 Gradle 插件,定义约定,我们可以有选择地应用到“主”构建中的任何子项目。

(这是 Gradle 和 Maven 之间的主要区别。在 Gradle 中,子项目可以由任意数量的插件配置。在 Maven 中,只有一个父项目。组合 vs 继承!)

Gradle 文档有 a full guide on setting up convention plugins ,所以我将在这里简要总结解决方案。

1. 设置 ./buildSrc

在项目根目录中创建一个名为 buildSrc 的目录。

因为 buildSrc 是一个独立的项目,所以创建一个 ./buildSrc/build.gradle.kts./buildSrc/settings.gradle.kts 文件,就像通常的项目一样。

./buildSrc/build.gradle.kts 中,

  1. 应用 kotlin-dsl 插件
  2. 添加对你想在项目中任何地方使用的 Gradle 插件的依赖项

    
    // ./buildSrc/build.gradle.kts
    plugins {
    `kotlin-dsl` // this will produce our Gradle convention plugins
    kotlin("jvm") version embeddedKotlinVersion 
    // What's embeddedKotlinVersion? 
    // It's a long story, but Gradle uses an embedded version of Kotlin,
    // which means using the the latest version can be tricky. 
    // embeddedKotlinVersion is a shortcut for the embedded version.
    // It's annoying but not important. The Kotlin plugin version below, in dependencies { }, 
    // will be used for building our 'main' project.
    // https://github.com/gradle/gradle/issues/16345
    }

val kotlinVersion = "1.6.20"

dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
}


请注意,我使用了 Kotlin Gradle 插件的 **Maven 存储库** 坐标,而不是插件 ID!

如果您愿意,还可以将其他依赖项添加到  `./buildSrc/build.gradle.kts`  中。如果您想在构建脚本中解析 JSON,请添加对 JSON 解析器的依赖项,例如  `kotlinx-serialization` 。

#### 2. 创建约定插件

创建可应用于任何 Kotlin JVM 子项目的 Kotlin JVM 约定。

// ./buildSrc/src/main/kotlin/my/project/convention/kotlin-jvm.gradle.kts
package my.project.convention

plugins {
kotlin("jvm") // don't include a version - that's provided by ./buildSrc/build.gradle.kts
}

dependencies {
// you can define default dependencies, if desired
// testImplementation(kotlin("test"))
}

kotlin {
jvmToolchain {
check(this is JavaToolchainSpec)
languageVersion.set(JavaLanguageVersion.of(11))
}
}
}


不要忘记添加  `package`  声明!我已经忘记了几次,它会导致难以弄清楚的错误。

#### 3. 应用约定插件

就像 Gradle 插件有 ID 一样,我们的约定插件也是如此。它是包名 +  `.gradle.kts`  之前的位。所以在我们的例子中,ID 是  `my.project.convention.kotlin-jvm` 

我们可以像普通的 Gradle 插件一样应用它......

// ./subprojects/my-project/build.gradle.kts
plugins {
id("my.project.convention.kotlin-jvm")
}


(约定插件也可以导入其他约定插件,使用  `id("...")` )

此外,由于我们使用的是 Kotlin,因此还有一种更好的方法。您知道如何包含 Gradle 插件,例如  `java`  和  `java-library` 。我们可以以同样的方式导入我们的约定插件!

// ./subprojects/my-project/build.gradle.kts
plugins {
// id("my.project.convention.kotlin-jvm")
my.project.convention.kotlin-jvm // this works just like id("...") does
}


请注意插件 ID 周围的反引号 - 因为连字符,所以需要它们。

(警告:这种非  `id("...")`  方式在  `buildSrc`  内不起作用,仅在主项目中)

#### 结果

现在根  `./build.gradle.kts`  可以保持非常干净和整洁 - 它只需要定义项目的组和版本。

因为我们使用的是约定插件而不是一揽子  `subprojects` ,所以每个子项目都可以专门化,并且只导入它需要的约定插件,而无需重复。

* * *

* * *

* * *

#### 站点注释:在  `buildSrc`  和主项目之间共享存储库

通常您希望在  `buildSrc`  和主项目之间共享存储库。因为 Gradle 插件不是专门针对项目的,所以我们可以为任何东西编写插件,包括  `settings.gradle.kts` !

我所做的是创建一个包含我想要使用的所有存储库的文件......

// ./buildSrc/repositories.settings.gradle.kts
@Suppress("UnstableApiUsage") // centralised repository definitions are incubating
dependencyResolutionManagement {

repositories {
mavenCentral()
jitpack()
gradlePluginPortal()
}

pluginManagement {
repositories {
jitpack()
gradlePluginPortal()
mavenCentral()
}
}
}

fun RepositoryHandler.jitpack() {
maven("https://jitpack.io")
}


(名称  `repositories.settings.gradle.kts`  并不重要 - 但将其命名为  `*.settings.gradle.kts`  应该意味着 IntelliJ 提供了建议,但目前这是错误的。)

然后我可以将它作为插件导入其他  `settings.gradle.kts`  文件中,就像您将 Kotlin JVM 插件应用于子项目一样。

// ./buildSrc/settings.gradle.kts
apply(from = "./repositories.settings.gradle.kts")

// ./settings.gradle.kts
apply(from = "./buildSrc/repositories.settings.gradle.kts")


答案:

相关问题