From 405c94e0bb1efe9dae1a592d7c1097cdf3ff374b Mon Sep 17 00:00:00 2001 From: Sebastian Sellmair Date: Tue, 8 Nov 2022 09:56:28 +0100 Subject: [PATCH] [Gradle] Initial port of IdeaKpmProjectModelBuilder into TCS IdeMultiplatformImport KT-54825 --- .../kotlin/gradle/plugin/ide/IdeDependency.kt | 55 +++++++ .../plugin/ide/IdeDependencyCoordinates.kt | 26 +++ .../gradle/plugin/ide/IdeDependencyEffect.kt | 21 +++ .../plugin/ide/IdeDependencyResolver.kt | 51 ++++++ .../plugin/ide/IdeDependencyTransformer.kt | 59 +++++++ .../ide/IdeExtrasSerializationExtension.kt | 46 ++++++ .../gradle/plugin/ide/IdeExtrasSerializer.kt | 27 ++++ .../IdeJavaIoSerializableExtrasSerializer.kt | 43 +++++ .../plugin/ide/IdeMultiplatformImport.kt | 150 ++++++++++++++++++ .../ide/IdeMultiplatformImportFactory.kt | 26 +++ .../plugin/ide/IdeMultiplatformImportImpl.kt | 132 +++++++++++++++ .../plugin/ide/IdeSerializationContext.kt | 11 ++ .../plugin/ide/IdeSerializationLogger.kt | 21 +++ .../IdeDependsOnDependencyResolver.kt | 32 ++++ .../plugin/sources/DefaultKotlinSourceSet.kt | 4 +- .../plugin/sources/InternalKotlinSourceSet.kt | 2 + ...latformResolveDependsOnDependenciesTest.kt | 63 ++++++++ .../ide/ideDependencyResolutionAssertions.kt | 91 +++++++++++ 18 files changed, 859 insertions(+), 1 deletion(-) create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeDependency.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeDependencyCoordinates.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeDependencyEffect.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeDependencyResolver.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeDependencyTransformer.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeExtrasSerializationExtension.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeExtrasSerializer.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeJavaIoSerializableExtrasSerializer.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeMultiplatformImport.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeMultiplatformImportFactory.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeMultiplatformImportImpl.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeSerializationContext.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeSerializationLogger.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/dependencyResolvers/IdeDependsOnDependencyResolver.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/ide/IdeMultiplatformResolveDependsOnDependenciesTest.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/ide/ideDependencyResolutionAssertions.kt diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeDependency.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeDependency.kt new file mode 100644 index 00000000000..117287c3332 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeDependency.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2010-2022 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. + */ + +package org.jetbrains.kotlin.gradle.plugin.ide + +import org.jetbrains.kotlin.tooling.core.Extras +import java.io.File +import java.io.Serializable + +sealed class IdeDependency { + abstract val coordinates: IdeDependencyCoordinates? + abstract val extras: Extras + + /* Reflection helper */ + + val isSourceDependency: Boolean get() = this is IdeSourceDependency + val isBinaryDependency: Boolean get() = this is IdeBinaryDependency + val isResolvedBinaryDependency: Boolean get() = this is IdeResolvedBinaryDependency + val isUnresolvedBinaryDependency: Boolean get() = this is IdeUnresolvedBinaryDependency + + companion object { + const val CLASSPATH_BINARY_TYPE = "org.jetbrains.kotlin.gradle.plugin.ide.IdeDependency.classpathBinaryType" + const val SOURCES_BINARY_TYPE = "org.jetbrains.kotlin.gradle.plugin.ide.IdeDependency.sourcesBinaryType" + const val DOCUMENTATION_BINARY_TYPE = "org.jetbrains.kotlin.gradle.plugin.ide.IdeDependency.documentationBinaryType" + } +} + +data class IdeSourceDependency( + val type: Type, + override val coordinates: IdeSourceCoordinates, + override val extras: Extras +) : IdeDependency() { + enum class Type : Serializable { + Regular, Friend, DependsOn + } +} + +sealed class IdeBinaryDependency : IdeDependency() { + abstract override val coordinates: IdeBinaryCoordinates? +} + +data class IdeUnresolvedBinaryDependency( + val cause: String?, + override val coordinates: IdeBinaryCoordinates?, + override val extras: Extras +) : IdeBinaryDependency() + +data class IdeResolvedBinaryDependency( + val binaryType: String, + val binaryFile: File, + override val extras: Extras, + override val coordinates: IdeBinaryCoordinates? +) : IdeBinaryDependency() \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeDependencyCoordinates.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeDependencyCoordinates.kt new file mode 100644 index 00000000000..d4c2b598b34 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeDependencyCoordinates.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2010-2022 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. + */ + +package org.jetbrains.kotlin.gradle.plugin.ide + +sealed class IdeDependencyCoordinates { + /* Reflection helpers */ + val isBinaryCoordinates get() = this is IdeBinaryCoordinates + val isSourceCoordinates get() = this is IdeSourceCoordinates +} + +data class IdeBinaryCoordinates( + val group: String, + val module: String, + val version: String?, + val sourceSetName: String? +) : IdeDependencyCoordinates() + +data class IdeSourceCoordinates( + val buildId: String, + val projectPath: String, + val projectName: String, + val sourceSetName: String +) : IdeDependencyCoordinates() \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeDependencyEffect.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeDependencyEffect.kt new file mode 100644 index 00000000000..4f3f1e8d200 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeDependencyEffect.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2010-2022 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. + */ + +package org.jetbrains.kotlin.gradle.plugin.ide + +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet + +/** + * An [IdeDependencyEffect] is invoked last in the dependency resolution pipeline. + * It is not intended/capable of changing the resolution result, but can trigger side-effects, being sure + * that the dependency resolution result is final. + */ +fun interface IdeDependencyEffect { + operator fun invoke(sourceSet: KotlinSourceSet, dependencies: Set) +} + +fun IdeDependencyResolver.withEffect(effect: IdeDependencyEffect) = IdeDependencyResolver { sourceSet -> + this@withEffect.resolve(sourceSet).also { dependencies -> effect(sourceSet, dependencies) } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeDependencyResolver.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeDependencyResolver.kt new file mode 100644 index 00000000000..ba1739b2b9c --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeDependencyResolver.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2010-2022 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. + */ + +package org.jetbrains.kotlin.gradle.plugin.ide + +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet + +fun interface IdeDependencyResolver { + fun resolve(sourceSet: KotlinSourceSet): Set + + object Empty : IdeDependencyResolver { + override fun resolve(sourceSet: KotlinSourceSet): Set = emptySet() + } +} + +fun IdeDependencyResolver( + resolvers: Iterable +): IdeDependencyResolver { + val resolversList = resolvers.filterNotNull() + if (resolversList.isEmpty()) return IdeDependencyResolver.Empty + return IdeCompositeDependencyResolver(resolversList) +} + +fun IdeDependencyResolver( + vararg resolvers: IdeDependencyResolver? +): IdeDependencyResolver = IdeDependencyResolver(resolvers.toList()) + + +operator fun IdeDependencyResolver.plus(other: IdeDependencyResolver): IdeDependencyResolver { + if (this is IdeCompositeDependencyResolver && other is IdeCompositeDependencyResolver) + return IdeCompositeDependencyResolver(this.children + other.children) + + if (this is IdeCompositeDependencyResolver) { + return IdeCompositeDependencyResolver(this.children + other) + } + + if (other is IdeCompositeDependencyResolver) { + return IdeCompositeDependencyResolver(listOf(this) + other.children) + } + return IdeCompositeDependencyResolver(listOf(this, other)) +} + +private class IdeCompositeDependencyResolver( + val children: List +) : IdeDependencyResolver { + override fun resolve(sourceSet: KotlinSourceSet): Set { + return children.flatMap { child -> child.resolve(sourceSet) }.toSet() + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeDependencyTransformer.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeDependencyTransformer.kt new file mode 100644 index 00000000000..b621c6a844a --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeDependencyTransformer.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2010-2022 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. + */ + +package org.jetbrains.kotlin.gradle.plugin.ide + +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet + +/** + * An [IdeDependencyTransformer] will be invoked after all [IdeDependencyResolver] finished. + * The transformation step is allowed to entirely modify the resolution result. Typical applications would be dependency filtering, + * or grouping. Adding additional dependencies might be harmful, since other transformers might not be able to see this additional + * dependency, running before the addition. + * + * Transformations can be scheduled in different phases see: [IdeMultiplatformImport.DependencyTransformationPhase] + */ +fun interface IdeDependencyTransformer { + fun transform(sourceSet: KotlinSourceSet, dependencies: Set): Set +} + +fun IdeDependencyResolver.withTransformer(transformer: IdeDependencyTransformer) = IdeDependencyResolver { sourceSet -> + transformer.transform(sourceSet, this@withTransformer.resolve(sourceSet)) +} + +fun IdeDependencyTransformer( + transformers: List +): IdeDependencyTransformer = IdeCompositeDependencyTransformer(transformers.filterNotNull()) + +fun IdeDependencyTransformer( + vararg transformers: IdeDependencyTransformer? +): IdeDependencyTransformer = IdeDependencyTransformer(transformers.toList()) + +operator fun IdeDependencyTransformer.plus(other: IdeDependencyTransformer): + IdeDependencyTransformer { + if (this is IdeCompositeDependencyTransformer && other is IdeCompositeDependencyTransformer) { + return IdeCompositeDependencyTransformer(this.transformers + other.transformers) + } + + if (this is IdeCompositeDependencyTransformer) { + return IdeCompositeDependencyTransformer(this.transformers + other) + } + + if (other is IdeCompositeDependencyTransformer) { + return IdeCompositeDependencyTransformer(listOf(this) + other.transformers) + } + + return IdeCompositeDependencyTransformer(listOf(this, other)) +} + +private class IdeCompositeDependencyTransformer( + val transformers: List +) : IdeDependencyTransformer { + override fun transform( + sourceSet: KotlinSourceSet, dependencies: Set + ): Set { + return transformers.fold(dependencies) { currentDependencies, transformer -> transformer.transform(sourceSet, currentDependencies) } + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeExtrasSerializationExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeExtrasSerializationExtension.kt new file mode 100644 index 00000000000..d0b0df03d99 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeExtrasSerializationExtension.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2010-2022 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. + */ + +package org.jetbrains.kotlin.gradle.plugin.ide + +import org.jetbrains.kotlin.tooling.core.Extras + +interface IdeExtrasSerializationExtension { + fun serializer(key: Extras.Key): IdeExtrasSerializer? + + object Empty : IdeExtrasSerializationExtension { + override fun serializer(key: Extras.Key): IdeExtrasSerializer? = null + } +} + +interface IdeExtrasSerializationExtensionBuilder { + fun register(key: Extras.Key, serializer: IdeExtrasSerializer) +} + +fun IdeExtrasSerializationExtension( + builder: IdeExtrasSerializationExtensionBuilder.() -> Unit +): IdeExtrasSerializationExtension { + return IdeaKpmExtrasSerializationExtensionImpl( + IdeaExtrasSerializationExtensionBuilderImpl().apply(builder).serializers + ) +} + +private class IdeaExtrasSerializationExtensionBuilderImpl : IdeExtrasSerializationExtensionBuilder { + val serializers = mutableMapOf, IdeExtrasSerializer<*>>() + + override fun register(key: Extras.Key, serializer: IdeExtrasSerializer) { + serializers[key] = serializer + } +} + + +private class IdeaKpmExtrasSerializationExtensionImpl( + private val map: Map, IdeExtrasSerializer<*>> +) : IdeExtrasSerializationExtension { + override fun serializer(key: Extras.Key): IdeExtrasSerializer? { + @Suppress("unchecked_cast") + return map[key] as? IdeExtrasSerializer + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeExtrasSerializer.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeExtrasSerializer.kt new file mode 100644 index 00000000000..44a247fe98d --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeExtrasSerializer.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2010-2022 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. + */ + +package org.jetbrains.kotlin.gradle.plugin.ide + +import kotlin.reflect.KClass + + +interface IdeExtrasSerializer { + fun serialize(context: IdeSerializationContext, value: T): ByteArray? + fun deserialize(context: IdeSerializationContext, data: ByteArray): T? + + companion object { + /** + * Returns a [IdeExtrasSerializer] based upon [java.io.Serializable] + */ + fun javaIoSerializable(clazz: KClass): IdeExtrasSerializer { + return IdeJavaIoSerializableExtrasSerializer(clazz) + } + + inline fun javaIoSerializable(): IdeExtrasSerializer { + return javaIoSerializable(T::class) + } + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeJavaIoSerializableExtrasSerializer.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeJavaIoSerializableExtrasSerializer.kt new file mode 100644 index 00000000000..acf654d580a --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeJavaIoSerializableExtrasSerializer.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2010-2022 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. + */ + +package org.jetbrains.kotlin.gradle.plugin.ide + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import kotlin.reflect.KClass + +internal class IdeJavaIoSerializableExtrasSerializer( + private val clazz: KClass +) : IdeExtrasSerializer { + + override fun serialize(context: IdeSerializationContext, value: T): ByteArray? { + return try { + ByteArrayOutputStream().use { byteArrayOutputStream -> + ObjectOutputStream(byteArrayOutputStream).use { oos -> oos.writeObject(value) } + byteArrayOutputStream.toByteArray() + } + } catch (t: Throwable) { + context.logger.error("${ErrorMessages.SERIALIZATION_FAILURE} $value", t) + null + } + } + + override fun deserialize(context: IdeSerializationContext, data: ByteArray): T? { + return try { + ObjectInputStream(ByteArrayInputStream(data)).use { stream -> clazz.java.cast(stream.readObject()) } + } catch (t: Throwable) { + context.logger.error("${ErrorMessages.DESERIALIZATION_FAILURE} ${clazz.java}", t) + null + } + } + + object ErrorMessages { + const val SERIALIZATION_FAILURE = "Failed to serialize" + const val DESERIALIZATION_FAILURE = "Failed to deserialize" + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeMultiplatformImport.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeMultiplatformImport.kt new file mode 100644 index 00000000000..f4738affbc0 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeMultiplatformImport.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2010-2022 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. + */ + + +package org.jetbrains.kotlin.gradle.plugin.ide + +import org.gradle.api.Project +import org.jetbrains.kotlin.gradle.ExternalKotlinTargetApi +import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.extraProperties +import org.jetbrains.kotlin.gradle.plugin.ide.IdeMultiplatformImport.DependencyResolutionLevel.Default +import org.jetbrains.kotlin.gradle.plugin.ide.IdeMultiplatformImport.DependencyResolutionLevel.Overwrite +import org.jetbrains.kotlin.gradle.plugin.ide.IdeMultiplatformImport.SourceSetConstraint +import org.jetbrains.kotlin.gradle.targets.metadata.isNativeSourceSet +import org.jetbrains.kotlin.gradle.utils.getOrPut +import org.jetbrains.kotlin.tooling.core.Extras + +interface IdeMultiplatformImport { + + fun resolveDependencies(sourceSet: KotlinSourceSet): Set + + fun serialize(key: Extras.Key, value: T): ByteArray? + + @ExternalKotlinTargetApi + fun registerDependencyResolver( + resolver: IdeDependencyResolver, + constraint: SourceSetConstraint, + phase: DependencyResolutionPhase, + level: DependencyResolutionLevel = Default, + ) + + @ExternalKotlinTargetApi + fun registerDependencyTransformer( + transformer: IdeDependencyTransformer, + constraint: SourceSetConstraint, + phase: DependencyTransformationPhase + ) + + @ExternalKotlinTargetApi + fun registerDependencyEffect( + effect: IdeDependencyEffect, + constraint: SourceSetConstraint + ) + + @ExternalKotlinTargetApi + fun registerExtrasSerializationExtension( + extension: IdeExtrasSerializationExtension + ) + + /** + * Any [IdeDependencyResolver] has to be registered for a given dependency resolution phase in which it participates + * The resolution phases will be executed in the order of their enum's ordinal. + */ + enum class DependencyResolutionPhase { + /** + * Generic phase intended to run before all other resolvers + */ + PreDependencyResolution, + + /** + * Resolution phase intended to resolve project to project (source) dependencies + */ + SourceDependencyResolution, + + /** + * Resolution phase intended to resolve binary dependencies (downloading and transforming from repositories) + */ + BinaryDependencyResolution, + + /** + * Generic phase intended to run after all other resolvers. + */ + PostDependencyResolution + } + + /** + * Any [IdeDependencyResolver] has to be registered specifying a certain resolution level. + * Generally, all resolvers registered in a given resolution level will work collaboratively, meaning the dependency resolution + * result is the aggregation of all resolvers running. + * + * However, only the resolvers in the highest resolution result will run e.g. + * If resolvers with level [Overwrite] are found, then only those will contribute to the dependency resolution. + * Otherwise, all [Default] resolvers will run. + */ + enum class DependencyResolutionLevel { + Default, Overwrite + } + + /** + * Any [IdeDependencyResolver] has to be registered for a given transformation phase. + * The phases will be executed in the order of this enums ordinal. + */ + enum class DependencyTransformationPhase { + /** + * Generic dependency transformation phase, intended to run a transformation before all other transformers. + */ + PreDependencyTransformationPhase, + + /** + * Dependency transformation phase that is free entirely free in its transformation type. + * Note: Adding dependencies to the resolution result might most likely better be modelled as [IdeDependencyResolver] + */ + FreeDependencyTransformationPhase, + + /** + * Special dependency transformation phase intended for filtering dependencies. + * This phase is guaranteed to run after the [FreeDependencyTransformationPhase] alongside other dependency filtering + * transformations. Adding filters here is the safest. + */ + DependencyFilteringPhase, + + /** + * Generic dependency transformation phase, intended to run a transformation after all other transformers + */ + PostDependencyTransformationPhase + } + + /** + * Used for scoping [IdeDependencyResolver], [IdeDependencyResolver] and [IdeDependencyEffect] + */ + fun interface SourceSetConstraint { + operator fun invoke(sourceSet: KotlinSourceSet): Boolean + + companion object { + val unconstrained = SourceSetConstraint { true } + val isNative = SourceSetConstraint { sourceSet -> isNativeSourceSet(sourceSet) } + } + } + + companion object { + @JvmStatic + fun instance(project: Project): IdeMultiplatformImport { + return project.extraProperties.getOrPut(IdeMultiplatformImport::class.java.name) { + IdeMultiplatformImport(project.multiplatformExtension) + } + } + } +} + +internal val Project.kotlinIdeMultiplatformImport: IdeMultiplatformImport get() = IdeMultiplatformImport.instance(project) + +@ExternalKotlinTargetApi +fun IdeMultiplatformImport.registerExtrasSerializationExtension( + builder: IdeExtrasSerializationExtensionBuilder.() -> Unit +) { + registerExtrasSerializationExtension(IdeExtrasSerializationExtension(builder)) +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeMultiplatformImportFactory.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeMultiplatformImportFactory.kt new file mode 100644 index 00000000000..5819b839c0f --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeMultiplatformImportFactory.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2010-2022 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. + */ + +package org.jetbrains.kotlin.gradle.plugin.ide + +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.kpm.idea.kotlinDebugKey +import org.jetbrains.kotlin.gradle.plugin.ide.dependencyResolvers.IdeDependsOnDependencyResolver + +fun IdeMultiplatformImport(extension: KotlinMultiplatformExtension): IdeMultiplatformImport { + return IdeMultiplatformImportImpl(extension).apply { + + registerDependencyResolver( + resolver = IdeDependsOnDependencyResolver, + constraint = IdeMultiplatformImport.SourceSetConstraint.unconstrained, + phase = IdeMultiplatformImport.DependencyResolutionPhase.SourceDependencyResolution, + level = IdeMultiplatformImport.DependencyResolutionLevel.Default + ) + + registerExtrasSerializationExtension { + register(kotlinDebugKey, IdeExtrasSerializer.javaIoSerializable()) + } + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeMultiplatformImportImpl.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeMultiplatformImportImpl.kt new file mode 100644 index 00000000000..edafed96cbc --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeMultiplatformImportImpl.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2010-2022 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. + */ + +package org.jetbrains.kotlin.gradle.plugin.ide + +import org.jetbrains.kotlin.gradle.ExternalKotlinTargetApi +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.ide.IdeMultiplatformImport.* +import org.jetbrains.kotlin.tooling.core.Extras + + +internal class IdeMultiplatformImportImpl( + private val extension: KotlinMultiplatformExtension +) : IdeMultiplatformImport { + + override fun resolveDependencies(sourceSet: KotlinSourceSet): Set { + return createDependencyResolver().resolve(sourceSet) + } + + override fun serialize(key: Extras.Key, value: T): ByteArray? { + return null + } + + private data class RegisteredDependencyResolver( + val resolver: IdeDependencyResolver, + val constraint: SourceSetConstraint, + val phase: DependencyResolutionPhase, + val level: DependencyResolutionLevel, + ) + + private data class RegisteredDependencyTransformer( + val transformer: IdeDependencyTransformer, + val constraint: SourceSetConstraint, + val phase: DependencyTransformationPhase + ) + + private data class RegisteredDependencyEffect( + val effect: IdeDependencyEffect, + val constraint: SourceSetConstraint, + ) + + private val registeredDependencyResolvers = mutableListOf() + private val registeredDependencyTransformers = mutableListOf() + private val registeredDependencyEffects = mutableListOf() + private val registeredExtrasSerializationExtensions = mutableListOf() + + @ExternalKotlinTargetApi + override fun registerDependencyResolver( + resolver: IdeDependencyResolver, + constraint: SourceSetConstraint, + phase: DependencyResolutionPhase, + level: DependencyResolutionLevel + ) { + registeredDependencyResolvers.add( + RegisteredDependencyResolver(resolver, constraint, phase, level) + ) + } + + @ExternalKotlinTargetApi + override fun registerDependencyTransformer( + transformer: IdeDependencyTransformer, + constraint: SourceSetConstraint, + phase: DependencyTransformationPhase + ) { + registeredDependencyTransformers.add( + RegisteredDependencyTransformer(transformer, constraint, phase) + ) + } + + @ExternalKotlinTargetApi + override fun registerDependencyEffect(effect: IdeDependencyEffect, constraint: SourceSetConstraint) { + registeredDependencyEffects.add( + RegisteredDependencyEffect(effect, constraint) + ) + } + + @ExternalKotlinTargetApi + override fun registerExtrasSerializationExtension(extension: IdeExtrasSerializationExtension) { + registeredExtrasSerializationExtensions.add(extension) + } + + private fun createDependencyResolver(): IdeDependencyResolver { + return IdeDependencyResolver(DependencyResolutionPhase.values().map { phase -> + createDependencyResolver(phase) + }).withTransformer(createDependencyTransformer()) + .withEffect(createDependencyEffect()) + } + + private fun createDependencyResolver(phase: DependencyResolutionPhase) = IdeDependencyResolver resolve@{ sourceSet -> + val applicableResolvers = registeredDependencyResolvers + .filter { it.phase == phase } + .filter { it.constraint(sourceSet) } + .groupBy { it.level } + + /* Find resolvers in the highest resolution level and only consider those */ + DependencyResolutionLevel.values().reversed().forEach { level -> + val resolvers = applicableResolvers[level].orEmpty().map { it.resolver } + if (resolvers.isNotEmpty()) { + return@resolve IdeDependencyResolver(resolvers).resolve(sourceSet) + } + } + + /* No resolvers found */ + emptySet() + } + + private fun createDependencyTransformer(): IdeDependencyTransformer { + return IdeDependencyTransformer(DependencyTransformationPhase.values().map { phase -> + createDependencyTransformer(phase) + }) + } + + private fun createDependencyTransformer(phase: DependencyTransformationPhase): IdeDependencyTransformer { + return IdeDependencyTransformer { sourceSet, dependencies -> + IdeDependencyTransformer( + registeredDependencyTransformers + .filter { it.phase == phase } + .filter { it.constraint(sourceSet) } + .map { it.transformer } + ).transform(sourceSet, dependencies) + } + } + + private fun createDependencyEffect(): IdeDependencyEffect = IdeDependencyEffect { sourceSet, dependencies -> + registeredDependencyEffects + .filter { it.constraint(sourceSet) } + .forEach { it.effect(sourceSet, dependencies) } + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeSerializationContext.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeSerializationContext.kt new file mode 100644 index 00000000000..2e1681f0c7f --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeSerializationContext.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2010-2022 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. + */ + +package org.jetbrains.kotlin.gradle.plugin.ide + +interface IdeSerializationContext { + val logger: IdeSerializationLogger + val extrasSerializationExtension: IdeExtrasSerializationExtension +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeSerializationLogger.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeSerializationLogger.kt new file mode 100644 index 00000000000..7f6e8b88fd0 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/IdeSerializationLogger.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2010-2022 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. + */ + +package org.jetbrains.kotlin.gradle.plugin.ide + +interface IdeSerializationLogger { + enum class Severity { + WARNING, + ERROR, + } + + fun report(severity: Severity, message: String, cause: Throwable? = null) + fun warn(message: String, cause: Throwable? = null) = report(Severity.WARNING, message, cause) + fun error(message: String, cause: Throwable? = null) = report(Severity.ERROR, message, cause) + + object None : IdeSerializationLogger { + override fun report(severity: Severity, message: String, cause: Throwable?) = Unit + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/dependencyResolvers/IdeDependsOnDependencyResolver.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/dependencyResolvers/IdeDependsOnDependencyResolver.kt new file mode 100644 index 00000000000..2913aea55fe --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/dependencyResolvers/IdeDependsOnDependencyResolver.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2010-2022 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. + */ + +package org.jetbrains.kotlin.gradle.plugin.ide.dependencyResolvers + +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.ide.IdeDependency +import org.jetbrains.kotlin.gradle.plugin.ide.IdeDependencyResolver +import org.jetbrains.kotlin.gradle.plugin.ide.IdeSourceCoordinates +import org.jetbrains.kotlin.gradle.plugin.ide.IdeSourceDependency +import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.currentBuildId +import org.jetbrains.kotlin.gradle.plugin.sources.internal +import org.jetbrains.kotlin.tooling.core.emptyExtras + +internal object IdeDependsOnDependencyResolver : IdeDependencyResolver { + override fun resolve(sourceSet: KotlinSourceSet): Set { + return sourceSet.internal.dependsOnClosure.map { dependsOnSourceSet -> + IdeSourceDependency( + type = IdeSourceDependency.Type.DependsOn, + coordinates = IdeSourceCoordinates( + buildId = dependsOnSourceSet.internal.project.currentBuildId().name, + projectPath = dependsOnSourceSet.internal.project.path, + projectName = dependsOnSourceSet.internal.project.name, + sourceSetName = dependsOnSourceSet.name + ), + extras = emptyExtras() + ) + }.toSet() + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/DefaultKotlinSourceSet.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/DefaultKotlinSourceSet.kt index 99b82bf4cb0..46d9c870ccc 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/DefaultKotlinSourceSet.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/DefaultKotlinSourceSet.kt @@ -11,6 +11,7 @@ import org.gradle.api.Action import org.gradle.api.Project import org.gradle.api.file.SourceDirectorySet import org.jetbrains.kotlin.build.DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS +import org.jetbrains.kotlin.gradle.InternalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet @@ -24,8 +25,9 @@ import javax.inject.Inject const val METADATA_CONFIGURATION_NAME_SUFFIX = "DependenciesMetadata" +@InternalKotlinGradlePluginApi abstract class DefaultKotlinSourceSet @Inject constructor( - private val project: Project, + final override val project: Project, val displayName: String ) : AbstractKotlinSourceSet() { diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/InternalKotlinSourceSet.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/InternalKotlinSourceSet.kt index 8537d052de8..cc4ab373f4a 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/InternalKotlinSourceSet.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/InternalKotlinSourceSet.kt @@ -5,6 +5,7 @@ package org.jetbrains.kotlin.gradle.plugin.sources +import org.gradle.api.Project import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet import org.jetbrains.kotlin.gradle.utils.MutableObservableSet @@ -16,6 +17,7 @@ internal val KotlinSourceSet.internal: InternalKotlinSourceSet ) internal interface InternalKotlinSourceSet : KotlinSourceSet { + val project: Project override val dependsOn: ObservableSet val dependsOnClosure: ObservableSet val withDependsOnClosure: ObservableSet diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/ide/IdeMultiplatformResolveDependsOnDependenciesTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/ide/IdeMultiplatformResolveDependsOnDependenciesTest.kt new file mode 100644 index 00000000000..2be72ca5764 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/ide/IdeMultiplatformResolveDependsOnDependenciesTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2010-2022 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.ide + +import org.jetbrains.kotlin.gradle.buildProjectWithMPP +import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.ide.IdeSourceDependency +import org.jetbrains.kotlin.gradle.plugin.ide.kotlinIdeMultiplatformImport +import kotlin.test.Test + +class IdeMultiplatformResolveDependsOnDependenciesTest { + + @Test + fun `test - sample 0 - default dependsOn to commonMain and commonTest`() { + val project = buildProjectWithMPP() + val kotlin = project.multiplatformExtension + kotlin.jvm() + + val commonMain = kotlin.sourceSets.getByName("commonMain") + val commonTest = kotlin.sourceSets.getByName("commonTest") + val jvmMain = kotlin.sourceSets.getByName("jvmMain") + val jvmTest = kotlin.sourceSets.getByName("jvmTest") + + + project.kotlinIdeMultiplatformImport.resolveDependencies(jvmMain) + .filterIsInstance() + .assertMatches(dependsOnDependency(commonMain)) + + + project.kotlinIdeMultiplatformImport.resolveDependencies(jvmTest) + .filterIsInstance() + .assertMatches(dependsOnDependency(commonTest)) + } + + @Test + fun `test - sample 1 - custom dependsOn edge`() { + val project = buildProjectWithMPP() + val kotlin = project.multiplatformExtension + kotlin.jvm() + val commonMain = kotlin.sourceSets.getByName("commonMain") + val customMain = kotlin.sourceSets.create("customMain") + val jvmMain = kotlin.sourceSets.getByName("jvmMain") + + jvmMain.dependsOn(customMain) + customMain.dependsOn(commonMain) + + project.kotlinIdeMultiplatformImport.resolveDependencies(jvmMain) + .filterIsInstance() + .assertMatches( + dependsOnDependency(commonMain), + dependsOnDependency(customMain) + ) + + project.kotlinIdeMultiplatformImport.resolveDependencies(customMain) + .filterIsInstance() + .assertMatches(dependsOnDependency(commonMain)) + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/ide/ideDependencyResolutionAssertions.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/ide/ideDependencyResolutionAssertions.kt new file mode 100644 index 00000000000..5aa4b148bab --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/ide/ideDependencyResolutionAssertions.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2010-2022 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. + */ + +package org.jetbrains.kotlin.gradle.ide + +import org.gradle.api.Project +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.ide.IdeDependency +import org.jetbrains.kotlin.gradle.plugin.ide.IdeSourceDependency +import org.jetbrains.kotlin.gradle.plugin.sources.internal +import org.junit.jupiter.api.fail + +fun buildIdeDependencyMatchers(notation: Any?): List { + return when (notation) { + null -> return emptyList() + is IdeDependencyMatcher -> listOf(notation) + is Iterable<*> -> notation.flatMap { buildIdeDependencyMatchers(it) } + else -> error("Can't build ${IdeSourceDependencyMatcher::class.java.simpleName} from $notation") + } +} + +fun ideSourceDependency(type: IdeSourceDependency.Type, project: Project, sourceSetName: String): IdeDependencyMatcher { + return IdeSourceDependencyMatcher(type, project.path, sourceSetName) +} + +fun dependsOnDependency(project: Project, sourceSetName: String) = + ideSourceDependency(IdeSourceDependency.Type.DependsOn, project, sourceSetName) + +fun dependsOnDependency(sourceSet: KotlinSourceSet) = + dependsOnDependency(sourceSet.internal.project, sourceSet.name) + +interface IdeDependencyMatcher { + val description: String + fun matches(dependency: IdeDependency): Boolean +} + +private class IdeSourceDependencyMatcher( + val type: IdeSourceDependency.Type, + val projectPath: String, + val sourceSetName: String +) : IdeDependencyMatcher { + override val description: String = + "source($type)::$projectPath/$sourceSetName" + + override fun matches(dependency: IdeDependency): Boolean { + if (dependency !is IdeSourceDependency) return false + return dependency.type == type && + dependency.coordinates.projectPath == projectPath && + dependency.coordinates.sourceSetName == sourceSetName + } +} + +fun Iterable.assertMatches(vararg notation: Any?) { + val thisList = toList() + val matchers = notation.flatMap { buildIdeDependencyMatchers(it) } + + val unexpectedDependencies = thisList.filter { dependency -> matchers.none { matcher -> matcher.matches(dependency) } } + val missingDependencies = matchers.filter { matcher -> thisList.none { dependency -> matcher.matches(dependency) } } + + if (unexpectedDependencies.isEmpty() && missingDependencies.isEmpty()) { + return + } + + fail( + buildString { + if (unexpectedDependencies.isNotEmpty()) { + appendLine() + appendLine("Unexpected dependency found:") + unexpectedDependencies.forEach { unexpectedDependency -> + appendLine("\"${unexpectedDependency}\",") + } + } + + if (missingDependencies.isNotEmpty()) { + appendLine() + appendLine("Missing dependencies:") + missingDependencies.forEach { missingDependency -> + appendLine(missingDependency.description) + } + } + + appendLine() + appendLine("Dependencies:") + thisList.forEach { dependency -> + appendLine("\"${dependency}\",") + } + } + ) +} \ No newline at end of file