Gradle 5.0-rc1 and afterEvaluate

(Ingo Kegel) #1

In Gradle 5.0-rc1, the following build file

tasks {
    val a by creating {
        afterEvaluate {
            println("Hello")
        }
    }
}

fails with the error message

> Could not create task ':a'.
   > Project#afterEvaluate(Action) on root project 'gradle5bug' cannot be executed 
     in the current context.

I also get such an error when calling afterEvaluate in the apply method of a custom plugin.

In 4.10, this restriction was only present when registering tasks. Is this a bug or a design change (I hope not :pray:)?

(Sterling Greene) #2

This is a change in behavior because creating is now registering tasks.

What are you trying to do with afterEvaluate?

(Ingo Kegel) #3

Thanks for your reply.

What are you trying to do with afterEvaluate ?

I’m setting properties on tasks that depend on properties from other tasks. Mostly I use gradle.projectsEvaluated to pull in property values from other projects, which also does not work anymore with this change. I actually do that quite a lot, and this change will cause me to extract these closures from the creating closure which will make the code a lot less readable and cause me a lot of pain to migrate.

Why is creating now registering tasks when registering is supposed to do that? I would prefer that the closure is executed immediately during evaluation. I thought that was the point of having both createing and registering.

(Sterling Greene) #4

OK, afterEvaluate and projectsEvaluated can be a brittle way of doing what you need.

You should be able to replace where you’re using afterEvaluate with the Provider API. This lets you wire to things together without having to wait for one or the other to be set first.

Let’s say you have this:

class Producer extends DefaultTask {
    @OutputFile File outputFile
    // other properties and action
}
class Consumer extends DefaultTask {
    @InputFile File inputFile
    // other properties and action
}

To make this work with afterEvaluate, you may be doing something like:

task producer(type: Producer) 

task consumer(type: Consumer) {
    dependsOn producer
    afterEvaluate {
        inputFile = producer.outputFile
    }
    // other properties
}

producer {
    outputFile = file("build/producer")
    // other properties
}

This will seem to work most of the time, but something could come along and change outputFile in an afterEvaluate itself. There are other similar ways to get the ordering not quite right even when using afterEvaluate.

We’re working on some changes to make this easier and clearer, but the idea is to use Provider and Propertys instead.

The earlier tasks change:

class Producer extends DefaultTask {
    @OutputFile final RegularFileProperty outputFile = project.objects.fileProperty()
    // other properties and action
}
class Consumer extends DefaultTask {
    @InputFile final RegularFileProperty inputFile = project.objects.fileProperty()
    // other properties and action
}

To wire things together:

task producer(type: Producer) 

task consumer(type: Consumer) {
    inputFile = producer.outputFile
    // other properties
}

producer {
    outputFile = layout.buildDirectory.file("producer")
    // other properties
}

This is using the eager task APIs, but it’s similar with the other API and Kotlin:

tasks {
    val producer = register("producer", Producer::class)
    val consumer = register("consumer", Consumer::class) {
        inputFile = producer.get().outputFile
        // other properties
    }
    // somewhere else
    named("producer", Producer::class) {
        outputFile = project.layout.buildDirectory.file("producer")
        // other properties
    }
}

(Apologies if I’ve typoed something above, I just typed this out without an editor)

If some of the producer tasks that you’re wiring together don’t expose Provider or Property, you can also use project.provider {} to create a Provider that’ll lazily provide the value.

What sort of properties do you need to access across project boundaries?

@eskatos want to weigh in here? I would have expected creating to still use create, but I know that we wanted to use the new APIs from the start with Kotlin DSL.

You can still reach the eager APIs via create(...).

(Ingo Kegel) #5

Thanks for taking the time to explain this, I really appreciate it. I was aware of the Provider API and I do use in my custom tasks. I use gradle.projectsEvaluate in situations without custom tasks where there are only two parties involved.

What sort of properties do you need to access across project boundaries?

For example, a use case for using gradle.projectsEvaluate across projects is having an obfuscation task that collects archivePath properties from Jar tasks in other projects.

I did some experimentation and it seems that when you write

val a: Jar by creating(Jar::class) {
    println("inside creating")
}

the closure is executed as soon as you access any member of the decorated Jar task, even something like a.javaClass.

but I know that we wanted to use the new APIs from the start with Kotlin DSL.

I’m concerned that the above way of creating a task has a different execution semantics than

val a = create<Jar>("a") {
    println("inside creating")
}

where the closure is indeed executed immediately. The latter way of writing things is not nice because I have to specify the task name name twice.

(Ingo Kegel) #6

If anybody else has the same problem, I’m now using different functions instead of the versions of creating, getting and named that take a configuration closure. If you add the below code to your buildSrc, you can use instantiating, configuring and configure to get eager evaluation like in Gradle 4.10:

import org.gradle.api.NamedDomainObjectCollection
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.PolymorphicDomainObjectContainer
import org.gradle.kotlin.dsl.*
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KClass
import kotlin.reflect.KProperty

fun <T : Any, U : T> PolymorphicDomainObjectContainer<T>.instantiating(type: KClass<U>, configuration: (U.() -> Unit)? = null) =
    PolymorphicInstantiatingDelegateProvider(this, type.java, configuration)

class PolymorphicInstantiatingDelegateProvider<T : Any, U : T>(
    val container: PolymorphicDomainObjectContainer<T>,
    val type: Class<U>,
    val configuration: (U.() -> Unit)? = null
) {
    operator fun provideDelegate(thisRef: Any?, property: KProperty<*>) = object : ReadOnlyProperty<Any?, U> {
        val delegate = when (configuration) {
            null -> container.create(property.name, type)
            else -> container.create(property.name, type, configuration)
        }

        override operator fun getValue(thisRef: Any?, property: KProperty<*>): U = delegate
    }
}

fun <T : Any> NamedDomainObjectContainer<T>.instantiating(configuration: T.() -> Unit) =
    InstantiatingDelegateProvider(this, configuration)

class InstantiatingDelegateProvider<T : Any>(
    val container: NamedDomainObjectContainer<T>,
    val configuration: (T.() -> Unit)? = null
) {
    operator fun provideDelegate(thisRef: Any?, property: KProperty<*>) = object : ReadOnlyProperty<Any?, T> {
        val delegate = when (configuration) {
            null -> container.create(property.name)
            else -> container.create(property.name, configuration)
        }

        override operator fun getValue(thisRef: Any?, property: KProperty<*>): T = delegate
    }
}

fun <T : Any, U : T> PolymorphicDomainObjectContainer<T>.configuring(type: KClass<U>, configuration: (U.() -> Unit)? = null) =
    PolymorphicConfiguringDelegateProvider(this, type, configuration)

class PolymorphicConfiguringDelegateProvider<T : Any, U : T>(
    val container: NamedDomainObjectContainer<T>,
    val type: KClass<U>,
    val configuration: (U.() -> Unit)? = null
) {
    operator fun provideDelegate(thisRef: Any?, property: KProperty<*>) = object : ReadOnlyProperty<Any?, U> {
        val delegate = container.named(property.name, type).get().also {
            configuration?.invoke(it)
        }

        override operator fun getValue(thisRef: Any?, property: KProperty<*>): U = delegate
    }
}

inline fun <reified T : Any> NamedDomainObjectCollection<out Any>.configure(name: String, noinline configuration: T.() -> Unit): NamedDomainObjectProvider<T> =
    named<T>(name).apply {
        configuration(get())
    }
(Rodrigo B. de Oliveira) #7

Hey @sterling,

Exactly, the rationale was that the Kotlin DSL sugar should be optimised for and always prefer the configuration avoidance APIs. The distinction between creating/getting and registering/existing is in the return type only.

(Paul Merlin) #8

Thank you for your feedback @ingokegel. To close the loop on getting/creating, 5.0-RC3 will make them eager, for the sake of least astonishment, see gradle/kotlin-dsl#1246 and gradle/kotlin-dsl#1247.

1 Like
(Ingo Kegel) #9

That’s great, thank you!