我有两个项目。一个具有生产者配置:
// gen/build.gradle.kts
...
val outDir = layout.projectDirectory.dir("output")
val run = tasks.named<JavaExec>("run") {
args = listOf(outDir.asFile.absolutePath)
}
configurations.create("generated") {
isCanBeResolved = false
isCanBeConsumed = true
}
artifacts {
add("generated", outDir) {
builtBy(run)
}
}
然后,根项目具有在应用程序中使用的使用者配置:
// build.gradle.kts
...
val generated by configurations.creating<Configuration> {
isCanBeResolved = true
isCanBeConsumed = false
}
dependencies {
generated(project(mapOf(
"path" to ":gen",
"configuration" to "generated",
)))
api(generated)
}
sourceSets {
main {
kotlin {
srcDir(generated.files)
}
}
}
如您所见,我目前正在使用dependencies
来声明:generated
配置取决于:gen:generated
配置。这就是文档当前建议声明此依赖项的方式。
但是,它会触发弃用警告:Adding a Configuration as a dependency is a confusing behavior which isn't recommended. This behaviour has been deprecated and is scheduled to be removed in Gradle 8.0. If you're interested in inheriting the dependencies from the Configuration you are adding, you should use Configuration#extendsFrom instead. See https://docs.gradle.org/7.4.2/dsl/org.gradle.api.artifacts.Configuration.html#org.gradle.api.artifacts.Configuration:extendsFrom(org.gradle.api.artifacts.Configuration[]) for more details.
所以我尝试改用extendsFrom
:
// In build.gradle.kts
val generated by configurations.creating<Configuration> {
isCanBeResolved = true
isCanBeConsumed = false
extendsFrom(project(":gen").configurations.getByName("generated"))
}
dependencies {
api(generated)
}
并得到:Configuration with name 'generated' not found.
基于这个 StackOverflow 答案,我怀疑这可能是由于gen
项目的配置阶段尚未发生引起的。所以我尝试:
// In build.gradle.kts
val generated by configurations.creating<Configuration> {
isCanBeResolved = true
isCanBeConsumed = false
project(":gen").afterEvaluate {
extendsFrom(project(":gen").configurations.getByName("generated"))
}
}
但现在看来,宣布继承为时已晚:
A problem occurred configuring project ':gen'.
> Cannot change dependencies of dependency configuration ':generated' after it has been resolved.
我应该怎么做才能避免此弃用警告?
欢迎来到在 Gradle 子项目之间共享输出的有趣世界!
有几种不同的方法可以解决这个问题。
由于您正在使用 JVM 项目,因此您可能希望配置功能变体。然后,Gradle 将创建一个新的源代码集,您可以将文件生成到其中。
但是,如果您不使用JVM项目,那么事情就会变得有点复杂。我将解释最简单,最直接的方法。请别笑了,这不是开玩笑。是的,我知道,它仍然很复杂。相信我,这是值得的。
我们最终会得到一种强大的方式,以可缓存(与构建缓存和配置缓存兼容)、灵活、可重用的方式在项目之间共享文件,并很好地理解 Gradle 的工作原理。
任务
以下是需要完成的工作摘要:
- 创建一个构建Src约定插件
- 设置一些配置*以提供和使用文件 将
- 自定义变体属性定义为标记,以将我们的配置与其他配置区分开来
- 将任务生成的文件放入传出配置
- 使用传入配置解析其他子项目中的文件
*是的,名称配置令人困惑。在这种情况下,"配置"应该更好地重命名为"DependencyContainer" - 它们只是可能传出或传入子项目的文件的集合,以及一些描述内容的元数据。
创建构建 Src 约定插件
我们需要能够在提供和使用子项目中设置提供和使用代码。虽然从技术上讲,我们可以复制粘贴它,但这种很糟糕,而且太挑剔了。在 Gradle 中共享配置的最佳方式是使用约定插件。
我已经在另一个答案中介绍了设置约定插件(为 Gradle 子项目配置 Kotlin 扩展),所以我在这里总结一下这些步骤。
-
为 buildSrc 创建构建配置。
由于 buildSrc 实际上是一个独立的项目,因此最好创建一个
settings.gradle.kts
文件。我喜欢使用集中式存储库声明。// buildSrc/settings.gradle.kts rootProject.name = "buildSrc" pluginManagement { repositories { mavenCentral() gradlePluginPortal() } } @Suppress("UnstableApiUsage") dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) repositories { mavenCentral() gradlePluginPortal() } }
build.gradle.kts
只需要 Kotlin DSL 插件。这将使编写我们的约定插件作为预编译脚本插件变得更加容易// buildSrc/build.gradle.kts plugins { `kotlin-dsl` }
-
创建我们的约定插件。文件名(
.gradle.kts
之前的所有内容)和package
(如果定义了)将是插件 ID// buildSrc/src/main/kotlin/generated-files-sharing.gradle.kts logger.lifecycle("I don't do anything yet...")
做!您可以通过将此无所事事的插件应用于您的子项目来测试它。
// my-generator/build.gradle.kts
plugins {
id("generated-files-sharing")
}
// my-consumer/build.gradle.kts
plugins {
id("generated-files-sharing")
}
当您运行任务时(例如./gradlew help
),您应该会看到消息I don't do anything yet...
记录到控制台。
创建用于提供和使用文件的配置
下一步是创建一些配置,这些配置是 Gradle 依赖项世界的运输容器。
我们将进行两种配置,一种用于传入文件,另一种用于传出文件。
记下isCanBeConsumed
和isCanBeResolved
*。在 Gradle 术语中,传入的将由子项目解决,传出的将由其他子项目使用。使用正确的组合很重要。
*同样,我们有一些令人困惑的名字。术语"消费"和"已解决"不是很清楚,它们对我来说都是同义词。最好重命名它们以表明consumed=true && resolved=false
表示OUTGOING
,而consumed=false && resolved=true
表示INCOMING
// buildSrc/src/main/kotlin/generated-files-sharing.gradle.kts
// register the incoming configuration
val generatedFiles by configurations.registering {
description = "consumes generated files from other subprojects"
// the dependencies in this configuration will be resolved by this subproject...
isCanBeResolved = true
// and so they won't be consumed by other subprojects
isCanBeConsumed = false
}
// register the outgoing configuration
val generatedFilesProvider by configurations.registering {
description = "provides generated files to other subprojects"
// the dependencies in this configuration won't be resolved by this subproject...
isCanBeResolved = false
// but they will be consumed by other subprojects
isCanBeConsumed = true
}
这一步很酷的地方在于,如果您现在跳转到子项目,则可以使用这些配置,就像它们内置到 Gradle 中一样(如有必要,在 IDE 同步之后)。
// my-consumer/build.gradle.kts
plugins {
id("generated-files-sharing")
}
dependencies {
generatedFiles(project(":my-generator"))
}
Gradle 为generatedFiles
配置生成了一个类型安全的访问器。
但是,我们还没有完成。我们尚未将任何文件放入传出配置中,因此使用项目当然无法解析任何文件。但在我们这样做之前,我们需要添加我提到的元数据。
使用变体属性区分我们的配置
提供生成的文件的子项目很可能还具有具有完全不同的文件类型的其他配置。但是我们不想用大量文件填充generatedFiles
配置,我们只希望生成任务生成的文件。
这就是变体属性的用武之地。如果配置是装运集装箱,则变型属性是外部的装运标签,用于描述内容应发送到的位置。
在基本层面上,变体属性只是键值字符串,只是键必须在 Gradle 中注册。不过,如果我们使用可以使用的内置标准属性,我们可以跳过该注册。"用法"属性是一个不错的选择。这是非常常用的,只要我们选择一个独特的值,Gradle 就能够通过比较值来区分两种配置。
// buildSrc/src/main/kotlin/generated-files-sharing.gradle.kts
// create a custom Usage attribute value, with a distinctive value
val generatedFilesUsageAttribute: Usage =
objects.named<Usage>("my.library.generated-files")
val generatedFiles by configurations.registering {
description = "consumes generated files from other subprojects"
isCanBeResolved = true
isCanBeConsumed = false
// add the attribute to the incoming configuration
attributes {
attribute(Usage.USAGE_ATTRIBUTE, generatedFilesUsageAttribute)
}
}
val generatedFilesProvider by configurations.registering {
description = "provides generated files to other subprojects"
isCanBeResolved = false
isCanBeConsumed = true
// also add the attribute to the outgoing configuration
attributes {
attribute(Usage.USAGE_ATTRIBUTE, generatedFilesUsageAttribute)
}
}
重要的是,将相同的属性键和值添加到两个配置中。现在,Gradle 可以愉快地匹配传入和传出配置!
现在所有部件都已到位。我们快完成了!是时候开始将文件推送到配置中,并将文件拉出了。
将文件放入传出配置
在生成生成的文件的子项目中,假设我们有一些生成一些文件的任务。我将使用同步任务作为实际生成器任务的替身。
// my-generator/build.gradle.kts
plugins {
id("generated-files-sharing")
}
val myGeneratorTask by tasks.registering(Sync::class) {
from(resources.text.fromString("hello, world!"))
into(temporaryDir)
}
请注意,输出目录并不重要,因为借助 Gradle 的提供程序 API,可以将任务转换为文件提供程序。
// my-generator/build.gradle.kts
configurations.generatedFilesProvider.configure {
outgoing {
artifact(myGeneratorTask.map { it.temporaryDir })
}
}
这样做的好处是,现在 Gradle只会在请求时配置和运行myGeneratorTask
任务。当经常使用这种配置避免时,它确实可以帮助加快 Gradle 的构建速度。
解析传入配置
我们正处于最后一步!
在消费项目中,我们可以使用常规dependencies {}
块添加对提供项目的依赖。
// my-consumer/build.gradle.kts
plugins {
id("generated-files-sharing")
}
dependencies {
generatedFiles(project(":my-generator"))
}
现在我们可以从传入配置中获取传入文件:
// my-consumer/build.gradle.kts
val myConsumerTask by tasks.registering(Sync::class) {
from(configurations.generatedFiles.map { it.incoming.artifacts.artifactFiles })
into(temporaryDir)
}
现在,如果您运行./gradlew myConsumerTask
,您会注意到,即使您没有明确设置任何任务依赖项,Gradle 也会自动运行myGeneratorTask
。
如果你检查myConsumerTask
的临时目录(./my-consumer/build/tmp/myConsumerTask/
)的内容,你会看到生成的文件。
如果您重新运行相同的命令,那么您应该会看到 Gradle 将避免运行这些任务,因为它们是最新的。
您还可以启用 Gradle 构建缓存 (./gradlew myConsumerTask --build-cache
),即使您删除生成的文件(删除./my-generator/build/
目录),您也应该看到myGeneratorTask
和myConsumerTask
都是从缓存加载的