'Conditionally Change a Gradle Property Based on a Provider Value
I'm writing a Gradle convention plugin that uses Gradle's Lazy Configuration APIs to configure tasks. In one case, the plugin needs to conditionally change the value of a Property
, and that condition is based on the effective value of a Provider
. That is, if the Provider
has a certain value, update the value of the Property
; else, leave the Property
as-is.
If not for the Provider
semantics, this would be a simple logic statement like:
if (someValue > 10) {
property.set(someValue)
}
but, because the Provider
's value is not-yet-known, this is more complicated.
I naively tried the following, but it results in a stack overflow error, because the transformer for the property includes a retrieval of that same property.
// stack overflow error
property.set(provider.map { if (it > 10) it else property.get() })
A more complete example:
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 StackOverflowError
logger.log(LogLevel.LIFECYCLE, bar.get())
}
Is there an API I'm missing that would allow me to conditionally update the value of a Property
based on the value of a Provider
?
Solution 1:[1]
In your simplified example, you don't actually need providers.
// 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
That's because the logger is working in the configuration phase (see 'Gradle Phases Recap' below). Usually (but not always) properties and providers are to avoid computation during the configuration phase.
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")
.
Why use providers?
Because you're using 'register', the task isn't configured until it's required. So I'll tweak your example to make things a bit worse, to see how ugly it gets.
// 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")
}
Now every time Gradle loads build.gradle.kts
, it takes 10 seconds! Even if we don't run a task! Or an unrelated task! That won't do. This is one good justification for using providers.
Solutions
There are a few different paths here, depending on what you actually want to achieve.
Only log in the execution phase
We can move the log statement to a doFirst {}
or doLast {}
block. The contents of these blocks run in the execution phase. That's the point of the providers, to delay work until this phase. So we can call .get()
to evaluate them.
// 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")
}
}
Now even though the task is eagerly created, foo
and bar
won't be evaluated until execution time - when it's okay to do work.
Combine two providers
I think this option is closer to what you were originally asking. Instead of recursively setting baz
back into itself, create a new provider.
Gradle will only evaluate foo
and bar
when fooBarZipped.get()
is called.
// 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()}")
}
Note that this fooBarZipped.get()
will also cause bar
to be evaluated, even though it might not be used! In this case we can just use map()
(which is different to Kotlin's Collection<T>.map()
extension function!)
Mapping providers
This one is a little more lazy.
// 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()}")
}
Custom task
Sometimes it's easier to define tasks in a build.gradle.kts
, but often it's more clear to specifically make a MyPrintTask
class. This part is optional - do whatever is best for your situation.
It's a lot to learn the strange Gradle style of creating tasks, so I won't dive into it all. But the point I want to make is that @get:Input
is really important.
// 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) } }
Now you can define the task in the build script.
// 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)
}
It doesn't seem like there's much benefit here, but @get:Input
can be really important if foo
or bar
are themselves mapped or zipped, or depend on the output of a task. Now Gradle will chain tasks together, so even if you just run gradle :print
, it knows how to trigger a whole cascade of necessary precursor tasks!
Gradle phases recap
- Initialization - projects are loaded, inside
settings.gradle.kts
. That's not interesting for us right now. - Configuration - this is what happens when loading
build.gradle.kts
, or defining a task - Execution - a task is triggered! Tasks run, providers and properties are computed.
// 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.")
}
Task Configuration Avoidance
https://docs.gradle.org/current/userguide/task_configuration_avoidance.html
Why is this relevant to providers and properties? Because they ensure 2 things
work is not done during the configuration phase
When Gradle registers a task, we don't want it to actually run the task! we want to delay that until it's needed.
Gradle can create a 'directed acyclic graph'
Basically, Gradle isn't a single-track production line, with a single starting point. It's a hive-mind of little workers that have input and output, and Gradle chains the workers together based on where they get the input from.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | aSemy |