[Gradle] KotlinPluginLifecycle: Detect failure and abort further configuration

KT-58275
This commit is contained in:
Sebastian Sellmair
2023-05-03 16:05:37 +02:00
committed by Space Team
parent 8e9c3a21c0
commit f616fffeb0
3 changed files with 117 additions and 37 deletions
@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.gradle.plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.* import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.*
import org.jetbrains.kotlin.gradle.utils.failures
import org.jetbrains.kotlin.gradle.utils.getOrPut import org.jetbrains.kotlin.gradle.utils.getOrPut
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.* import java.util.*
@@ -137,12 +138,11 @@ internal suspend fun Stage.await() {
currentKotlinPluginLifecycle().await(this) currentKotlinPluginLifecycle().await(this)
} }
/** /**
* See [newProperty] * See [newProperty]
*/ */
internal inline fun <reified T : Any> Project.newKotlinPluginLifecycleAwareProperty( internal inline fun <reified T : Any> Project.newKotlinPluginLifecycleAwareProperty(
finaliseIn: Stage = Stage.FinaliseDsl, initialValue: T? = null finaliseIn: Stage = Stage.FinaliseDsl, initialValue: T? = null,
): LifecycleAwareProperty<T> { ): LifecycleAwareProperty<T> {
return kotlinPluginLifecycle.newProperty(T::class.java, finaliseIn, initialValue) return kotlinPluginLifecycle.newProperty(T::class.java, finaliseIn, initialValue)
} }
@@ -163,7 +163,7 @@ internal inline fun <reified T : Any> Project.newKotlinPluginLifecycleAwarePrope
* ``` * ```
*/ */
internal inline fun <reified T : Any> KotlinPluginLifecycle.newProperty( internal inline fun <reified T : Any> KotlinPluginLifecycle.newProperty(
finaliseIn: Stage = Stage.FinaliseDsl, initialValue: T? = null finaliseIn: Stage = Stage.FinaliseDsl, initialValue: T? = null,
): LifecycleAwareProperty<T> { ): LifecycleAwareProperty<T> {
return newProperty(T::class.java, finaliseIn, initialValue) return newProperty(T::class.java, finaliseIn, initialValue)
} }
@@ -334,7 +334,7 @@ internal interface KotlinPluginLifecycle {
suspend fun await(stage: Stage) suspend fun await(stage: Stage)
fun <T : Any> newProperty( fun <T : Any> newProperty(
type: Class<T>, finaliseIn: Stage, initialValue: T? type: Class<T>, finaliseIn: Stage, initialValue: T?,
): LifecycleAwareProperty<T> ): LifecycleAwareProperty<T>
class IllegalLifecycleException(message: String) : IllegalStateException(message) class IllegalLifecycleException(message: String) : IllegalStateException(message)
@@ -371,8 +371,10 @@ private class KotlinPluginLifecycleImpl(override val project: Project) : KotlinP
private val loopRunning = AtomicBoolean(false) private val loopRunning = AtomicBoolean(false)
private val isStarted = AtomicBoolean(false) private val isStarted = AtomicBoolean(false)
private val isFinished = AtomicBoolean(false) private val isFinishedSuccessfully = AtomicBoolean(false)
private val isFinishedWithExceptions = AtomicBoolean(false)
override var stage: Stage = Stage.values.first()
private val properties = WeakHashMap<Property<*>, WeakReference<LifecycleAwareProperty<*>>>() private val properties = WeakHashMap<Property<*>, WeakReference<LifecycleAwareProperty<*>>>()
fun start() { fun start() {
@@ -400,10 +402,21 @@ private class KotlinPluginLifecycleImpl(override val project: Project) : KotlinP
} }
} }
loopIfNecessary() val failures = project.failures
if (failures.isNotEmpty()) {
finishWithFailures(failures)
return
}
try {
loopIfNecessary()
} catch (t: Throwable) {
finishWithFailures(listOf(t))
throw t
}
stage = stage.nextOrNull ?: run { stage = stage.nextOrNull ?: run {
isFinished.set(true) finishSuccessfully()
return return
} }
@@ -425,7 +438,16 @@ private class KotlinPluginLifecycleImpl(override val project: Project) : KotlinP
} }
} }
override var stage: Stage = Stage.values.first() private fun finishWithFailures(failures: List<Throwable>) {
assert(failures.isNotEmpty())
assert(isStarted.get())
assert(!isFinishedWithExceptions.getAndSet(true))
}
private fun finishSuccessfully() {
assert(isStarted.get())
assert(!isFinishedSuccessfully.getAndSet(true))
}
override fun enqueue(stage: Stage, action: KotlinPluginLifecycle.() -> Unit) { override fun enqueue(stage: Stage, action: KotlinPluginLifecycle.() -> Unit) {
if (stage < this.stage) { if (stage < this.stage) {
@@ -437,11 +459,15 @@ private class KotlinPluginLifecycleImpl(override val project: Project) : KotlinP
This is desirable, so that .enqueue (and .launch) functions that are scheduled in execution phase This is desirable, so that .enqueue (and .launch) functions that are scheduled in execution phase
will be executed right away (no suspend necessary or wanted) will be executed right away (no suspend necessary or wanted)
*/ */
if (isFinished.get()) { if (isFinishedSuccessfully.get()) {
action() action()
return return
} }
if (isFinishedWithExceptions.get()) {
throw IllegalLifecycleException("Cannot enqueue Action: Lifecycle already finished with Exceptions")
}
enqueuedActions.getValue(stage).addLast(action) enqueuedActions.getValue(stage).addLast(action)
if (stage == Stage.EvaluateBuildscript && isStarted.get()) { if (stage == Stage.EvaluateBuildscript && isStarted.get()) {
@@ -490,7 +516,7 @@ private class KotlinPluginLifecycleImpl(override val project: Project) : KotlinP
private class LifecycleAwarePropertyImpl<T : Any>( private class LifecycleAwarePropertyImpl<T : Any>(
override val finaliseIn: Stage, override val finaliseIn: Stage,
override val property: Property<T> override val property: Property<T>,
) : LifecycleAwareProperty<T> { ) : LifecycleAwareProperty<T> {
override suspend fun awaitFinalValue(): T? { override suspend fun awaitFinalValue(): T? {
@@ -501,7 +527,7 @@ private class KotlinPluginLifecycleImpl(override val project: Project) : KotlinP
} }
private class KotlinPluginLifecycleCoroutineContextElement( private class KotlinPluginLifecycleCoroutineContextElement(
val lifecycle: KotlinPluginLifecycle val lifecycle: KotlinPluginLifecycle,
) : CoroutineContext.Element { ) : CoroutineContext.Element {
companion object Key : CoroutineContext.Key<KotlinPluginLifecycleCoroutineContextElement> companion object Key : CoroutineContext.Key<KotlinPluginLifecycleCoroutineContextElement>
@@ -0,0 +1,78 @@
/*
* 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.utils
import org.gradle.api.Project
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.tooling.model.kotlin.dsl.KotlinDslModelsParameters
import org.jetbrains.kotlin.gradle.plugin.internal.configurationTimePropertiesAccessor
import org.jetbrains.kotlin.gradle.plugin.internal.usedAtConfigurationTime
/**
* Returns *all* failures that have already happened during project configuration.
* This property will respect a special mode called 'ClasspathMode' which is triggered by the IDE.
* In this mode, Gradle will catch all exceptions and put it into a special 'Collector'.
* This exceptions will also be available in the returned list.
*
* In regular (non 'ClasspathMode'), the returned list is expected to have only one or zero elements.
*/
internal val Project.failures: List<Throwable>
get() {
/* Respecting special mode in IDE that catches exceptions and collects them outside of Project.state.failure */
val failuresFromIdeaSyncClasspathMode = if (ideaSyncClasspathModeUtil.isClasspathModeActive) ideaSyncClasspathModeUtil.exceptions
else emptyList()
val regularFailures = listOfNotNull(project.state.failure)
return regularFailures + failuresFromIdeaSyncClasspathMode
}
/**
* Special Mode only active during IDEA sync (using the 'classpath button' instead of the 'reload button').
* In this ide sync mode, Gradle is configured to actually catch exceptions during the buildscript evaluation.
* Those exceptions will be collected into a special Gradle service.
*
* Accessing those exceptions here via internal APIs is necessary, because afterEvaluate based hooks
* are still executed. The should *not* run if the project is in a 'bad' failure state.
*
* @see runProjectConfigurationHealthCheck
* @see runProjectConfigurationHealthCheckWhenEvaluated
*/
private val Project.ideaSyncClasspathModeUtil
get() = object {
val isClasspathModeActive: Boolean
/*
ConfigurationTimePropertiesAccessorVariantFactory type is not known for plugin variants.
Be lenient in cases where the factory is not available (e.g. functionalTests where just a blank project is used)
*/
get() = runCatching {
providers
.systemProperty(KotlinDslModelsParameters.PROVIDER_MODE_SYSTEM_PROPERTY_NAME)
.usedAtConfigurationTime(configurationTimePropertiesAccessor)
.orNull == KotlinDslModelsParameters.CLASSPATH_MODE_SYSTEM_PROPERTY_VALUE
}.getOrElse { failure ->
logger.error("Failed to access '${KotlinDslModelsParameters.PROVIDER_MODE_SYSTEM_PROPERTY_NAME}'", failure)
false
}
val exceptions: List<Exception>
get() {
try {
val classPathModeExceptionCollectionClass = Class.forName(
"org.gradle.kotlin.dsl.provider.ClassPathModeExceptionCollector"
)
val exceptionCollector = (project as ProjectInternal).services.get(classPathModeExceptionCollectionClass)
@Suppress("unchecked_cast")
return classPathModeExceptionCollectionClass.methods
.first { it.name == "getExceptions" }
.invoke(exceptionCollector) as List<Exception>
} catch (t: Throwable) {
logger.error("Failed to access 'ClassPathModeExceptionCollector'", t)
return emptyList()
}
}
}
@@ -6,14 +6,9 @@
package org.jetbrains.kotlin.gradle.utils package org.jetbrains.kotlin.gradle.utils
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.api.provider.Provider
import org.gradle.tooling.model.kotlin.dsl.KotlinDslModelsParameters
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle
import org.jetbrains.kotlin.gradle.plugin.internal.configurationTimePropertiesAccessor
import org.jetbrains.kotlin.gradle.plugin.internal.usedAtConfigurationTime
import org.jetbrains.kotlin.gradle.plugin.launchInStage
import org.jetbrains.kotlin.gradle.plugin.whenEvaluated import org.jetbrains.kotlin.gradle.plugin.whenEvaluated
import org.jetbrains.kotlin.gradle.plugin.launchInStage
/** /**
* Function used to wrap any checks/assertions done on the current project configuration / project model. * Function used to wrap any checks/assertions done on the current project configuration / project model.
@@ -62,32 +57,13 @@ import org.jetbrains.kotlin.gradle.plugin.whenEvaluated
*/ */
internal inline fun Project.runProjectConfigurationHealthCheck(check: Project.() -> Unit) { internal inline fun Project.runProjectConfigurationHealthCheck(check: Project.() -> Unit) {
/* Running configuration checks on a failed project will only lead to false positive error messages */ /* Running configuration checks on a failed project will only lead to false positive error messages */
if (state.failure != null || (inLenientMode() && syncExceptionsAreNotEmpty())) { if (failures.isNotEmpty()) {
return return
} }
check() check()
} }
// ClassPathModeExceptionCollector is available only via 'gradleKotlinDsl()' dependency which brings in full Gradle jar
private fun Project.syncExceptionsAreNotEmpty(): Boolean {
val classPathModeExceptionCollectionClass = Class.forName("org.gradle.kotlin.dsl.provider.ClassPathModeExceptionCollector")
val exceptionCollector = (this as ProjectInternal).services.get(classPathModeExceptionCollectionClass)
@Suppress("UNCHECKED_CAST")
val exceptionsList = classPathModeExceptionCollectionClass.methods
.first { it.name == "getExceptions" }
.invoke(exceptionCollector) as List<Exception>
return exceptionsList.isNotEmpty()
}
private val Project.providerModeSystemPropertyValue: Provider<String>
get() = providers
.systemProperty(KotlinDslModelsParameters.PROVIDER_MODE_SYSTEM_PROPERTY_NAME)
.usedAtConfigurationTime(configurationTimePropertiesAccessor)
private fun Project.inLenientMode() =
providerModeSystemPropertyValue.orNull == KotlinDslModelsParameters.CLASSPATH_MODE_SYSTEM_PROPERTY_VALUE
/** /**
* Convenience function for * Convenience function for