diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 1010bfbd321..136791891a9 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -15,4 +15,4 @@
-
\ No newline at end of file
+
diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/NewMultiplatformIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/NewMultiplatformIT.kt
index 806f596c50c..dfe2f44a24b 100644
--- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/NewMultiplatformIT.kt
+++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/NewMultiplatformIT.kt
@@ -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"
diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-mpp-classpathMode/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-mpp-classpathMode/build.gradle.kts
new file mode 100644
index 00000000000..544b9e229cc
--- /dev/null
+++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-mpp-classpathMode/build.gradle.kts
@@ -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()
+ 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")
diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-mpp-classpathMode/settings.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-mpp-classpathMode/settings.gradle.kts
new file mode 100644
index 00000000000..ecdece6ecfe
--- /dev/null
+++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-mpp-classpathMode/settings.gradle.kts
@@ -0,0 +1,10 @@
+pluginManagement {
+ repositories {
+ mavenLocal()
+ gradlePluginPortal()
+ }
+ val kotlin_version: String by settings
+ plugins {
+ kotlin("multiplatform").version(kotlin_version)
+ }
+}
diff --git a/libraries/tools/kotlin-gradle-plugin/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin/build.gradle.kts
index aac5ac66ccf..2f660868bfc 100644
--- a/libraries/tools/kotlin-gradle-plugin/build.gradle.kts
+++ b/libraries/tools/kotlin-gradle-plugin/build.gradle.kts
@@ -26,6 +26,7 @@ pill {
}
dependencies {
+ compileOnly(gradleKotlinDsl())
api(project(":kotlin-gradle-plugin-api"))
api(project(":kotlin-gradle-plugin-model"))
compileOnly(project(":compiler"))
diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt
index 63ac7b13ff0..a3d01eec927 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt
@@ -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
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
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 }
diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinMultiplatformPlugin.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinMultiplatformPlugin.kt
index 57b8d5000a0..c89a84000c0 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinMultiplatformPlugin.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinMultiplatformPlugin.kt
@@ -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 {
UnusedSourceSetsChecker.checkSourceSets(project)
- project.whenEvaluated {
+ project.runPostEvaluationProjectConfigurationHealthCheck {
checkSourceSetVisibilityRequirements(project)
}
}
diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/pm20/KotlinGradleFragment.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/pm20/KotlinGradleFragment.kt
index 19e6ae35e4b..6028e62ccba 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/pm20/KotlinGradleFragment.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/pm20/KotlinGradleFragment.kt
@@ -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
- )
\ No newline at end of file
+ )
diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/DefaultKotlinSourceSet.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/DefaultKotlinSourceSet.kt
index ddff31440e0..8b0e048839b 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/DefaultKotlinSourceSet.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/DefaultKotlinSourceSet.kt
@@ -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()
diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsPlugin.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsPlugin.kt
index 97675ecd6b7..2204ac30c3f 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsPlugin.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsPlugin.kt
@@ -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(
"""
diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTargetPreset.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTargetPreset.kt
index b9b1b24b277..26c16cba7a9 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTargetPreset.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTargetPreset.kt
@@ -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(
"""
diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTargetPreset.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTargetPreset.kt
index b5223ab4291..159eb8b9b7b 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTargetPreset.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTargetPreset.kt
@@ -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(
"""
diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/utils/runProjectConfigurationCheck.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/utils/runProjectConfigurationCheck.kt
new file mode 100644
index 00000000000..afabf4b33aa
--- /dev/null
+++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/utils/runProjectConfigurationCheck.kt
@@ -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().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)
+ }
+}