KT-49835: Exclude org.gradle.jvm.environment attribute from publishing

Not publishing this attribute fixes the consumers that don't set it.
Those may encounter either a conflict of Kotlin <= 1.5.31 rule on
{JVM, Android} -> Android vs Gradle rule
{standard-jvm, android} -> standard-jvm (causing disambiguation
failures). Or they may run Gradle < 7.0 which doesn't have
disambiguation rules for `org.gradle.jvm.environment` and therefore
treats it as an ordinary extra attribute, thus eventually preferring
variants which don't have it if no other disambiguation rule worked
(again causing disambiguation failures since some rules may prefer the
variants where this attribute *is* set).

Also set a "non-jvm" value of this attribute for non-JVM variants for
project-to-project dependencies in order to "align" them so that Gradle
doesn't prefer variants not marked with this attribute.

Extend the set of dependency resolution tests which check
compatibility of different kinds of Android & JVM consumers against
project and published library: add Kotlin 1.5.31 consumers, also check
consumption with different Gradle version than the one which did the
publication.

Issue #KT-49835
This commit is contained in:
Sergey Igushkin
2021-11-23 22:07:06 +04:00
committed by Space
parent 8a0969156f
commit 844876a974
5 changed files with 495 additions and 273 deletions
@@ -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<Any, File>
@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<Array<Any>> {
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<ResolvedVariantRequest, String>()
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<ResolvedVariantRequest, String>,
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<ResolvedVariantRequest>,
buildOptions: BaseGradleIT.BuildOptions = project.testCase.defaultBuildOptions()
): Map<ResolvedVariantRequest, List<ModuleVariantResult>> = 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<ResolvedVariantRequest, List<ModuleVariantResult>>
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)
}
}
@@ -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)
}
@@ -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<Array<Boolean>> =
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)
}
}
}
@@ -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) {
@@ -168,8 +168,7 @@ class DefaultKotlinUsageContext(
to.attribute<T>(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<Capability> = emptySet()
override fun getGlobalExcludes(): Set<ExcludeRule> = emptySet()
private fun filterOutNonPublishableAttributes(attributes: Set<Attribute<*>>): Set<Attribute<*>> =
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"
}
}