[Gradle] Introduce 'LifecycleAwareProperty'
KT-34662
This commit is contained in:
committed by
Space Team
parent
96edfe9048
commit
80ffd72f25
+127
-14
@@ -5,24 +5,79 @@
|
||||
|
||||
package org.jetbrains.kotlin.gradle.plugin
|
||||
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import org.gradle.api.Project
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginLifecycle.IllegalLifecycleException
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginLifecycle.Stage
|
||||
import org.gradle.api.provider.Property
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginLifecycle.*
|
||||
import org.jetbrains.kotlin.gradle.utils.getOrPut
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.collections.ArrayDeque
|
||||
import kotlin.collections.Map
|
||||
import kotlin.collections.associateWith
|
||||
import kotlin.collections.getValue
|
||||
import kotlin.collections.set
|
||||
import kotlin.collections.toList
|
||||
import kotlin.coroutines.*
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
/*
|
||||
Util functions
|
||||
*/
|
||||
|
||||
internal fun Project.launch(block: suspend KotlinMultiplatformPluginLifecycle.() -> Unit) {
|
||||
kotlinMultiplatformPluginLifecycle.launch(block)
|
||||
}
|
||||
|
||||
internal fun Project.launch(stage: Stage, block: suspend KotlinMultiplatformPluginLifecycle.() -> Unit) {
|
||||
launch {
|
||||
await(stage)
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
internal val Project.kotlinMultiplatformPluginLifecycle: KotlinMultiplatformPluginLifecycle
|
||||
get() = extraProperties.getOrPut(KotlinMultiplatformPluginLifecycle::class.java.name) { KotlinMultiplatformPluginLifecycleImpl() }
|
||||
get() = extraProperties.getOrPut(KotlinMultiplatformPluginLifecycle::class.java.name) {
|
||||
KotlinMultiplatformPluginLifecycleImpl(project)
|
||||
}
|
||||
|
||||
internal fun Project.startKotlinMultiplatformPluginLifecycle() {
|
||||
(kotlinMultiplatformPluginLifecycle as KotlinMultiplatformPluginLifecycleImpl).start(this)
|
||||
(kotlinMultiplatformPluginLifecycle as KotlinMultiplatformPluginLifecycleImpl).start()
|
||||
}
|
||||
|
||||
internal inline fun Project.enqueue(stage: Stage, crossinline action: Project.() -> Unit) {
|
||||
kotlinMultiplatformPluginLifecycle.enqueue(stage) { action() }
|
||||
internal suspend fun currentMultiplatformPluginLifecycle(): KotlinMultiplatformPluginLifecycle {
|
||||
return currentCoroutineContext()[KotlinMultiplatformPluginLifecycleCoroutineContextElement]?.lifecycle
|
||||
?: error("Missing $KotlinMultiplatformPluginLifecycleCoroutineContextElement in currentCoroutineContext")
|
||||
}
|
||||
|
||||
internal suspend fun await(stage: Stage) {
|
||||
currentMultiplatformPluginLifecycle().await(stage)
|
||||
}
|
||||
|
||||
internal inline fun <reified T : Any> Project.newLifecycleAwareProperty(
|
||||
finaliseIn: Stage, initialValue: T? = null
|
||||
): LifecycleAwareProperty<T> {
|
||||
return kotlinMultiplatformPluginLifecycle.newLifecycleAwareProperty(T::class.java, finaliseIn, initialValue)
|
||||
}
|
||||
|
||||
internal suspend fun <T : Any> Property<T>.findLifecycleAwareProperty(): LifecycleAwareProperty<T>? {
|
||||
return (currentMultiplatformPluginLifecycle() as KotlinMultiplatformPluginLifecycleImpl).findLifecycleAwareProperty(this)
|
||||
}
|
||||
|
||||
internal suspend fun <T : Any> Property<T>.awaitFinalValue(): T? {
|
||||
val lifecycleAwareProperty = findLifecycleAwareProperty() ?: throw IllegalArgumentException("Property is not lifecycle aware")
|
||||
return lifecycleAwareProperty.awaitFinalValue()
|
||||
}
|
||||
|
||||
internal suspend fun Property<*>.isLifecycleAware(): Boolean {
|
||||
return findLifecycleAwareProperty() != null
|
||||
}
|
||||
|
||||
/*
|
||||
Definition of the Lifecycle and its stages
|
||||
*/
|
||||
|
||||
internal interface KotlinMultiplatformPluginLifecycle {
|
||||
enum class Stage {
|
||||
Configure,
|
||||
@@ -39,25 +94,47 @@ internal interface KotlinMultiplatformPluginLifecycle {
|
||||
Finalised
|
||||
}
|
||||
|
||||
interface LifecycleAwareProperty<T : Any> {
|
||||
val finaliseIn: Stage
|
||||
val property: Property<T>
|
||||
suspend fun awaitFinalValue(): T?
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): Property<T> = this.property
|
||||
}
|
||||
|
||||
val project: Project
|
||||
|
||||
val stage: Stage
|
||||
|
||||
fun enqueue(stage: Stage, action: () -> Unit)
|
||||
fun enqueue(stage: Stage, action: KotlinMultiplatformPluginLifecycle.() -> Unit)
|
||||
|
||||
fun launch(block: suspend KotlinMultiplatformPluginLifecycle.() -> Unit)
|
||||
|
||||
suspend fun await(stage: Stage)
|
||||
|
||||
fun <T : Any> newLifecycleAwareProperty(
|
||||
type: Class<T>, finaliseIn: Stage, initialValue: T?
|
||||
): LifecycleAwareProperty<T>
|
||||
|
||||
class IllegalLifecycleException(message: String) : IllegalStateException(message)
|
||||
}
|
||||
|
||||
private class KotlinMultiplatformPluginLifecycleImpl : KotlinMultiplatformPluginLifecycle {
|
||||
|
||||
/*
|
||||
Implementation
|
||||
*/
|
||||
|
||||
private class KotlinMultiplatformPluginLifecycleImpl(override val project: Project) : KotlinMultiplatformPluginLifecycle {
|
||||
private val enqueuedStages: ArrayDeque<Stage> = ArrayDeque(Stage.values().toList())
|
||||
private val enqueuedActions: Map<Stage, ArrayDeque<() -> Unit>> = Stage.values().associateWith { ArrayDeque() }
|
||||
private val enqueuedActions: Map<Stage, ArrayDeque<KotlinMultiplatformPluginLifecycle.() -> Unit>> =
|
||||
Stage.values().associateWith { ArrayDeque() }
|
||||
|
||||
private var configureLoopRunning = AtomicBoolean(false)
|
||||
private var isStarted = AtomicBoolean(false)
|
||||
private var isFinished = AtomicBoolean(false)
|
||||
|
||||
fun start(project: Project) {
|
||||
private val properties = WeakHashMap<Property<*>, WeakReference<LifecycleAwareProperty<*>>>()
|
||||
|
||||
fun start() {
|
||||
check(!isStarted.getAndSet(true)) {
|
||||
"${KotlinMultiplatformPluginLifecycle::class.java.name} already started"
|
||||
}
|
||||
@@ -76,7 +153,7 @@ private class KotlinMultiplatformPluginLifecycleImpl : KotlinMultiplatformPlugin
|
||||
val queue = enqueuedActions.getValue(stage)
|
||||
do {
|
||||
val action = queue.removeFirstOrNull()
|
||||
action?.invoke()
|
||||
action?.invoke(this)
|
||||
} while (action != null)
|
||||
val nextStage = enqueuedStages.removeFirstOrNull() ?: run {
|
||||
isFinished.set(true)
|
||||
@@ -95,7 +172,7 @@ private class KotlinMultiplatformPluginLifecycleImpl : KotlinMultiplatformPlugin
|
||||
val queue = enqueuedActions.getValue(Stage.Configure)
|
||||
do {
|
||||
val action = queue.removeFirstOrNull()
|
||||
action?.invoke()
|
||||
action?.invoke(this)
|
||||
} while (action != null)
|
||||
} finally {
|
||||
configureLoopRunning.set(false)
|
||||
@@ -104,7 +181,7 @@ private class KotlinMultiplatformPluginLifecycleImpl : KotlinMultiplatformPlugin
|
||||
|
||||
override var stage: Stage = enqueuedStages.removeFirst()
|
||||
|
||||
override fun enqueue(stage: Stage, action: () -> Unit) {
|
||||
override fun enqueue(stage: Stage, action: KotlinMultiplatformPluginLifecycle.() -> Unit) {
|
||||
if (stage < this.stage) {
|
||||
throw IllegalLifecycleException("Cannot enqueue Action for stage '${this.stage}' in current stage '${this.stage}'")
|
||||
}
|
||||
@@ -117,10 +194,14 @@ private class KotlinMultiplatformPluginLifecycleImpl : KotlinMultiplatformPlugin
|
||||
}
|
||||
|
||||
override fun launch(block: suspend KotlinMultiplatformPluginLifecycle.() -> Unit) {
|
||||
val lifecycle = this
|
||||
check(isStarted.get()) { "Cannot launch when ${KotlinMultiplatformPluginLifecycle::class.simpleName} is not started" }
|
||||
check(!isFinished.get()) { "Cannot launch when ${KotlinMultiplatformPluginLifecycle::class.simpleName} is already finished" }
|
||||
val coroutine = block.createCoroutine(this, object : Continuation<Unit> {
|
||||
override val context: CoroutineContext = EmptyCoroutineContext
|
||||
|
||||
override val context: CoroutineContext = EmptyCoroutineContext +
|
||||
KotlinMultiplatformPluginLifecycleCoroutineContextElement(lifecycle)
|
||||
|
||||
override fun resumeWith(result: Result<Unit>) = result.getOrThrow()
|
||||
})
|
||||
|
||||
@@ -137,4 +218,36 @@ private class KotlinMultiplatformPluginLifecycleImpl : KotlinMultiplatformPlugin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T : Any> newLifecycleAwareProperty(type: Class<T>, finaliseIn: Stage, initialValue: T?): LifecycleAwareProperty<T> {
|
||||
val property = project.objects.property(type)
|
||||
if (initialValue != null) property.set(initialValue)
|
||||
enqueue(finaliseIn) { property.finalizeValue() }
|
||||
val lifecycleAwareProperty = LifecycleAwarePropertyImpl(finaliseIn, property)
|
||||
properties[property] = WeakReference(lifecycleAwareProperty)
|
||||
return lifecycleAwareProperty
|
||||
}
|
||||
|
||||
fun <T : Any> findLifecycleAwareProperty(property: Property<T>): LifecycleAwareProperty<T>? {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return properties[property]?.get() as? LifecycleAwareProperty<T>
|
||||
}
|
||||
|
||||
private class LifecycleAwarePropertyImpl<T : Any>(
|
||||
override val finaliseIn: Stage,
|
||||
override val property: Property<T>
|
||||
) : LifecycleAwareProperty<T> {
|
||||
override suspend fun awaitFinalValue(): T? {
|
||||
await(finaliseIn)
|
||||
return property.orNull
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class KotlinMultiplatformPluginLifecycleCoroutineContextElement(
|
||||
val lifecycle: KotlinMultiplatformPluginLifecycle
|
||||
) : CoroutineContext.Element {
|
||||
companion object Key : CoroutineContext.Key<KotlinMultiplatformPluginLifecycleCoroutineContextElement>
|
||||
|
||||
override val key: CoroutineContext.Key<KotlinMultiplatformPluginLifecycleCoroutineContextElement> = Key
|
||||
}
|
||||
|
||||
+2
-1
@@ -77,6 +77,8 @@ abstract class DefaultKotlinBasePlugin : KotlinBasePlugin {
|
||||
|
||||
checkGradleCompatibility()
|
||||
|
||||
project.startKotlinMultiplatformPluginLifecycle()
|
||||
|
||||
project.gradle.projectsEvaluated {
|
||||
whenBuildEvaluated(project)
|
||||
}
|
||||
@@ -323,7 +325,6 @@ abstract class AbstractKotlinMultiplatformPluginWrapper : KotlinBasePluginWrappe
|
||||
|
||||
override fun apply(project: Project) {
|
||||
super.apply(project)
|
||||
project.startKotlinMultiplatformPluginLifecycle()
|
||||
project.runMultiplatformAndroidGradlePluginCompatibilityHealthCheckWhenAndroidIsApplied()
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -214,12 +214,12 @@ internal fun applyUserDefinedAttributes(target: AbstractKotlinTarget) {
|
||||
// To copy the attributes to the output configurations, find those output configurations and their producing compilations
|
||||
// based on the target's components:
|
||||
val outputConfigurationsWithCompilations = target.kotlinComponents.filterIsInstance<KotlinVariant>().flatMap { kotlinVariant ->
|
||||
kotlinVariant.usages.mapNotNull { usageContext ->
|
||||
project.configurations.findByName(usageContext.dependencyConfigurationName)?.let { configuration ->
|
||||
configuration to usageContext.compilation
|
||||
}
|
||||
kotlinVariant.usages.mapNotNull { usageContext ->
|
||||
project.configurations.findByName(usageContext.dependencyConfigurationName)?.let { configuration ->
|
||||
configuration to usageContext.compilation
|
||||
}
|
||||
}.toMutableList()
|
||||
}
|
||||
}.toMutableList()
|
||||
|
||||
val mainCompilation = target.compilations.findByName(KotlinCompilation.MAIN_COMPILATION_NAME)
|
||||
val defaultTargetConfiguration = project.configurations.findByName(target.defaultConfigurationName)
|
||||
|
||||
+1
-1
@@ -175,7 +175,7 @@ class KotlinMetadataTargetConfigurator :
|
||||
private fun createMetadataCompilationsForCommonSourceSets(
|
||||
target: KotlinMetadataTarget,
|
||||
allMetadataJar: TaskProvider<out Jar>
|
||||
) = target.project.enqueue(KotlinMultiplatformPluginLifecycle.Stage.FinaliseCompilations) {
|
||||
) = target.project.launch(KotlinMultiplatformPluginLifecycle.Stage.FinaliseCompilations) {
|
||||
// Do this after all targets are configured by the user build script
|
||||
|
||||
val publishedCommonSourceSets: Set<KotlinSourceSet> = getCommonSourceSetsForMetadataCompilation(project)
|
||||
|
||||
+8
-7
@@ -14,6 +14,7 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.targetHierarchy.KotlinTargetHierar
|
||||
import org.jetbrains.kotlin.gradle.plugin.mpp.targetHierarchy.KotlinTargetHierarchy.Node
|
||||
import org.jetbrains.kotlin.gradle.plugin.mpp.targetHierarchy.buildKotlinTargetHierarchy
|
||||
import org.jetbrains.kotlin.gradle.util.buildProjectWithMPP
|
||||
import org.jetbrains.kotlin.gradle.util.runLifecycleAwareTest
|
||||
import org.jetbrains.kotlin.gradle.util.main
|
||||
import org.junit.Test
|
||||
import kotlin.test.*
|
||||
@@ -24,7 +25,7 @@ class KotlinTargetHierarchyDescriptorTest {
|
||||
private val kotlin = project.multiplatformExtension
|
||||
|
||||
@Test
|
||||
fun `test - simple descriptor`() {
|
||||
fun `test - simple descriptor`() = project.runLifecycleAwareTest {
|
||||
val descriptor = KotlinTargetHierarchyDescriptor {
|
||||
common {
|
||||
group("groupA") { withCompilations { it.target.name == "a" } }
|
||||
@@ -61,7 +62,7 @@ class KotlinTargetHierarchyDescriptorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - extend`() {
|
||||
fun `test - extend`() = project.runLifecycleAwareTest {
|
||||
val descriptor = KotlinTargetHierarchyDescriptor { group("base") }.extend {
|
||||
group("base") {
|
||||
group("extension") {
|
||||
@@ -82,7 +83,7 @@ class KotlinTargetHierarchyDescriptorTest {
|
||||
|
||||
|
||||
@Test
|
||||
fun `test - extend - with new root`() {
|
||||
fun `test - extend - with new root`() = project.runLifecycleAwareTest {
|
||||
val descriptor = KotlinTargetHierarchyDescriptor { group("base") }.extend {
|
||||
group("newRoot") {
|
||||
group("base") {
|
||||
@@ -107,7 +108,7 @@ class KotlinTargetHierarchyDescriptorTest {
|
||||
|
||||
|
||||
@Test
|
||||
fun `test - extend - with two new roots and two extensions`() {
|
||||
fun `test - extend - with two new roots and two extensions`() = project.runLifecycleAwareTest {
|
||||
val descriptor = KotlinTargetHierarchyDescriptor { group("base") }
|
||||
.extend {
|
||||
group("newRoot1") {
|
||||
@@ -163,7 +164,7 @@ class KotlinTargetHierarchyDescriptorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - cycle`() {
|
||||
fun `test - cycle`() = project.runLifecycleAwareTest {
|
||||
val descriptor = KotlinTargetHierarchyDescriptor {
|
||||
group("x") { // decoy 1
|
||||
group("a") {
|
||||
@@ -193,7 +194,7 @@ class KotlinTargetHierarchyDescriptorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - filterCompilations`() {
|
||||
fun `test - filterCompilations`() = project.runLifecycleAwareTest {
|
||||
val descriptor = KotlinTargetHierarchyDescriptor {
|
||||
filterCompilations { it.name in setOf("a", "b") }
|
||||
common {
|
||||
@@ -223,7 +224,7 @@ class KotlinTargetHierarchyDescriptorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - filterCompilations - include them again`() {
|
||||
fun `test - filterCompilations - include them again`() = project.runLifecycleAwareTest {
|
||||
val descriptor = KotlinTargetHierarchyDescriptor {
|
||||
withCompilations { true }
|
||||
filterCompilations { it.name == "a" }
|
||||
|
||||
+1
-1
@@ -183,7 +183,7 @@ class KotlinTargetHierarchyDslTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - hierarchy default - is only applied to main and test compilations`() {
|
||||
fun `test - hierarchy default - is only applied to main and test compilations`() = project.runLifecycleAwareTest {
|
||||
assertNotNull(defaultKotlinTargetHierarchy.buildKotlinTargetHierarchy(kotlin.linuxX64().compilations.main))
|
||||
assertNotNull(defaultKotlinTargetHierarchy.buildKotlinTargetHierarchy(kotlin.linuxX64().compilations.test))
|
||||
assertNull(defaultKotlinTargetHierarchy.buildKotlinTargetHierarchy(kotlin.linuxX64().compilations.maybeCreate("custom")))
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.*
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginLifecycle.Stage.*
|
||||
import org.jetbrains.kotlin.gradle.plugin.awaitFinalValue
|
||||
import org.jetbrains.kotlin.gradle.plugin.currentMultiplatformPluginLifecycle
|
||||
import org.jetbrains.kotlin.gradle.plugin.launch
|
||||
import org.jetbrains.kotlin.gradle.plugin.newLifecycleAwareProperty
|
||||
import org.jetbrains.kotlin.gradle.util.buildProjectWithMPP
|
||||
import org.jetbrains.kotlin.gradle.util.runLifecycleAwareTest
|
||||
import org.jetbrains.kotlin.gradle.utils.newProperty
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class LifecycleAwarePropertyTest {
|
||||
private val project = buildProjectWithMPP()
|
||||
|
||||
@Test
|
||||
fun `test - awaitFinalValue`() = project.runLifecycleAwareTest {
|
||||
val property by project.newLifecycleAwareProperty<Int>(AfterFinaliseRefinesEdges)
|
||||
assertTrue(property.isLifecycleAware())
|
||||
|
||||
launch(BeforeFinaliseRefinesEdges) {
|
||||
property.set(1)
|
||||
}
|
||||
|
||||
launch(FinaliseRefinesEdges) {
|
||||
assertEquals(1, property.get())
|
||||
property.set(2)
|
||||
}
|
||||
|
||||
assertEquals(Configure, currentMultiplatformPluginLifecycle().stage)
|
||||
assertEquals(2, property.awaitFinalValue())
|
||||
assertEquals(AfterFinaliseRefinesEdges, currentMultiplatformPluginLifecycle().stage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - awaitFinalValue - on non lifecycle aware property`() = project.runLifecycleAwareTest {
|
||||
val property = project.newProperty<String>()
|
||||
assertFalse(property.isLifecycleAware())
|
||||
assertFailsWith<IllegalArgumentException> { property.awaitFinalValue() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - changing value after finalized`() = project.runLifecycleAwareTest {
|
||||
val property by project.newLifecycleAwareProperty<Int>(AfterEvaluate)
|
||||
property.set(1)
|
||||
|
||||
launch(AfterEvaluate) {
|
||||
assertFailsWith<IllegalStateException> { property.set(2) }
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* 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.util
|
||||
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.internal.project.ProjectInternal
|
||||
import org.jetbrains.kotlin.gradle.plugin.kotlinMultiplatformPluginLifecycle
|
||||
|
||||
fun Project.runLifecycleAwareTest(block: suspend Project.() -> Unit) {
|
||||
kotlinMultiplatformPluginLifecycle.launch { block() }
|
||||
(this as ProjectInternal).evaluate()
|
||||
}
|
||||
Reference in New Issue
Block a user