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) + } +}