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:
+451
@@ -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)
|
||||
}
|
||||
}
|
||||
+13
-2
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
-268
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-1
@@ -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) {
|
||||
|
||||
+19
-2
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user