From f616fffeb0b23b840af57fe4927cbf01f2d0b940 Mon Sep 17 00:00:00 2001 From: Sebastian Sellmair Date: Wed, 3 May 2023 16:05:37 +0200 Subject: [PATCH] [Gradle] KotlinPluginLifecycle: Detect failure and abort further configuration KT-58275 --- .../gradle/plugin/KotlinPluginLifecycle.kt | 48 +++++++++--- .../jetbrains/kotlin/gradle/utils/failures.kt | 78 +++++++++++++++++++ .../runProjectConfigurationHealthCheck.kt | 28 +------ 3 files changed, 117 insertions(+), 37 deletions(-) create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/failures.kt diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginLifecycle.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginLifecycle.kt index d2604d7c59c..f4ec3af0ef2 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginLifecycle.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginLifecycle.kt @@ -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 Project.newKotlinPluginLifecycleAwareProperty( - finaliseIn: Stage = Stage.FinaliseDsl, initialValue: T? = null + finaliseIn: Stage = Stage.FinaliseDsl, initialValue: T? = null, ): LifecycleAwareProperty { return kotlinPluginLifecycle.newProperty(T::class.java, finaliseIn, initialValue) } @@ -163,7 +163,7 @@ internal inline fun Project.newKotlinPluginLifecycleAwarePrope * ``` */ internal inline fun KotlinPluginLifecycle.newProperty( - finaliseIn: Stage = Stage.FinaliseDsl, initialValue: T? = null + finaliseIn: Stage = Stage.FinaliseDsl, initialValue: T? = null, ): LifecycleAwareProperty { return newProperty(T::class.java, finaliseIn, initialValue) } @@ -334,7 +334,7 @@ internal interface KotlinPluginLifecycle { suspend fun await(stage: Stage) fun newProperty( - type: Class, finaliseIn: Stage, initialValue: T? + type: Class, finaliseIn: Stage, initialValue: T?, ): LifecycleAwareProperty 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, WeakReference>>() 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) { + 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( override val finaliseIn: Stage, - override val property: Property + override val property: Property, ) : LifecycleAwareProperty { 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 diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/failures.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/failures.kt new file mode 100644 index 00000000000..4c5310e88fc --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/failures.kt @@ -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 + 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 + 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 + } catch (t: Throwable) { + logger.error("Failed to access 'ClassPathModeExceptionCollector'", t) + return emptyList() + } + } + } diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/runProjectConfigurationHealthCheck.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/runProjectConfigurationHealthCheck.kt index 18794ebf718..36639201919 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/runProjectConfigurationHealthCheck.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/runProjectConfigurationHealthCheck.kt @@ -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 - - return exceptionsList.isNotEmpty() -} - -private val Project.providerModeSystemPropertyValue: Provider - 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