[Gradle] Introduce 'LifecycleAwareProperty'

KT-34662
This commit is contained in:
Sebastian Sellmair
2023-03-08 16:46:37 +01:00
committed by Space Team
parent 96edfe9048
commit 80ffd72f25
8 changed files with 222 additions and 29 deletions
@@ -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
}
@@ -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()
}
@@ -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)
@@ -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)
@@ -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" }
@@ -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")))
@@ -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) }
}
}
}
@@ -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()
}