[Gradle] KotlinPluginLifecycle: Implement CoroutineStart and start futures 'Undispatched' by default
This API was inspired by kotlinx.coroutines and shall help making Kotlin Gradle Plugins 'Future' type more applicable for situations when the Future is used inside the Configuration Phase ^KT-60158 In Progress
This commit is contained in:
committed by
Space Team
parent
2d43476811
commit
42d648f775
Generated
+2
-1
@@ -7,6 +7,7 @@
|
||||
<w>instantiator</w>
|
||||
<w>interops</w>
|
||||
<w>klibrary</w>
|
||||
<w>undispatched</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
</component>
|
||||
|
||||
+21
-3
@@ -58,8 +58,11 @@ Util functions
|
||||
*
|
||||
* If the lifecycle already finished and Gradle moved to its execution phase, then the block will be invoked right away.
|
||||
*/
|
||||
internal fun Project.launch(block: suspend KotlinPluginLifecycle.() -> Unit) {
|
||||
kotlinPluginLifecycle.launch(block)
|
||||
internal fun Project.launch(
|
||||
start: CoroutineStart = CoroutineStart.Default,
|
||||
block: suspend KotlinPluginLifecycle.() -> Unit,
|
||||
) {
|
||||
kotlinPluginLifecycle.launch(start, block)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,6 +292,18 @@ internal interface KotlinPluginLifecycle {
|
||||
}
|
||||
}
|
||||
|
||||
enum class CoroutineStart {
|
||||
/**
|
||||
* Puts a coroutine at the end of the current execution queue
|
||||
*/
|
||||
Default,
|
||||
|
||||
/**
|
||||
* Immediately executes the coroutine until its first suspension point in the current thread
|
||||
*/
|
||||
Undispatched
|
||||
}
|
||||
|
||||
sealed class ProjectConfigurationResult {
|
||||
object Success : ProjectConfigurationResult()
|
||||
data class Failure(val failures: List<Throwable>) : ProjectConfigurationResult()
|
||||
@@ -302,7 +317,10 @@ internal interface KotlinPluginLifecycle {
|
||||
|
||||
val isFinished: Boolean
|
||||
|
||||
fun launch(block: suspend KotlinPluginLifecycle.() -> Unit)
|
||||
fun launch(
|
||||
start: CoroutineStart = CoroutineStart.Default,
|
||||
block: suspend KotlinPluginLifecycle.() -> Unit,
|
||||
)
|
||||
|
||||
suspend fun await(stage: Stage)
|
||||
|
||||
|
||||
+9
-3
@@ -6,6 +6,8 @@
|
||||
package org.jetbrains.kotlin.gradle.plugin
|
||||
|
||||
import org.gradle.api.Project
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.CoroutineStart.Default
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.CoroutineStart.Undispatched
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.ProjectConfigurationResult
|
||||
import org.jetbrains.kotlin.gradle.utils.CompletableFuture
|
||||
import org.jetbrains.kotlin.gradle.utils.failures
|
||||
@@ -151,7 +153,10 @@ internal class KotlinPluginLifecycleImpl(override val project: Project) : Kotlin
|
||||
}
|
||||
}
|
||||
|
||||
override fun launch(block: suspend KotlinPluginLifecycle.() -> Unit) {
|
||||
override fun launch(
|
||||
start: KotlinPluginLifecycle.CoroutineStart,
|
||||
block: suspend KotlinPluginLifecycle.() -> Unit,
|
||||
) {
|
||||
val lifecycle = this
|
||||
|
||||
val coroutine = block.createCoroutine(this, object : Continuation<Unit> {
|
||||
@@ -161,8 +166,9 @@ internal class KotlinPluginLifecycleImpl(override val project: Project) : Kotlin
|
||||
override fun resumeWith(result: Result<Unit>) = result.getOrThrow()
|
||||
})
|
||||
|
||||
enqueue(stage) {
|
||||
coroutine.resume(Unit)
|
||||
when (start) {
|
||||
Default -> enqueue(stage) { coroutine.resume(Unit) }
|
||||
Undispatched -> coroutine.resume(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+20
-6
@@ -10,6 +10,7 @@ import org.gradle.api.file.FileCollection
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
|
||||
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.Stage.AfterEvaluateBuildscript
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
|
||||
import org.jetbrains.kotlin.gradle.plugin.await
|
||||
import org.jetbrains.kotlin.gradle.plugin.mpp.MetadataDependencyResolution.ChooseVisibleSourceSets.MetadataProvider.ProjectMetadataProvider
|
||||
@@ -19,23 +20,23 @@ import org.jetbrains.kotlin.gradle.targets.native.internal.*
|
||||
private typealias SourceSetName = String
|
||||
|
||||
internal fun ProjectMetadataProvider(
|
||||
sourceSetMetadataOutputs: Map<SourceSetName, SourceSetMetadataOutputs>
|
||||
sourceSetMetadataOutputs: Map<SourceSetName, SourceSetMetadataOutputs>,
|
||||
): ProjectMetadataProvider {
|
||||
return ProjectMetadataProviderImpl(sourceSetMetadataOutputs)
|
||||
}
|
||||
|
||||
internal class SourceSetMetadataOutputs(
|
||||
val metadata: FileCollection?,
|
||||
val cinterop: CInterop?
|
||||
val cinterop: CInterop?,
|
||||
) {
|
||||
class CInterop(
|
||||
val forCli: FileCollection,
|
||||
val forIde: FileCollection
|
||||
val forIde: FileCollection,
|
||||
)
|
||||
}
|
||||
|
||||
private class ProjectMetadataProviderImpl(
|
||||
private val sourceSetMetadataOutputs: Map<SourceSetName, SourceSetMetadataOutputs>
|
||||
private val sourceSetMetadataOutputs: Map<SourceSetName, SourceSetMetadataOutputs>,
|
||||
) : ProjectMetadataProvider() {
|
||||
|
||||
override fun getSourceSetCompiledMetadata(sourceSetName: String): FileCollection? {
|
||||
@@ -55,6 +56,19 @@ private class ProjectMetadataProviderImpl(
|
||||
}
|
||||
|
||||
internal suspend fun Project.collectSourceSetMetadataOutputs(): Map<SourceSetName, SourceSetMetadataOutputs> {
|
||||
/*
|
||||
Usually we can safely access the kotlin project extension inside a coroutine, as the Kotlin Gradle Plugin is
|
||||
the only entity that could launch coroutines (hence the extension being available).
|
||||
|
||||
However, this code is crossing project boundaries here:
|
||||
There is _some_ Kotlin Gradle plugin that requests the 'ProjectData' being collected for
|
||||
*all* projects (breaking project isolation).
|
||||
|
||||
Therefore, it might happen that the Kotlin Plugin was not even applied at this point, when this
|
||||
coroutine starts executing. We therefore await the wait for after the buildscript was evaluated to check
|
||||
if the multiplatformExtension is present.
|
||||
*/
|
||||
AfterEvaluateBuildscript.await()
|
||||
val multiplatformExtension = multiplatformExtensionOrNull ?: return emptyMap()
|
||||
|
||||
val sourceSetMetadata = multiplatformExtension.sourceSetsMetadataOutputs()
|
||||
@@ -83,7 +97,7 @@ private suspend fun KotlinMultiplatformExtension.sourceSetsMetadataOutputs(): Ma
|
||||
}
|
||||
|
||||
private suspend fun KotlinMultiplatformExtension.cInteropMetadataOfSourceSets(
|
||||
sourceSets: Iterable<KotlinSourceSet>
|
||||
sourceSets: Iterable<KotlinSourceSet>,
|
||||
): Map<KotlinSourceSet, SourceSetMetadataOutputs.CInterop?> {
|
||||
val taskForCLI = project.commonizeCInteropTask() ?: return emptyMap()
|
||||
val taskForIde = project.copyCommonizeCInteropForIdeTask() ?: return emptyMap()
|
||||
@@ -95,4 +109,4 @@ private suspend fun KotlinMultiplatformExtension.cInteropMetadataOfSourceSets(
|
||||
forIde = taskForIde.get().commonizedOutputLibraries(dependent)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-5
@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.gradle.utils
|
||||
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.CoroutineStart.Undispatched
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.IllegalLifecycleException
|
||||
import org.jetbrains.kotlin.gradle.plugin.kotlinPluginLifecycle
|
||||
import org.jetbrains.kotlin.tooling.core.ExtrasLazyProperty
|
||||
@@ -75,7 +76,10 @@ internal inline fun <Receiver, reified T> futureExtension(
|
||||
}
|
||||
}
|
||||
|
||||
internal fun <T> Project.future(block: suspend Project.() -> T): Future<T> = kotlinPluginLifecycle.future { block() }
|
||||
internal fun <T> Project.future(
|
||||
start: KotlinPluginLifecycle.CoroutineStart = Undispatched,
|
||||
block: suspend Project.() -> T,
|
||||
): Future<T> = kotlinPluginLifecycle.future(start) { block() }
|
||||
|
||||
internal val <T> Future<T>.lenient: LenientFuture<T> get() = LenientFutureImpl(this)
|
||||
|
||||
@@ -87,11 +91,14 @@ internal val <T> Future<T>.lenient: LenientFuture<T> get() = LenientFutureImpl(t
|
||||
*
|
||||
* basically creating a future, which is launched lazily
|
||||
*/
|
||||
internal fun <T> Project.lazyFuture(block: suspend Project.() -> T): Future<T> = LazyFutureImpl(lazy { future(block) })
|
||||
internal fun <T> Project.lazyFuture(block: suspend Project.() -> T): Future<T> = LazyFutureImpl(lazy { future { block() } })
|
||||
|
||||
internal fun <T> KotlinPluginLifecycle.future(block: suspend () -> T): Future<T> {
|
||||
internal fun <T> KotlinPluginLifecycle.future(
|
||||
start: KotlinPluginLifecycle.CoroutineStart = Undispatched,
|
||||
block: suspend () -> T,
|
||||
): Future<T> {
|
||||
return FutureImpl<T>(lifecycle = this).also { future ->
|
||||
launch { future.completeWith(runCatching { block() }) }
|
||||
launch(start) { future.completeWith(runCatching { block() }) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,4 +274,4 @@ private class Completable<T>(
|
||||
waitingContinuations.add(continuation)
|
||||
}.getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -10,6 +10,7 @@ package org.jetbrains.kotlin.gradle.unitTests
|
||||
import org.jetbrains.kotlin.gradle.idea.testFixtures.utils.deserialize
|
||||
import org.jetbrains.kotlin.gradle.idea.testFixtures.utils.serialize
|
||||
import org.jetbrains.kotlin.gradle.plugin.*
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.CoroutineStart
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.IllegalLifecycleException
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.Stage.FinaliseDsl
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.Stage.ReadyForExecution
|
||||
@@ -150,4 +151,17 @@ class FutureTest {
|
||||
future.await()
|
||||
assertEquals(1, mappedFuture.getOrThrow())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - future is launched 'Undispatched' by default`() = project.runLifecycleAwareTest {
|
||||
val future = project.future { }
|
||||
future.getOrThrow()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - future with CoroutineStart 'Default'`() = project.runLifecycleAwareTest {
|
||||
val future = project.future(CoroutineStart.Default) {}
|
||||
assertFailsWith<IllegalLifecycleException> { future.getOrThrow() }
|
||||
future.await()
|
||||
}
|
||||
}
|
||||
|
||||
+125
-6
@@ -8,12 +8,10 @@
|
||||
package org.jetbrains.kotlin.gradle.unitTests
|
||||
|
||||
import org.gradle.api.ProjectConfigurationException
|
||||
import org.gradle.internal.impldep.org.testng.TestException
|
||||
import org.gradle.testfixtures.ProjectBuilder
|
||||
import org.jetbrains.kotlin.gradle.plugin.*
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.IllegalLifecycleException
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.*
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.ProjectConfigurationResult.Failure
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.Stage
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.Stage.*
|
||||
import org.jetbrains.kotlin.gradle.util.applyMultiplatformPlugin
|
||||
import org.jetbrains.kotlin.gradle.util.buildProjectWithMPP
|
||||
@@ -561,12 +559,133 @@ class KotlinPluginLifecycleTest {
|
||||
fun `test - runLifecycleAwareTest - in a project with lifecycle`() {
|
||||
val project = ProjectBuilder.builder().build()
|
||||
project.applyMultiplatformPlugin()
|
||||
project.runLifecycleAwareTest{}
|
||||
project.runLifecycleAwareTest {}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - runLifecycleAwareTest - in a project without lifecycle`() {
|
||||
val project = ProjectBuilder.builder().build()
|
||||
assertFailsWith<AssertionError> { project.runLifecycleAwareTest{} }
|
||||
assertFailsWith<AssertionError> { project.runLifecycleAwareTest {} }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - launch with CoroutineStart - Default`() = project.runLifecycleAwareTest {
|
||||
val rootCoroutineExecuted = AtomicBoolean()
|
||||
val coroutineAExecuted = AtomicBoolean()
|
||||
val coroutineBExecuted = AtomicBoolean()
|
||||
|
||||
launch(CoroutineStart.Default) coroutineA@{
|
||||
/*
|
||||
This 'coroutine A' is launched using 'Default' which means that it will
|
||||
be 'dispatched' by being placed at the back of the queue.
|
||||
|
||||
The currently executing 'root coroutine' therefore will finish.
|
||||
Only if the previous queue is empty, this coroutine will start execution
|
||||
*/
|
||||
assertTrue(
|
||||
rootCoroutineExecuted.get(),
|
||||
"Expected 'root coroutine' to have executed"
|
||||
)
|
||||
|
||||
|
||||
assertFalse(
|
||||
coroutineBExecuted.get(),
|
||||
"Expected 'coroutineA' being executed before 'coroutineB'"
|
||||
)
|
||||
|
||||
assertFalse(coroutineAExecuted.getAndSet(true))
|
||||
}
|
||||
|
||||
launch(CoroutineStart.Default) coroutineB@{
|
||||
/*
|
||||
See code comment above: This 'coroutineB' is put at the end of the current execution queue
|
||||
*/
|
||||
assertTrue(
|
||||
rootCoroutineExecuted.get(),
|
||||
"Expected 'root coroutine' to have executed"
|
||||
)
|
||||
assertTrue(
|
||||
coroutineAExecuted.get(),
|
||||
"Expected 'coroutineA' being executed before 'coroutineB'"
|
||||
)
|
||||
assertFalse(coroutineBExecuted.getAndSet(true))
|
||||
}
|
||||
|
||||
assertFalse(rootCoroutineExecuted.getAndSet(true))
|
||||
|
||||
assertFalse(
|
||||
coroutineAExecuted.get(),
|
||||
"Expected 'coroutineA' to not be executed yet"
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
coroutineBExecuted.get(),
|
||||
"Expected 'coroutineB' to not be executed yet"
|
||||
)
|
||||
|
||||
launch(CoroutineStart.Default) {
|
||||
assertTrue(
|
||||
coroutineAExecuted.get(),
|
||||
"Expected 'coroutineA' to be executed"
|
||||
)
|
||||
|
||||
assertTrue(
|
||||
coroutineBExecuted.get(),
|
||||
"Expected 'coroutineB' to be executed"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - launch with CoroutineStart - Undispatched`() = project.runLifecycleAwareTest {
|
||||
val rootCoroutineFinished = AtomicBoolean()
|
||||
val coroutineAExecuted = AtomicBoolean()
|
||||
val coroutineBExecuted = AtomicBoolean()
|
||||
|
||||
launch(CoroutineStart.Undispatched) coroutineA@{
|
||||
/*
|
||||
Undispatched will start executing the coroutine 'inline' and will
|
||||
only suspend once the first 'suspension' point is hit.
|
||||
This means, that at this point, the 'root coroutine' had no chance to finsih yet.
|
||||
*/
|
||||
assertFalse(
|
||||
rootCoroutineFinished.get(),
|
||||
"Expected 'root coroutine' to not have finished yet"
|
||||
)
|
||||
|
||||
|
||||
assertFalse(
|
||||
coroutineBExecuted.get(),
|
||||
"Expected 'coroutineA' to be executed before 'coroutineB'"
|
||||
)
|
||||
|
||||
assertFalse(coroutineAExecuted.getAndSet(true))
|
||||
}
|
||||
|
||||
assertTrue(
|
||||
coroutineAExecuted.get(),
|
||||
"Expected 'coroutineA' to be executed 'inline'"
|
||||
)
|
||||
|
||||
launch(CoroutineStart.Undispatched) coroutineB@{
|
||||
/*
|
||||
See comment in coroutineA
|
||||
*/
|
||||
assertFalse(
|
||||
rootCoroutineFinished.get(),
|
||||
"Expected 'root coroutine' to not have finished yet"
|
||||
)
|
||||
|
||||
assertTrue(
|
||||
coroutineAExecuted.get(),
|
||||
"Expected 'coroutineA' to be executed before 'coroutineB'"
|
||||
)
|
||||
assertFalse(coroutineBExecuted.getAndSet(true))
|
||||
}
|
||||
|
||||
assertFalse(rootCoroutineFinished.getAndSet(true))
|
||||
assertTrue(coroutineAExecuted.get())
|
||||
assertTrue(coroutineBExecuted.get())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user