From 409a85f55290a5ccca34372760b0c9336e74e546 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Sat, 30 Jul 2022 23:06:29 +0200 Subject: [PATCH] Split dependency management code into 3 files ^KT-41642 Fixed --- .../kotlin/gradle/ConfigurationCacheIT.kt | 2 +- .../gradle/KotlinSpecificDependenciesIT.kt | 3 +- .../internal/KotlinDependenciesManagement.kt | 369 +----------------- .../kotlinTestDependencyManagement.kt | 214 ++++++++++ .../internal/stdlibDependencyManagement.kt | 198 ++++++++++ 5 files changed, 415 insertions(+), 371 deletions(-) create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/internal/kotlinTestDependencyManagement.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/internal/stdlibDependencyManagement.kt diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/ConfigurationCacheIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/ConfigurationCacheIT.kt index 89c09eb1a0b..70b63e8a4f2 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/ConfigurationCacheIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/ConfigurationCacheIT.kt @@ -94,7 +94,7 @@ class ConfigurationCacheIT : AbstractConfigurationCacheIT() { build("build", buildOptions = buildOptions) { // Reduce the problem numbers when a Task become compatible with GCC. // When all tasks support GCC, replace these assertions with `testConfigurationCacheOf` - assertOutputContains("15 problems were found storing the configuration cache, 5 of which seem unique.") + assertOutputContains("16 problems were found storing the configuration cache, 6 of which seem unique.") configCacheIncompatibleTaskTypes.forEach { taskType -> assertOutputContains( """Task `\S+` of type `[\w.]+$taskType`: .+(at execution time is unsupported)|(not supported with the configuration cache)""" diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinSpecificDependenciesIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinSpecificDependenciesIT.kt index 058c9dbfbaf..b9636bd610d 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinSpecificDependenciesIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinSpecificDependenciesIT.kt @@ -201,7 +201,6 @@ class KotlinSpecificDependenciesIT : KGPBaseTest() { buildGradle.appendText( """ - kotlin.target.compilations["main"].kotlinOptions.jvmTarget = "1.8" dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") } """.trimIndent() ) @@ -669,7 +668,7 @@ class KotlinSpecificDependenciesIT : KGPBaseTest() { """.trimIndent() ) - build("${subproject?.prependIndent(":").orEmpty()}:$printingTaskName", forceOutput = true) { + build("${subproject?.prependIndent(":").orEmpty()}:$printingTaskName") { val itemsLine = output.lines().single { "###$printingTaskName" in it }.substringAfter(printingTaskName) val items = itemsLine.removeSurrounding("[", "]").split(", ").toSet() checkAnyItemsContains.forEach { pattern -> diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/internal/KotlinDependenciesManagement.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/internal/KotlinDependenciesManagement.kt index d9a62ea708e..271c5e08165 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/internal/KotlinDependenciesManagement.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/internal/KotlinDependenciesManagement.kt @@ -5,40 +5,14 @@ package org.jetbrains.kotlin.gradle.internal -import com.android.build.gradle.api.TestVariant -import com.android.build.gradle.api.UnitTestVariant -import org.gradle.api.NamedDomainObjectSet import org.gradle.api.Project -import org.gradle.api.Task import org.gradle.api.artifacts.* import org.gradle.api.artifacts.dsl.DependencyHandler import org.gradle.api.provider.Provider -import org.gradle.api.tasks.TaskContainer -import org.gradle.api.tasks.TaskProvider -import org.gradle.api.tasks.testing.Test -import org.gradle.api.tasks.testing.junit.JUnitOptions -import org.gradle.api.tasks.testing.junitplatform.JUnitPlatformOptions -import org.gradle.api.tasks.testing.testng.TestNGOptions import org.jetbrains.kotlin.gradle.dsl.* -import org.jetbrains.kotlin.gradle.execution.KotlinAggregateExecutionSource import org.jetbrains.kotlin.gradle.plugin.* import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider import org.jetbrains.kotlin.gradle.plugin.mpp.* -import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.GradleKpmFragment -import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.GradleKpmModule -import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.hasKpmModel -import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.kpmModules -import org.jetbrains.kotlin.gradle.plugin.sources.KotlinDependencyScope -import org.jetbrains.kotlin.gradle.plugin.sources.android.AndroidBaseSourceSetName -import org.jetbrains.kotlin.gradle.plugin.sources.android.AndroidVariantType -import org.jetbrains.kotlin.gradle.plugin.sources.android.androidSourceSetInfoOrNull -import org.jetbrains.kotlin.gradle.plugin.sources.dependsOnClosure -import org.jetbrains.kotlin.gradle.plugin.sources.sourceSetDependencyConfigurationByScope -import org.jetbrains.kotlin.gradle.targets.js.npm.SemVer -import org.jetbrains.kotlin.gradle.targets.jvm.JvmCompilationsTestRunSource -import org.jetbrains.kotlin.gradle.tasks.locateTask -import org.jetbrains.kotlin.gradle.testing.KotlinTaskTestRun -import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName import org.jetbrains.kotlin.gradle.utils.providerWithLazyConvention import org.jetbrains.kotlin.gradle.utils.withType @@ -122,348 +96,7 @@ private fun KotlinTarget.excludeStdlibAndKotlinTestCommonFromPlatformCompilation } } -//region stdlib -private fun Project.configureStdlibDefaultDependency( - topLevelExtension: KotlinTopLevelExtension, - coreLibrariesVersion: Provider -) { - - when { - project.hasKpmModel -> addStdlibToKpmProject(project, coreLibrariesVersion) - topLevelExtension is KotlinJsProjectExtension -> topLevelExtension.registerTargetObserver { target -> - target?.addStdlibDependency(configurations, dependencies, coreLibrariesVersion) - } - topLevelExtension is KotlinSingleTargetExtension<*> -> topLevelExtension - .target - .addStdlibDependency(configurations, dependencies, coreLibrariesVersion) - - topLevelExtension is KotlinMultiplatformExtension -> topLevelExtension - .targets - .configureEach { target -> - target.addStdlibDependency(configurations, dependencies, coreLibrariesVersion) - } - } -} - -private fun addStdlibToKpmProject( - project: Project, - coreLibrariesVersion: Provider -) { - project.kpmModules.named(GradleKpmModule.MAIN_MODULE_NAME) { main -> - main.fragments.named(GradleKpmFragment.COMMON_FRAGMENT_NAME) { common -> - common.dependencies { - api(project.dependencies.kotlinDependency("kotlin-stdlib-common", coreLibrariesVersion.get())) - } - } - main.variants.configureEach { variant -> - val dependencyHandler = project.dependencies - val stdlibModule = when (variant.platformType) { - KotlinPlatformType.common -> error("variants are not expected to be common") - KotlinPlatformType.jvm -> chooseStdlibJvmDependency(coreLibrariesVersion) - KotlinPlatformType.js -> "kotlin-stdlib-js" - KotlinPlatformType.wasm -> "kotlin-stdlib-wasm" - KotlinPlatformType.androidJvm -> null // TODO: expect support on the AGP side? - KotlinPlatformType.native -> null - } - if (stdlibModule != null) { - variant.dependencies { - api(dependencyHandler.kotlinDependency(stdlibModule, coreLibrariesVersion.get())) - } - } - } - } -} - -private fun KotlinTarget.addStdlibDependency( - configurations: ConfigurationContainer, - dependencies: DependencyHandler, - coreLibrariesVersion: Provider -) { - compilations.configureEach { compilation -> - compilation.allKotlinSourceSets.forEach { kotlinSourceSet -> - val scope = if (compilation.isTest() || - (this is KotlinAndroidTarget && - kotlinSourceSet.isRelatedToAndroidTestSourceSet() - ) - ) { - KotlinDependencyScope.IMPLEMENTATION_SCOPE - } else { - KotlinDependencyScope.API_SCOPE - } - val scopeConfiguration = configurations - .sourceSetDependencyConfigurationByScope(kotlinSourceSet, scope) - - scopeConfiguration.withDependencies { dependencySet -> - // Check if stdlib is directly added to SourceSet - if (isStdlibAddedByUser(configurations, stdlibModules, kotlinSourceSet)) return@withDependencies - - val stdlibModule = compilation - .platformType - .stdlibPlatformType(coreLibrariesVersion, this, kotlinSourceSet) - ?: return@withDependencies - - // Check if stdlib module is added to SourceSets hierarchy - if ( - isStdlibAddedByUser( - configurations, - setOf(stdlibModule), - *kotlinSourceSet.dependsOnClosure.toTypedArray() - ) - ) return@withDependencies - - dependencySet.addLater( - coreLibrariesVersion.map { - dependencies.kotlinDependency(stdlibModule, it) - } - ) - } - } - } -} - -private fun isStdlibAddedByUser( - configurations: ConfigurationContainer, - stdlibModules: Set, - vararg sourceSets: KotlinSourceSet -): Boolean { - return sourceSets - .asSequence() - .flatMap { sourceSet -> - KotlinDependencyScope.values().map { scope -> - configurations.sourceSetDependencyConfigurationByScope(sourceSet, scope) - }.asSequence() - } - .flatMap { it.allNonProjectDependencies().asSequence() } - .any { dependency -> - dependency.group == KOTLIN_MODULE_GROUP && dependency.name in stdlibModules - } -} - -private fun KotlinPlatformType.stdlibPlatformType( - coreLibrariesVersion: Provider, - kotlinTarget: KotlinTarget, - kotlinSourceSet: KotlinSourceSet -): String? = when (this) { - KotlinPlatformType.jvm -> chooseStdlibJvmDependency(coreLibrariesVersion) - KotlinPlatformType.androidJvm -> { - if (kotlinTarget is KotlinAndroidTarget && - kotlinSourceSet.androidSourceSetInfoOrNull?.androidSourceSetName == AndroidBaseSourceSetName.Main.name - ) { - chooseStdlibJvmDependency(coreLibrariesVersion) - } else { - null - } - } - - KotlinPlatformType.js -> "kotlin-stdlib-js" - KotlinPlatformType.wasm -> "kotlin-stdlib-wasm" - KotlinPlatformType.native -> null - KotlinPlatformType.common -> // there's no platform compilation that the source set is default for - "kotlin-stdlib-common" -} - -private val kotlin180Version = SemVer(1.toBigInteger(), 8.toBigInteger(), 0.toBigInteger()) - -private fun chooseStdlibJvmDependency( - coreLibrariesVersion: Provider -): String { - // Current 'SemVer.satisfies' release always returns `false` for any "-SNAPSHOT" version. - return if (SemVer.from(coreLibrariesVersion.get()) < kotlin180Version) { - "kotlin-stdlib-jdk8" - } else { - "kotlin-stdlib" - } -} - -private val androidTestVariants = setOf(AndroidVariantType.UnitTest, AndroidVariantType.InstrumentedTest) - -private fun KotlinSourceSet.isRelatedToAndroidTestSourceSet(): Boolean { - val androidVariant = androidSourceSetInfoOrNull?.androidVariantType ?: return false - return androidVariant in androidTestVariants -} - -private val stdlibModules = setOf("kotlin-stdlib-common", "kotlin-stdlib", "kotlin-stdlib-jdk7", "kotlin-stdlib-jdk8", "kotlin-stdlib-js") -//endregion - -//region kotlin-test -private val Dependency.isKotlinTestRootDependency: Boolean - get() = group == KOTLIN_MODULE_GROUP && name == KOTLIN_TEST_ROOT_MODULE_NAME - -private val kotlin150Version = SemVer(1.toBigInteger(), 5.toBigInteger(), 0.toBigInteger()) - -private fun isAtLeast1_5(version: String) = SemVer.from(version) >= kotlin150Version - -private val jvmPlatforms = setOf(KotlinPlatformType.jvm, KotlinPlatformType.androidJvm) - -private fun Project.configureKotlinTestDependency( - topLevelExtension: KotlinTopLevelExtension, - coreLibrariesVersion: Provider, -) { - when (topLevelExtension) { - is KotlinJsProjectExtension -> topLevelExtension.registerTargetObserver { target -> - target?.configureKotlinTestDependency( - configurations, - coreLibrariesVersion, - objects, - dependencies, - tasks - ) - } - - is KotlinSingleTargetExtension<*> -> topLevelExtension.target.configureKotlinTestDependency( - configurations, - coreLibrariesVersion, - dependencies, - tasks - ) - - is KotlinMultiplatformExtension -> topLevelExtension.targets.configureEach { target -> - target.configureKotlinTestDependency( - configurations, - coreLibrariesVersion, - dependencies, - tasks - ) - } - } -} - -private fun KotlinTarget.configureKotlinTestDependency( - configurations: ConfigurationContainer, - coreLibrariesVersion: Provider, - dependencyHandler: DependencyHandler, - tasks: TaskContainer -) { - compilations.configureEach { compilation -> - val platformType = compilation.platformType - if (platformType in jvmPlatforms) { - // Checking dependencies which were added via dependsOn(KotlinSourceSet) call - // Compilation has own configurations for these that are different from KotlinSourceSet configurations - KotlinDependencyScope.values() - .map { configurations.sourceSetDependencyConfigurationByScope(compilation, it) } - .forEach { - it.maybeAddTestDependencyCapability( - compilation, - coreLibrariesVersion, - dependencyHandler, - tasks - ) - } - - compilation.kotlinSourceSets.forEach { sourceSet -> - KotlinDependencyScope.values() - .map { configurations.sourceSetDependencyConfigurationByScope(sourceSet, it) } - .forEach { - it.maybeAddTestDependencyCapability( - compilation, - coreLibrariesVersion, - dependencyHandler, - tasks - ) - } - } - } - } -} - -private fun Configuration.maybeAddTestDependencyCapability( - compilation: KotlinCompilation<*>, - coreLibrariesVersion: Provider, - dependencyHandler: DependencyHandler, - tasks: TaskContainer -) { - withDependencies { dependencies -> - val testRootDependency = allNonProjectDependencies() - .singleOrNull { it.isKotlinTestRootDependency } - - if (testRootDependency != null) { - val depVersion = testRootDependency.version ?: coreLibrariesVersion.get() - if (!isAtLeast1_5(depVersion)) return@withDependencies - - val testCapability = compilation.kotlinTestCapabilityForJvmSourceSet(tasks) - if (testCapability != null) { - dependencies.addLater( - testCapability.map { capability -> - dependencyHandler - .kotlinDependency(KOTLIN_TEST_ROOT_MODULE_NAME, depVersion) - .apply { - (this as ExternalDependency).capabilities { - it.requireCapability(capability) - } - } - } - ) - } - } - } -} - -private fun KotlinCompilation<*>.kotlinTestCapabilityForJvmSourceSet( - tasks: TaskContainer -): Provider? { - val compilationTarget = target - val testTaskList: List> = when { - compilationTarget is KotlinTargetWithTests<*, *> -> compilationTarget - .findTestRunsByCompilation(this) - .matching { it is KotlinTaskTestRun<*, *> } - .mapNotNull { (it as KotlinTaskTestRun<*, *>).executionTask } - - compilationTarget is KotlinWithJavaTarget<*> && - name == KotlinCompilation.TEST_COMPILATION_NAME -> - listOfNotNull(tasks.locateTask(compilationTarget.testTaskName)) - - this is KotlinJvmAndroidCompilation -> when (androidVariant) { - is UnitTestVariant -> listOfNotNull(tasks.locateTask(lowerCamelCaseName("test", androidVariant.name))) - is TestVariant -> listOfNotNull((androidVariant as TestVariant).connectedInstrumentTestProvider) - else -> emptyList() - } - - else -> emptyList() - } - - if (testTaskList.isEmpty()) return null - - return testTaskList - .singleOrNull() - ?.map { task -> - val framework = when (task) { - is Test -> testFrameworkOf(task) - else -> // Android connected test tasks don't inherit from Test, but we use JUnit for them - KotlinTestJvmFramework.junit - } - - "$KOTLIN_MODULE_GROUP:$KOTLIN_TEST_ROOT_MODULE_NAME-framework-$framework" - } -} - -internal const val KOTLIN_TEST_ROOT_MODULE_NAME = "kotlin-test" - -private enum class KotlinTestJvmFramework { - junit, testng, junit5 -} - -private fun testFrameworkOf(testTask: Test): KotlinTestJvmFramework = when (testTask.options) { - is JUnitOptions -> KotlinTestJvmFramework.junit - is JUnitPlatformOptions -> KotlinTestJvmFramework.junit5 - is TestNGOptions -> KotlinTestJvmFramework.testng - else -> // failed to detect, fallback to junit - KotlinTestJvmFramework.junit -} - -private fun KotlinTargetWithTests<*, *>.findTestRunsByCompilation( - byCompilation: KotlinCompilation<*> -): NamedDomainObjectSet> { - fun KotlinExecution.ExecutionSource.isProducedFromTheCompilation(): Boolean = when (this) { - is CompilationExecutionSource<*> -> compilation == byCompilation - is JvmCompilationsTestRunSource -> byCompilation in testCompilations - is KotlinAggregateExecutionSource<*> -> this.executionSources.any { it.isProducedFromTheCompilation() } - else -> false - } - return testRuns.matching { it.executionSource.isProducedFromTheCompilation() } -} -//endregion - internal fun DependencyHandler.kotlinDependency(moduleName: String, versionOrNull: String?) = create("$KOTLIN_MODULE_GROUP:$moduleName${versionOrNull?.prependIndent(":").orEmpty()}") -private fun Configuration.allNonProjectDependencies() = allDependencies.matching { it !is ProjectDependency } +internal fun Configuration.allNonProjectDependencies() = allDependencies.matching { it !is ProjectDependency } diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/internal/kotlinTestDependencyManagement.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/internal/kotlinTestDependencyManagement.kt new file mode 100644 index 00000000000..2e728dda869 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/internal/kotlinTestDependencyManagement.kt @@ -0,0 +1,214 @@ +/* + * Copyright 2010-2022 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.internal + +import com.android.build.gradle.api.TestVariant +import com.android.build.gradle.api.UnitTestVariant +import org.gradle.api.NamedDomainObjectSet +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.ConfigurationContainer +import org.gradle.api.artifacts.Dependency +import org.gradle.api.artifacts.ExternalDependency +import org.gradle.api.artifacts.dsl.DependencyHandler +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskContainer +import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.testing.Test +import org.gradle.api.tasks.testing.junit.JUnitOptions +import org.gradle.api.tasks.testing.junitplatform.JUnitPlatformOptions +import org.gradle.api.tasks.testing.testng.TestNGOptions +import org.jetbrains.kotlin.gradle.dsl.KotlinJsProjectExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinTopLevelExtension +import org.jetbrains.kotlin.gradle.execution.KotlinAggregateExecutionSource +import org.jetbrains.kotlin.gradle.plugin.* +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinWithJavaTarget +import org.jetbrains.kotlin.gradle.plugin.sources.KotlinDependencyScope +import org.jetbrains.kotlin.gradle.plugin.sources.sourceSetDependencyConfigurationByScope +import org.jetbrains.kotlin.gradle.targets.js.npm.SemVer +import org.jetbrains.kotlin.gradle.targets.jvm.JvmCompilationsTestRunSource +import org.jetbrains.kotlin.gradle.tasks.locateTask +import org.jetbrains.kotlin.gradle.testing.KotlinTaskTestRun +import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName + +private val Dependency.isKotlinTestRootDependency: Boolean + get() = group == KOTLIN_MODULE_GROUP && name == KOTLIN_TEST_ROOT_MODULE_NAME + +private val kotlin150Version = SemVer(1.toBigInteger(), 5.toBigInteger(), 0.toBigInteger()) + +private fun isAtLeast1_5(version: String) = SemVer.from(version) >= kotlin150Version + +private val jvmPlatforms = setOf(KotlinPlatformType.jvm, KotlinPlatformType.androidJvm) + +internal fun Project.configureKotlinTestDependency( + topLevelExtension: KotlinTopLevelExtension, + coreLibrariesVersion: Provider, +) { + when (topLevelExtension) { + is KotlinJsProjectExtension -> topLevelExtension.registerTargetObserver { target -> + target?.configureKotlinTestDependency( + configurations, + coreLibrariesVersion, + dependencies, + tasks + ) + } + + is KotlinSingleTargetExtension<*> -> topLevelExtension.target.configureKotlinTestDependency( + configurations, + coreLibrariesVersion, + dependencies, + tasks + ) + + is KotlinMultiplatformExtension -> topLevelExtension.targets.configureEach { target -> + target.configureKotlinTestDependency( + configurations, + coreLibrariesVersion, + dependencies, + tasks + ) + } + } +} + +private fun KotlinTarget.configureKotlinTestDependency( + configurations: ConfigurationContainer, + coreLibrariesVersion: Provider, + dependencyHandler: DependencyHandler, + tasks: TaskContainer +) { + compilations.configureEach { compilation -> + val platformType = compilation.platformType + if (platformType in jvmPlatforms) { + // Checking dependencies which were added via dependsOn(KotlinSourceSet) call + // Compilation has own configurations for these that are different from KotlinSourceSet configurations + KotlinDependencyScope.values() + .map { configurations.sourceSetDependencyConfigurationByScope(compilation, it) } + .forEach { + it.maybeAddTestDependencyCapability( + compilation, + coreLibrariesVersion, + dependencyHandler, + tasks + ) + } + + compilation.kotlinSourceSets.forEach { sourceSet -> + KotlinDependencyScope.values() + .map { configurations.sourceSetDependencyConfigurationByScope(sourceSet, it) } + .forEach { + it.maybeAddTestDependencyCapability( + compilation, + coreLibrariesVersion, + dependencyHandler, + tasks + ) + } + } + } + } +} + +private fun Configuration.maybeAddTestDependencyCapability( + compilation: KotlinCompilation<*>, + coreLibrariesVersion: Provider, + dependencyHandler: DependencyHandler, + tasks: TaskContainer +) { + withDependencies { dependencies -> + val testRootDependency = allNonProjectDependencies() + .singleOrNull { it.isKotlinTestRootDependency } + + if (testRootDependency != null) { + val depVersion = testRootDependency.version ?: coreLibrariesVersion.get() + if (!isAtLeast1_5(depVersion)) return@withDependencies + + val testCapability = compilation.kotlinTestCapabilityForJvmSourceSet(tasks) + if (testCapability != null) { + dependencies.addLater( + testCapability.map { capability -> + dependencyHandler + .kotlinDependency(KOTLIN_TEST_ROOT_MODULE_NAME, depVersion) + .apply { + (this as ExternalDependency).capabilities { + it.requireCapability(capability) + } + } + } + ) + } + } + } +} + +private fun KotlinCompilation<*>.kotlinTestCapabilityForJvmSourceSet( + tasks: TaskContainer, +): Provider? { + val compilationTarget = target + val testTaskList: List> = when { + compilationTarget is KotlinTargetWithTests<*, *> -> compilationTarget + .findTestRunsByCompilation(this) + .matching { it is KotlinTaskTestRun<*, *> } + .mapNotNull { (it as KotlinTaskTestRun<*, *>).executionTask } + + compilationTarget is KotlinWithJavaTarget<*> && + name == KotlinCompilation.TEST_COMPILATION_NAME -> + listOfNotNull(tasks.locateTask(compilationTarget.testTaskName)) + + this is KotlinJvmAndroidCompilation -> when (androidVariant) { + is UnitTestVariant -> listOfNotNull(tasks.locateTask(lowerCamelCaseName("test", androidVariant.name))) + is TestVariant -> listOfNotNull((androidVariant as TestVariant).connectedInstrumentTestProvider) + else -> emptyList() + } + + else -> emptyList() + } + + if (testTaskList.isEmpty()) return null + + return testTaskList + .singleOrNull() + ?.map { task -> + val framework = when (task) { + is Test -> testFrameworkOf(task) + else -> // Android connected test tasks don't inherit from Test, but we use JUnit for them + KotlinTestJvmFramework.junit + } + + "$KOTLIN_MODULE_GROUP:$KOTLIN_TEST_ROOT_MODULE_NAME-framework-$framework" + } +} + +internal const val KOTLIN_TEST_ROOT_MODULE_NAME = "kotlin-test" + +private enum class KotlinTestJvmFramework { + junit, testng, junit5 +} + +private fun testFrameworkOf(testTask: Test): KotlinTestJvmFramework = when (testTask.options) { + is JUnitOptions -> KotlinTestJvmFramework.junit + is JUnitPlatformOptions -> KotlinTestJvmFramework.junit5 + is TestNGOptions -> KotlinTestJvmFramework.testng + else -> // failed to detect, fallback to junit + KotlinTestJvmFramework.junit +} + +private fun KotlinTargetWithTests<*, *>.findTestRunsByCompilation( + byCompilation: KotlinCompilation<*> +): NamedDomainObjectSet> { + fun KotlinExecution.ExecutionSource.isProducedFromTheCompilation(): Boolean = when (this) { + is CompilationExecutionSource<*> -> compilation == byCompilation + is JvmCompilationsTestRunSource -> byCompilation in testCompilations + is KotlinAggregateExecutionSource<*> -> this.executionSources.any { it.isProducedFromTheCompilation() } + else -> false + } + return testRuns.matching { it.executionSource.isProducedFromTheCompilation() } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/internal/stdlibDependencyManagement.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/internal/stdlibDependencyManagement.kt new file mode 100644 index 00000000000..b346dcd8fe7 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/internal/stdlibDependencyManagement.kt @@ -0,0 +1,198 @@ +/* + * Copyright 2010-2022 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.internal + +import org.gradle.api.Project +import org.gradle.api.artifacts.ConfigurationContainer +import org.gradle.api.artifacts.dsl.DependencyHandler +import org.gradle.api.provider.Provider +import org.jetbrains.kotlin.gradle.dsl.KotlinJsProjectExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinTopLevelExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.isTest +import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.GradleKpmFragment +import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.GradleKpmModule +import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.hasKpmModel +import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.kpmModules +import org.jetbrains.kotlin.gradle.plugin.sources.KotlinDependencyScope +import org.jetbrains.kotlin.gradle.plugin.sources.android.AndroidBaseSourceSetName +import org.jetbrains.kotlin.gradle.plugin.sources.android.AndroidVariantType +import org.jetbrains.kotlin.gradle.plugin.sources.android.androidSourceSetInfoOrNull +import org.jetbrains.kotlin.gradle.plugin.sources.dependsOnClosure +import org.jetbrains.kotlin.gradle.plugin.sources.sourceSetDependencyConfigurationByScope +import org.jetbrains.kotlin.gradle.targets.js.npm.SemVer + +internal fun Project.configureStdlibDefaultDependency( + topLevelExtension: KotlinTopLevelExtension, + coreLibrariesVersion: Provider +) { + + when { + project.hasKpmModel -> addStdlibToKpmProject(project, coreLibrariesVersion) + topLevelExtension is KotlinJsProjectExtension -> topLevelExtension.registerTargetObserver { target -> + target?.addStdlibDependency(configurations, dependencies, coreLibrariesVersion) + } + topLevelExtension is KotlinSingleTargetExtension<*> -> topLevelExtension + .target + .addStdlibDependency(configurations, dependencies, coreLibrariesVersion) + + topLevelExtension is KotlinMultiplatformExtension -> topLevelExtension + .targets + .configureEach { target -> + target.addStdlibDependency(configurations, dependencies, coreLibrariesVersion) + } + } +} + +private fun addStdlibToKpmProject( + project: Project, + coreLibrariesVersion: Provider +) { + project.kpmModules.named(GradleKpmModule.MAIN_MODULE_NAME) { main -> + main.fragments.named(GradleKpmFragment.COMMON_FRAGMENT_NAME) { common -> + common.dependencies { + api(project.dependencies.kotlinDependency("kotlin-stdlib-common", coreLibrariesVersion.get())) + } + } + main.variants.configureEach { variant -> + val dependencyHandler = project.dependencies + val stdlibModule = when (variant.platformType) { + KotlinPlatformType.common -> error("variants are not expected to be common") + KotlinPlatformType.jvm -> chooseStdlibJvmDependency(coreLibrariesVersion) + KotlinPlatformType.js -> "kotlin-stdlib-js" + KotlinPlatformType.wasm -> "kotlin-stdlib-wasm" + KotlinPlatformType.androidJvm -> null // TODO: expect support on the AGP side? + KotlinPlatformType.native -> null + } + if (stdlibModule != null) { + variant.dependencies { + api(dependencyHandler.kotlinDependency(stdlibModule, coreLibrariesVersion.get())) + } + } + } + } +} + +private fun KotlinTarget.addStdlibDependency( + configurations: ConfigurationContainer, + dependencies: DependencyHandler, + coreLibrariesVersion: Provider +) { + compilations.configureEach { compilation -> + compilation.allKotlinSourceSets.forEach { kotlinSourceSet -> + val scope = if (compilation.isTest() || + (this is KotlinAndroidTarget && + kotlinSourceSet.isRelatedToAndroidTestSourceSet() + ) + ) { + KotlinDependencyScope.IMPLEMENTATION_SCOPE + } else { + KotlinDependencyScope.API_SCOPE + } + val scopeConfiguration = configurations + .sourceSetDependencyConfigurationByScope(kotlinSourceSet, scope) + + scopeConfiguration.withDependencies { dependencySet -> + // Check if stdlib is directly added to SourceSet + if (isStdlibAddedByUser(configurations, stdlibModules, kotlinSourceSet)) return@withDependencies + + val stdlibModule = compilation + .platformType + .stdlibPlatformType(coreLibrariesVersion, this, kotlinSourceSet) + ?: return@withDependencies + + // Check if stdlib module is added to SourceSets hierarchy + if ( + isStdlibAddedByUser( + configurations, + setOf(stdlibModule), + *kotlinSourceSet.dependsOnClosure.toTypedArray() + ) + ) return@withDependencies + + dependencySet.addLater( + coreLibrariesVersion.map { + dependencies.kotlinDependency(stdlibModule, it) + } + ) + } + } + } +} + +private fun isStdlibAddedByUser( + configurations: ConfigurationContainer, + stdlibModules: Set, + vararg sourceSets: KotlinSourceSet +): Boolean { + return sourceSets + .asSequence() + .flatMap { sourceSet -> + KotlinDependencyScope.values().map { scope -> + configurations.sourceSetDependencyConfigurationByScope(sourceSet, scope) + }.asSequence() + } + .flatMap { it.allNonProjectDependencies().asSequence() } + .any { dependency -> + dependency.group == KOTLIN_MODULE_GROUP && dependency.name in stdlibModules + } +} + +private fun KotlinPlatformType.stdlibPlatformType( + coreLibrariesVersion: Provider, + kotlinTarget: KotlinTarget, + kotlinSourceSet: KotlinSourceSet +): String? = when (this) { + KotlinPlatformType.jvm -> chooseStdlibJvmDependency(coreLibrariesVersion) + KotlinPlatformType.androidJvm -> { + if (kotlinTarget is KotlinAndroidTarget && + kotlinSourceSet.androidSourceSetInfoOrNull?.androidSourceSetName == AndroidBaseSourceSetName.Main.name + ) { + chooseStdlibJvmDependency(coreLibrariesVersion) + } else { + null + } + } + + KotlinPlatformType.js -> "kotlin-stdlib-js" + KotlinPlatformType.wasm -> "kotlin-stdlib-wasm" + KotlinPlatformType.native -> null + KotlinPlatformType.common -> // there's no platform compilation that the source set is default for + "kotlin-stdlib-common" +} + +private val kotlin180Version = SemVer(1.toBigInteger(), 8.toBigInteger(), 0.toBigInteger()) + +private fun chooseStdlibJvmDependency( + coreLibrariesVersion: Provider +): String { + // Current 'SemVer.satisfies' release always returns `false` for any "-SNAPSHOT" version. + return if (SemVer.from(coreLibrariesVersion.get()) < kotlin180Version) { + "kotlin-stdlib-jdk8" + } else { + "kotlin-stdlib" + } +} + +private val androidTestVariants = setOf(AndroidVariantType.UnitTest, AndroidVariantType.InstrumentedTest) + +private fun KotlinSourceSet.isRelatedToAndroidTestSourceSet(): Boolean { + val androidVariant = androidSourceSetInfoOrNull?.androidVariantType ?: return false + return androidVariant in androidTestVariants +} + +private val stdlibModules = setOf( + "kotlin-stdlib-common", + "kotlin-stdlib", + "kotlin-stdlib-jdk7", + "kotlin-stdlib-jdk8", + "kotlin-stdlib-js" +)