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( private fun Project.checkConfigurationContent(
configurationName: String, configurationName: String,
subproject: String? = null, subproject: String? = null,
@@ -8,9 +8,12 @@ package org.jetbrains.kotlin.gradle.internal
import com.android.build.gradle.TestedExtension import com.android.build.gradle.TestedExtension
import com.android.build.gradle.api.TestVariant import com.android.build.gradle.api.TestVariant
import com.android.build.gradle.api.UnitTestVariant import com.android.build.gradle.api.UnitTestVariant
import org.gradle.api.InvalidUserDataException
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.Task import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.Dependency import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.DependencySet
import org.gradle.api.artifacts.ExternalDependency import org.gradle.api.artifacts.ExternalDependency
import org.gradle.api.tasks.testing.AbstractTestTask import org.gradle.api.tasks.testing.AbstractTestTask
import org.gradle.api.tasks.testing.Test import org.gradle.api.tasks.testing.Test
@@ -54,66 +57,82 @@ private fun configureDefaultVersionsResolutionStrategy(project: Project) {
} }
//region stdlib //region stdlib
internal fun configureStdlibDefaultDependency(project: Project) = project.whenEvaluated { internal fun configureStdlibDefaultDependency(project: Project) = with(project) {
if (!PropertiesProvider(project).stdlibDefaultDependency) if (!PropertiesProvider(project).stdlibDefaultDependency)
return@whenEvaluated return
val scopesToHandleConfigurations = listOf(KotlinDependencyScope.API_SCOPE, KotlinDependencyScope.IMPLEMENTATION_SCOPE)
project.kotlinExtension.sourceSets.all { kotlinSourceSet -> project.kotlinExtension.sourceSets.all { kotlinSourceSet ->
val scope = if (isRelatedToAndroidTestSourceSet(project, kotlinSourceSet)) // AGP deprecates API configurations in test source sets scopesToHandleConfigurations.forEach { scope ->
KotlinDependencyScope.IMPLEMENTATION_SCOPE val scopeConfiguration = project.sourceSetDependencyConfigurationByScope(kotlinSourceSet, scope)
else KotlinDependencyScope.API_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 -> if (scope != scopeToAddStdlibDependency)
val sourceSetDependencyConfigurations = return@tryWithDependenciesIfUnresolved
KotlinDependencyScope.values().map { project.sourceSetDependencyConfigurationByScope(kotlinSourceSet, it) }
val hierarchySourceSetsDependencyConfigurations = kotlinSourceSet.getSourceSetHierarchy().flatMap { hierarchySourceSet -> chooseAndAddStdlibDependency(project, kotlinSourceSet, dependencies)
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 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? { private fun Project.findAndroidTarget(): KotlinAndroidTarget? {
val kotlinExtension = project.kotlinExtension val kotlinExtension = project.kotlinExtension
return when (kotlinExtension) { return when (kotlinExtension) {
@@ -178,7 +197,7 @@ internal fun configureKotlinTestDependencies(project: Project) {
} }
} }
configuration.withDependencies { dependencies -> project.tryWithDependenciesIfUnresolved(configuration) { dependencies ->
val parentOrOwnVersions: List<String?> = val parentOrOwnVersions: List<String?> =
kotlinSourceSet.getSourceSetHierarchy().filter(versionOrNullBySourceSet::contains).map(versionOrNullBySourceSet::get) kotlinSourceSet.getSourceSetHierarchy().filter(versionOrNullBySourceSet::contains).map(versionOrNullBySourceSet::get)
@@ -284,3 +303,21 @@ private fun KotlinTargetWithTests<*, *>.findTestRunsByCompilation(byCompilation:
return testRuns.filter { it.executionSource.isProducedFromTheCompilation() }.takeIf { it.isNotEmpty() } 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 -> ?: project.configurations.create(kotlinUsageContext.name).also { configuration ->
configuration.isCanBeConsumed = false configuration.isCanBeConsumed = false
configuration.isCanBeResolved = false configuration.isCanBeResolved = false
/** Add dependencies lazily to avoid freezing the content of the configuration held by [kotlinUsageContext] */ configuration.extendsFrom(project.configurations.getByName(kotlinUsageContext.dependencyConfigurationName))
configuration.withDependencies { dependencies ->
dependencies.addAll(kotlinUsageContext.dependencies)
}
configuration.dependencyConstraints.addAll(kotlinUsageContext.dependencyConstraints)
configuration.artifacts.addAll(kotlinUsageContext.artifacts) configuration.artifacts.addAll(kotlinUsageContext.artifacts)
val attributes = kotlinUsageContext.attributes val attributes = kotlinUsageContext.attributes