From 0230f809c4d94bea7e0a396075076bea423ff1e8 Mon Sep 17 00:00:00 2001 From: Anton Lakotka Date: Wed, 4 Oct 2023 10:21:50 +0200 Subject: [PATCH] [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 --- .../plugin/KotlinCommonSourceSetProcessor.kt | 2 +- .../plugin/mpp/ProjectMetadataProviderImpl.kt | 24 +++--- ...tInSubpluginAndEarlyTaskMaterialization.kt | 75 +++++++++++++++++++ 3 files changed, 85 insertions(+), 16 deletions(-) create mode 100644 libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/regressionTests/KT61652AddSourceSetInSubpluginAndEarlyTaskMaterialization.kt diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinCommonSourceSetProcessor.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinCommonSourceSetProcessor.kt index 3c2af2bf282..125be56df61 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinCommonSourceSetProcessor.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinCommonSourceSetProcessor.kt @@ -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 -> diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/ProjectMetadataProviderImpl.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/ProjectMetadataProviderImpl.kt index 029727ae8b6..b3d38c513f8 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/ProjectMetadataProviderImpl.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/ProjectMetadataProviderImpl.kt @@ -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 { - 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 } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/regressionTests/KT61652AddSourceSetInSubpluginAndEarlyTaskMaterialization.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/regressionTests/KT61652AddSourceSetInSubpluginAndEarlyTaskMaterialization.kt new file mode 100644 index 00000000000..1195bf3a3ab --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/regressionTests/KT61652AddSourceSetInSubpluginAndEarlyTaskMaterialization.kt @@ -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> { + val project = kotlinCompilation.project + val emptyResult = project.provider { emptyList() } + + 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() } + } +} \ No newline at end of file