[Gradle] Fix CME when subplugins create source sets

GranularMetadataTransformation.ProjectData is created for all projects
There was a chance when it is created too early before some projects
evaluation. And if that happened that these projects contained some
subplugins (for example KSP) that add more source sets. Then old
algorithm of collecting sourceSetsMetadataOutputs() would fail with
ConcurrentModificationException. For example in this scenario:

Lets assume we evaluate sourceSetsMetadataOutputs for project with
just one common source set: commonMain.
And therefore one metadata compilation whose default source set name is:
commonMain.
On top of that a subplugin applied to this project. This subplugin
creates additional source set for each metadata compilation.

Then with old algorithm following would happen:

* start iterating over sourceSets
* for `commonMain` source set await its Metadata Compilation
* * When Metadata Compilation gets configured at some point of time
 it will reach KotlinCommonSourceSetProcessor.doTargetSpecificProcessing
 method. Which will apply subplugins.
* * When subplugin is applied it adds a new Source Set to
  the sourceSets collections: CME is occurred!

Adding a source set in subplugins should be regulated
either allow it and fix related side effects OR deprecate it.

The current fix doesn't solve all potential issues with adding new
source sets from subplugins. It primarily focused on CME occurrence
for the case above.

^KT-62299 Verification Pending
This commit is contained in:
Anton Lakotka
2023-10-04 10:21:50 +02:00
committed by Space Team
parent f124ba627d
commit 0230f809c4
3 changed files with 85 additions and 16 deletions
@@ -30,7 +30,7 @@ internal class KotlinCommonSourceSetProcessor(
}
}
project.whenEvaluated {
project.launchInStage(KotlinPluginLifecycle.Stage.AfterEvaluateBuildscript) {
val subpluginEnvironment: SubpluginEnvironment = SubpluginEnvironment.loadSubplugins(project)
/* Not supported in KPM yet */
compilationInfo.tcs.compilation.let { compilation ->
@@ -9,13 +9,11 @@ import org.gradle.api.Project
import org.gradle.api.file.FileCollection
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.Stage.AfterEvaluateBuildscript
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.await
import org.jetbrains.kotlin.gradle.plugin.mpp.MetadataDependencyResolution.ChooseVisibleSourceSets.MetadataProvider.ProjectMetadataProvider
import org.jetbrains.kotlin.gradle.targets.metadata.findMetadataCompilation
import org.jetbrains.kotlin.gradle.targets.native.internal.*
import org.jetbrains.kotlin.gradle.targets.metadata.awaitMetadataCompilationsCreated
private typealias SourceSetName = String
@@ -34,7 +32,7 @@ private class ProjectMetadataProviderImpl(
) : ProjectMetadataProvider() {
override fun getSourceSetCompiledMetadata(sourceSetName: String): FileCollection? {
val metadataOutputs = sourceSetMetadataOutputs[sourceSetName] ?: error("Unexpected source set '$sourceSetName'")
val metadataOutputs = sourceSetMetadataOutputs[sourceSetName] ?: return null
return metadataOutputs.metadata
}
@@ -64,14 +62,10 @@ internal suspend fun Project.collectSourceSetMetadataOutputs(): Map<SourceSetNam
}
private suspend fun KotlinMultiplatformExtension.sourceSetsMetadataOutputs(): Map<KotlinSourceSet, FileCollection?> {
return awaitSourceSets().associateWith { sourceSet ->
when (val compilation = project.findMetadataCompilation(sourceSet)) {
null -> null
is KotlinCommonCompilation -> compilation.output.classesDirs
is KotlinSharedNativeCompilation -> compilation.output.classesDirs
else -> error("Unexpected compilation type: $compilation")
}
}
}
val metadataTarget = metadata() as KotlinMetadataTarget
return metadataTarget
.awaitMetadataCompilationsCreated()
// TODO: KT-62332/Stop-Creating-legacy-metadata-compilation-with-name-main
.filter { if (it is KotlinCommonCompilation) it.isKlibCompilation else true }
.associate { it.defaultSourceSet to it.output.classesDirs }
}
@@ -0,0 +1,75 @@
/*
* Copyright 2010-2023 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("FunctionName")
package org.jetbrains.kotlin.gradle.regressionTests
import org.gradle.api.provider.Provider
import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin
import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact
import org.jetbrains.kotlin.gradle.plugin.SubpluginOption
import org.jetbrains.kotlin.gradle.plugin.mpp.GranularMetadataTransformation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMetadataCompilation
import org.jetbrains.kotlin.gradle.util.buildProject
import org.jetbrains.kotlin.gradle.util.buildProjectWithMPP
import org.jetbrains.kotlin.gradle.util.kotlin
import org.jetbrains.kotlin.util.assertDoesNotThrow
import kotlin.test.Test
class KT61652AddSourceSetInSubpluginAndEarlyTaskMaterialization {
@Test
fun `test GranularMetadataTransformation doesn't fail when it is created too early and subplugin applied`() {
class TestSubplugin : KotlinCompilerPluginSupportPlugin {
override fun getCompilerPluginId(): String = "test"
override fun getPluginArtifact(): SubpluginArtifact = SubpluginArtifact("test", "test")
override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean = true
override fun applyToCompilation(kotlinCompilation: KotlinCompilation<*>): Provider<List<SubpluginOption>> {
val project = kotlinCompilation.project
val emptyResult = project.provider { emptyList<SubpluginOption>() }
if (kotlinCompilation !is KotlinMetadataCompilation) return emptyResult
if (kotlinCompilation.name == "main") return emptyResult
project
.kotlinExtension
.sourceSets
.create("generatedBy" + kotlinCompilation.defaultSourceSet.name)
return emptyResult
}
}
val root = buildProject {}
val lib = buildProjectWithMPP(projectBuilder = { withParent(root).withName("lib") }) {
kotlin { jvm(); linuxX64() }
}
val app = buildProjectWithMPP(projectBuilder = { withParent(root).withName("app") }) {
kotlin {
jvm()
linuxX64()
sourceSets.getByName("commonMain").dependencies { api(project(":lib")) }
}
}
lib.plugins.apply(TestSubplugin::class.java)
app.evaluate()
/**
* Create GranularMetadataTransformation.Params artificially to reproduce real-case scenario when
* either IDE dependencies resolvers create it or transform*DependenciesMetadata
*/
val gmtParams = GranularMetadataTransformation.Params(app, app.kotlinExtension.sourceSets.getByName("commonMain"))
val allProjectsData = gmtParams.projectData
lib.evaluate()
assertDoesNotThrow { allProjectsData[":lib"]!!.sourceSetMetadataOutputs.getOrThrow() }
}
}