Always rewrite dependencies in POMs when publishing an MPP (KT-28482)

As non-Gradle consumers and Gradle consumers with metadata disabled
cannot read Gradle metadata, in POMs (the only source of dependencies
for the consumers mentioned above), we should publish the dependencies
on modules with metadata as the target artifact IDs rather than the root
MPP module ID (e.g. 'foo-jvm' rather than 'foo').

To do that, we rewrite the POMs of the publications even when Gradle
metadata is enabled. Note: in the POMs, a project dependency is already
written in the form of the artifact ID of the root Kotlin software
component, so it complicates the dependencies rewriting a little.

To rewrite third-party dependencies, we detect dependencies that
resolved to no artifact and have a single child in the Gradle
dependencies graph of the resolved configuration – this is what a
dependency looks like which was redirected to another module via
'available-at'.

Issue #KT-28482 Fixed
This commit is contained in:
Sergey Igushkin
2019-01-10 22:17:50 +03:00
parent 2fd524f1c0
commit f9c45c30a2
6 changed files with 250 additions and 77 deletions
@@ -10,7 +10,10 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.NativeOutputKind
import org.jetbrains.kotlin.gradle.plugin.mpp.UnusedSourceSetsChecker
import org.jetbrains.kotlin.gradle.plugin.sources.METADATA_CONFIGURATION_NAME_SUFFIX
import org.jetbrains.kotlin.gradle.plugin.sources.SourceSetConsistencyChecks
import org.jetbrains.kotlin.gradle.util.*
import org.jetbrains.kotlin.gradle.util.checkBytecodeContains
import org.jetbrains.kotlin.gradle.util.isWindows
import org.jetbrains.kotlin.gradle.util.modify
import org.jetbrains.kotlin.gradle.util.testResolveAllConfigurations
import org.jetbrains.kotlin.konan.target.CompilerOutputKind
import org.jetbrains.kotlin.konan.target.HostManager
import org.junit.Assert
@@ -603,7 +606,7 @@ class NewMultiplatformIT : BaseGradleIT() {
fun testPublishWithoutGradleMetadata() {
val libProject = Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")
with (libProject) {
with(libProject) {
setupWorkingDir()
projectDir.resolve("settings.gradle").modify { it.replace("enableFeaturePreview", "// enableFeaturePreview") }
@@ -1160,8 +1163,34 @@ class NewMultiplatformIT : BaseGradleIT() {
}
@Test
fun testPublishMultimoduleProjectWithNoMetadata() {
val libProject = Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")
fun testPublishMultimoduleProjectWithNoMetadata() = doTestPublishMultimoduleProject(withMetadata = false)
@Test
fun testPublishMultimoduleProjectWithMetadata() = doTestPublishMultimoduleProject(withMetadata = true)
private fun doTestPublishMultimoduleProject(withMetadata: Boolean) {
val libProject = Project("sample-lib", gradleVersion, "new-mpp-lib-and-app").apply {
if (withMetadata) {
setupWorkingDir()
// Make another publication that will be consumed as a module with metadata, see a similar conditional block below:
projectDir.resolve("settings.gradle").writeText("rootProject.name = 'external'; enableFeaturePreview 'GRADLE_METADATA'")
gradleBuildScript().appendText(
"\n" + """
group = "com.external.dependency"
version = "1.2.3"
publishing {
repositories {
maven { url "file://${'$'}{rootProject.projectDir.absolutePath.replace('\\', '/')}/repo" }
}
}
""".trimIndent()
)
build("publish") {
assertSuccessful()
gradleBuildScript().appendText("\ngroup = 'com.example'; version = '1.0'")
}
}
}
val appProject = Project("sample-app", gradleVersion, "new-mpp-lib-and-app")
with(libProject) {
@@ -1169,7 +1198,7 @@ class NewMultiplatformIT : BaseGradleIT() {
appProject.setupWorkingDir()
appProject.projectDir.copyRecursively(projectDir.resolve("sample-app"))
projectDir.resolve("settings.gradle").writeText("include 'sample-app'") // also disables feature preview 'GRADLE_METADATA'
gradleSettingsScript().writeText("include 'sample-app'") // disables feature preview 'GRADLE_METADATA', resets rootProject name
gradleBuildScript("sample-app").modify {
it.replace("'com.example:sample-lib:1.0'", "project(':')") + "\n" + """
apply plugin: 'maven-publish'
@@ -1182,6 +1211,20 @@ class NewMultiplatformIT : BaseGradleIT() {
}
""".trimIndent()
}
if (withMetadata) {
gradleSettingsScript().appendText("\nenableFeaturePreview(\"GRADLE_METADATA\")")
// Add a dependency that is resolved with metadata:
gradleBuildScript("sample-app").appendText(
"\n" + """
repositories {
maven { url "file://${'$'}{rootProject.projectDir.absolutePath.replace('\\', '/')}/repo" }
}
dependencies {
commonMainApi 'com.external.dependency:external:1.2.3'
}
""".trimIndent()
)
}
gradleBuildScript().appendText("\n" + """
publishing {
@@ -1195,7 +1238,7 @@ class NewMultiplatformIT : BaseGradleIT() {
}
""".trimIndent())
build("publish") {
build("clean", "publish") {
assertSuccessful()
assertFileContains(
"repo/com/exampleapp/sample-app-nodejs/1.0/sample-app-nodejs-1.0.pom",
@@ -1209,6 +1252,22 @@ class NewMultiplatformIT : BaseGradleIT() {
"<artifactId>bar</artifactId>",
"<version>42</version>"
)
if (withMetadata) {
// Check that the external dependency that was resolved with metadata is written to the POM as the artifactId it
// resolved to:
assertFileContains(
"repo/com/exampleapp/sample-app-jvm8/1.0/sample-app-jvm8-1.0.pom",
"<groupId>com.external.dependency</groupId>",
"<artifactId>external-jvm6</artifactId>",
"<version>1.2.3</version>"
)
// Check that, despite the rewritten POM, the module metadata contains the original dependency:
val moduleMetadata = projectDir.resolve("repo/com/exampleapp/sample-app-jvm8/1.0/sample-app-jvm8-1.0.module").readText()
.replace("\\s+".toRegex(), "").replace("\n", "")
assertTrue { "\"group\":\"com.example\",\"module\":\"sample-lib\"" in moduleMetadata }
assertTrue { "\"group\":\"com.external.dependency\",\"module\":\"external\"" in moduleMetadata }
}
assertFileExists("repo/foo/bar/42/bar-42.jar")
}
}
@@ -1285,10 +1344,10 @@ class NewMultiplatformIT : BaseGradleIT() {
assertFileContains("build/classes/kotlin/nodeJs/main/sample-lib.js", "Override")
val (compilerPluginArgsBySourceSet, compilerPluginClasspathBySourceSet) =
listOf(compilerPluginArgsRegex, compilerPluginClasspathRegex)
.map { marker ->
marker.findAll(output).associate { it.groupValues[1] to it.groupValues[2] }
}
listOf(compilerPluginArgsRegex, compilerPluginClasspathRegex)
.map { marker ->
marker.findAll(output).associate { it.groupValues[1] to it.groupValues[2] }
}
// TODO once Kotlin/Native properly supports compiler plugins, expand this to all source sets:
listOf("commonMain", "commonTest", "jvm6Main", "jvm6Test", "nodeJsMain", "nodeJsTest").forEach {
@@ -12,9 +12,8 @@ import org.gradle.util.ConfigureUtil
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.KotlinTargetPreset
import org.jetbrains.kotlin.gradle.plugin.KotlinTargetsContainerWithPresets
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinCommonCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMultiplatformPlugin
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinOnlyTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.*
import org.jetbrains.kotlin.gradle.utils.isGradleVersionAtLeast
open class KotlinMultiplatformExtension : KotlinProjectExtension(), KotlinTargetContainerWithPresetFunctions {
override lateinit var presets: NamedDomainObjectCollection<KotlinTargetPreset<*>>
@@ -44,6 +43,14 @@ open class KotlinMultiplatformExtension : KotlinProjectExtension(), KotlinTarget
fun targetFromPreset(preset: KotlinTargetPreset<*>) = targetFromPreset(preset, preset.name) { }
fun targetFromPreset(preset: KotlinTargetPreset<*>, name: String) = targetFromPreset(preset, name) { }
fun targetFromPreset(preset: KotlinTargetPreset<*>, configure: Closure<*>) = targetFromPreset(preset, preset.name, configure)
internal val rootSoftwareComponent: KotlinSoftwareComponent by lazy {
if (isGradleVersionAtLeast(4, 7)) {
KotlinSoftwareComponentWithCoordinatesAndPublication("kotlin", targets)
} else {
KotlinSoftwareComponent("kotlin", targets)
}
}
}
internal fun KotlinTarget.isProducedFromPreset(kotlinTargetPreset: KotlinTargetPreset<*>): Boolean =
@@ -6,8 +6,12 @@
package org.jetbrains.kotlin.gradle.plugin.mpp
import groovy.lang.Closure
import groovy.util.Node
import groovy.util.NodeList
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.XmlProvider
import org.gradle.api.artifacts.ModuleDependency
import org.gradle.api.attributes.Attribute
import org.gradle.api.attributes.AttributeContainer
import org.gradle.api.internal.FeaturePreviews
@@ -156,7 +160,7 @@ class KotlinMultiplatformPlugin(
}
val targets = project.multiplatformExtension!!.targets
val kotlinSoftwareComponent = KotlinSoftwareComponent("kotlin", targets)
val kotlinSoftwareComponent = project.multiplatformExtension!!.rootSoftwareComponent
project.extensions.configure(PublishingExtension::class.java) { publishing ->
@@ -203,6 +207,8 @@ class KotlinMultiplatformPlugin(
}
(this as MavenPublicationInternal).publishWithOriginalFileName()
artifactId = variant.defaultArtifactId
pom.withXml { xml -> project.rewritePomMppDependenciesToActualTargetModules(xml, variant) }
}
(variant as? KotlinTargetComponentWithPublication)?.publicationDelegate = variantPublication
@@ -308,3 +314,62 @@ internal fun compilationsBySourceSet(project: Project): Map<KotlinSourceSet, Set
}
}
}
private fun Project.rewritePomMppDependenciesToActualTargetModules(
pomXml: XmlProvider,
component: KotlinTargetComponent
) {
if (component !is SoftwareComponentInternal)
return
val dependenciesNodeList = pomXml.asNode().get("dependencies") as NodeList
val dependencyNodes = dependenciesNodeList.filterIsInstance<Node>().flatMap {
(it.get("dependency") as? NodeList).orEmpty()
}.filterIsInstance<Node>()
val dependencyByNode = mutableMapOf<Node, ModuleDependency>()
// Collect all the dependencies from the nodes:
val dependencies = dependencyNodes.map { dependencyNode ->
fun Node.getSingleChildValueOrNull(childName: String): String? =
((get(childName) as NodeList?)?.singleOrNull() as Node?)?.text()
val groupId = dependencyNode.getSingleChildValueOrNull("groupId")
val artifactId = dependencyNode.getSingleChildValueOrNull("artifactId")
val version = dependencyNode.getSingleChildValueOrNull("version")
(project.dependencies.module("$groupId:$artifactId:$version") as ModuleDependency)
.also { dependencyByNode[dependencyNode] = it }
}.toSet()
// Get the dependencies mapping according to the component's UsageContexts:
val resultDependenciesForEachUsageContext =
component.usages.mapNotNull { usage ->
if (usage is KotlinUsageContext)
rewriteDependenciesToActualModuleDependencies(usage, dependencies)
// We are only interested in dependencies that are mapped to some other dependencies:
.filter { (from, to) -> Triple(from.group, from.name, from.version) != Triple(to.group, to.name, to.version) }
else null
}
// Rewrite the dependency nodes according to the mapping:
dependencyNodes.forEach { dependencyNode ->
val moduleDependency = dependencyByNode[dependencyNode]
val mapDependencyTo = resultDependenciesForEachUsageContext.find { moduleDependency in it }?.get(moduleDependency)
if (mapDependencyTo != null) {
fun Node.setChildNodeByName(name: String, value: String?) {
val childNode: Node? = (get(name) as NodeList?)?.firstOrNull() as Node?
if (value != null) {
(childNode ?: appendNode(name)).setValue(value)
} else {
childNode?.let { remove(it) }
}
}
dependencyNode.setChildNodeByName("groupId", mapDependencyTo.group)
dependencyNode.setChildNodeByName("artifactId", mapDependencyTo.name)
dependencyNode.setChildNodeByName("version", mapDependencyTo.version)
}
}
}
@@ -10,15 +10,17 @@ import org.gradle.api.artifacts.*
import org.gradle.api.attributes.AttributeContainer
import org.gradle.api.attributes.Usage
import org.gradle.api.capabilities.Capability
import org.gradle.api.component.ComponentWithCoordinates
import org.gradle.api.component.ComponentWithVariants
import org.gradle.api.internal.component.SoftwareComponentInternal
import org.gradle.api.internal.component.UsageContext
import org.gradle.api.publish.maven.MavenPublication
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.utils.ifEmpty
class KotlinSoftwareComponent(
open class KotlinSoftwareComponent(
private val name: String,
private val kotlinTargets: Iterable<KotlinTarget>
protected val kotlinTargets: Iterable<KotlinTarget>
) : SoftwareComponentInternal, ComponentWithVariants {
override fun getUsages(): Set<UsageContext> = emptySet()
@@ -27,6 +29,18 @@ class KotlinSoftwareComponent(
kotlinTargets.flatMap { it.components }.toSet()
override fun getName(): String = name
// This property is declared in the parent type to allow the usages to reference it without forcing the subtypes to load,
// which is needed for compatibility with older Gradle versions
var publicationDelegate: MavenPublication? = null
}
class KotlinSoftwareComponentWithCoordinatesAndPublication(name: String, kotlinTargets: Iterable<KotlinTarget>) :
KotlinSoftwareComponent(name, kotlinTargets), ComponentWithCoordinates {
override fun getCoordinates(): ModuleVersionIdentifier = getCoordinatesFromPublicationDelegateAndProject(
publicationDelegate, kotlinTargets.first().project, null
)
}
// At the moment all KN artifacts have JAVA_API usage.
@@ -45,7 +59,6 @@ class DefaultKotlinUsageContext(
override val compilation: KotlinCompilation<*>,
private val usage: Usage,
override val dependencyConfigurationName: String,
private val publishWithGradleMetadata: Boolean,
override val sourcesArtifact: PublishArtifact? = null,
private val overrideConfigurationArtifacts: Set<PublishArtifact>? = null
) : KotlinUsageContext {
@@ -65,10 +78,7 @@ class DefaultKotlinUsageContext(
get() = project.configurations.getByName(dependencyConfigurationName)
override fun getDependencies(): MutableSet<out ModuleDependency> =
if (publishWithGradleMetadata)
configuration.incoming.dependencies.withType(ModuleDependency::class.java)
else
rewriteMppDependenciesToTargetModuleDependencies(this, configuration).toMutableSet()
configuration.incoming.dependencies.withType(ModuleDependency::class.java)
override fun getDependencyConstraints(): MutableSet<out DependencyConstraint> =
configuration.incoming.dependencyConstraints
@@ -86,14 +96,14 @@ class DefaultKotlinUsageContext(
override fun getGlobalExcludes(): Set<ExcludeRule> = emptySet()
}
private fun rewriteMppDependenciesToTargetModuleDependencies(
internal fun rewriteDependenciesToActualModuleDependencies(
usageContext: KotlinUsageContext,
fromConfiguration: Configuration
): Set<ModuleDependency> = with(usageContext.compilation.target.project) {
moduleDependencies: Set<ModuleDependency>
): Map<ModuleDependency, ModuleDependency> {
val compilation = usageContext.compilation
val moduleDependencies = fromConfiguration.incoming.dependencies.withType(ModuleDependency::class.java).ifEmpty { return emptySet() }
val project = compilation.target.project
val targetCompileDependenciesConfiguration = project.configurations.getByName(
val targetDependenciesConfiguration = project.configurations.getByName(
when (compilation) {
is KotlinJvmAndroidCompilation -> {
// TODO handle Android configuration names in a general way once we drop AGP < 3.0.0
@@ -112,52 +122,86 @@ private fun rewriteMppDependenciesToTargetModuleDependencies(
}
)
val resolvedCompileDependencies by lazy {
val resolvedDependencies by lazy {
// don't resolve if no project dependencies on MPP projects are found
targetCompileDependenciesConfiguration.resolvedConfiguration.lenientConfiguration.allModuleDependencies.associateBy {
targetDependenciesConfiguration.resolvedConfiguration.lenientConfiguration.allModuleDependencies.associateBy {
Triple(it.moduleGroup, it.moduleName, it.moduleVersion)
}
}
moduleDependencies.map { dependency ->
when (dependency) {
!is ProjectDependency -> dependency
else -> {
val dependencyProject = dependency.dependencyProject
val dependencyProjectKotlinExtension = dependencyProject.multiplatformExtension
?: return@map dependency
val resolvedModulesByRootModuleCoordinates = targetDependenciesConfiguration
.allDependencies.withType(ModuleDependency::class.java)
.associate { dependency ->
when (dependency) {
is ProjectDependency -> {
val dependencyProject = dependency.dependencyProject
val dependencyProjectKotlinExtension = dependencyProject.multiplatformExtension
?: return@associate dependency to dependency
if (dependencyProjectKotlinExtension.isGradleMetadataAvailable)
return@map dependency
val resolved = resolvedDependencies[Triple(dependency.group, dependency.name, dependency.version)]
?: return@associate dependency to dependency
val resolved = resolvedCompileDependencies[Triple(dependency.group, dependency.name, dependency.version)]
?: return@map dependency
val resolvedToConfiguration = resolved.configuration
val dependencyTargetComponent: KotlinTargetComponent = run {
dependencyProjectKotlinExtension.targets.forEach { target ->
target.components.forEach { component ->
if (component.findUsageContext(resolvedToConfiguration) != null)
return@run component
val resolvedToConfiguration = resolved.configuration
val dependencyTargetComponent: KotlinTargetComponent = run {
dependencyProjectKotlinExtension.targets.forEach { target ->
target.components.forEach { component ->
if (component.findUsageContext(resolvedToConfiguration) != null)
return@run component
}
}
// Failed to find a matching component:
return@associate dependency to dependency
}
// Failed to find a matching component:
return@map dependency
val targetModulePublication = (dependencyTargetComponent as? KotlinTargetComponentWithPublication)?.publicationDelegate
val rootModulePublication = dependencyProjectKotlinExtension.rootSoftwareComponent.publicationDelegate
// During Gradle POM generation, a project dependency is already written as the root module's coordinates. In the
// dependencies mapping, map the root module to the target's module:
val rootModule = project.dependencies.module(
listOf(
rootModulePublication?.groupId ?: dependency.group,
rootModulePublication?.artifactId ?: dependencyProject.name,
rootModulePublication?.version ?: dependency.version
).joinToString(":")
) as ModuleDependency
rootModule to project.dependencies.module(
listOf(
targetModulePublication?.groupId ?: dependency.group,
targetModulePublication?.artifactId ?: dependencyTargetComponent.defaultArtifactId,
targetModulePublication?.version ?: dependency.version
).joinToString(":")
) as ModuleDependency
}
else -> {
val resolvedDependency = resolvedDependencies[Triple(dependency.group, dependency.name, dependency.version)]
?: return@associate dependency to dependency
val publicationDelegate = (dependencyTargetComponent as? KotlinTargetComponentWithPublication)?.publicationDelegate
if (resolvedDependency.moduleArtifacts.isEmpty() && resolvedDependency.children.size == 1) {
// This is a dependency on a module that resolved to another module; map the original dependency to the target module
val targetModule = resolvedDependency.children.single()
dependency to project.dependencies.module(
listOf(
targetModule.moduleGroup,
targetModule.moduleName,
targetModule.moduleVersion
).joinToString(":")
) as ModuleDependency
dependencies.module(
listOf(
publicationDelegate?.groupId ?: dependency.group,
publicationDelegate?.artifactId ?: dependencyTargetComponent.defaultArtifactId,
publicationDelegate?.version ?: dependency.version
).joinToString(":")
) as ModuleDependency
} else {
dependency to dependency
}
}
}
}
}.toSet()
}.mapKeys { (key, _) -> Triple(key.group, key.name, key.version) }
return moduleDependencies.associate { dependency ->
val key = Triple(dependency.group, dependency.name, dependency.version)
val value = resolvedModulesByRootModuleCoordinates[key] ?: dependency
dependency to value
}
}
internal fun KotlinTargetComponent.findUsageContext(configurationName: String): UsageContext? {
@@ -95,9 +95,6 @@ abstract class AbstractKotlinTarget(
componentName: String,
artifactNameAppendix: String
): Set<DefaultKotlinUsageContext> {
val publishWithKotlinMetadata = (project.kotlinExtension as? KotlinMultiplatformExtension)?.isGradleMetadataAvailable
?: true // In non-MPP project, these usage contexts do not get published anyway
val sourcesJarArtifact = sourcesJarArtifact(producingCompilation, componentName, artifactNameAppendix)
// Here, `JAVA_API` and `JAVA_RUNTIME_JARS` are used intentionally as Gradle needs this for
@@ -111,7 +108,6 @@ abstract class AbstractKotlinTarget(
producingCompilation,
project.usageByName(usageName),
dependenciesConfigurationName,
publishWithKotlinMetadata,
sourcesJarArtifact
)
}
@@ -295,9 +291,6 @@ open class KotlinAndroidTarget(
classifierPrefix = artifactClassifier
)
val publishWithKotlinMetadata = (project.kotlinExtension as? KotlinMultiplatformExtension)?.isGradleMetadataAvailable
?: true // In non-MPP project, these usage contexts do not get published anyway
val variantName = getVariantName(variant)
val outputTask = getLibraryOutputTask(variant) ?: return emptySet()
val artifact = run {
@@ -325,7 +318,6 @@ open class KotlinAndroidTarget(
compilation,
project.usageByName(usageName),
dependencyConfigurationName,
publishWithKotlinMetadata,
sourcesJarArtifact,
overrideConfigurationArtifacts = setOf(artifact)
)
@@ -5,6 +5,7 @@
package org.jetbrains.kotlin.gradle.plugin.mpp
import org.gradle.api.Project
import org.gradle.api.artifacts.ExcludeRule
import org.gradle.api.artifacts.ModuleDependency
import org.gradle.api.artifacts.ModuleIdentifier
@@ -26,30 +27,35 @@ internal interface KotlinTargetComponentWithPublication : KotlinTargetComponent
var publicationDelegate: MavenPublication?
}
private interface KotlinTargetComponentWithCoordinatesAndPublication :
KotlinTargetComponentWithPublication,
ComponentWithCoordinates /* Gradle 4.7+ API, don't use with older versions */
{
override fun getCoordinates() = object : ModuleVersionIdentifier {
private val project get() = target.project
internal fun getCoordinatesFromPublicationDelegateAndProject(
publication: MavenPublication?,
project: Project,
target: KotlinTarget?
): ModuleVersionIdentifier =
object : ModuleVersionIdentifier {
private val moduleName: String
get() =
publicationDelegate?.artifactId ?: "${project.name}-${target.name.toLowerCase()}"
publication?.artifactId ?: lowerSpinalCaseName(project.name, target?.name)
private val moduleGroup: String
get() =
publicationDelegate?.groupId ?: project.group.toString()
publication?.groupId ?: project.group.toString()
override fun getGroup() = moduleGroup
override fun getName() = moduleName
override fun getVersion() = publicationDelegate?.version ?: project.version.toString()
override fun getVersion() = publication?.version ?: project.version.toString()
override fun getModule(): ModuleIdentifier = object : ModuleIdentifier {
override fun getGroup(): String = moduleGroup
override fun getName(): String = moduleName
}
}
private interface KotlinTargetComponentWithCoordinatesAndPublication :
KotlinTargetComponentWithPublication,
ComponentWithCoordinates /* Gradle 4.7+ API, don't use with older versions */
{
override fun getCoordinates() = getCoordinatesFromPublicationDelegateAndProject(publicationDelegate, target.project, target)
}
open class KotlinVariant(