根据提供程序值有条件地更改 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")
}
}
现在即使任务是急切创建的,foo
和 bar
也不会在执行时间之前被评估 - 当可以工作时。
合并两个提供商
我认为这个选项更接近你最初的要求。不要递归地将 baz
设置回自身,而是创建一个新的提供者。
当调用 fooBarZipped.get()
时,Gradle 只会评估 foo
和 bar
。
// 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)
}
这里似乎没有太多好处,但是如果 foo
或 bar
本身被映射或压缩,或者取决于输出,@get:Input
可能非常重要一个任务。现在 Gradle 会将任务链接在一起,因此即使您只是 运行 gradle :print
,它也知道如何触发一整套必要的前置任务!
Gradle 阶段回顾
- 初始化 - 项目已加载,在
settings.gradle.kts
内。我们现在对此不感兴趣。
- 配置 - 这是加载
build.gradle.kts
或定义任务时发生的情况
- 执行 - 任务被触发!计算任务 运行、提供程序和属性。
// 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
为什么这与提供者和属性相关?因为他们确保两件事
配置阶段未完成工作
当Gradle 注册一个任务时,我们不希望它实际上是运行 任务!我们想推迟到需要的时候。
Gradle可以创建一个'directed acyclic graph'
基本上,Gradle 不是 single-track 生产线,只有一个起点。这是一个 hive-mind 有输入和输出的小工人,并且 Gradle 根据他们从哪里获得输入将工人链接在一起。
我正在编写一个 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
vsprovider
The difference is akin to Kotlin's
var
andval
.property
should be used for letting a user set a custom value,provider
is for read-only values, like environment variablesproviders.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")
}
}
现在即使任务是急切创建的,foo
和 bar
也不会在执行时间之前被评估 - 当可以工作时。
合并两个提供商
我认为这个选项更接近你最初的要求。不要递归地将 baz
设置回自身,而是创建一个新的提供者。
fooBarZipped.get()
时,Gradle 只会评估 foo
和 bar
。
// 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)
}
这里似乎没有太多好处,但是如果 foo
或 bar
本身被映射或压缩,或者取决于输出,@get:Input
可能非常重要一个任务。现在 Gradle 会将任务链接在一起,因此即使您只是 运行 gradle :print
,它也知道如何触发一整套必要的前置任务!
Gradle 阶段回顾
- 初始化 - 项目已加载,在
settings.gradle.kts
内。我们现在对此不感兴趣。 - 配置 - 这是加载
build.gradle.kts
或定义任务时发生的情况 - 执行 - 任务被触发!计算任务 运行、提供程序和属性。
// 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
为什么这与提供者和属性相关?因为他们确保两件事
配置阶段未完成工作
当Gradle 注册一个任务时,我们不希望它实际上是运行 任务!我们想推迟到需要的时候。
Gradle可以创建一个'directed acyclic graph'
基本上,Gradle 不是 single-track 生产线,只有一个起点。这是一个 hive-mind 有输入和输出的小工人,并且 Gradle 根据他们从哪里获得输入将工人链接在一起。