diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/AndroidAndJavaConsumeMppLibIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/AndroidAndJavaConsumeMppLibIT.kt new file mode 100644 index 00000000000..2c5c709251d --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/AndroidAndJavaConsumeMppLibIT.kt @@ -0,0 +1,451 @@ +/* + * 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. + */ +@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + +package org.jetbrains.kotlin.gradle + +import org.jetbrains.kotlin.gradle.ResolvedVariantChecker.ResolvedVariantRequest +import org.jetbrains.kotlin.gradle.util.AGPVersion +import org.jetbrains.kotlin.gradle.util.checkedReplace +import org.jetbrains.kotlin.gradle.util.modify +import org.jetbrains.kotlin.test.util.KtTestUtil +import org.junit.* +import org.junit.rules.ErrorCollector +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import java.io.File +import kotlin.test.assertTrue +import java.lang.Boolean as RefBoolean + +class AndroidAndJavaConsumeMppLibBuiltByGradle69IT : AndroidAndJavaConsumeMppLibIT() { + override val producerAgpVersion: AGPVersion = AGPVersion.v4_2_0 + override val producerGradleVersion: GradleVersionRequired = GradleVersionRequired.Exact("6.9") +} + +class AndroidAndJavaConsumeMppLibBuiltByGradle7IT : AndroidAndJavaConsumeMppLibIT() { + override val producerAgpVersion: AGPVersion = AGPVersion.v7_0_0 + override val producerGradleVersion: GradleVersionRequired = GradleVersionRequired.AtLeast("7.0") +} + +@RunWith(Parameterized::class) +abstract class AndroidAndJavaConsumeMppLibIT : BaseGradleIT() { + abstract val producerAgpVersion: AGPVersion + abstract val producerGradleVersion: GradleVersionRequired + + @field:Rule + @JvmField + var collector: ErrorCollector = ErrorCollector() + + companion object { + @field:ClassRule + @JvmField + val publishedLibraryCacheDir = TemporaryFolder() + + lateinit var publishedLibraryCache: MutableMap + + @JvmStatic + @BeforeClass + fun initCache() { + publishedLibraryCache = mutableMapOf() + } + + @JvmStatic + @AfterClass + fun deleteCache() { + publishedLibraryCacheDir.delete() + } + + @JvmStatic + @Parameterized.Parameters(name = "Consumer(AGP={3}, Gradle={4}), flavors={0}, debugOnly={1}, published={2}") + fun testCases(): List> { + val consumers = listOf( + AGPVersion.v4_2_0 to GradleVersionRequired.Exact("6.9"), + AGPVersion.v4_2_0 to GradleVersionRequired.AtLeast("7.0"), + AGPVersion.v7_0_0 to GradleVersionRequired.AtLeast("7.0"), + ) + val buildParams = listOf( + /* useFlavors, isAndroidPublishDebugOnly, isPublishedLibrary */ + arrayOf(false, false, false), + arrayOf(false, true, false), + arrayOf(true, false, false), + arrayOf(true, true, false), + arrayOf(false, false, true), + arrayOf(false, true, true), + arrayOf(true, false, true), + arrayOf(true, true, true), + ) + return consumers.flatMap { (agpVersion, gradleVersion) -> + buildParams.map { arrayOf(*it, agpVersion, gradleVersion) } + } + } + } + + @Parameterized.Parameter(0) + lateinit var useFlavorsParameter: RefBoolean + + @Parameterized.Parameter(1) + lateinit var isAndroidPublishDebugOnlyParameter: RefBoolean + + @Parameterized.Parameter(2) + lateinit var isPublishedLibraryParameter: RefBoolean + + @Parameterized.Parameter(3) + lateinit var consumerAgpVersion: AGPVersion + + @Parameterized.Parameter(4) + lateinit var consumerGradleVersion: GradleVersionRequired + + private val isAndroidPublishDebugOnly get() = isAndroidPublishDebugOnlyParameter.booleanValue() + private val useFlavors get() = useFlavorsParameter.booleanValue() + private val isPublishedLibrary get() = isPublishedLibraryParameter.booleanValue() + + private lateinit var dependencyProject: Project + + @Before + override fun setUp() { + super.setUp() + + val jdk11Home = File(System.getProperty("jdk11Home")) + Assume.assumeTrue("This test requires JDK11 for AGP7", jdk11Home.isDirectory) + + val producerBuildOptions = defaultBuildOptions().copy( + javaHome = jdk11Home, + androidHome = KtTestUtil.findAndroidSdk(), + androidGradlePluginVersion = producerAgpVersion + ) + producerBuildOptions.androidHome?.let { acceptAndroidSdkLicenses(it) } + + dependencyProject = Project("new-mpp-android", producerGradleVersion).apply { + projectDir.deleteRecursively() + setupWorkingDir() + // Don't need custom attributes here + gradleBuildScript("lib").modify { text -> + text.lines().filterNot { it.trimStart().startsWith("attribute(") }.joinToString("\n") + .let { + if (isAndroidPublishDebugOnly) it.checkedReplace( + "publishAllLibraryVariants()", + "publishLibraryVariants(\"${if (useFlavors) "flavor1Debug" else "debug"}\")" + ) else it + } + .let { + if (useFlavors) { + it + "\n" + """ + android { + flavorDimensions "myFlavor" + productFlavors { + flavor1 { dimension "myFlavor" } + } + } + """.trimIndent() + } else it + }.let { + // Simulate the behavior with user-defined consumable configuration added with no proper attributes: + it + "\n" + """ + configurations.create("legacyConfiguration") { + def bundlingAttribute = Attribute.of("org.gradle.dependency.bundling", String) + attributes.attribute(bundlingAttribute, "external") + } + """.trimIndent() + } + } + } + + if (isPublishedLibrary) { + val cacheKey = listOf( + useFlavors, + isAndroidPublishDebugOnly, + producerGradleVersion, + producerAgpVersion + ) + val cachedPublishedLibrary = publishedLibraryCache.computeIfAbsent(cacheKey) { + val newTempDir = publishedLibraryCacheDir.newFolder() + dependencyProject.build(":lib:publish", options = producerBuildOptions) { + assertSuccessful() + } + dependencyProject.projectDir.copyRecursively(newTempDir) + newTempDir + } + cachedPublishedLibrary.copyRecursively(dependencyProject.projectDir, overwrite = true) + } + } + + @Test + fun test() { + runConsumerTest(dependencyProject, withKotlinVersion = null) + runConsumerTest(dependencyProject, withKotlinVersion = defaultBuildOptions().kotlinVersion) + + // We don't want to test the behavior with old Kotlin project-to-project dependencies, only compatibility with publications: + if (isPublishedLibrary) { + runConsumerTest(dependencyProject, withKotlinVersion = "1.5.31") + } + } + + /** Use [withKotlinVersion] = null for testing without Kotlin Gradle plugin */ + private fun runConsumerTest( + dependencyProject: Project, + withKotlinVersion: String? + ) { + if (producerGradleVersion != consumerGradleVersion && !isPublishedLibrary) { + println("Testing project-to-project dependencies is only possible with one Gradle version on the consumer and producer sides") + return + } + + val repositoryLinesIfNeeded = if (isPublishedLibrary) """ + repositories { + maven { setUrl("${dependencyProject.projectDir.resolve("lib/build/repo").toURI()}") } + } + """.trimIndent() else "" + + val dependencyNotation = + if (isPublishedLibrary) + """"com.example:lib:1.0"""" + else "project(\":${dependencyProject.projectName}:lib\")" + + val consumerProject = Project("AndroidProject", consumerGradleVersion).apply { + projectDir.deleteRecursively() + if (!isPublishedLibrary) { + embedProject(dependencyProject) + gradleSettingsScript().appendText( + "\ninclude(\":${dependencyProject.projectName}:lib\")" + ) + } + setupWorkingDir() + + gradleBuildScript("Lib").apply { + writeText( + // Remove the Kotlin plugin from the consumer project to check how pure-AGP Kotlin-less consumers resolve the dependency + readText().let { + if (withKotlinVersion != null) it else it.checkedReplace( + "id 'org.jetbrains.kotlin.android'", + "//" + ) + } + .let { text -> + // If the test case doesn't assume flavors, remove the flavor setup lines: + if (useFlavors) text else text.lines().filter { !it.trim().startsWith("flavor") }.joinToString("\n") + } + "\n" + """ + android { + buildTypes { + // We create a build type that is missing in the library in order to check how it resolves such an MPP lib + create("staging") { initWith(getByName("debug")) } + } + } + $repositoryLinesIfNeeded + dependencies { + implementation($dependencyNotation) + } + """.trimIndent() + ) + } + } + + val variantNamePublishedSuffix = if (isPublishedLibrary) "-published" else "" + + val variantForReleaseAndStaging = if (isAndroidPublishDebugOnly && isPublishedLibrary) + "debugApiElements$variantNamePublishedSuffix" + else "releaseApiElements$variantNamePublishedSuffix" + + @Suppress("DEPRECATION") + fun nameWithFlavorIfNeeded(name: String) = if (useFlavors) "flavor1${name.capitalize()}" else name + + val configurationToExpectedVariant = listOf( + nameWithFlavorIfNeeded("debugCompileClasspath") to nameWithFlavorIfNeeded("debugApiElements$variantNamePublishedSuffix"), + nameWithFlavorIfNeeded("releaseCompileClasspath") to nameWithFlavorIfNeeded(variantForReleaseAndStaging), + nameWithFlavorIfNeeded("stagingCompileClasspath") to + if (isPublishedLibrary) + nameWithFlavorIfNeeded(variantForReleaseAndStaging) + // NB: unlike published library, both the release and debug variants provide the build type attribute + // and therefore are not compatible with the "staging" consumer. So it can only use the JVM variant + else "jvmLibApiElements" + ) + + val dependencyInsightModuleName = + if (isPublishedLibrary) + "com.example:lib:1.0" + else ":${dependencyProject.projectName}:lib" + + val consumerBuildOptions = defaultBuildOptions().copy( + javaHome = File(System.getProperty("jdk11Home")), + androidHome = KtTestUtil.findAndroidSdk(), + androidGradlePluginVersion = consumerAgpVersion, + kotlinVersion = withKotlinVersion ?: defaultBuildOptions().kotlinVersion + ) + + val variantCheckRequests = mutableMapOf() + + configurationToExpectedVariant.forEach { (configuration, expected) -> + /* TODO: the issue KT-30961 is only fixed for AGP 7+. Older versions still reproduce the issue; + * This test asserts the existing incorrect behavior for older Gradle versions + * in the absence of the Kotlin Gradle plugin, in order to detect unintentional changes + */ + val expectedVariant = if (consumerAgpVersion < AGPVersion.v7_0_0 && withKotlinVersion == null && !isPublishedLibrary) { + "jvmLibApiElements" + } else expected + + val resolvedVariantRequest = ResolvedVariantRequest("Lib", configuration, dependencyInsightModuleName) + variantCheckRequests[resolvedVariantRequest] = expectedVariant + } + + // Also test that a pure Java (Kotlin-less) project is able to resolve the MPP library dependency to the JVM variants: + consumerProject.apply { + gradleSettingsScript().appendText("\ninclude(\"pure-java\")") + projectDir.resolve("pure-java/build.gradle.kts").also { it.parentFile.mkdirs() }.writeText( + """ + plugins { + java + ${if (withKotlinVersion != null) "kotlin(\"jvm\")" else ""} + } + $repositoryLinesIfNeeded + dependencies { + implementation($dependencyNotation) + } + """.trimIndent() + ) + + val resolvedVariantRequest = ResolvedVariantRequest("pure-java", "compileClasspath", dependencyInsightModuleName) + variantCheckRequests[resolvedVariantRequest] = "jvmLibApiElements$variantNamePublishedSuffix" + } + + try { + ResolvedVariantChecker().assertResolvedSingleVariantsBatch(consumerProject, variantCheckRequests, consumerBuildOptions) + } catch (e: AssertionError) { + collector.addError(AssertionError("Failure with Kotlin version=${withKotlinVersion}", e)) + } + } +} + +class ResolvedVariantChecker { + data class ResolvedVariantRequest(val subproject: String, val configuration: String, val dependencyFilter: String) + + data class ModuleVariantResult(val moduleName: String, val variantString: String) + + fun assertResolvedSingleVariantsBatch( + project: BaseGradleIT.Project, + assertions: Map, + buildOptions: BaseGradleIT.BuildOptions = project.testCase.defaultBuildOptions() + ) { + val actualResults = getResolvedVariantsBatch(project, assertions.keys, buildOptions) + val mismatches = assertions.filterKeys { + assertions.getValue(it) != actualResults.get(it)?.singleOrNull()?.variantString + } + if (mismatches.isNotEmpty()) { + throw AssertionError( + "Expected and actual variants do not match: \n" + mismatches.keys.joinToString("\n") { + val matches = actualResults.get(it) + val actualString = when { + matches == null || matches.isEmpty() -> "got no resolution result or resolution error!" + matches.size == 1 -> "got " + matches.singleOrNull()?.variantString + else -> "got multiple dependencies matched: $matches" + } + " * in configuration ${it.subproject}.${it.configuration}, " + + "dependency ${it.dependencyFilter} " + + "expected variant ${assertions.getValue(it)}, " + + actualString + }) + } + } + + fun getResolvedVariantsBatch( + project: BaseGradleIT.Project, + requests: Iterable, + buildOptions: BaseGradleIT.BuildOptions = project.testCase.defaultBuildOptions() + ): Map> = with(project.testCase) { + with(project) { + val requestsBySubproject = requests.groupBy { it.subproject } + val tasksBySubproject = requestsBySubproject.entries.associate { (subproject, subprojectRequests) -> + val buildScript = gradleBuildScript(subproject) + val taskCodeByRequest = subprojectRequests.associateWith { request -> + when (buildScript.extension) { + "gradle" -> generateResolvedVariantTaskCodeGroovy(request.configuration, request.dependencyFilter) + "kts" -> generateResolvedVariantTaskCodeKts(request.configuration, request.dependencyFilter) + else -> error("unexpected build script $buildScript in project $projectName") + } + } + buildScript.appendText(taskCodeByRequest.values.joinToString("\n\n", "\n\n") { it.taskCode }) + subproject to taskCodeByRequest + } + + val requestsByTaskName = + tasksBySubproject.values.flatMap { it.entries }.associate { (request, task) -> task.taskName to request } + + val tasksToRun = tasksBySubproject.entries.flatMap { (subproject, tasks) -> tasks.map { ":$subproject:${it.value.taskName}" } } + + lateinit var result: Map> + build(*tasksToRun.toTypedArray(), options = buildOptions) { + assertSuccessful() + + val variantReportRegex = Regex("$separator(.*?)$separator(.*?)$separator(.*)") + val variantReports = variantReportRegex.findAll(output).map { match -> + val (task, module, variant) = match.destructured + val request = requestsByTaskName.getValue(task) + request to ModuleVariantResult(module, variant) + } + result = variantReports.groupBy(keySelector = { it.first }, valueTransform = { it.second }) + } + result + } + } + + private data class TaskWithName(val taskCode: String, val taskName: String) + + private var taskId = 0 + private fun nextTaskId() = taskId++ + + private val separator = "#ResolvedVariantChecker#" + + private fun generateResolvedVariantTaskCodeGroovy(configuration: String, dependencyNotation: String): TaskWithName { + val taskName = "getResolvedVariants${nextTaskId()}" + val taskCode = """ + tasks.register("$taskName") { + doLast { + configurations.getByName("$configuration") + .incoming.resolutionResult + .allDependencies + .findAll { it.requested.displayName.contains("$dependencyNotation") } + .forEach { + def variantString = + (it instanceof org.gradle.api.artifacts.result.ResolvedDependencyResult) ? it.selected.variants.collect { it.displayName }.join(",") : + (it instanceof org.gradle.api.artifacts.result.UnresolvedDependencyResult) ? "error:" + it.failure.message?.replace("\n", "\\n") : + "" + println( + "$separator" + name + "$separator" + + it.requested.displayName + "$separator" + + variantString + ) + } + } + } + """.trimIndent() + return TaskWithName(taskCode, taskName) + } + + private fun generateResolvedVariantTaskCodeKts(configuration: String, dependencyNotation: String): TaskWithName { + val taskName = "getResolvedVariants${nextTaskId()}" + val taskCode = """ + tasks.register("$taskName") { + doLast { + configurations.getByName("$configuration") + .incoming.resolutionResult + .allDependencies + .filter { "$dependencyNotation" in it.requested.displayName } + .forEach { + val variantString = when (it) { + is org.gradle.api.artifacts.result.ResolvedDependencyResult -> it.selected.variants.map { it.displayName }.joinToString(",") + is org.gradle.api.artifacts.result.UnresolvedDependencyResult -> "error:" + it.failure.message?.replace("\n", "\\n") + else -> "" + } + println( + "$separator" + name + "$separator" + + it.requested.displayName + "$separator" + + variantString + ) + } + } + } + """.trimIndent() + return TaskWithName(taskCode, taskName) + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/GradleVersionRequired.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/GradleVersionRequired.kt index 8bcf6f2cc99..255dc0bb7c3 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/GradleVersionRequired.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/GradleVersionRequired.kt @@ -17,6 +17,7 @@ package org.jetbrains.kotlin.gradle import org.gradle.util.GradleVersion +import org.jetbrains.kotlin.com.google.common.base.Objects import org.jetbrains.kotlin.gradle.GradleVersionRequired.Companion.OLDEST_SUPPORTED import org.jetbrains.kotlin.gradle.utils.minSupportedGradleVersion import org.junit.Assume @@ -28,15 +29,25 @@ sealed class GradleVersionRequired(val minVersion: String, val maxVersion: Strin val FOR_MPP_SUPPORT = AtLeast(OLDEST_SUPPORTED) } - class Exact(version: String) : GradleVersionRequired(version, version) + class Exact(version: String) : GradleVersionRequired(version, version) { + override fun toString(): String = "Exact(${minVersion})" + } - class AtLeast(version: String) : GradleVersionRequired(version, null) + class AtLeast(version: String) : GradleVersionRequired(version, null) { + override fun toString(): String = "AtLeast(${minVersion}" + } class InRange(minVersion: String, maxVersion: String) : GradleVersionRequired(minVersion, maxVersion) class Until(maxVersion: String) : GradleVersionRequired(OLDEST_SUPPORTED, maxVersion) object None : GradleVersionRequired(OLDEST_SUPPORTED, null) + + override fun equals(other: Any?): Boolean = other is GradleVersionRequired && + minVersion == other.minVersion && + maxVersion == other.maxVersion + + override fun hashCode(): Int = java.util.Objects.hash(minVersion, maxVersion) } diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/PureAndroidAndJavaConsumeMppLibIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/PureAndroidAndJavaConsumeMppLibIT.kt deleted file mode 100644 index 125e256260c..00000000000 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/PureAndroidAndJavaConsumeMppLibIT.kt +++ /dev/null @@ -1,268 +0,0 @@ -/* - * 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. - */ -@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") - -package org.jetbrains.kotlin.gradle - -import org.jetbrains.kotlin.gradle.util.AGPVersion -import org.jetbrains.kotlin.gradle.util.checkedReplace -import org.jetbrains.kotlin.gradle.util.modify -import org.jetbrains.kotlin.test.util.KtTestUtil -import org.junit.Assume -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.rules.ErrorCollector -import org.junit.runner.RunWith -import org.junit.runners.Parameterized -import java.io.File -import java.lang.Boolean as RefBoolean - -/** - * It is important to run both pre-7.0 and post-7.0 tests, as Gradle 7.0 + AGP 7 introduces a new attribute to distinguish JVM vs Android - */ -class PureAndroidAndJavaConsumeMppLibPreGradle7IT : PureAndroidAndJavaConsumeMppLibIT() { - override val agpVersion: AGPVersion - get() = AGPVersion.v4_2_0 - - override val gradleVersion: GradleVersionRequired - get() = GradleVersionRequired.Exact("6.9") -} - -/** - * It is important to run both pre-7.0 and post-7.0 tests, as Gradle 7.0 + AGP 7 introduces a new attribute to distinguish JVM vs Android - */ -class PureAndroidAndJavaConsumeMppLibGradle7PlusIT : PureAndroidAndJavaConsumeMppLibIT() { - override val agpVersion: AGPVersion - get() = AGPVersion.v7_0_0 - - override val gradleVersion: GradleVersionRequired - get() = GradleVersionRequired.AtLeast("7.0") -} - -@RunWith(Parameterized::class) -abstract class PureAndroidAndJavaConsumeMppLibIT : BaseGradleIT() { - abstract val agpVersion: AGPVersion - abstract val gradleVersion: GradleVersionRequired - - @field:Rule - @JvmField - var collector: ErrorCollector = ErrorCollector() - - companion object { - @JvmStatic - @Parameterized.Parameters(name = "useFlavors: {0}, isAndroidPublishDebugOnly: {1}, isPublishedLibrary: {2}") - fun testCases(): List> = - listOf( - /* useFlavors, isAndroidPublishDebugOnly, isPublishedLibrary */ - arrayOf(false, false, false), - arrayOf(false, true, false), - arrayOf(true, false, false), - arrayOf(true, true, false), - arrayOf(false, false, true), - arrayOf(false, true, true), - arrayOf(true, false, true), - arrayOf(true, true, true), - ) - } - - @Parameterized.Parameter(0) - lateinit var useFlavorsParameter: RefBoolean - - @Parameterized.Parameter(1) - lateinit var isAndroidPublishDebugOnlyParameter: RefBoolean - - lateinit var buildOptions: BuildOptions - - @Before - override fun setUp() { - val jdk11Home = File(System.getProperty("jdk11Home")) - Assume.assumeTrue("This test requires JDK11 for AGP7", jdk11Home.isDirectory) - - buildOptions = defaultBuildOptions().copy( - javaHome = jdk11Home, - androidHome = KtTestUtil.findAndroidSdk(), - androidGradlePluginVersion = agpVersion - ) - buildOptions.androidHome?.let { acceptAndroidSdkLicenses(it) } - - super.setUp() - } - - @Parameterized.Parameter(2) - lateinit var isPublishedLibraryParameter: RefBoolean - - @Test - fun test() { - val isAndroidPublishDebugOnly = isAndroidPublishDebugOnlyParameter.booleanValue() - val useFlavors = useFlavorsParameter.booleanValue() - val isPublishedLibrary = isPublishedLibraryParameter.booleanValue() - - val dependencyProject = Project("new-mpp-android", gradleVersion).apply { - projectDir.deleteRecursively() - setupWorkingDir() - // Don't need custom attributes here - gradleBuildScript("lib").modify { text -> - text.lines().filterNot { it.trimStart().startsWith("attribute(") }.joinToString("\n") - .let { - if (isAndroidPublishDebugOnly) it.checkedReplace( - "publishAllLibraryVariants()", - "publishLibraryVariants(\"${if (useFlavors) "flavor1Debug" else "debug"}\")" - ) else it - } - .let { - if (useFlavors) { - it + "\n" + """ - android { - flavorDimensions "myFlavor" - productFlavors { - flavor1 { dimension "myFlavor" } - } - } - """.trimIndent() - } else it - }.let { - // Simulate the behavior with user-defined consumable configuration added with no proper attributes: - it + "\n" + """ - configurations.create("legacyConfiguration") { - def bundlingAttribute = Attribute.of("org.gradle.dependency.bundling", String) - attributes.attribute(bundlingAttribute, "external") - } - """.trimIndent() - } - } - } - - if (isPublishedLibrary) { - dependencyProject.build(":lib:publish", options = buildOptions) { - assertSuccessful() - } - } - - val repositoryLinesIfNeeded = if (isPublishedLibrary) """ - repositories { - maven { setUrl("${dependencyProject.projectDir.resolve("lib/build/repo").toURI()}") } - } - """.trimIndent() else "" - - val dependencyNotation = - if (isPublishedLibrary) - """"com.example:lib:1.0"""" - else "project(\":${dependencyProject.projectName}:lib\")" - - val variantNamePublishedSuffix = if (isPublishedLibrary) "-published" else "" - - val dependencyInsightModuleName = - if (isPublishedLibrary) - "com.example:lib" - else ":${dependencyProject.projectName}:lib" - - val consumerProject = Project("AndroidProject", gradleVersion).apply { - projectDir.deleteRecursively() - if (!isPublishedLibrary) { - embedProject(dependencyProject) - gradleSettingsScript().appendText( - "\ninclude(\":${dependencyProject.projectName}:lib\")" - ) - } - setupWorkingDir() - - gradleBuildScript("Lib").apply { - writeText( - // Remove the Kotlin plugin from the consumer project to check how pure-AGP Kotlin-less consumers resolve the dependency - readText().checkedReplace("id 'org.jetbrains.kotlin.android'", "//").let { text -> - // If the test case doesn't assume flavors, remove the flavor setup lines: - if (useFlavors) text else text.lines().filter { !it.trim().startsWith("flavor") }.joinToString("\n") - } + "\n" + """ - android { - buildTypes { - // We create a build type that is missing in the library in order to check how it resolves such an MPP lib - create("staging") { initWith(getByName("debug")) } - } - } - $repositoryLinesIfNeeded - dependencies { - implementation($dependencyNotation) - } - """.trimIndent() - ) - } - } - val variantForReleaseAndStaging = if (isAndroidPublishDebugOnly && isPublishedLibrary) - "debugApiElements$variantNamePublishedSuffix" - else "releaseApiElements$variantNamePublishedSuffix" - - fun nameWithFlavorIfNeeded(name: String) = if (useFlavors) "flavor1${name.capitalize()}" else name - - val configurationToExpectedVariant = listOf( - nameWithFlavorIfNeeded("debugCompileClasspath") to nameWithFlavorIfNeeded("debugApiElements$variantNamePublishedSuffix"), - nameWithFlavorIfNeeded("releaseCompileClasspath") to nameWithFlavorIfNeeded(variantForReleaseAndStaging), - nameWithFlavorIfNeeded("stagingCompileClasspath") to - if (isPublishedLibrary) - nameWithFlavorIfNeeded(variantForReleaseAndStaging) - // NB: unlike published library, both the release and debug variants provide the build type attribute - // and therefore are not compatible with the "staging" consumer. So it can only use the JVM variant - else "jvmLibApiElements" - ) - configurationToExpectedVariant.forEach { (configuration, expected) -> - consumerProject.build( - ":Lib:dependencyInsight", - "--configuration", configuration, - "--dependency", dependencyInsightModuleName, - options = buildOptions - ) { - assertSuccessful() - if (project.testGradleVersionBelow("7.0") && !isPublishedLibrary) { - /* TODO: the issue KT-30961 is only fixed for Gradle 7.0+ and AGP 7+. Older versions still reproduce the issue; - * This test asserts the existing incorrect behavior for older Gradle versions in order to detect unintentional changes - */ - assertVariantInDependencyInsight("jvmLibApiElements") - } else - assertVariantInDependencyInsight(expected) - } - } - - // Also test that a pure Java (Kotlin-less) project is able to resolve the MPP library dependency to the JVM variants: - consumerProject.apply { - gradleSettingsScript().appendText("\ninclude(\"pure-java\")") - projectDir.resolve("pure-java/build.gradle.kts").also { it.parentFile.mkdirs() }.writeText( - """ - plugins { - java - } - $repositoryLinesIfNeeded - dependencies { - implementation($dependencyNotation) - } - """.trimIndent() - ) - - build( - ":pure-java:dependencyInsight", - "--configuration", "compileClasspath", - "--dependency", dependencyInsightModuleName, - options = buildOptions - ) { - assertSuccessful() - assertVariantInDependencyInsight("jvmLibApiElements$variantNamePublishedSuffix") - } - } - } - - private fun CompiledProject.assertVariantInDependencyInsight(variantName: String) { - try { - assertContains("variant \"$variantName\" [") - } catch (originalError: AssertionError) { - val matchedVariants = Regex("variant \"(.*?)\" \\[").findAll(output).toList() - val failure = AssertionError( - "Expected variant $variantName. " + if (matchedVariants.isNotEmpty()) "Matched instead: " + matchedVariants - .joinToString { it.groupValues[1] } else "No match.", - originalError - ) - this.output - collector.addError(failure) - } - } -} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetConfigurator.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetConfigurator.kt index bf31b1dc4c3..e5b99cfc97d 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetConfigurator.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetConfigurator.kt @@ -508,7 +508,18 @@ fun Configuration.usesPlatformOf(target: KotlinTarget): Configuration { when (target.platformType) { KotlinPlatformType.jvm -> setJavaTargetEnvironmentAttributeIfSupported(target.project, "standard-jvm") KotlinPlatformType.androidJvm -> setJavaTargetEnvironmentAttributeIfSupported(target.project, "android") - else -> Unit + /** + * We set this attribute even for non-JVM-like targets (JS, Native) to avoid issues with Gradle variant-aware dependency resolution + * treating variants which don't have a particular attribute more preferable than those having it in those cases when Gradle failed + * to choose the best match by biggest compatible attributes set (by inclusion). Having an attribute not + * set on some variants might break if there appears one more third-party attribute such that: + * * it is not set on some variants; + * * according to the other attributes which are set on all variants, there are both compatible candidate variants + * which have this attribute and those which don't; + * Note that this attribute is not published to avoid issues with older Kotlin versions combined with newer Gradle + * see [org.jetbrains.kotlin.gradle.plugin.mpp.DefaultKotlinUsageContext.filterOutNonPublishableAttributes] + */ + else -> setJavaTargetEnvironmentAttributeIfSupported(target.project, "non-jvm") } if (target is KotlinJsTarget) { diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinSoftwareComponent.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinSoftwareComponent.kt index d0113c7287e..c843c75b225 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinSoftwareComponent.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinSoftwareComponent.kt @@ -168,8 +168,7 @@ class DefaultKotlinUsageContext( to.attribute(attribute, from.getAttribute(attribute)!!) } - configurationAttributes.keySet() - .filter { it != ProjectLocalConfigurations.ATTRIBUTE } + filterOutNonPublishableAttributes(configurationAttributes.keySet()) .forEach { copyAttribute(it, configurationAttributes, result) } return result @@ -178,4 +177,22 @@ class DefaultKotlinUsageContext( override fun getCapabilities(): Set = emptySet() override fun getGlobalExcludes(): Set = emptySet() + + private fun filterOutNonPublishableAttributes(attributes: Set>): Set> = + attributes.filterTo(mutableSetOf()) { + it != ProjectLocalConfigurations.ATTRIBUTE && + /** + * We exclude the attribute "org.gradle.jvm.environment" from publishing to avoid two issues: + * + * 1. Kotlin < 1.6.0 consumers which don't set this attribute on the consumer side. If this attribute is not set on the + * consumer side, then the Gradle built-in disambiguation rule applies: { standard-jvm, android } -> standard-jvm. + * In Kotlin 1.5.31, this would conflict with the rule on o.j.k.platform.type: { androidJvm, jvm } -> androidJvm, so the + * two rules would choose different closes match variants, and disambiguation would fail. + * + * 2. If this attribute is published, but not present on all the variants in a multiplatform library, and is also + * missing on the consumer side (like Gradle < 7.0, Kotlin 1.6.0), then there is a + * case when Gradle fails to choose a variant in a completely reasonable setup. + */ + it.name != "org.gradle.jvm.environment" + } }