[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.provider.Property
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.*
import org.jetbrains.kotlin.gradle.utils.failures
import org.jetbrains.kotlin.gradle.utils.getOrPut
import java.lang.ref.WeakReference
import java.util.*
@@ -137,12 +138,11 @@ internal suspend fun Stage.await() {
currentKotlinPluginLifecycle().await(this)
}
/**
* See [newProperty]
*/
internal inline fun <reified T : Any> Project.newKotlinPluginLifecycleAwareProperty(
finaliseIn: Stage = Stage.FinaliseDsl, initialValue: T? = null
finaliseIn: Stage = Stage.FinaliseDsl, initialValue: T? = null,
): LifecycleAwareProperty<T> {
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(
finaliseIn: Stage = Stage.FinaliseDsl, initialValue: T? = null
finaliseIn: Stage = Stage.FinaliseDsl, initialValue: T? = null,
): LifecycleAwareProperty<T> {
return newProperty(T::class.java, finaliseIn, initialValue)
}
@@ -334,7 +334,7 @@ internal interface KotlinPluginLifecycle {
suspend fun await(stage: Stage)
fun <T : Any> newProperty(
type: Class<T>, finaliseIn: Stage, initialValue: T?
type: Class<T>, finaliseIn: Stage, initialValue: T?,
): LifecycleAwareProperty<T>
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 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<*>>>()
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 {
isFinished.set(true)
finishSuccessfully()
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) {
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
will be executed right away (no suspend necessary or wanted)
*/
if (isFinished.get()) {
if (isFinishedSuccessfully.get()) {
action()
return
}
if (isFinishedWithExceptions.get()) {
throw IllegalLifecycleException("Cannot enqueue Action: Lifecycle already finished with Exceptions")
}
enqueuedActions.getValue(stage).addLast(action)
if (stage == Stage.EvaluateBuildscript && isStarted.get()) {
@@ -490,7 +516,7 @@ private class KotlinPluginLifecycleImpl(override val project: Project) : KotlinP
private class LifecycleAwarePropertyImpl<T : Any>(
override val finaliseIn: Stage,
override val property: Property<T>
override val property: Property<T>,
) : LifecycleAwareProperty<T> {
override suspend fun awaitFinalValue(): T? {
@@ -501,7 +527,7 @@ private class KotlinPluginLifecycleImpl(override val project: Project) : KotlinP
}
private class KotlinPluginLifecycleCoroutineContextElement(
val lifecycle: KotlinPluginLifecycle
val lifecycle: KotlinPluginLifecycle,
) : CoroutineContext.Element {
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
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.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.launchInStage
/**
* 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) {
/* 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
}
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