[Gradle] KotlinPluginLifecycle: Add documentation
^KT-34662 Verification Pending
This commit is contained in:
committed by
Space Team
parent
72ee12cfc8
commit
d277502237
+198
-1
@@ -23,10 +23,57 @@ import kotlin.reflect.KProperty
|
||||
Util functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Launches the given [block] as coroutine inside the Kotlin Gradle Plugin which allows to suspend execution of the code.
|
||||
* Intended use cases for suspensions are:
|
||||
*
|
||||
* #### Waiting for a given [KotlinPluginLifecycle.Stage]
|
||||
*
|
||||
* ```kotlin
|
||||
* project.launch {
|
||||
* // code
|
||||
* await(Stage.AfterEvaluate) // <- suspends
|
||||
* assertEquals(Stage.AfterEvaluate, stage)
|
||||
*
|
||||
* await(Stage.FinaliseDsl) // suspends
|
||||
* assertEquals(Stage.FinaliseDsl, stage)
|
||||
* // code
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* #### Waiting for some Gradle property to return a final value
|
||||
*
|
||||
* ```kotlin
|
||||
* project.launch {
|
||||
* val value = myProperty.awaitFinalValue() // <- suspends until final value is available!
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* #### Waiting for other suspending code in the Kotlin Gradle Plugin
|
||||
*
|
||||
* ```kotlin
|
||||
* project.launch {
|
||||
* // code
|
||||
* callMyOtherSuspendingFunction() // <- suspends
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* When launching a coroutine, the execution start of the [block] is not guaranteed.
|
||||
* It can be executed right away, effectively executing the [block] before this launch function returns
|
||||
* However, when called inside an already existing coroutine (or once Gradle has started executing afterEvaluate listeners),
|
||||
* then this block executed after this launch function returns and put at the end of the execution queue
|
||||
*/
|
||||
internal fun Project.launch(block: suspend KotlinPluginLifecycle.() -> Unit) {
|
||||
kotlinPluginLifecycle.launch(block)
|
||||
}
|
||||
|
||||
/**
|
||||
* See [launch] and [launchInRequiredStage]
|
||||
*
|
||||
* This is a shortcut to [launch] and immediately awaiting the specified [stage]:
|
||||
* @param block Is guaranteed to be executed *not before* [stage]. However, when this function is called in a higher stage then specified
|
||||
* the [block] will still be launched.
|
||||
*/
|
||||
internal fun Project.launchInStage(stage: Stage, block: suspend KotlinPluginLifecycle.() -> Unit) {
|
||||
launch {
|
||||
await(stage)
|
||||
@@ -34,6 +81,21 @@ internal fun Project.launchInStage(stage: Stage, block: suspend KotlinPluginLife
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See also [launch]
|
||||
*
|
||||
* Launches the given block in the specified lifecycle stage [stage].
|
||||
* It is guaranteed that [block] is only executed in the specified [stage]. Leaving the stage is forbidden.
|
||||
*
|
||||
* @throws IllegalLifecycleException if the [stage] was already executed or [block] tries to exit the required [stage]
|
||||
*
|
||||
* ```kotlin
|
||||
* project.launchInRequiredStage(Stage.BeforeFinaliseDsl) {
|
||||
* assertEquals(Stage.BeforeFinaliseDsl, stage) // guaranteed!
|
||||
* assertFails { await(Stage.FinaliseDsl) } // <- forbidden, as it tried to leave the required stage!
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
internal fun Project.launchInRequiredStage(stage: Stage, block: suspend KotlinPluginLifecycle.() -> Unit) {
|
||||
launchInStage(stage) {
|
||||
requiredStage(stage) {
|
||||
@@ -42,52 +104,125 @@ internal fun Project.launchInRequiredStage(stage: Stage, block: suspend KotlinPl
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal way of retrieving the current lifecycle
|
||||
* Also: See [currentKotlinPluginLifecycle]
|
||||
*/
|
||||
internal val Project.kotlinPluginLifecycle: KotlinPluginLifecycle
|
||||
get() = extraProperties.getOrPut(KotlinPluginLifecycle::class.java.name) {
|
||||
KotlinPluginLifecycleImpl(project)
|
||||
}
|
||||
|
||||
/**
|
||||
* Will start the lifecycle, this shall be called before the [kotlinPluginLifecycle] is effectively used
|
||||
*/
|
||||
internal fun Project.startKotlinPluginLifecycle() {
|
||||
(kotlinPluginLifecycle as KotlinPluginLifecycleImpl).start()
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to [currentCoroutineContext]: Returns the current [KotlinPluginLifecycle] instance used to launch
|
||||
* the currently running coroutine. Throws if this coroutine was not started using a [KotlinPluginLifecycle]
|
||||
*/
|
||||
internal suspend fun currentKotlinPluginLifecycle(): KotlinPluginLifecycle {
|
||||
return currentCoroutineContext()[KotlinMultiplatformPluginLifecycleCoroutineContextElement]?.lifecycle
|
||||
?: error("Missing $KotlinMultiplatformPluginLifecycleCoroutineContextElement in currentCoroutineContext")
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspends execution until we *at least* reached the specified [stage]
|
||||
* This will return right away if the specified [stage] was already executed or we are currently executing the [stage]
|
||||
*/
|
||||
internal suspend fun await(stage: Stage) {
|
||||
currentKotlinPluginLifecycle().await(stage)
|
||||
}
|
||||
|
||||
/**
|
||||
* See [LifecycleAwareProperty]
|
||||
* Will create a new [LifecycleAwareProperty] which is going to finalise its value in stage [finaliseIn]
|
||||
* and the initialValue [initialValue]
|
||||
*
|
||||
* ## Sample
|
||||
* ```kotlin
|
||||
* val myProperty by project.newKotlinPluginLifecycleAwareProperty<String>()
|
||||
* myProperty.set("hello")
|
||||
* //...
|
||||
* project.launch {
|
||||
* val myFinalValue = myProperty.awaitFinalValue() // <- suspends until final value is known!
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
internal inline fun <reified T : Any> Project.newKotlinPluginLifecycleAwareProperty(
|
||||
finaliseIn: Stage = Stage.FinaliseDsl, initialValue: T? = null
|
||||
): LifecycleAwareProperty<T> {
|
||||
return kotlinPluginLifecycle.newLifecycleAwareProperty(T::class.java, finaliseIn, initialValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return the [LifecycleAwareProperty] instance if the given receiver was created by [newKotlinPluginLifecycleAwareProperty]
|
||||
*/
|
||||
internal suspend fun <T : Any> Property<T>.findKotlinPluginLifecycleAwareProperty(): LifecycleAwareProperty<T>? {
|
||||
return (currentKotlinPluginLifecycle() as KotlinPluginLifecycleImpl).findLifecycleAwareProperty(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Will suspend until the property finalises its value and therefore a final value can returned.
|
||||
* Note: This only works on properties that are [isKotlinPluginLifecycleAware]
|
||||
* (e.g. by being created using [newKotlinPluginLifecycleAwareProperty]).
|
||||
*/
|
||||
internal suspend fun <T : Any> Property<T>.awaitFinalValue(): T? {
|
||||
val lifecycleAwareProperty = findKotlinPluginLifecycleAwareProperty()
|
||||
?: throw IllegalArgumentException("Property is not lifecycle aware")
|
||||
return lifecycleAwareProperty.awaitFinalValue()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this property has an associated [LifecycleAwareProperty]
|
||||
*/
|
||||
internal suspend fun Property<*>.isKotlinPluginLifecycleAware(): Boolean {
|
||||
return findKotlinPluginLifecycleAwareProperty() != null
|
||||
}
|
||||
|
||||
/**
|
||||
* See also [withRestrictedStages]
|
||||
*
|
||||
* Will ensure that the given [block] can only execute in the given [stage]
|
||||
*/
|
||||
internal suspend fun <T> requiredStage(stage: Stage, block: suspend () -> T): T {
|
||||
return withRestrictedStages(hashSetOf(stage), block)
|
||||
}
|
||||
|
||||
/**
|
||||
* See also [withRestrictedStages]
|
||||
*
|
||||
* Will ensure that the given [block] cannot leave the current stage
|
||||
* e.g.
|
||||
*
|
||||
* ```kotlin
|
||||
* project.launch {
|
||||
* requireCurrentStage {
|
||||
* await(stage.nextOrThrow) // <- fails! We are not allowed to switch stages!
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
internal suspend fun <T> requireCurrentStage(block: suspend () -> T): T {
|
||||
return requiredStage(currentKotlinPluginLifecycle().stage, block)
|
||||
}
|
||||
|
||||
/**
|
||||
* Will ensure that the given [block] cannot leave the specified allowed stages [allowed]
|
||||
* e.g.
|
||||
*
|
||||
* ```kotlin
|
||||
* project.launchInStage(Stage.BeforeFinaliseDsl) {
|
||||
* withRestrictedStages(Stage.upTo(Stage.FinaliseDsl)) {
|
||||
* await(Stage.FinaliseDsl) // <- OK, since still in allowed stages
|
||||
* await(Stage.AfterFinaliseDsl) // <- fails, since not in allowed stages!
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
internal suspend fun <T> withRestrictedStages(allowed: Set<Stage>, block: suspend () -> T): T {
|
||||
return withContext(RestrictedLifecycleStages(currentKotlinPluginLifecycle(), allowed)) {
|
||||
block()
|
||||
@@ -100,22 +235,71 @@ Definition of the Lifecycle and its stages
|
||||
|
||||
internal interface KotlinPluginLifecycle {
|
||||
enum class Stage {
|
||||
/**
|
||||
* Configure Phase of Gradle: No .afterEvaluate {} listeners have been called yet,
|
||||
* the buildscript is still evaluated!
|
||||
*/
|
||||
Configure,
|
||||
|
||||
/**
|
||||
* The buildscript has been evaluated, first .afterEvaluate {} listeners get executed.
|
||||
* This can include .afterEvaluates from other plugins as well as other users!
|
||||
*/
|
||||
AfterEvaluate,
|
||||
|
||||
/**
|
||||
* Before [FinaliseDsl]
|
||||
*/
|
||||
BeforeFinaliseDsl,
|
||||
|
||||
/**
|
||||
* Last changes are allowed to be done to the DSL.
|
||||
* E.g. Gradle properties shall call their [Property.finalizeValue] functions to
|
||||
* disallow further changes
|
||||
*/
|
||||
FinaliseDsl,
|
||||
|
||||
/**
|
||||
* After [FinaliseDsl]
|
||||
*/
|
||||
AfterFinaliseDsl,
|
||||
|
||||
/**
|
||||
* Before [FinaliseRefinesEdges]
|
||||
*/
|
||||
BeforeFinaliseRefinesEdges,
|
||||
|
||||
/**
|
||||
* All refines edges ([KotlinSourceSet.dependsOn]) have to be finalised here.
|
||||
* Adding edges after this stage is forbidden and will throw an exception!
|
||||
*/
|
||||
FinaliseRefinesEdges,
|
||||
|
||||
/**
|
||||
* After [FinaliseRefinesEdges]
|
||||
*/
|
||||
AfterFinaliseRefinesEdges,
|
||||
|
||||
/**
|
||||
* Before [FinaliseCompilations]
|
||||
*/
|
||||
BeforeFinaliseCompilations,
|
||||
|
||||
/**
|
||||
* [KotlinCompilation] instances have to finalised: Creating compilations after this stage is forbidden.
|
||||
* Values and configuration of compilations also shall be finalised already
|
||||
*/
|
||||
FinaliseCompilations,
|
||||
|
||||
/**
|
||||
* After [FinaliseCompilations]
|
||||
*/
|
||||
AfterFinaliseCompilations,
|
||||
|
||||
Finalised;
|
||||
/**
|
||||
* Done; Configuration Phase passed. All tasks are configured and execution can be scheduled
|
||||
*/
|
||||
ReadyForExecution;
|
||||
|
||||
val previousOrFirst: Stage get() = previousOrNull ?: values.first()
|
||||
val previousOrNull: Stage? get() = values.getOrNull(ordinal - 1)
|
||||
@@ -152,9 +336,22 @@ internal interface KotlinPluginLifecycle {
|
||||
|
||||
class IllegalLifecycleException(message: String) : IllegalStateException(message)
|
||||
|
||||
/**
|
||||
* Wrapper around Gradle's [Property] that is aware of the [KotlinPluginLifecycle] and ensures that
|
||||
* the given [property] is finalised in [stage] (also calling [Property.finalizeValue]).
|
||||
*
|
||||
* A property finalised in a given [stage] will allow to safely get the final value using the
|
||||
* [awaitFinalValue] function, suspending the execution until the value is indeed finalised.
|
||||
*
|
||||
* See [Project.newKotlinPluginLifecycleAwareProperty] to create a new instance
|
||||
*/
|
||||
interface LifecycleAwareProperty<T : Any> {
|
||||
val finaliseIn: Stage
|
||||
val property: Property<T>
|
||||
|
||||
/**
|
||||
* See [LifecycleAwareProperty]
|
||||
*/
|
||||
suspend fun awaitFinalValue(): T?
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): Property<T> = this.property
|
||||
}
|
||||
|
||||
+4
-4
@@ -94,7 +94,7 @@ class KotlinPluginLifecycleTest {
|
||||
val action2Invocations = AtomicInteger(0)
|
||||
val action3Invocations = AtomicInteger(0)
|
||||
|
||||
lifecycle.enqueue(Finalised) action3@{
|
||||
lifecycle.enqueue(ReadyForExecution) action3@{
|
||||
assertEquals(1, action1Invocations.get(), "Expected action1 to be executed before action3")
|
||||
assertEquals(1, action2Invocations.get(), "Expected action2 to be executed before action3")
|
||||
assertEquals(1, action3Invocations.incrementAndGet())
|
||||
@@ -165,7 +165,7 @@ class KotlinPluginLifecycleTest {
|
||||
@Test
|
||||
fun `test - enqueue of already executed stage - throws exception`() {
|
||||
val executed = AtomicBoolean(false)
|
||||
lifecycle.enqueue(Finalised) {
|
||||
lifecycle.enqueue(ReadyForExecution) {
|
||||
assertFailsWith<IllegalLifecycleException> {
|
||||
lifecycle.enqueue(AfterEvaluate) { fail("This code shall not be executed!") }
|
||||
}
|
||||
@@ -313,7 +313,7 @@ class KotlinPluginLifecycleTest {
|
||||
|
||||
requireCurrentStage {
|
||||
/* Fails because of stage transition using 'await' */
|
||||
assertFailsWith<IllegalLifecycleException> { await(Finalised) }
|
||||
assertFailsWith<IllegalLifecycleException> { await(ReadyForExecution) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -324,7 +324,7 @@ class KotlinPluginLifecycleTest {
|
||||
assertEquals(AfterEvaluate, stage)
|
||||
await(AfterEvaluate)
|
||||
assertEquals(AfterEvaluate, stage)
|
||||
assertFailsWith<IllegalLifecycleException> { await(Finalised) }
|
||||
assertFailsWith<IllegalLifecycleException> { await(ReadyForExecution) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ class InternalKotlinSourceSetTest {
|
||||
nativeMain.internal.compilations
|
||||
)
|
||||
|
||||
project.launchInStage(KotlinPluginLifecycle.Stage.Finalised) {
|
||||
project.launchInStage(KotlinPluginLifecycle.Stage.ReadyForExecution) {
|
||||
assertEquals<Set<KotlinCompilation<*>>>(
|
||||
setOf(
|
||||
metadataCompilation,
|
||||
|
||||
Reference in New Issue
Block a user