Fix JVM maven-publish caching by not capturing state in POM rewriting

In POM rewriting logic, ensure that non-serializable entities are
accessed from within project.provider { ... } and their evaluation
results are therefore properly serialized.

Issue #KT-43054 Fixed
This commit is contained in:
Sergey Igushkin
2020-11-06 21:18:06 +03:00
parent d1ba2f3d46
commit 3666327c81
4 changed files with 184 additions and 117 deletions
@@ -18,6 +18,27 @@ class ConfigurationCacheIT : AbstractConfigurationCacheIT() {
testConfigurationCacheOf(":compileKotlin") testConfigurationCacheOf(":compileKotlin")
} }
@Test
fun testJvmWithMavenPublish() = with(Project("kotlinProject")) {
setupWorkingDir()
gradleBuildScript().appendText("""
apply plugin: "maven-publish"
group = "com.example"
version = "1.0"
publishing.repositories {
maven {
url = "${'$'}buildDir/repo"
}
}
publishing.publications {
maven(MavenPublication) {
from(components["java"])
}
}
""".trimIndent())
testConfigurationCacheOf(":publishMavenPublicationToMavenRepository", checkUpToDateOnRebuild = false)
}
@Test @Test
fun testIncrementalKaptProject() = with(Project("kaptIncrementalCompilationProject")) { fun testIncrementalKaptProject() = with(Project("kaptIncrementalCompilationProject")) {
setupIncrementalAptProject("AGGREGATING") setupIncrementalAptProject("AGGREGATING")
@@ -57,6 +78,7 @@ abstract class AbstractConfigurationCacheIT : BaseGradleIT() {
protected fun Project.testConfigurationCacheOf( protected fun Project.testConfigurationCacheOf(
vararg taskNames: String, vararg taskNames: String,
executedTaskNames: List<String>? = null, executedTaskNames: List<String>? = null,
checkUpToDateOnRebuild: Boolean = true,
buildOptions: BuildOptions = defaultBuildOptions() buildOptions: BuildOptions = defaultBuildOptions()
) { ) {
// First, run a build that serializes the tasks state for instant execution in further builds // First, run a build that serializes the tasks state for instant execution in further builds
@@ -81,9 +103,11 @@ abstract class AbstractConfigurationCacheIT : BaseGradleIT() {
assertContains("Reusing configuration cache.") assertContains("Reusing configuration cache.")
} }
build(*taskNames, options = buildOptions) { if (checkUpToDateOnRebuild) {
assertSuccessful() build(*taskNames, options = buildOptions) {
assertTasksUpToDate(executedTask) assertSuccessful()
assertTasksUpToDate(executedTask)
}
} }
} }
@@ -22,6 +22,8 @@ import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.plugins.MavenPluginConvention import org.gradle.api.plugins.MavenPluginConvention
import org.gradle.api.provider.Provider import org.gradle.api.provider.Provider
import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPom
import org.gradle.api.artifacts.maven.MavenPom as OldMavenPom
import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.tasks.* import org.gradle.api.tasks.*
import org.gradle.api.tasks.compile.AbstractCompile import org.gradle.api.tasks.compile.AbstractCompile
@@ -415,19 +417,32 @@ internal abstract class AbstractKotlinPlugin(
project.components.addAll(target.components) project.components.addAll(target.components)
} }
private fun rewritePom(pom: MavenPom, rewriter: PomDependenciesRewriter, shouldRewritePom: Provider<Boolean>) {
pom.withXml { xml ->
if (shouldRewritePom.get())
rewriter.rewritePomMppDependenciesToActualTargetModules(xml)
}
}
private fun rewritePom(pom: OldMavenPom, rewriter: PomDependenciesRewriter, shouldRewritePom: Provider<Boolean>) {
pom.withXml { xml ->
if (shouldRewritePom.get())
rewriter.rewritePomMppDependenciesToActualTargetModules(xml)
}
}
private fun rewriteMppDependenciesInPom(target: AbstractKotlinTarget) { private fun rewriteMppDependenciesInPom(target: AbstractKotlinTarget) {
val project = target.project val project = target.project
fun shouldRewritePoms(): Boolean = val shouldRewritePoms = project.provider {
PropertiesProvider(project).keepMppDependenciesIntactInPoms != true PropertiesProvider(project).keepMppDependenciesIntactInPoms != true
}
project.pluginManager.withPlugin("maven-publish") { project.pluginManager.withPlugin("maven-publish") {
project.extensions.configure(PublishingExtension::class.java) { publishing -> project.extensions.configure(PublishingExtension::class.java) { publishing ->
val pomRewriter = PomDependenciesRewriter(project, target.kotlinComponents.single())
publishing.publications.withType(MavenPublication::class.java).all { publication -> publishing.publications.withType(MavenPublication::class.java).all { publication ->
publication.pom.withXml { xml -> rewritePom(publication.pom, pomRewriter, shouldRewritePoms)
if (shouldRewritePoms())
project.rewritePomMppDependenciesToActualTargetModules(xml, target.kotlinComponents.single())
}
} }
} }
} }
@@ -435,10 +450,8 @@ internal abstract class AbstractKotlinPlugin(
project.pluginManager.withPlugin("maven") { project.pluginManager.withPlugin("maven") {
project.tasks.withType(Upload::class.java).all { uploadTask -> project.tasks.withType(Upload::class.java).all { uploadTask ->
uploadTask.repositories.withType(MavenResolver::class.java).all { mavenResolver -> uploadTask.repositories.withType(MavenResolver::class.java).all { mavenResolver ->
mavenResolver.pom.withXml { xml -> val pomRewriter = PomDependenciesRewriter(project, target.kotlinComponents.single())
if (shouldRewritePoms()) rewritePom(mavenResolver.pom, pomRewriter, shouldRewritePoms)
project.rewritePomMppDependenciesToActualTargetModules(xml, target.kotlinComponents.single())
}
} }
} }
@@ -13,8 +13,10 @@ import org.gradle.api.attributes.AttributeContainer
import org.gradle.api.internal.FeaturePreviews import org.gradle.api.internal.FeaturePreviews
import org.gradle.api.internal.plugins.DslObject import org.gradle.api.internal.plugins.DslObject
import org.gradle.api.plugins.JavaBasePlugin import org.gradle.api.plugins.JavaBasePlugin
import org.gradle.api.provider.Provider
import org.gradle.api.publish.PublicationContainer import org.gradle.api.publish.PublicationContainer
import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPom
import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.publish.maven.internal.publication.MavenPublicationInternal import org.gradle.api.publish.maven.internal.publication.MavenPublicationInternal
import org.gradle.api.tasks.SourceTask import org.gradle.api.tasks.SourceTask
@@ -234,6 +236,18 @@ class KotlinMultiplatformPlugin(
project.components.add(kotlinSoftwareComponent) project.components.add(kotlinSoftwareComponent)
} }
private fun rewritePom(
pom: MavenPom,
pomRewriter: PomDependenciesRewriter,
shouldRewritePomDependencies: Provider<Boolean>,
includeOnlySpecifiedDependencies: Provider<Set<ModuleCoordinates>>?
) {
pom.withXml { xml ->
if (shouldRewritePomDependencies.get())
pomRewriter.rewritePomMppDependenciesToActualTargetModules(xml, includeOnlySpecifiedDependencies)
}
}
private fun AbstractKotlinTarget.createMavenPublications(publications: PublicationContainer) { private fun AbstractKotlinTarget.createMavenPublications(publications: PublicationContainer) {
components components
.map { gradleComponent -> gradleComponent to kotlinComponents.single { it.name == gradleComponent.name } } .map { gradleComponent -> gradleComponent to kotlinComponents.single { it.name == gradleComponent.name } }
@@ -250,12 +264,16 @@ class KotlinMultiplatformPlugin(
(this as MavenPublicationInternal).publishWithOriginalFileName() (this as MavenPublicationInternal).publishWithOriginalFileName()
artifactId = kotlinComponent.defaultArtifactId artifactId = kotlinComponent.defaultArtifactId
pom.withXml { xml -> val pomRewriter = PomDependenciesRewriter(project, kotlinComponent)
if (PropertiesProvider(project).keepMppDependenciesIntactInPoms != true) val shouldRewritePomDependencies =
project.rewritePomMppDependenciesToActualTargetModules(xml, kotlinComponent) { id -> project.provider { PropertiesProvider(project).keepMppDependenciesIntactInPoms != true }
filterMetadataDependencies(this@createMavenPublications, id)
} rewritePom(
} pom,
pomRewriter,
shouldRewritePomDependencies,
dependenciesForPomRewriting(this@createMavenPublications)
)
} }
(kotlinComponent as? KotlinTargetComponentWithPublication)?.publicationDelegate = componentPublication (kotlinComponent as? KotlinTargetComponentWithPublication)?.publicationDelegate = componentPublication
@@ -269,23 +287,24 @@ class KotlinMultiplatformPlugin(
* can't read Gradle module metadata won't resolve a dependency on an MPP to the granular metadata variant and won't then choose the * can't read Gradle module metadata won't resolve a dependency on an MPP to the granular metadata variant and won't then choose the
* right dependencies for each source set, we put only the dependencies of the legacy common variant into the POM, i.e. commonMain API. * right dependencies for each source set, we put only the dependencies of the legacy common variant into the POM, i.e. commonMain API.
*/ */
private fun filterMetadataDependencies(target: AbstractKotlinTarget, groupNameVersion: Triple<String?, String, String?>): Boolean { private fun dependenciesForPomRewriting(target: AbstractKotlinTarget): Provider<Set<ModuleCoordinates>>? =
if (target !is KotlinMetadataTarget || !target.project.isKotlinGranularMetadataEnabled) { if (target !is KotlinMetadataTarget || !target.project.isKotlinGranularMetadataEnabled)
return true null
else {
val commonMain = target.project.kotlinExtension.sourceSets.findByName(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME)
if (commonMain == null)
null
else
target.project.provider {
val project = target.project
// Only the commonMain API dependencies can be published for consumers who can't read Gradle project metadata
val commonMainApi = project.sourceSetDependencyConfigurationByScope(commonMain, KotlinDependencyScope.API_SCOPE)
val commonMainDependencies = commonMainApi.allDependencies
commonMainDependencies.map { ModuleCoordinates(it.group, it.name, it.version) }.toSet()
}
} }
val (group, name, _) = groupNameVersion
val project = target.project
val commonMain = project.kotlinExtension.sourceSets?.findByName(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME)
?: return true
// Only the commonMain API dependencies can be published for consumers who can't read Gradle project metadata
val commonMainApi = project.sourceSetDependencyConfigurationByScope(commonMain, KotlinDependencyScope.API_SCOPE)
return commonMainApi.allDependencies.any { it.group == group && it.name == name }
}
private fun configureSourceSets(project: Project) = with(project.multiplatformExtension) { private fun configureSourceSets(project: Project) = with(project.multiplatformExtension) {
val production = sourceSets.create(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME) val production = sourceSets.create(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME)
val test = sourceSets.create(KotlinSourceSet.COMMON_TEST_SOURCE_SET_NAME) val test = sourceSets.create(KotlinSourceSet.COMMON_TEST_SOURCE_SET_NAME)
@@ -14,82 +14,103 @@ import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.attributes.Usage import org.gradle.api.attributes.Usage
import org.gradle.api.internal.component.SoftwareComponentInternal import org.gradle.api.internal.component.SoftwareComponentInternal
import org.gradle.api.internal.component.UsageContext import org.gradle.api.internal.component.UsageContext
import org.gradle.api.provider.Provider
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilationToRunnableFiles import org.jetbrains.kotlin.gradle.plugin.KotlinCompilationToRunnableFiles
import org.jetbrains.kotlin.gradle.plugin.KotlinTargetComponent import org.jetbrains.kotlin.gradle.plugin.KotlinTargetComponent
import org.jetbrains.kotlin.gradle.utils.getValue
internal fun Project.rewritePomMppDependenciesToActualTargetModules( internal data class ModuleCoordinates(
pomXml: XmlProvider, val group: String?,
component: KotlinTargetComponent, val name: String,
filterDependencies: (groupNameVersion: Triple<String?, String, String?>) -> Boolean = { true } val version: String?
)
internal class PomDependenciesRewriter(
project: Project,
@field:Transient
private val component: KotlinTargetComponent
) { ) {
if (component !is SoftwareComponentInternal)
return
val dependenciesNode = (pomXml.asNode().get("dependencies") as NodeList).filterIsInstance<Node>().singleOrNull() ?: return
val dependencyNodes = (dependenciesNode.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: // Get the dependencies mapping according to the component's UsageContexts:
val resultDependenciesForEachUsageContext = private val dependenciesMappingForEachUsageContext by project.provider {
component.usages.mapNotNull { usage -> (component as SoftwareComponentInternal).usages.mapNotNull { usage ->
if (usage is KotlinUsageContext) if (usage is KotlinUsageContext)
associateDependenciesWithActualModuleDependencies(usage, dependencies) associateDependenciesWithActualModuleDependencies(usage)
// We are only interested in dependencies that are mapped to some other 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) } .filter { (from, to) -> Triple(from.group, from.name, from.version) != Triple(to.group, to.name, to.version) }
else null else null
} }
}
// Rewrite the dependency nodes according to the mapping: fun rewritePomMppDependenciesToActualTargetModules(
dependencyNodes.forEach { dependencyNode -> pomXml: XmlProvider,
val moduleDependency = dependencyByNode[dependencyNode] includeOnlySpecifiedDependencies: Provider<Set<ModuleCoordinates>>? = null
) {
if (component !is SoftwareComponentInternal)
return
if (moduleDependency != null) { val dependenciesNode = (pomXml.asNode().get("dependencies") as NodeList).filterIsInstance<Node>().singleOrNull() ?: return
val groupNameVersion = Triple(moduleDependency.group, moduleDependency.name, moduleDependency.version)
if (!filterDependencies(groupNameVersion)) { val dependencyNodes = (dependenciesNode.get("dependency") as? NodeList).orEmpty().filterIsInstance<Node>()
dependenciesNode.remove(dependencyNode)
return@forEach val dependencyByNode = mutableMapOf<Node, ModuleCoordinates>()
}
// 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")
?: error("unexpected dependency in POM with no artifact ID: $dependenciesNode")
val version = dependencyNode.getSingleChildValueOrNull("version")
(ModuleCoordinates(groupId, artifactId, version)).also { dependencyByNode[dependencyNode] = it }
}.toSet()
val resultDependenciesForEachUsageContext = dependencies.associate { key ->
val map = dependenciesMappingForEachUsageContext.find { key in it }
val value = map?.get(key) ?: key
key to value
} }
val mapDependencyTo = resultDependenciesForEachUsageContext.find { moduleDependency in it }?.get(moduleDependency) val includeOnlySpecifiedDependenciesSet = includeOnlySpecifiedDependencies?.get()
if (mapDependencyTo != null) { // Rewrite the dependency nodes according to the mapping:
dependencyNodes.forEach { dependencyNode ->
val moduleDependency = dependencyByNode[dependencyNode]
fun Node.setChildNodeByName(name: String, value: String?) { if (moduleDependency != null) {
val childNode: Node? = (get(name) as NodeList?)?.firstOrNull() as Node? if (includeOnlySpecifiedDependenciesSet != null && moduleDependency !in includeOnlySpecifiedDependenciesSet) {
if (value != null) { dependenciesNode.remove(dependencyNode)
(childNode ?: appendNode(name)).setValue(value) return@forEach
} else {
childNode?.let { remove(it) }
} }
} }
dependencyNode.setChildNodeByName("groupId", mapDependencyTo.group) val mapDependencyTo = resultDependenciesForEachUsageContext.get(moduleDependency)
dependencyNode.setChildNodeByName("artifactId", mapDependencyTo.name)
dependencyNode.setChildNodeByName("version", mapDependencyTo.version) 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)
}
} }
} }
} }
private fun associateDependenciesWithActualModuleDependencies( private fun associateDependenciesWithActualModuleDependencies(
usageContext: KotlinUsageContext, usageContext: KotlinUsageContext
moduleDependencies: Set<ModuleDependency> ): Map<ModuleCoordinates, ModuleCoordinates> {
): Map<ModuleDependency, ModuleDependency> {
val compilation = usageContext.compilation val compilation = usageContext.compilation
val project = compilation.target.project val project = compilation.target.project
@@ -119,17 +140,19 @@ private fun associateDependenciesWithActualModuleDependencies(
} }
} }
val resolvedModulesByRootModuleCoordinates = targetDependenciesConfiguration return targetDependenciesConfiguration
.allDependencies.withType(ModuleDependency::class.java) .allDependencies.withType(ModuleDependency::class.java)
.associate { dependency -> .associate { dependency ->
val coordinates = ModuleCoordinates(dependency.group, dependency.name, dependency.version)
val noMapping = coordinates to coordinates
when (dependency) { when (dependency) {
is ProjectDependency -> { is ProjectDependency -> {
val dependencyProject = dependency.dependencyProject val dependencyProject = dependency.dependencyProject
val dependencyProjectKotlinExtension = dependencyProject.multiplatformExtensionOrNull val dependencyProjectKotlinExtension = dependencyProject.multiplatformExtensionOrNull
?: return@associate dependency to dependency ?: return@associate noMapping
val resolved = resolvedDependencies[Triple(dependency.group, dependency.name, dependency.version)] val resolved = resolvedDependencies[Triple(dependency.group, dependency.name, dependency.version)]
?: return@associate dependency to dependency ?: return@associate noMapping
val resolvedToConfiguration = resolved.configuration val resolvedToConfiguration = resolved.configuration
val dependencyTargetComponent: KotlinTargetComponent = run { val dependencyTargetComponent: KotlinTargetComponent = run {
@@ -140,7 +163,7 @@ private fun associateDependenciesWithActualModuleDependencies(
} }
} }
// Failed to find a matching component: // Failed to find a matching component:
return@associate dependency to dependency return@associate noMapping
} }
val targetModulePublication = (dependencyTargetComponent as? KotlinTargetComponentWithPublication)?.publicationDelegate val targetModulePublication = (dependencyTargetComponent as? KotlinTargetComponentWithPublication)?.publicationDelegate
@@ -149,49 +172,37 @@ private fun associateDependenciesWithActualModuleDependencies(
// During Gradle POM generation, a project dependency is already written as the root module's coordinates. In the // 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: // dependencies mapping, map the root module to the target's module:
val rootModule = project.dependencies.module( val rootModule = ModuleCoordinates(
listOf( rootModulePublication?.groupId ?: dependency.group,
rootModulePublication?.groupId ?: dependency.group, rootModulePublication?.artifactId ?: dependencyProject.name,
rootModulePublication?.artifactId ?: dependencyProject.name, rootModulePublication?.version ?: dependency.version
rootModulePublication?.version ?: dependency.version )
).joinToString(":")
) as ModuleDependency
rootModule to project.dependencies.module( rootModule to ModuleCoordinates(
listOf( targetModulePublication?.groupId ?: dependency.group,
targetModulePublication?.groupId ?: dependency.group, targetModulePublication?.artifactId ?: dependencyTargetComponent.defaultArtifactId,
targetModulePublication?.artifactId ?: dependencyTargetComponent.defaultArtifactId, targetModulePublication?.version ?: dependency.version
targetModulePublication?.version ?: dependency.version )
).joinToString(":")
) as ModuleDependency
} }
else -> { else -> {
val resolvedDependency = resolvedDependencies[Triple(dependency.group, dependency.name, dependency.version)] val resolvedDependency = resolvedDependencies[Triple(dependency.group, dependency.name, dependency.version)]
?: return@associate dependency to dependency ?: return@associate noMapping
if (resolvedDependency.moduleArtifacts.isEmpty() && resolvedDependency.children.size == 1) { 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 // 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() val targetModule = resolvedDependency.children.single()
dependency to project.dependencies.module( coordinates to ModuleCoordinates(
listOf( targetModule.moduleGroup,
targetModule.moduleGroup, targetModule.moduleName,
targetModule.moduleName, targetModule.moduleVersion
targetModule.moduleVersion )
).joinToString(":")
) as ModuleDependency
} else { } else {
dependency to dependency noMapping
} }
} }
} }
}.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
}
} }
private fun KotlinTargetComponent.findUsageContext(configurationName: String): UsageContext? { private fun KotlinTargetComponent.findUsageContext(configurationName: String): UsageContext? {