我正在为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))
        }
    }
}

try 设置工具链会导致构建在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

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

推荐答案

这是我最喜欢在Gradle中解决的问题之一,它展示了可能的灵活性(同时也展示了为什么Gradle会很复杂!

首先,我将给出一些关于subprojects {} DSL的背景信息,然后我将展示如何修复您的脚本,最后我将展示与buildSrc约定插件共享构建逻辑的最佳方式.(即使这是最后一次,我还是建议使用buildSrc!)

组合与继承

使用allprojects {}subprojects {}真的很常见,我经常看到.它更类似于Maven的工作方式,所有配置都在"父"构建文件中定义.然而,Gradle并不推荐它.

[A] 不鼓励在子项目之间共享构建逻辑的方法是通过subprojects {}allprojects {} DSL构造进行跨项目配置.

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插件,定义约定,我们可以有 Select 地将其应用于"主"构建中的任何子项目.

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

Gradle文档有a full guide on setting up convention plugins个,所以这里我只简单总结一下解决方案.

1. Set up ./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 "1.5.31" 
  // yes, use 1.5.31. It's a long story, but Gradle 7.4 uses an embedded version of Kotlin. 
  // It's annoying but not important. The Kotlin plugin version below, in dependencies { }, 
  // will be used for 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 repository个坐标,而不是插件ID!

如果愿意,还可以在./buildSrc/build.gradle.ts中添加其他依赖项.如果您想在构建脚本中解析JSON,那么可以在JSON解析器上添加一个依赖项,比如kotlinx-serialization.

2. Create a convention plugins

创建可以应用于任何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. Applying the convention plugin

就像Gradle插件有ID一样,我们的常规插件也有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插件,比如javajava-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内部不起作用,只在主项目中起作用)

Result

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

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




Site note: sharing repositories between buildSrc and the main project

通常,您希望在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")

Kotlin相关问答推荐

如何在Kotlin中模拟www.example.com()?

Kotlin-删除按钮周围的空格

Kotlin中是否可以混合使用推断和显式的通用类型参数?

在 Kotlin 中,为什么在 `+` 之前但在 `.` 之前没有换行符?

Koin Android:org.koin.error.NoBeanDefFoundException

将 Gradle 子元素与 Kotlin 多平台一起使用

取消信号上的kotlin流量采集

Kotlin中的测试无法访问受保护(protected)的方法

用mockk验证属性设置程序吗?

如何从kotlin中的类实例化对象

如何获取Kotlin中变量的名称?

未解决的参考 dagger 2 + kotlin + android gradle

Kotlin 中更好的回调方法是什么?侦听器与高阶函数

Kotlin 与 C# 中的标志枚举变量具有相似效果的方式是什么

可以在函数参数中使用解构吗?

如何在Kotlin中使用Handler和handleMessage?

在多平台子元素中使用kapt

用 kotlin 学习 Android MVVM 架构组件

带有注释为@RegisterExtension的字段的 JUnit 5 测试在 Kotlin 中不起作用

RxJava - 每秒发出一个 observable