如何避免在Gradle中“将配置添加为依赖项是一种令人困惑的行为,不建议”

qybjjes1  于 2023-04-12  发布在  其他
关注(0)|答案(1)|浏览(172)

我有两个项目。一个有生产者配置:

// 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.
基于this StackOverflow answer,我怀疑这可能是由于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.

我该怎么做才能避免这个弃用警告?

zxlwwiss

zxlwwiss1#

欢迎来到sharing outputs between Gradle subprojects的有趣世界!
解决这个问题有几种不同的方法。
由于您正在使用JVM项目,因此可能需要配置feature variant。Gradle将创建一个新的源集,您可以将文件生成到其中。
但是,如果你没有使用JVM项目,那么事情就会变得有点复杂。我将解释最简单和最直接的方法。请不要笑了,这不是一个笑话。是的,我知道,它仍然很复杂。相信我,这是值得的。
我们最终将以一种强大的方式在项目之间共享文件,这种方式可缓存(与Build CacheConfiguration Cache兼容),灵活,可重用,并能很好地理解Gradle的工作原理。

任务

以下是需要做的事情:

  • 创建buildSrc约定插件
  • 为 * 提供 * 和 * 使用 * 文件设置一些配置
  • 定义一个自定义的variant attribute作为标记,以区分我们的配置和其他配置
  • 将任务生成的文件放入 outgoing 配置
  • 使用 incoming 配置解析来自其他子项目的文件
    **是的,configuration这个名字很容易让人混淆。在这种情况下,'configuration'应该更好地重命名为'DependencyContainer' -它们只是一个文件的集合,可能是子项目的 * 传出 * 或 * 传入 ,沿着一些描述内容的元数据。

创建buildSrc约定插件

我们需要能够在提供和消费子项目中设置我们的提供和消费代码。虽然从技术上讲,我们可以复制粘贴它,但这种方式很糟糕,而且太挑剔了。在Gradle中共享配置的最佳方式是使用约定插件。
我已经在另一个答案(为Gradle子项目配置Kotlin扩展)中介绍了设置约定插件,因此我将在这里总结步骤。
1.为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只需要KotlinDSL插件。这将使编写我们的约定插件更容易,因为预编译脚本插件

// buildSrc/build.gradle.kts

plugins {
  `kotlin-dsl`
}

1.创建我们的约定插件。文件名(.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依赖关系世界的shipping containers
我们将做两个配置,一个用于 * 传入 * 文件,另一个用于 * 传出 * 文件。
请注意isCanBeConsumedisCanBeResolved *。在Gradle术语中,传入的将由子项目“解析”,传出的将由其他子项目“使用”。使用正确的组合很重要。

**同样,我们有一些令人困惑的名称。术语'consumed'和'resolved'不是很清楚,它们在我看来都是同义词。它们最好重新命名,以表明consumed=true && resolved=false表示OUTGOINGconsumed=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 sync之后)。

// my-consumer/build.gradle.kts

plugins {
  id("generated-files-sharing")
}

dependencies {
  generatedFiles(project(":my-generator"))
}

Gradle为generatedFiles配置生成了一个类型安全的访问器。
然而,我们还没有完成。我们还没有将任何文件放入传出配置中,所以当然消费项目将无法解析任何文件。但在此之前,我们需要添加我提到的元数据。

通过变量属性区分我们的配置

很有可能提供生成文件的子项目也有其他配置,它们的文件类型完全不同。但是我们不想用大量的文件填充generatedFiles配置,我们 * 只 * 需要生成任务生成的文件。
这就是变量属性的用武之地。如果配置是运输容器,那么变量属性就是外部的运输标签,描述内容应该发送到哪里。
在基本层面上,变量属性只是键值字符串,除了键必须在Gradle中注册。如果我们使用可以使用的内置标准属性,我们可以跳过注册。Usage属性是一个很好的选择。它非常常用,只要我们选择一个独特的值,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可以很好地匹配传入和传出配置!
现在所有的部分都已经就位了。我们几乎完成了!现在是时候开始将文件推送到配置中,并将文件拉出。

将文件放入 outgoing 配置

在生成生成文件的子项目中,假设我们有一个生成文件的任务,我将使用一个Sync任务作为实际生成器任务的替身。

// 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的Provider API可以将任务转换为文件提供程序。

// my-generator/build.gradle.kts

configurations.generatedFilesProvider.configure { 
  outgoing { 
    artifact(myGeneratorTask.map { it.temporaryDir })
  }
}

这一点的好处是,现在Gradle只会在请求时配置和运行myGeneratorTask任务。当这种configuration avoidance经常使用时,它确实可以帮助加速Gradle构建。

解析传入配置

最后一步了!
在消费项目中,我们可以使用常规的dependencies {}块添加对提供项目的依赖。

// my-consumer/build.gradle.kts

plugins {
  id("generated-files-sharing")
}

dependencies {
  generatedFiles(project(":my-generator"))
}

现在我们可以从传入配置中获取 incoming 文件:

// 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将避免运行任务,因为它们是UP-TO-DATE
您还可以启用Gradle Build Cache./gradlew myConsumerTask --build-cache),即使删除生成的文件(删除./my-generator/build/目录),您也应该看到myGeneratorTaskmyConsumerTask都是从缓存加载的

相关问题