diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginLifecycle.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginLifecycle.kt index 89fe02aabb2..1935adc1c3e 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginLifecycle.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginLifecycle.kt @@ -426,7 +426,7 @@ private class KotlinPluginLifecycleImpl(override val project: Project) : KotlinP enqueuedActions.getValue(stage).addLast(action) - if (stage == Stage.Configure) { + if (stage == Stage.Configure || isFinished.get()) { loopIfNecessary() } } @@ -434,7 +434,6 @@ private class KotlinPluginLifecycleImpl(override val project: Project) : KotlinP override fun launch(block: suspend KotlinPluginLifecycle.() -> Unit) { val lifecycle = this check(isStarted.get()) { "Cannot launch when ${KotlinPluginLifecycle::class.simpleName} is not started" } - check(!isFinished.get()) { "Cannot launch when ${KotlinPluginLifecycle::class.simpleName} is already finished" } val coroutine = block.createCoroutine(this, object : Continuation { override val context: CoroutineContext = EmptyCoroutineContext + @@ -475,6 +474,7 @@ private class KotlinPluginLifecycleImpl(override val project: Project) : KotlinP override val finaliseIn: Stage, override val property: Property ) : LifecycleAwareProperty { + override suspend fun awaitFinalValue(): T? { await(finaliseIn) return property.orNull diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/Future.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/Future.kt new file mode 100644 index 00000000000..f3191bbb8ed --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/Future.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.gradle.utils + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.completeWith +import org.gradle.api.Project +import org.jetbrains.kotlin.gradle.plugin.HasProject +import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle +import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.IllegalLifecycleException +import org.jetbrains.kotlin.gradle.plugin.kotlinPluginLifecycle +import org.jetbrains.kotlin.gradle.plugin.launch +import org.jetbrains.kotlin.tooling.core.ExtrasLazyProperty +import org.jetbrains.kotlin.tooling.core.HasMutableExtras +import org.jetbrains.kotlin.tooling.core.extrasLazyProperty + +/** + * See [KotlinPluginLifecycle]: + * This [Future] represents a value that will be available in some 'future' time. + * + * + * #### Simple use case example: Deferring the value of a property to a given [KotlinPluginLifecycle.Stage]: + * + * ```kotlin + * val myFutureProperty: Future = project.future { + * await(FinaliseDsl) // <- suspends + * 42 + * } + * ``` + * + * Futures can also be used to dynamically extend entities implementing [HasMutableExtras] and [HasProject] + * #### Example Usage: Extending KotlinSourceSet with a property that relies on the final refines edges. + * + * ```kotlin + * internal val KotlinSourceSet.dependsOnCommonMain: Future by lazyFuture("dependsOnCommonMain") { + * await(AfterFinaliseRefinesEdges) + * return dependsOn.contains { it.name == "commonMain") } + * } + * ``` + */ +internal interface Future { + suspend fun await(): T + fun getOrThrow(): T +} + +internal inline fun lazyFuture( + name: String? = null, noinline block: suspend Receiver.() -> T +): ExtrasLazyProperty> where Receiver : HasMutableExtras, Receiver : HasProject { + return extrasLazyProperty>(name) { + project.future { block() } + } +} + +@OptIn(ExperimentalCoroutinesApi::class) +internal fun Project.future(block: suspend Project.() -> T): Future { + val deferred = CompletableDeferred() + + launch { + deferred.completeWith(runCatching { block() }) + } + + return object : Future { + override suspend fun await(): T { + return deferred.await() + } + + override fun getOrThrow(): T { + return if (deferred.isCompleted) deferred.getCompleted() else throw IllegalLifecycleException( + "Future was not completed yet. Stage: '${kotlinPluginLifecycle.stage}'" + ) + } + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/FutureTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/FutureTest.kt new file mode 100644 index 00000000000..4c9110d8c43 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/FutureTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +@file:Suppress("FunctionName") + +package org.jetbrains.kotlin.gradle.unitTests + +import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.IllegalLifecycleException +import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.Stage.FinaliseDsl +import org.jetbrains.kotlin.gradle.plugin.await +import org.jetbrains.kotlin.gradle.plugin.currentKotlinPluginLifecycle +import org.jetbrains.kotlin.gradle.plugin.startKotlinPluginLifecycle +import org.jetbrains.kotlin.gradle.util.buildProject +import org.jetbrains.kotlin.gradle.util.runLifecycleAwareTest +import org.jetbrains.kotlin.gradle.utils.future +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class FutureTest { + + private val project = buildProject().also { project -> + project.startKotlinPluginLifecycle() + } + + @Test + fun `test - simple deferred future`() = project.runLifecycleAwareTest { + val future = project.future { + await(FinaliseDsl) + 42 + } + + assertFailsWith { future.getOrThrow() } + assertEquals(42, future.await()) + assertEquals(42, future.getOrThrow()) + assertEquals(FinaliseDsl, currentKotlinPluginLifecycle().stage) + } + + @Test + fun `test - future depending on another future`() = project.runLifecycleAwareTest { + val futureA = project.future { + await(FinaliseDsl) + 42 + } + + val futureB = project.future { + futureA.await().toString() + } + + assertEquals("42", futureB.await()) + assertEquals("42", futureB.getOrThrow()) + assertEquals(FinaliseDsl, currentKotlinPluginLifecycle().stage) + } + + @Test + fun `test - after lifecycle finished`() { + val project = buildProject() + project.startKotlinPluginLifecycle() + project.evaluate() + + val future = project.future { + await(FinaliseDsl) + 42 + } + + assertEquals(42, future.getOrThrow()) + } +} \ No newline at end of file