Fix user-specified attributes in Android targets (KT-27714)

Ensure that custom target and compilation attributes are copied to
relevant configurations of Android targets as well, which did not happen
because of Android variants (and missing simple apiElements,
runtimeElements) and Android compilations being created late in the
project configuration.

Issue #KT-27714 Fixed
This commit is contained in:
Sergey Igushkin
2019-02-27 20:44:19 +03:00
parent 6fa610156e
commit b3eef05e6e
7 changed files with 135 additions and 36 deletions
@@ -196,7 +196,71 @@ class KotlinAndroid32GradleIT : KotlinAndroid3GradleIT(androidGradlePluginVersio
} }
} }
} }
}
@Test
fun testCustomAttributesInAndroidTargets() = with(Project("new-mpp-android", GradleVersionRequired.AtLeast("4.7"))) {
// Test the fix for KT-27714
setupWorkingDir()
// Enable publishing for all Android variants:
gradleBuildScript("lib").appendText("\nkotlin.android('androidLib') { publishAllLibraryVariants() }")
val groupDir = "lib/build/repo/com/example/"
build("publish") {
assertSuccessful()
// Also check that custom user-specified attributes are written in all Android modules metadata:
assertFileContains(
groupDir + "lib-androidlib/1.0/lib-androidlib-1.0.module",
"\"com.example.target\": \"androidLib\"",
"\"com.example.compilation\": \"release\""
)
assertFileContains(
groupDir + "lib-androidlib-debug/1.0/lib-androidlib-debug-1.0.module",
"\"com.example.target\": \"androidLib\"",
"\"com.example.compilation\": \"debug\""
)
projectDir.resolve(groupDir).deleteRecursively()
}
// Check that the consumer side uses custom attributes specified in the target and compilations:
run {
val appBuildScriptBackup = gradleBuildScript("app").readText()
gradleBuildScript("app").appendText(
"\n" + """
kotlin.targets.androidApp.attributes.attribute(
Attribute.of("com.example.target", String),
"notAndroidLib"
)
""".trimIndent()
)
build(":app:compileDebugKotlinAndroidApp") {
assertFailed() // dependency resolution should fail
assertContains("Required com.example.target 'notAndroidLib'")
}
gradleBuildScript("app").writeText(
appBuildScriptBackup + "\n" + """
kotlin.targets.androidApp.compilations.all {
attributes.attribute(
Attribute.of("com.example.compilation", String),
"notDebug"
)
}
""".trimIndent()
)
build(":app:compileDebugKotlinAndroidApp") {
assertFailed()
assertContains("Required com.example.compilation 'notDebug'")
}
}
} }
@Test @Test
@@ -66,7 +66,17 @@ kotlin {
} }
targets { targets {
fromPreset(presets.android, 'androidLib') fromPreset(presets.android, 'androidLib') {
attributes {
attribute(Attribute.of("com.example.target", String), "androidLib")
}
compilations.all {
attributes {
attribute(Attribute.of("com.example.compilation", String), compilationName)
}
}
}
fromPreset(presets.jvm, 'jvmLib') fromPreset(presets.jvm, 'jvmLib')
fromPreset(presets.js, 'jsLib') fromPreset(presets.js, 'jsLib')
} }
@@ -2,6 +2,7 @@ package org.jetbrains.kotlin.gradle.plugin
import com.android.build.gradle.BaseExtension import com.android.build.gradle.BaseExtension
import com.android.build.gradle.BasePlugin import com.android.build.gradle.BasePlugin
import com.android.build.gradle.api.BaseVariant
import com.android.builder.model.SourceProvider import com.android.builder.model.SourceProvider
import groovy.lang.Closure import groovy.lang.Closure
import org.gradle.api.* import org.gradle.api.*
@@ -748,6 +749,8 @@ abstract class AbstractAndroidProjectHandler<V>(private val kotlinConfigurationT
// in afterEvaluate, a user's build script might have already attached item handlers to the compilations container, and those // in afterEvaluate, a user's build script might have already attached item handlers to the compilations container, and those
// handlers might break when fired on a compilation that is not yet properly configured (e.g. KT-29964): // handlers might break when fired on a compilation that is not yet properly configured (e.g. KT-29964):
kotlinAndroidTarget.compilationFactory.create(variantName).let { compilation -> kotlinAndroidTarget.compilationFactory.create(variantName).let { compilation ->
compilation.androidVariant = variant as BaseVariant
setUpDependencyResolution(variant, compilation) setUpDependencyResolution(variant, compilation)
processVariant(variant, compilation, project, ext, plugin, kotlinOptions, kotlinConfigurationTools.kotlinTasksProvider) processVariant(variant, compilation, project, ext, plugin, kotlinOptions, kotlinConfigurationTools.kotlinTasksProvider)
@@ -78,8 +78,6 @@ class KotlinMultiplatformPlugin(
configureDefaultVersionsResolutionStrategy(project, kotlinPluginVersion) configureDefaultVersionsResolutionStrategy(project, kotlinPluginVersion)
configureSourceSets(project) configureSourceSets(project)
setUpConfigurationAttributes(project)
// set up metadata publishing // set up metadata publishing
targetsFromPreset.fromPreset( targetsFromPreset.fromPreset(
KotlinMetadataTargetPreset(project, instantiator, fileResolver, kotlinPluginVersion), KotlinMetadataTargetPreset(project, instantiator, fileResolver, kotlinPluginVersion),
@@ -87,6 +85,8 @@ class KotlinMultiplatformPlugin(
) )
configurePublishingWithMavenPublish(project) configurePublishingWithMavenPublish(project)
setUpConfigurationAttributes(project)
// propagate compiler plugin options to the source set language settings // propagate compiler plugin options to the source set language settings
setupCompilerPluginOptions(project) setupCompilerPluginOptions(project)
@@ -226,40 +226,49 @@ class KotlinMultiplatformPlugin(
} }
} }
/**
* The attributes attached to the targets and compilations need to be propagated to the relevant Gradle configurations:
* 1. Output configurations of each target need the corresponding compilation's attributes (and, indirectly, the target's attributes)
* 2. Resolvable configurations of each compilation need the compilation's attributes
*/
private fun setUpConfigurationAttributes(project: Project) { private fun setUpConfigurationAttributes(project: Project) {
val targets = project.multiplatformExtension.targets val targets = project.multiplatformExtension.targets
project.afterEvaluate { project.afterEvaluate {
targets.all { target -> targets.all { target ->
val mainCompilationAttributes = target.compilations.findByName(KotlinCompilation.MAIN_COMPILATION_NAME)?.attributes fun copyAttributes(from: AttributeContainer, to: AttributeContainer) {
?: return@all fun <T> copyAttribute(key: Attribute<T>, from: AttributeContainer, to: AttributeContainer) {
to.attribute(key, from.getAttribute(key)!!)
}
fun <T> copyAttribute(key: Attribute<T>, from: AttributeContainer, to: AttributeContainer) { from.keySet().forEach { key -> copyAttribute(key, from, to) }
to.attribute(key, from.getAttribute(key)!!)
} }
listOf( // To copy the attributes to the output configurations, find those output configurations and their producing compilations
target.apiElementsConfigurationName, // based on the target's components:
target.runtimeElementsConfigurationName, val outputConfigurationsWithCompilations =
target.defaultConfigurationName target.components.filterIsInstance<KotlinVariant>().flatMap { kotlinVariant ->
) kotlinVariant.usages.filterIsInstance<KotlinUsageContext>().mapNotNull { usageContext ->
.mapNotNull { configurationName -> target.project.configurations.findByName(configurationName) } project.configurations.findByName(usageContext.dependencyConfigurationName)?.let { configuration ->
.forEach { configuration -> configuration to usageContext.compilation
mainCompilationAttributes.keySet().forEach { key -> }
copyAttribute(key, mainCompilationAttributes, configuration.attributes)
} }
} } + listOfNotNull(
target.compilations.findByName(KotlinCompilation.MAIN_COMPILATION_NAME)?.let { mainCompilation ->
project.configurations.findByName(target.defaultConfigurationName)?.to(mainCompilation)
}
)
outputConfigurationsWithCompilations.forEach { (configuration, compilation) ->
copyAttributes(compilation.attributes, configuration.attributes)
}
target.compilations.all { compilation -> target.compilations.all { compilation ->
val compilationAttributes = compilation.attributes val compilationAttributes = compilation.attributes
compilation.relatedConfigurationNames compilation.relatedConfigurationNames
.mapNotNull { configurationName -> target.project.configurations.findByName(configurationName) } .mapNotNull { configurationName -> target.project.configurations.findByName(configurationName) }
.forEach { configuration -> .forEach { configuration -> copyAttributes(compilationAttributes, configuration.attributes) }
compilationAttributes.keySet().forEach { key ->
copyAttribute(key, compilationAttributes, configuration.attributes)
}
}
} }
} }
} }
@@ -20,7 +20,6 @@ import org.jetbrains.kotlin.gradle.utils.addExtendsFromRelation
import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName
import java.util.* import java.util.*
import java.util.concurrent.Callable import java.util.concurrent.Callable
import kotlin.collections.HashSet
internal fun KotlinCompilation<*>.composeName(prefix: String? = null, suffix: String? = null): String { internal fun KotlinCompilation<*>.composeName(prefix: String? = null, suffix: String? = null): String {
val compilationNamePart = compilationName.takeIf { it != KotlinCompilation.MAIN_COMPILATION_NAME } val compilationNamePart = compilationName.takeIf { it != KotlinCompilation.MAIN_COMPILATION_NAME }
@@ -106,10 +106,15 @@ class KotlinVariantWithMetadataDependency(
} }
private fun metadataDependency(): ModuleDependency { private fun metadataDependency(): ModuleDependency {
val metadataPublication = (metadataTarget.kotlinComponents.single() as KotlinTargetComponentWithPublication).publicationDelegate!! val metadataComponent = metadataTarget.kotlinComponents.single() as KotlinTargetComponentWithPublication
val metadataGroupId = metadataPublication.groupId val project = metadataTarget.project
val metadataArtifactId = metadataPublication.artifactId
val metadataVersion = metadataPublication.version // The metadata component may not be published, e.g. if the whole project is not published:
val metadataPublication: MavenPublication? = metadataComponent.publicationDelegate
val metadataGroupId = metadataPublication?.groupId ?: project.group
val metadataArtifactId = metadataPublication?.artifactId ?: metadataComponent.defaultArtifactId
val metadataVersion = metadataPublication?.version ?: project.version
return target.project.dependencies.module("$metadataGroupId:$metadataArtifactId:$metadataVersion") as ModuleDependency return target.project.dependencies.module("$metadataGroupId:$metadataArtifactId:$metadataVersion") as ModuleDependency
} }
@@ -6,15 +6,24 @@
@file:Suppress("PackageDirectoryMismatch") // Old package for compatibility @file:Suppress("PackageDirectoryMismatch") // Old package for compatibility
package org.jetbrains.kotlin.gradle.plugin.mpp package org.jetbrains.kotlin.gradle.plugin.mpp
import com.android.build.gradle.api.BaseVariant
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
class KotlinJvmAndroidCompilation( class KotlinJvmAndroidCompilation(
target: KotlinAndroidTarget, target: KotlinAndroidTarget,
name: String name: String
) : AbstractKotlinCompilationToRunnableFiles<KotlinJvmOptions>(target, name) { ) : AbstractKotlinCompilationToRunnableFiles<KotlinJvmOptions>(target, name) {
override val compileKotlinTask: org.jetbrains.kotlin.gradle.tasks.KotlinCompile
get() = super.compileKotlinTask as org.jetbrains.kotlin.gradle.tasks.KotlinCompile
override val relatedConfigurationNames: List<String> lateinit internal var androidVariant: BaseVariant
get() = super.relatedConfigurationNames + listOf("${name}ApiElements", "${name}RuntimeElements")
} override val compileKotlinTask: org.jetbrains.kotlin.gradle.tasks.KotlinCompile
get() = super.compileKotlinTask as org.jetbrains.kotlin.gradle.tasks.KotlinCompile
override val relatedConfigurationNames: List<String>
get() = super.relatedConfigurationNames + listOf(
"${androidVariant.name}ApiElements",
"${androidVariant.name}RuntimeElements",
androidVariant.compileConfiguration.name,
androidVariant.runtimeConfiguration.name
)
}