[Gradle] Don't run post-evaluation-checks on projects that failed to configure

When in 'lenient' or 'classpath' mode, Gradle will catch exceptions
during project evaluation and collect them into the
'ClassPathModeExceptionCollector'.

Running checks on the project after evaluation should only happen
if the project is properly configured (aka no such exceptions collected)
to prevent false positive error reports (that will then fail
the Gradle process and swallow the real cause)

^KT-48823 Verification Pending
This commit is contained in:
sebastian.sellmair
2021-09-27 10:41:37 +02:00
committed by Space
parent e4827efb4a
commit 29fb74d27d
13 changed files with 157 additions and 18 deletions
+1 -1
View File
@@ -15,4 +15,4 @@
<component name="KotlinCompilerSettings">
<option name="additionalArguments" value="-version -Xallow-kotlin-package -Xskip-metadata-version-check" />
</component>
</project>
</project>
@@ -1848,6 +1848,30 @@ class NewMultiplatformIT : BaseGradleIT() {
}
}
@Test
fun testErrorInClasspathMode() {
val classpathModeOptions = defaultBuildOptions().copy(
freeCommandLineArgs = listOf("-Dorg.gradle.kotlin.dsl.provider.mode=classpath")
)
with(Project("kotlin-mpp-classpathMode")) {
build("tasks") {
assertFailed()
assertContains("ERROR DURING CONFIGURATION PHASE")
}
build("tasks", options = classpathModeOptions) {
assertSuccessful()
}
build("listCollectedErrors", options = classpathModeOptions) {
assertSuccessful()
assertContains("Collected 1 exception(s)")
assertContains("ERROR DURING CONFIGURATION PHASE")
}
}
}
private fun detectNativeEnabledCompilation(): String = when {
HostManager.hostIsLinux -> "linuxX64"
HostManager.hostIsMingw -> "mingwX64"
@@ -0,0 +1,20 @@
import org.jetbrains.kotlin.gradle.plugin.sources.DefaultKotlinSourceSet
import org.jetbrains.kotlin.konan.target.HostManager
import org.gradle.kotlin.dsl.support.serviceOf
import org.gradle.kotlin.dsl.provider.ClassPathModeExceptionCollector
plugins {
kotlin("multiplatform")
}
tasks.create("listCollectedErrors") {
doFirst {
val exceptionCollector = serviceOf<ClassPathModeExceptionCollector>()
logger.quiet("Collected ${exceptionCollector.exceptions.size} exception(s)")
exceptionCollector.exceptions.forEach { e ->
logger.log(LogLevel.ERROR, "Exception: ${e.message}", e)
}
}
}
error("ERROR DURING CONFIGURATION PHASE")
@@ -0,0 +1,10 @@
pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
}
val kotlin_version: String by settings
plugins {
kotlin("multiplatform").version(kotlin_version)
}
}
@@ -26,6 +26,7 @@ pill {
}
dependencies {
compileOnly(gradleKotlinDsl())
api(project(":kotlin-gradle-plugin-api"))
api(project(":kotlin-gradle-plugin-model"))
compileOnly(project(":compiler"))
@@ -45,6 +45,7 @@ import org.jetbrains.kotlin.gradle.testing.internal.KotlinTestsRegistry
import org.jetbrains.kotlin.gradle.tooling.buildKotlinToolingMetadataTask
import org.jetbrains.kotlin.gradle.utils.checkGradleCompatibility
import org.jetbrains.kotlin.gradle.utils.loadPropertyFromResources
import org.jetbrains.kotlin.gradle.utils.runProjectConfigurationHealthCheck
import org.jetbrains.kotlin.statistics.metrics.StringMetrics
import javax.inject.Inject
import kotlin.reflect.KClass
@@ -201,7 +202,7 @@ open class KotlinJsPluginWrapper : KotlinBasePluginWrapper() {
override val projectExtensionClass: KClass<out KotlinJsProjectExtension>
get() = KotlinJsProjectExtension::class
override fun whenBuildEvaluated(project: Project) {
override fun whenBuildEvaluated(project: Project) = project.runProjectConfigurationHealthCheck {
val isJsTargetUninitialized = (project.kotlinExtension as KotlinJsProjectExtension)
._target == null
@@ -232,7 +233,7 @@ open class KotlinMultiplatformPluginWrapper : KotlinBasePluginWrapper() {
override val projectExtensionClass: KClass<out KotlinMultiplatformExtension>
get() = KotlinMultiplatformExtension::class
override fun whenBuildEvaluated(project: Project) {
override fun whenBuildEvaluated(project: Project) = project.runProjectConfigurationHealthCheck {
val isNoTargetsInitialized = (project.kotlinExtension as KotlinMultiplatformExtension)
.targets
.none { it !is KotlinMetadataTarget }
@@ -26,18 +26,16 @@ import org.jetbrains.kotlin.gradle.internal.customizeKotlinDependencies
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMultiplatformPlugin.Companion.sourceSetFreeCompilerArgsPropertyName
import org.jetbrains.kotlin.gradle.plugin.mpp.internal.handleHierarchicalStructureFlagsMigration
import org.jetbrains.kotlin.gradle.plugin.sources.*
import org.jetbrains.kotlin.gradle.plugin.sources.CleanupStaleSourceSetMetadataEntriesService
import org.jetbrains.kotlin.gradle.plugin.sources.DefaultLanguageSettingsBuilder
import org.jetbrains.kotlin.gradle.plugin.sources.SourceSetMetadataStorageForIde
import org.jetbrains.kotlin.gradle.plugin.sources.checkSourceSetVisibilityRequirements
import org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildStatsService
import org.jetbrains.kotlin.gradle.scripting.internal.ScriptingGradleSubplugin
import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTargetPreset
import org.jetbrains.kotlin.gradle.tasks.locateTask
import org.jetbrains.kotlin.gradle.tasks.registerTask
import org.jetbrains.kotlin.gradle.utils.*
import org.jetbrains.kotlin.gradle.utils.SingleActionPerBuild
import org.jetbrains.kotlin.gradle.utils.SingleWarningPerBuild
import org.jetbrains.kotlin.gradle.utils.checkGradleCompatibility
import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName
import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.konan.target.KonanTarget.*
import org.jetbrains.kotlin.konan.target.presetName
@@ -229,7 +227,7 @@ class KotlinMultiplatformPlugin : Plugin<Project> {
UnusedSourceSetsChecker.checkSourceSets(project)
project.whenEvaluated {
project.runPostEvaluationProjectConfigurationHealthCheck {
checkSourceSetVisibilityRequirements(project)
}
}
@@ -11,7 +11,8 @@ import org.gradle.api.Project
import org.gradle.api.file.SourceDirectorySet
import org.gradle.api.provider.Provider
import org.gradle.util.ConfigureUtil
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
import org.jetbrains.kotlin.gradle.plugin.LanguageSettingsBuilder
import org.jetbrains.kotlin.gradle.plugin.mpp.DefaultKotlinDependencyHandler
import org.jetbrains.kotlin.gradle.plugin.mpp.toModuleDependency
import org.jetbrains.kotlin.gradle.plugin.sources.DefaultLanguageSettingsBuilder
@@ -19,6 +20,7 @@ import org.jetbrains.kotlin.gradle.plugin.sources.FragmentConsistencyChecker
import org.jetbrains.kotlin.gradle.plugin.sources.FragmentConsistencyChecks
import org.jetbrains.kotlin.gradle.utils.addExtendsFromRelation
import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName
import org.jetbrains.kotlin.gradle.utils.runPostEvaluationProjectConfigurationHealthCheck
import org.jetbrains.kotlin.project.model.KotlinModuleDependency
import org.jetbrains.kotlin.project.model.KotlinModuleFragment
import org.jetbrains.kotlin.project.model.refinesClosure
@@ -54,7 +56,7 @@ open class KotlinGradleFragmentInternal @Inject constructor(
project.addExtendsFromRelation(getConfiguration(this), getConfiguration(other.get())) // todo eager instantiation; fix?
}
project.whenEvaluated {
project.runPostEvaluationProjectConfigurationHealthCheck {
kotlinGradleFragmentConsistencyChecker.runAllChecks(this@KotlinGradleFragmentInternal, other.get())
}
}
@@ -117,4 +119,4 @@ internal val kotlinGradleFragmentConsistencyChecker =
unitName = "fragment",
languageSettings = { languageSettings }
).allChecks
)
)
@@ -17,7 +17,6 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.LanguageSettingsBuilder
import org.jetbrains.kotlin.gradle.plugin.mpp.*
import org.jetbrains.kotlin.gradle.plugin.whenEvaluated
import org.jetbrains.kotlin.gradle.utils.*
import java.io.File
import java.util.*
@@ -93,7 +92,9 @@ class DefaultKotlinSourceSet(
// Fail-fast approach: check on each new added edge and report a circular dependency at once when the edge is added.
checkForCircularDependencies()
project.whenEvaluated { defaultSourceSetLanguageSettingsChecker.runAllChecks(this@DefaultKotlinSourceSet, other) }
project.runPostEvaluationProjectConfigurationHealthCheck {
defaultSourceSetLanguageSettingsChecker.runAllChecks(this@DefaultKotlinSourceSet, other)
}
}
private val dependsOnSourceSetsImpl = mutableSetOf<KotlinSourceSet>()
@@ -16,7 +16,6 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.TEST_COMPI
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsSingleTargetPreset
import org.jetbrains.kotlin.gradle.plugin.mpp.setupGeneralKotlinExtensionParameters
import org.jetbrains.kotlin.gradle.plugin.whenEvaluated
import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrSingleTargetPreset
import org.jetbrains.kotlin.gradle.utils.*
@@ -47,7 +46,7 @@ open class KotlinJsPlugin(
defaultJsCompilerType = PropertiesProvider(project).jsCompiler
}
project.whenEvaluated {
project.runPostEvaluationProjectConfigurationHealthCheck {
if (kotlinExtension._target == null) {
project.logger.warn(
"""
@@ -15,6 +15,7 @@ import org.jetbrains.kotlin.gradle.targets.js.KotlinJsTarget
import org.jetbrains.kotlin.gradle.targets.js.KotlinJsTargetConfigurator
import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTargetPreset
import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName
import org.jetbrains.kotlin.gradle.utils.runPostEvaluationProjectConfigurationHealthCheck
import org.jetbrains.kotlin.statistics.metrics.StringMetrics
open class KotlinJsTargetPreset(
@@ -54,7 +55,7 @@ open class KotlinJsTargetPreset(
}
this.isMpp = this@KotlinJsTargetPreset.isMpp
project.whenEvaluated {
project.runPostEvaluationProjectConfigurationHealthCheck {
if (!isBrowserConfigured && !isNodejsConfigured) {
project.logger.warn(
"""
@@ -12,6 +12,7 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinOnlyTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinOnlyTargetPreset
import org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildStatsService
import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName
import org.jetbrains.kotlin.gradle.utils.runPostEvaluationProjectConfigurationHealthCheck
import org.jetbrains.kotlin.statistics.metrics.StringMetrics
open class KotlinJsIrTargetPreset(
@@ -31,7 +32,7 @@ open class KotlinJsIrTargetPreset(
return project.objects.newInstance(KotlinJsIrTarget::class.java, project, platformType, mixedMode).apply {
this.isMpp = this@KotlinJsIrTargetPreset.isMpp
if (!mixedMode) {
project.whenEvaluated {
project.runPostEvaluationProjectConfigurationHealthCheck {
if (!isBrowserConfigured && !isNodejsConfigured) {
project.logger.warn(
"""
@@ -0,0 +1,81 @@
/*
* Copyright 2010-2021 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.kotlin.dsl.provider.ClassPathModeExceptionCollector
import org.gradle.kotlin.dsl.provider.inLenientMode
import org.gradle.kotlin.dsl.support.serviceOf
import org.jetbrains.kotlin.gradle.plugin.whenEvaluated
/**
* Function used to wrap any checks/assertions done on the current project configuration / project model.
*
* Runs the given [check] only on projects that are considered "healthy".
* A "healthy" project did evaluate correctly which means "without exceptions/errors".
*
* This function has to be used over "just running the check", because running project configuration checks
* on projects that failed to configure will lead to false positive error reporting.
* In most cases (when called in 'afterEvaluate') such false positive error message will even swallow the real root cause
* of configuration failure.
*
* Note:
* During Gradle/IDEA sync (import), Gradle will be set into `lenientMode` and will catch all exceptions
* during evaluation of the build script. Those exceptions will be put into the [ClassPathModeExceptionCollector].
* Any project that contains caught and collected exceptions in this 'collector' should be considered failed
* and running project model checks is undesirable. In this mode, throwing exceptions in `afterEvaluate` will even fail the process
* which would swallow the previously collected exceptions.
*
* Example:
* We have a post-evaluation check that will report users an error if no Kotlin target
* was registered.
*
* Consider the following build script:
*
* ```kotlin
* plugins {
* kotlin("multiplatform")
* }
*
* error("Something went wrong during the configuration phase")
*
* kotlin {
* jvm() // <- * Note: jvm target registered
* js() // <- * Note: js target registered
* }
* ```
*
* In this example, the exception is thrown before the configuration of Kotlin targets.
* During IDEA import, this exception will be caught and put into the [ClassPathModeExceptionCollector].
* When running the assertion just plainly (*without this wrapper function*), the user
* will not see the real cause of failure, but a rather bizarre:
* "Please initialize at least one Kotlin target"
* error message. Which is not helpful at all.
*
*/
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() && serviceOf<ClassPathModeExceptionCollector>().exceptions.isNotEmpty())) {
return
}
check()
}
/**
* Convenience function for
* ```kotlin
* whenEvaluated {
* runProjectConfigurationCheck(action)
* }
* ```
* @see runProjectConfigurationHealthCheck
*/
internal inline fun Project.runPostEvaluationProjectConfigurationHealthCheck(crossinline action: Project.() -> Unit) {
whenEvaluated {
runProjectConfigurationHealthCheck(action)
}
}