Fix stdlib-by-default failures when configuration has been resolved

Issue #KT-38221
This commit is contained in:
Sergey Igushkin
2020-07-17 17:25:06 +04:00
parent dccac34282
commit d3f9f4f3e8
3 changed files with 109 additions and 56 deletions
@@ -257,6 +257,26 @@ class KotlinSpecificDependenciesIT : BaseGradleIT() {
)
}
@Test
fun testNoFailureIfConfigurationIsObserved() = with(jvmProject()) {
lateinit var originalScript: String
try {
gradleBuildScript().modify {
originalScript = it
"""
configurations.create("api")
dependencies {
api("org.jetbrains.kotlin:kotlin-reflect")
}
println(configurations.api.incoming.dependencies.toList())
""".trimIndent() + "\n" + it
}
checkTaskCompileClasspath("compileKotlin", listOf("kotlin-reflect"))
} finally {
gradleBuildScript().writeText(originalScript)
}
}
private fun Project.checkConfigurationContent(
configurationName: String,
subproject: String? = null,
@@ -8,9 +8,12 @@ package org.jetbrains.kotlin.gradle.internal
import com.android.build.gradle.TestedExtension
import com.android.build.gradle.api.TestVariant
import com.android.build.gradle.api.UnitTestVariant
import org.gradle.api.InvalidUserDataException
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.DependencySet
import org.gradle.api.artifacts.ExternalDependency
import org.gradle.api.tasks.testing.AbstractTestTask
import org.gradle.api.tasks.testing.Test
@@ -54,66 +57,82 @@ private fun configureDefaultVersionsResolutionStrategy(project: Project) {
}
//region stdlib
internal fun configureStdlibDefaultDependency(project: Project) = project.whenEvaluated {
internal fun configureStdlibDefaultDependency(project: Project) = with(project) {
if (!PropertiesProvider(project).stdlibDefaultDependency)
return@whenEvaluated
return
val scopesToHandleConfigurations = listOf(KotlinDependencyScope.API_SCOPE, KotlinDependencyScope.IMPLEMENTATION_SCOPE)
project.kotlinExtension.sourceSets.all { kotlinSourceSet ->
val scope = if (isRelatedToAndroidTestSourceSet(project, kotlinSourceSet)) // AGP deprecates API configurations in test source sets
KotlinDependencyScope.IMPLEMENTATION_SCOPE
else KotlinDependencyScope.API_SCOPE
scopesToHandleConfigurations.forEach { scope ->
val scopeConfiguration = project.sourceSetDependencyConfigurationByScope(kotlinSourceSet, scope)
val scopeConfiguration = project.sourceSetDependencyConfigurationByScope(kotlinSourceSet, scope)
project.tryWithDependenciesIfUnresolved(scopeConfiguration) { dependencies ->
val scopeToAddStdlibDependency =
if (isRelatedToAndroidTestSourceSet(project, kotlinSourceSet)) // AGP deprecates API configurations in test source sets
KotlinDependencyScope.IMPLEMENTATION_SCOPE
else KotlinDependencyScope.API_SCOPE
scopeConfiguration.withDependencies { dependencies ->
val sourceSetDependencyConfigurations =
KotlinDependencyScope.values().map { project.sourceSetDependencyConfigurationByScope(kotlinSourceSet, it) }
if (scope != scopeToAddStdlibDependency)
return@tryWithDependenciesIfUnresolved
val hierarchySourceSetsDependencyConfigurations = kotlinSourceSet.getSourceSetHierarchy().flatMap { hierarchySourceSet ->
if (hierarchySourceSet === kotlinSourceSet) emptyList() else
KotlinDependencyScope.values().map { scope ->
project.sourceSetDependencyConfigurationByScope(hierarchySourceSet, scope)
}
}
val compilations = CompilationSourceSetUtil.compilationsBySourceSets(project).getValue(kotlinSourceSet)
.filter { it.target !is KotlinMetadataTarget }
val platformTypes = compilations.mapTo(mutableSetOf()) { it.platformType }
val sourceSetStdlibPlatformType = when {
platformTypes.isEmpty() -> KotlinPlatformType.common
platformTypes.size == 1 -> platformTypes.single()
setOf(KotlinPlatformType.jvm, KotlinPlatformType.androidJvm).containsAll(platformTypes) -> KotlinPlatformType.jvm
else -> KotlinPlatformType.common
}
val isStdlibAddedByUser = sourceSetDependencyConfigurations
.flatMap { it.allDependencies }
.any { dependency -> dependency.group == KOTLIN_MODULE_GROUP && dependency.name in stdlibModules }
if (!isStdlibAddedByUser) {
val stdlibModuleName = when (sourceSetStdlibPlatformType) {
KotlinPlatformType.jvm -> stdlibModuleForJvmCompilations(compilations)
KotlinPlatformType.androidJvm ->
if (kotlinSourceSet.name == androidMainSourceSetName(project)) stdlibModuleForJvmCompilations(compilations) else null
KotlinPlatformType.js -> "kotlin-stdlib-js"
KotlinPlatformType.native -> null
KotlinPlatformType.common -> // there's no platform compilation that the source set is default for
"kotlin-stdlib-common"
}
// If the exact same module is added in the source sets hierarchy, possibly even with a different scope, we don't add it
val moduleAddedInHierarchy = hierarchySourceSetsDependencyConfigurations.any {
it.allDependencies.any { dependency -> dependency.group == KOTLIN_MODULE_GROUP && dependency.name == stdlibModuleName }
}
if (stdlibModuleName != null && !moduleAddedInHierarchy)
dependencies.add(project.kotlinDependency(stdlibModuleName, project.kotlinExtension.coreLibrariesVersion))
chooseAndAddStdlibDependency(project, kotlinSourceSet, dependencies)
}
}
}
}
private fun chooseAndAddStdlibDependency(
project: Project,
kotlinSourceSet: KotlinSourceSet,
dependencies: DependencySet
) {
val sourceSetDependencyConfigurations =
KotlinDependencyScope.values().map { project.sourceSetDependencyConfigurationByScope(kotlinSourceSet, it) }
val hierarchySourceSetsDependencyConfigurations = kotlinSourceSet.getSourceSetHierarchy().flatMap { hierarchySourceSet ->
if (hierarchySourceSet === kotlinSourceSet) emptyList() else
KotlinDependencyScope.values().map { scope ->
project.sourceSetDependencyConfigurationByScope(hierarchySourceSet, scope)
}
}
val compilations = CompilationSourceSetUtil.compilationsBySourceSets(project).getValue(kotlinSourceSet)
.filter { it.target !is KotlinMetadataTarget }
val platformTypes = compilations.mapTo(mutableSetOf()) { it.platformType }
val sourceSetStdlibPlatformType = when {
platformTypes.isEmpty() -> KotlinPlatformType.common
platformTypes.size == 1 -> platformTypes.single()
setOf(KotlinPlatformType.jvm, KotlinPlatformType.androidJvm).containsAll(platformTypes) -> KotlinPlatformType.jvm
else -> KotlinPlatformType.common
}
val isStdlibAddedByUser = sourceSetDependencyConfigurations
.flatMap { it.allDependencies }
.any { dependency -> dependency.group == KOTLIN_MODULE_GROUP && dependency.name in stdlibModules }
if (!isStdlibAddedByUser) {
val stdlibModuleName = when (sourceSetStdlibPlatformType) {
KotlinPlatformType.jvm -> stdlibModuleForJvmCompilations(compilations)
KotlinPlatformType.androidJvm ->
if (kotlinSourceSet.name == androidMainSourceSetName(project)) stdlibModuleForJvmCompilations(compilations) else null
KotlinPlatformType.js -> "kotlin-stdlib-js"
KotlinPlatformType.native -> null
KotlinPlatformType.common -> // there's no platform compilation that the source set is default for
"kotlin-stdlib-common"
}
// If the exact same module is added in the source sets hierarchy, possibly even with a different scope, we don't add it
val moduleAddedInHierarchy = hierarchySourceSetsDependencyConfigurations.any {
it.allDependencies.any { dependency -> dependency.group == KOTLIN_MODULE_GROUP && dependency.name == stdlibModuleName }
}
if (stdlibModuleName != null && !moduleAddedInHierarchy)
dependencies.add(project.kotlinDependency(stdlibModuleName, project.kotlinExtension.coreLibrariesVersion))
}
}
private fun Project.findAndroidTarget(): KotlinAndroidTarget? {
val kotlinExtension = project.kotlinExtension
return when (kotlinExtension) {
@@ -178,7 +197,7 @@ internal fun configureKotlinTestDependencies(project: Project) {
}
}
configuration.withDependencies { dependencies ->
project.tryWithDependenciesIfUnresolved(configuration) { dependencies ->
val parentOrOwnVersions: List<String?> =
kotlinSourceSet.getSourceSetHierarchy().filter(versionOrNullBySourceSet::contains).map(versionOrNullBySourceSet::get)
@@ -283,4 +302,22 @@ private fun KotlinTargetWithTests<*, *>.findTestRunsByCompilation(byCompilation:
}
return testRuns.filter { it.executionSource.isProducedFromTheCompilation() }.takeIf { it.isNotEmpty() }
}
//endregion
//endregion
private fun Project.tryWithDependenciesIfUnresolved(configuration: Configuration, action: (DependencySet) -> Unit) {
fun reportAlreadyResolved() {
logger.info("Could not setup Kotlin-specific dependencies for $configuration as it is already resolved")
}
if (configuration.state != Configuration.State.UNRESOLVED)
return reportAlreadyResolved()
// Gradle doesn't provide any public API to check if it's safe to modify a configuration's dependencies.
// The state of the configuration may still be UNRESOLVED but it might have 'participated' in dependency resolution and its dependencies
// may now be frozen.
try {
configuration.withDependencies(action)
} catch (e: InvalidUserDataException) {
reportAlreadyResolved()
}
}
@@ -99,11 +99,7 @@ abstract class AbstractKotlinTarget(
?: project.configurations.create(kotlinUsageContext.name).also { configuration ->
configuration.isCanBeConsumed = false
configuration.isCanBeResolved = false
/** Add dependencies lazily to avoid freezing the content of the configuration held by [kotlinUsageContext] */
configuration.withDependencies { dependencies ->
dependencies.addAll(kotlinUsageContext.dependencies)
}
configuration.dependencyConstraints.addAll(kotlinUsageContext.dependencyConstraints)
configuration.extendsFrom(project.configurations.getByName(kotlinUsageContext.dependencyConfigurationName))
configuration.artifacts.addAll(kotlinUsageContext.artifacts)
val attributes = kotlinUsageContext.attributes