[Gradle] Implement initial 'KotlinMultiplatformPluginLifecycle'

KT-34662
This commit is contained in:
Sebastian Sellmair
2023-03-07 17:13:57 +01:00
committed by Space Team
parent cf8814823b
commit cc9bab4105
5 changed files with 325 additions and 1 deletions
@@ -0,0 +1,103 @@
/*
* 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.plugin
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginLifecycle.IllegalLifecycleException
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginLifecycle.Stage
import org.jetbrains.kotlin.gradle.utils.getOrPut
import java.util.concurrent.atomic.AtomicBoolean
internal val Project.kotlinMultiplatformPluginLifecycle: KotlinMultiplatformPluginLifecycle
get() = extraProperties.getOrPut(KotlinMultiplatformPluginLifecycle::class.java.name) { KotlinMultiplatformPluginLifecycleImpl() }
internal fun Project.startKotlinMultiplatformPluginLifecycle() {
(kotlinMultiplatformPluginLifecycle as KotlinMultiplatformPluginLifecycleImpl).start(this)
}
internal inline fun Project.enqueue(stage: Stage, crossinline action: Project.() -> Unit) {
kotlinMultiplatformPluginLifecycle.enqueue(stage) { action() }
}
internal interface KotlinMultiplatformPluginLifecycle {
enum class Stage {
Configure,
AfterEvaluate,
FinaliseRefinesEdges,
FinaliseCompilations,
Finalised
}
val stage: Stage
fun enqueue(stage: Stage, action: () -> Unit)
class IllegalLifecycleException(message: String) : IllegalStateException(message)
}
private class KotlinMultiplatformPluginLifecycleImpl : KotlinMultiplatformPluginLifecycle {
private val enqueuedStages = ArrayDeque(Stage.values().toList())
private val enqueuedActions = Stage.values().associateWith { ArrayDeque<() -> Unit>() }
private var configureLoopRunning = false
private var isStarted = AtomicBoolean(false)
fun start(project: Project) {
check(!isStarted.getAndSet(true)) {
"${KotlinMultiplatformPluginLifecycle::class.java.name} already started"
}
check(!project.state.executed) {
"${KotlinMultiplatformPluginLifecycle::class.java.name} cannot be started in ProjectState '${project.state}'"
}
project.whenEvaluated {
executeStage(project, enqueuedStages.removeFirst())
}
}
private fun executeStage(project: Project, stage: Stage) {
this.stage = stage
val queue = enqueuedActions.getValue(stage)
do {
val action = queue.removeFirstOrNull()
action?.invoke()
} while (action != null)
val nextStage = enqueuedStages.removeFirstOrNull() ?: return
project.afterEvaluate {
executeStage(project, nextStage)
}
}
private fun startConfigureLoopIfNecessary() {
check(stage == Stage.Configure) { "Cannot start 'configure loop' on stage '$stage'" }
if (configureLoopRunning) return
configureLoopRunning = true
try {
val queue = enqueuedActions.getValue(Stage.Configure)
do {
val action = queue.removeFirstOrNull()
action?.invoke()
} while (action != null)
} finally {
configureLoopRunning = false
}
}
override var stage: Stage = enqueuedStages.removeFirst()
override fun enqueue(stage: Stage, action: () -> Unit) {
if (stage < this.stage) {
throw IllegalLifecycleException("Cannot enqueue Action for stage '${this.stage}' in current stage '${this.stage}'")
}
enqueuedActions.getValue(stage).addLast(action)
if (stage == Stage.Configure) {
startConfigureLoopIfNecessary()
}
}
}
@@ -323,6 +323,7 @@ abstract class AbstractKotlinMultiplatformPluginWrapper : KotlinBasePluginWrappe
override fun apply(project: Project) {
super.apply(project)
project.startKotlinMultiplatformPluginLifecycle()
project.runMultiplatformAndroidGradlePluginCompatibilityHealthCheckWhenAndroidIsApplied()
}
@@ -8,7 +8,9 @@
package org.jetbrains.kotlin.gradle.plugin.sources
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginLifecycle
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.kotlinMultiplatformPluginLifecycle
import org.jetbrains.kotlin.gradle.utils.MutableObservableSet
import org.jetbrains.kotlin.gradle.utils.MutableObservableSetImpl
import org.jetbrains.kotlin.gradle.utils.ObservableSet
@@ -31,6 +33,11 @@ abstract class AbstractKotlinSourceSet : InternalKotlinSourceSet {
final override fun dependsOn(other: KotlinSourceSet) {
if (other == this) return
assert(project.kotlinMultiplatformPluginLifecycle.stage <= KotlinMultiplatformPluginLifecycle.Stage.FinaliseRefinesEdges) {
"Illegal 'dependsOn' call in stage '${project.kotlinMultiplatformPluginLifecycle.stage}'"
}
/*
Circular dependsOn hierarchies are not allowed:
Throw if this SourceSet is already present in the dependsOnClosure of 'other'
@@ -175,7 +175,7 @@ class KotlinMetadataTargetConfigurator :
private fun createMetadataCompilationsForCommonSourceSets(
target: KotlinMetadataTarget,
allMetadataJar: TaskProvider<out Jar>
) = target.project.whenEvaluated {
) = target.project.enqueue(KotlinMultiplatformPluginLifecycle.Stage.FinaliseCompilations) {
// Do this after all targets are configured by the user build script
val publishedCommonSourceSets: Set<KotlinSourceSet> = getCommonSourceSetsForMetadataCompilation(project)
@@ -0,0 +1,213 @@
/*
* 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.KotlinMultiplatformPluginLifecycle
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginLifecycle.Stage.*
import org.jetbrains.kotlin.gradle.plugin.kotlinMultiplatformPluginLifecycle
import org.jetbrains.kotlin.gradle.util.buildProjectWithMPP
import org.junit.Test
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import kotlin.test.*
class KotlinMultiplatformPluginLifecycleTest {
private val project = buildProjectWithMPP()
private val lifecycle = project.kotlinMultiplatformPluginLifecycle
@Test
fun `test - configure phase is executed right away`() {
val invocations = AtomicInteger(0)
lifecycle.enqueue(Configure) {
invocations.incrementAndGet()
}
assertEquals(1, invocations.get(), "Expected one invocation")
}
@Test
fun `test - configure phase - nested enqueue - is executed as queue`() {
val outerInvocations = AtomicInteger(0)
val nestedAInvocations = AtomicInteger(0)
val nestedBInvocations = AtomicInteger(0)
val nestedCInvocations = AtomicInteger(0)
lifecycle.enqueue(Configure) {
assertEquals(1, outerInvocations.incrementAndGet())
lifecycle.enqueue(Configure) nestedA@{
assertEquals(0, nestedBInvocations.get(), "Expected nestedA to be executed before nestedB")
assertEquals(1, nestedAInvocations.incrementAndGet())
lifecycle.enqueue(Configure) nestedC@{
assertEquals(1, nestedBInvocations.get(), "Expected nestedB to be executed before nestedC")
assertEquals(1, nestedCInvocations.incrementAndGet())
}
}
lifecycle.enqueue(Configure) nestedB@{
assertEquals(1, nestedAInvocations.get(), "Expected nestedA to be executed before nestedB")
assertEquals(0, nestedCInvocations.get(), "Expected nestedB to be executed before nestedC")
assertEquals(1, nestedBInvocations.incrementAndGet())
}
}
assertEquals(1, outerInvocations.get())
assertEquals(1, nestedAInvocations.get())
assertEquals(1, nestedBInvocations.get())
assertEquals(1, nestedCInvocations.get())
}
@Test
fun `test - all stages are executed in order`() {
val invocations = KotlinMultiplatformPluginLifecycle.Stage.values().associateWith { AtomicInteger(0) }
KotlinMultiplatformPluginLifecycle.Stage.values().toList().forEach { stage ->
lifecycle.enqueue(stage) {
KotlinMultiplatformPluginLifecycle.Stage.values().forEach { otherStage ->
when {
otherStage.ordinal < stage.ordinal -> assertEquals(1, invocations.getValue(otherStage).get())
otherStage.ordinal == stage.ordinal -> assertEquals(1, invocations.getValue(stage).incrementAndGet())
else -> assertEquals(0, invocations.getValue(otherStage).get())
}
}
}
}
project.evaluate()
invocations.forEach { (stage, invocations) ->
assertEquals(1, invocations.get(), "Expected stage '$stage' to be executed")
}
}
@Test
fun `test - afterEvaluate based stage executes queue in order`() {
val action1Invocations = AtomicInteger(0)
val action2Invocations = AtomicInteger(0)
val action3Invocations = AtomicInteger(0)
lifecycle.enqueue(Finalised) 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())
}
lifecycle.enqueue(AfterEvaluate) action1@{
assertEquals(0, action2Invocations.get(), "Expected action1 to be executed before action2")
assertEquals(0, action3Invocations.get(), "Expected action1 to be executed before action3")
assertEquals(1, action1Invocations.incrementAndGet())
}
lifecycle.enqueue(AfterEvaluate) action2@{
assertEquals(1, action1Invocations.get(), "Expected action1 to be executed before action2")
assertEquals(0, action3Invocations.get(), "Expected action2 to be executed before action3")
assertEquals(1, action2Invocations.incrementAndGet())
}
assertEquals(0, action1Invocations.get())
assertEquals(0, action2Invocations.get())
assertEquals(0, action3Invocations.get())
project.evaluate()
assertEquals(1, action1Invocations.get())
assertEquals(1, action2Invocations.get())
assertEquals(1, action3Invocations.get())
}
@Test
fun `test - afterEvaluate based stage - allows enqueue in current stage`() {
val outerInvocations = AtomicInteger(0)
val nestedAInvocations = AtomicInteger(0)
val nestedBInvocations = AtomicInteger(0)
val nestedCInvocations = AtomicInteger(0)
lifecycle.enqueue(AfterEvaluate) {
assertEquals(1, outerInvocations.incrementAndGet())
lifecycle.enqueue(AfterEvaluate) nestedA@{
assertEquals(0, nestedBInvocations.get(), "Expected nestedA to be executed before nestedB")
assertEquals(1, nestedAInvocations.incrementAndGet())
lifecycle.enqueue(AfterEvaluate) nestedC@{
assertEquals(1, nestedBInvocations.get(), "Expected nestedB to be executed before nestedC")
assertEquals(1, nestedCInvocations.incrementAndGet())
}
}
lifecycle.enqueue(AfterEvaluate) nestedB@{
assertEquals(1, nestedAInvocations.get(), "Expected nestedA to be executed before nestedB")
assertEquals(0, nestedCInvocations.get(), "Expected nestedB to be executed before nestedC")
assertEquals(1, nestedBInvocations.incrementAndGet())
}
}
assertEquals(0, outerInvocations.get())
assertEquals(0, nestedAInvocations.get())
assertEquals(0, nestedBInvocations.get())
assertEquals(0, nestedCInvocations.get())
project.evaluate()
assertEquals(1, outerInvocations.get())
assertEquals(1, nestedAInvocations.get())
assertEquals(1, nestedBInvocations.get())
assertEquals(1, nestedCInvocations.get())
}
@Test
fun `test - enqueue of already executed stage - throws exception`() {
val executed = AtomicBoolean(false)
lifecycle.enqueue(Finalised) {
assertFailsWith<KotlinMultiplatformPluginLifecycle.IllegalLifecycleException> {
lifecycle.enqueue(AfterEvaluate) { fail("This code shall not be executed!") }
}
assertFalse(executed.getAndSet(true))
}
project.evaluate()
assertTrue(executed.get())
}
@Test
fun `test - stage property is correct`() {
KotlinMultiplatformPluginLifecycle.Stage.values().forEach { stage ->
lifecycle.enqueue(stage) {
assertEquals(lifecycle.stage, stage)
}
}
project.evaluate()
}
@Test
fun `test - invoke configure twice`() {
val action1Invocations = AtomicInteger(0)
val action2Invocations = AtomicInteger(0)
val action3Invocations = AtomicInteger(0)
lifecycle.enqueue(Configure) action1@{
assertEquals(0, action2Invocations.get())
assertEquals(0, action3Invocations.get())
assertEquals(1, action1Invocations.incrementAndGet())
}
lifecycle.enqueue(Configure) action2@{
lifecycle.enqueue(Configure) action3@{
assertEquals(1, action1Invocations.get())
assertEquals(1, action2Invocations.get())
assertEquals(1, action3Invocations.incrementAndGet())
}
assertEquals(1, action1Invocations.get())
assertEquals(0, action3Invocations.get())
assertEquals(1, action2Invocations.incrementAndGet())
}
assertEquals(1, action1Invocations.get())
assertEquals(1, action2Invocations.get())
assertEquals(1, action3Invocations.get())
}
}