[Gradle] KotlinPluginLifecycle: Detect failure and abort further configuration
KT-58275
This commit is contained in:
committed by
Space Team
parent
8e9c3a21c0
commit
f616fffeb0
+37
-11
@@ -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>
|
||||
|
||||
|
||||
+78
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-26
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user