[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.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>
|
||||||
|
|
||||||
|
|||||||
+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
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user