根据提供程序值有条件地更改 Gradle 属性

Conditionally Change a Gradle Property Based on a Provider Value

我正在编写一个 Gradle 约定插件,它使用 Gradle 的 Lazy Configuration APIs 来配置任务。在一种情况下,插件需要有条件地更改 a Property 的值,并且该条件基于 a Provider 的有效值。即如果Provider有一定的值,更新Property的值;否则,保持 Property 不变。

如果没有 Provider 语义,这将是一个简单的逻辑语句,例如:

if (someValue > 10) {
  property.set(someValue)
}

但是,因为 Provider 的值还未知,所以这更复杂。

我天真地尝试了以下操作,但它会导致堆栈溢出错误,因为 属性 的转换器包含对相同 属性.

的检索
// stack overflow error
property.set(provider.map { if (it > 10) it else property.get() })

一个更完整的例子:

val foo = objects.property(String::class).convention("foo")
val bar = objects.property(String::class).convention("bar")

bar.set(foo.map { if (it != "foo") "baz" else bar.get()})


tasks.register("print") {
    // goal is to print "baz", but it is a WhosebugError
    logger.log(LogLevel.LIFECYCLE, bar.get())
}

是否有一个 API 让我可以根据 Provider 的值有条件地更新 Property 的值?

在您的简化示例中,您实际上不需要提供商。

// build.gradle.kts
val foo = objects.property(String::class).convention("foo")
val bar = objects.property(String::class).convention("bar")

tasks.register("print") {
    val evaluatedFoo = if (foo.get() != "foo") "baz" else bar.get()
    logger.lifecycle("evaluatedFoo $evaluatedFoo")
}
// output:
// evaluatedFoo bar

那是因为记录器正在配置阶段工​​作(请参阅下面的 'Gradle Phases Recap')。通常(但不总是)属性和提供程序是为了避免在配置阶段进行计算。

sidenote: property vs provider

The difference is akin to Kotlin's var and val. property should be used for letting a user set a custom value, provider is for read-only values, like environment variables providers.environmentVariable("HOME").

为什么要使用供应商?

因为您正在使用 'register'、the task isn't configured until it's required。所以我会调整你的例子,让事情变得更糟,看看它会变得多么丑陋。

// build.gradle.kts
val foo: Provider<String> = providers.provider {
    // pretend we're doing some heavy work, like an API call
    Thread.sleep(TimeUnit.SECONDS.toMillis(10))
    "foo"
}
val bar = objects.property(String::class).convention("bar")

// change from 'register' to 'create'
tasks.create("print") {
    val evaluatedFoo = if (foo.get() != "foo") "baz" else bar.get()
    logger.lifecycle("evaluatedFoo: $evaluatedFoo")
}

现在每次Gradle加载build.gradle.kts,需要10秒!就算我们不运行一个任务!或者一个不相关的任务!那不行。这是使用提供商的一个很好的理由。

解决方案

这里有几种不同的路径,具体取决于您实际想要实现的目标。

只在执行阶段登录

我们可以将日志语句移动到 doFirst {}doLast {} 块。这些块的内容运行在执行阶段。这就是供应商的目的,将工作推迟到这个阶段。所以我们可以调用.get()来评估它们。

// build.gradle.kts
tasks.create("print") {
    doFirst {
        // now we're in the execution phase, it's okay to crack open the providers
        val evaluatedFoo = if (foo.get() != "foo") "baz" else bar.get()
        logger.lifecycle("evaluatedFoo: $evaluatedFoo")
    }
}

现在即使任务是急切创建的,foobar 也不会在执行时间之前被评估 - 当可以工作时。

合并两个提供商

我认为这个选项更接近你最初的要求。不要递归地将 baz 设置回自身,而是创建一个新的提供者。

当调用 fooBarZipped.get() 时,

Gradle 只会评估 foobar

// build.gradle.kts
val foo = objects.property(String::class).convention("foo")
val bar = objects.property(String::class).convention("bar")

val fooBarZipped: Provider<String> = foo.zip(bar) { fooActual, barActual ->
    if (fooActual != "foo") {
        "baz"
    } else {
        barActual
    }
}

tasks.register("print") {
    logger.lifecycle("fooBarZipped: ${fooBarZipped.get()}")
}

请注意,此 fooBarZipped.get() 也会导致计算 bar,即使它可能不会被使用!在这种情况下,我们可以只使用 map()(这与 Kotlin 的 Collection<T>.map() 扩展函数不同!)

映射提供商

这个有点懒

// build.gradle.kts
val foo = objects.property(String::class).convention("foo")
val bar = objects.property(String::class).convention("bar")

val fooBarMapped: Provider<String> = foo.map { fooActual ->
    if (fooActual != "foo") {
        "baz"
    } else {
        bar.get() // bar will only be evaluated if required
    }
}

tasks.register("print") {
    logger.lifecycle("evaluatedFoo: ${fooBarMapped.get()}")
}

自定义任务

有时在 build.gradle.kts 中定义任务更容易,但通常在 MyPrintTask class 中更明确。这部分是可选的 - 做最适合你的情况。

学习奇怪的 Gradle 创建任务的风格有很多,所以我不会深入研究。但我想说的是 @get:Input 真的很重要。

// buildSrc/main/kotlin/MyPrintTask.kt
package my.project

abstract class MyPrintTask : DefaultTask() {
    @get:Input
    abstract val taskFoo: Property<String>
    @get:Input
    abstract val taskBar: Property<String>

    @get:Internal
    val toBePrinted: Provider<String> = project.provider {
        if (taskFoo.get() != "foo") {
            "baz"
        } else {
            taskBar.get()
        }
    }

    @TaskAction
    fun print() {
        logger.quiet("[PrintTask] ${toBePrinted.get()}")
    }
}

Aside: you can also define inputs with the Kotlin DSL, but it's not quite as fluid

//build.gradle.kts
tasks.register("print") {
    val taskFoo = foo // setting the property here helps Gradle configuration cache
    inputs.property("taskFoo", foo)
    val taskBar = foo
    inputs.property("taskBar", bar)
    
    doFirst {
        val evaluated = if (taskFoo.get() != "foo") {
            "baz"
        } else {
            taskBar.get()
        }
        logger.lifecycle(evaluated)
    }
}

现在您可以在构建脚本中定义任务了。

// build.gradle.kts
val foo = objects.property(String::class).convention("foo")
val bar = objects.property(String::class).convention("bar")

tasks.register<MyPrintTask>("print") {
    taskFoo.set(foo)
    taskBar.set(bar)
}

这里似乎没有太多好处,但是如果 foobar 本身被映射或压缩,或者取决于输出,@get:Input 可能非常重要一个任务。现在 Gradle 会将任务链接在一起,因此即使您只是 运行 gradle :print,它也知道如何触发一整套必要的前置任务!


Gradle 阶段回顾

Gradle has 3 phases

  1. 初始化 - 项目已加载,在 settings.gradle.kts 内。我们现在对此不感兴趣。
  2. 配置 - 这是加载 build.gradle.kts 或定义任务时发生的情况
  3. 执行 - 任务被触发!计算任务 运行、提供程序和属性。
// build.gradle.kts
println("This is executed during the configuration phase.")

tasks.register("configured") {
    println("This is also executed during the configuration phase, because :configured is used in the build.")
}

tasks.register("test") {
    doLast {
        println("This is executed during the execution phase.")
    }
}

tasks.register("testBoth") {
    doFirst {
        println("This is executed first during the execution phase.")
    }
    doLast {
        println("This is executed last during the execution phase.")
    }
    println("This is executed during the configuration phase as well, because :testBoth is used in the build.")
}

任务配置回避

https://docs.gradle.org/current/userguide/task_configuration_avoidance.html

为什么这与提供者和属性相关?因为他们确保两件事

  1. 配置阶段未完成工作

    当Gradle 注册一个任务时,我们不希望它实际上是运行 任务!我们想推迟到需要的时候。

  2. Gradle可以创建一个'directed acyclic graph'

    基本上,Gradle 不是 single-track 生产线,只有一个起点。这是一个 hive-mind 有输入和输出的小工人,并且 Gradle 根据他们从哪里获得输入将工人链接在一起。