From c5d61625782c78fa2ddac7ed183c4b303334f324 Mon Sep 17 00:00:00 2001 From: Sebastian Sellmair Date: Fri, 19 Aug 2022 14:39:25 +0200 Subject: [PATCH] [Gradle][MPP] Implement initial KotlinTargetHierarchy APIs ^KT-53570 Verification Pending --- .../kotlin-gradle-plugin-api.txt | 44 +++ .../gradle/dsl/KotlinTargetHierarchyDsl.kt | 86 ++++++ .../plugin/KotlinTargetHierarchyBuilder.kt | 32 +++ .../plugin/KotlinTargetHierarchyDescriptor.kt | 36 +++ .../dsl/KotlinMultiplatformExtension.kt | 12 +- .../targetHierarchy/KotlinTargetHierarchy.kt | 38 +++ .../KotlinTargetHierarchyDslImpl.kt | 37 +++ .../applyKotlinTargetHierarchy.kt | 44 +++ .../buildKotlinTargetHierarchy.kt | 116 ++++++++ .../naturalKotlinTargetHierarchy.kt | 36 +++ .../KotlinTargetHierarchyDescriptorTest.kt | 200 +++++++++++++ .../gradle/KotlinTargetHierarchyDslTest.kt | 268 ++++++++++++++++++ .../kotlin/gradle/compilationUtils.kt | 12 + 13 files changed, 957 insertions(+), 4 deletions(-) create mode 100644 libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinTargetHierarchyDsl.kt create mode 100644 libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetHierarchyBuilder.kt create mode 100644 libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetHierarchyDescriptor.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/KotlinTargetHierarchy.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/KotlinTargetHierarchyDslImpl.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/applyKotlinTargetHierarchy.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/buildKotlinTargetHierarchy.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/naturalKotlinTargetHierarchy.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/KotlinTargetHierarchyDescriptorTest.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/KotlinTargetHierarchyDslTest.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/compilationUtils.kt diff --git a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-gradle-plugin-api.txt b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-gradle-plugin-api.txt index f3b889c3443..606755c4a6d 100644 --- a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-gradle-plugin-api.txt +++ b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-gradle-plugin-api.txt @@ -505,6 +505,17 @@ public abstract interface class org/jetbrains/kotlin/gradle/dsl/KotlinNativeXCFr public abstract fun targets ([Lorg/jetbrains/kotlin/konan/target/KonanTarget;)V } +public abstract interface class org/jetbrains/kotlin/gradle/dsl/KotlinTargetHierarchyDsl { + public abstract fun apply (Lorg/jetbrains/kotlin/gradle/plugin/KotlinTargetHierarchyDescriptor;Lkotlin/jvm/functions/Function1;)V + public abstract fun custom (Lkotlin/jvm/functions/Function1;)V + public abstract fun default (Lkotlin/jvm/functions/Function1;)V +} + +public final class org/jetbrains/kotlin/gradle/dsl/KotlinTargetHierarchyDsl$DefaultImpls { + public static synthetic fun apply$default (Lorg/jetbrains/kotlin/gradle/dsl/KotlinTargetHierarchyDsl;Lorg/jetbrains/kotlin/gradle/plugin/KotlinTargetHierarchyDescriptor;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static synthetic fun default$default (Lorg/jetbrains/kotlin/gradle/dsl/KotlinTargetHierarchyDsl;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V +} + public abstract interface class org/jetbrains/kotlin/gradle/dsl/KotlinTopLevelExtensionConfig { public abstract fun explicitApi ()V public abstract fun explicitApiWarning ()V @@ -1017,6 +1028,39 @@ public abstract interface class org/jetbrains/kotlin/gradle/plugin/KotlinTargetE public abstract fun getTarget ()Lorg/jetbrains/kotlin/gradle/plugin/KotlinTarget; } +public abstract interface class org/jetbrains/kotlin/gradle/plugin/KotlinTargetHierarchyBuilder { + public abstract fun common (Lkotlin/jvm/functions/Function1;)V + public abstract fun getCompilation ()Lorg/jetbrains/kotlin/gradle/plugin/KotlinCompilation; + public abstract fun getTarget ()Lorg/jetbrains/kotlin/gradle/plugin/KotlinTarget; + public abstract fun group (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public abstract fun isAndroidJvm ()Z + public abstract fun isAndroidNative ()Z + public abstract fun isApple ()Z + public abstract fun isIos ()Z + public abstract fun isJs ()Z + public abstract fun isJsLegacy ()Z + public abstract fun isJvm ()Z + public abstract fun isLinux ()Z + public abstract fun isMacos ()Z + public abstract fun isNative ()Z + public abstract fun isTvos ()Z + public abstract fun isWatchos ()Z + public abstract fun isWindows ()Z +} + +public final class org/jetbrains/kotlin/gradle/plugin/KotlinTargetHierarchyBuilder$DefaultImpls { + public static fun common (Lorg/jetbrains/kotlin/gradle/plugin/KotlinTargetHierarchyBuilder;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun group$default (Lorg/jetbrains/kotlin/gradle/plugin/KotlinTargetHierarchyBuilder;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V +} + +public abstract interface class org/jetbrains/kotlin/gradle/plugin/KotlinTargetHierarchyDescriptor : kotlin/jvm/functions/Function1 { + public abstract fun extend (Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/kotlin/gradle/plugin/KotlinTargetHierarchyDescriptor; +} + +public final class org/jetbrains/kotlin/gradle/plugin/KotlinTargetHierarchyDescriptorKt { + public static final fun KotlinTargetHierarchyDescriptor (Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/kotlin/gradle/plugin/KotlinTargetHierarchyDescriptor; +} + public abstract interface class org/jetbrains/kotlin/gradle/plugin/KotlinTargetPreset : org/gradle/api/Named { public abstract fun createTarget (Ljava/lang/String;)Lorg/jetbrains/kotlin/gradle/plugin/KotlinTarget; } diff --git a/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinTargetHierarchyDsl.kt b/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinTargetHierarchyDsl.kt new file mode 100644 index 00000000000..6291a8d1cc8 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinTargetHierarchyDsl.kt @@ -0,0 +1,86 @@ +/* + * 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.dsl + +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +import org.jetbrains.kotlin.gradle.plugin.KotlinTargetHierarchyBuilder +import org.jetbrains.kotlin.gradle.plugin.KotlinTargetHierarchyDescriptor + +@ExperimentalKotlinGradlePluginApi +interface KotlinTargetHierarchyDsl { + fun apply( + hierarchyDescriptor: KotlinTargetHierarchyDescriptor, + describeExtension: (KotlinTargetHierarchyBuilder.() -> Unit)? = null + ) + + /** + * Set's up a 'natural'/'default' hierarchy withing [KotlinTarget]'s in the project. + * + * #### Example 1 + * + * ```kotlin + * kotlin { + * targets.hierarchy.default() // <- position of this call is not relevant! + * + * iosX64() + * iosArm64() + * linuxX64() + * linuxArm64() + * } + * ``` + * + * Will create the following SourceSets: + * `[iosMain, iosTest, appleMain, appleTest, linuxMain, linuxTest, nativeMain, nativeTest] + * + * + * Hierarchy: + * ``` + * common + * | + * +-----------------+-------------------+ + * | | + * + * native ... + * + * | + * | + * | + * +----------------------+--------------------+-----------------------+ + * | | | | + * + * apple linux windows androidNative + * + * | + * +-----------+------------+------------+ + * | | | | + * + * macos ios tvos watchos + * ``` + * + * #### Example 2: Adding custom groups + * Let's imagine we would additionally like to share code between linux and apple (unixLike) + * + * ```kotlin + * kotlin { + * targets.hierarchy.default { target -> + * if(target.isNative) { + * group("native") { // <- we can re-declare already existing groups and connect children to it! + * if(target.isLinux || target.isApple) { + * group("unixLike") + * } + * } + * } + * } + * } + * ``` + * + * @param describeExtension: Additional groups can be described to extend the 'default'/'natural' hierarchy: + * @see KotlinTargetHierarchyDescriptor.extend + */ + fun default(describeExtension: (KotlinTargetHierarchyBuilder.() -> Unit)? = null) + fun custom(describe: KotlinTargetHierarchyBuilder.() -> Unit) +} diff --git a/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetHierarchyBuilder.kt b/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetHierarchyBuilder.kt new file mode 100644 index 00000000000..3da6f75ef2f --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetHierarchyBuilder.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 + +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi + +@ExperimentalKotlinGradlePluginApi +interface KotlinTargetHierarchyBuilder { + val target: KotlinTarget + val compilation: KotlinCompilation<*> + + val isNative: Boolean + val isApple: Boolean + val isIos: Boolean + val isWatchos: Boolean + val isMacos: Boolean + val isTvos: Boolean + val isWindows: Boolean + val isLinux: Boolean + val isAndroidNative: Boolean + val isJvm: Boolean + val isAndroidJvm: Boolean + val isJsLegacy: Boolean + val isJsIr: Boolean + val isJs: Boolean + + fun common(build: KotlinTargetHierarchyBuilder.() -> Unit) = group("common", build) + fun group(name: String, build: KotlinTargetHierarchyBuilder.() -> Unit = {}) +} diff --git a/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetHierarchyDescriptor.kt b/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetHierarchyDescriptor.kt new file mode 100644 index 00000000000..4eccf3c855d --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetHierarchyDescriptor.kt @@ -0,0 +1,36 @@ +/* + * 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 + +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi + +@ExperimentalKotlinGradlePluginApi +interface KotlinTargetHierarchyDescriptor : (KotlinTargetHierarchyBuilder) -> Unit { + fun extend(describe: KotlinTargetHierarchyBuilder.() -> Unit): KotlinTargetHierarchyDescriptor +} + +@ExperimentalKotlinGradlePluginApi +fun KotlinTargetHierarchyDescriptor( + describe: KotlinTargetHierarchyBuilder.() -> Unit +): KotlinTargetHierarchyDescriptor { + return KotlinTargetHierarchyDescriptorImpl(describe) +} + +@ExperimentalKotlinGradlePluginApi +private class KotlinTargetHierarchyDescriptorImpl( + private val describe: KotlinTargetHierarchyBuilder.() -> Unit +) : KotlinTargetHierarchyDescriptor { + + override fun extend(describe: KotlinTargetHierarchyBuilder.() -> Unit): KotlinTargetHierarchyDescriptor { + val sourceDescribe = this.describe + return KotlinTargetHierarchyDescriptor { + sourceDescribe() + describe() + } + } + + override fun invoke(p1: KotlinTargetHierarchyBuilder) = p1.describe() +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinMultiplatformExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinMultiplatformExtension.kt index 393f4cefa0d..cef511830cd 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinMultiplatformExtension.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinMultiplatformExtension.kt @@ -5,15 +5,14 @@ package org.jetbrains.kotlin.gradle.dsl -import org.gradle.api.Action -import org.gradle.api.InvalidUserCodeException -import org.gradle.api.NamedDomainObjectCollection -import org.gradle.api.Project +import org.gradle.api.* import org.gradle.api.internal.plugins.DslObject import org.gradle.api.logging.Logger +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.plugin.* import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.Companion.kotlinPropertiesProvider import org.jetbrains.kotlin.gradle.plugin.mpp.* +import org.jetbrains.kotlin.gradle.plugin.mpp.targetHierarchy.KotlinTargetHierarchyDslImpl import javax.inject.Inject abstract class KotlinMultiplatformExtension(project: Project) : @@ -53,6 +52,9 @@ abstract class KotlinMultiplatformExtension(project: Project) : configure(presetExtension) } + @ExperimentalKotlinGradlePluginApi + val targetHierarchy: KotlinTargetHierarchyDsl get() = KotlinTargetHierarchyDslImpl(targets, sourceSets) + @Suppress("unused") // DSL val testableTargets: NamedDomainObjectCollection> get() = targets.withType(KotlinTargetWithTests::class.java) @@ -180,12 +182,14 @@ internal fun KotlinTargetsContainerWithPresets.configureOrCre configure(existingTarget as T) return existingTarget } + existingTarget == null -> { val newTarget = targetPreset.createTarget(targetName) targets.add(newTarget) configure(newTarget) return newTarget } + else -> { throw InvalidUserCodeException( "The target '$targetName' already exists, but it was not created with the '${targetPreset.name}' preset. " + diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/KotlinTargetHierarchy.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/KotlinTargetHierarchy.kt new file mode 100644 index 00000000000..e3834b08c38 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/KotlinTargetHierarchy.kt @@ -0,0 +1,38 @@ +/* + * 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.mpp.targetHierarchy + +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName + +internal data class KotlinTargetHierarchy( + val node: Node, val children: Set = emptySet() +) { + + sealed class Node { + abstract fun sharedSourceSetName(compilation: KotlinCompilation<*>): String? + + object Root : Node() { + override fun sharedSourceSetName(compilation: KotlinCompilation<*>): String? = null + } + + data class Group(val name: String) : Node() { + override fun sharedSourceSetName(compilation: KotlinCompilation<*>): String { + return lowerCamelCaseName(name, compilation.name) + } + } + + final override fun toString(): String = when (this) { + is Group -> name + is Root -> "" + } + } + + override fun toString(): String { + if (children.isEmpty()) return node.toString() + return node.toString() + "\n" + children.joinToString("\n").prependIndent("----") + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/KotlinTargetHierarchyDslImpl.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/KotlinTargetHierarchyDslImpl.kt new file mode 100644 index 00000000000..9a9b43dfa57 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/KotlinTargetHierarchyDslImpl.kt @@ -0,0 +1,37 @@ +/* + * 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.mpp.targetHierarchy + +import org.gradle.api.DomainObjectCollection +import org.gradle.api.NamedDomainObjectContainer +import org.jetbrains.kotlin.gradle.dsl.KotlinTargetHierarchyDsl +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +import org.jetbrains.kotlin.gradle.plugin.KotlinTargetHierarchyBuilder +import org.jetbrains.kotlin.gradle.plugin.KotlinTargetHierarchyDescriptor + +internal class KotlinTargetHierarchyDslImpl( + private val targets: DomainObjectCollection, + private val sourceSets: NamedDomainObjectContainer +) : KotlinTargetHierarchyDsl { + override fun apply( + hierarchyDescriptor: KotlinTargetHierarchyDescriptor, + describeExtension: (KotlinTargetHierarchyBuilder.() -> Unit)? + ) { + applyKotlinTargetHierarchy(hierarchyDescriptor.extendIfNotNull(describeExtension), targets, sourceSets) + } + + override fun default(describeExtension: (KotlinTargetHierarchyBuilder.() -> Unit)?) { + apply(naturalKotlinTargetHierarchy, describeExtension) + } + + override fun custom(describe: KotlinTargetHierarchyBuilder.() -> Unit) { + apply(KotlinTargetHierarchyDescriptor(describe)) + } +} + +private fun KotlinTargetHierarchyDescriptor.extendIfNotNull(describe: (KotlinTargetHierarchyBuilder.() -> Unit)?) = + if (describe == null) this else extend(describe) \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/applyKotlinTargetHierarchy.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/applyKotlinTargetHierarchy.kt new file mode 100644 index 00000000000..2b666e799e7 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/applyKotlinTargetHierarchy.kt @@ -0,0 +1,44 @@ +/* + * 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.mpp.targetHierarchy + +import org.gradle.api.DomainObjectCollection +import org.gradle.api.NamedDomainObjectContainer +import org.jetbrains.kotlin.gradle.plugin.* + +internal fun applyKotlinTargetHierarchy( + hierarchyDescriptor: KotlinTargetHierarchyDescriptor, + targets: DomainObjectCollection, + sourceSets: NamedDomainObjectContainer +) { + targets + .matching { target -> target.platformType != KotlinPlatformType.common } + .all { target -> + target.compilations.all { compilation -> + val hierarchy = hierarchyDescriptor.buildKotlinTargetHierarchy(compilation) + applyKotlinTargetHierarchy(hierarchy, compilation, sourceSets) + } + } +} + +private fun applyKotlinTargetHierarchy( + hierarchy: KotlinTargetHierarchy, + compilation: KotlinCompilation<*>, + sourceSets: NamedDomainObjectContainer +): KotlinSourceSet? { + val sharedSourceSetName = hierarchy.node.sharedSourceSetName(compilation) ?: return null + val sharedSourceSet = sourceSets.maybeCreate(sharedSourceSetName) + + hierarchy.children + .mapNotNull { childHierarchy -> applyKotlinTargetHierarchy(childHierarchy, compilation, sourceSets) } + .forEach { childSourceSet -> childSourceSet.dependsOn(sharedSourceSet) } + + if (hierarchy.children.isEmpty()) { + compilation.defaultSourceSet.dependsOn(sharedSourceSet) + } + + return sharedSourceSet +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/buildKotlinTargetHierarchy.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/buildKotlinTargetHierarchy.kt new file mode 100644 index 00000000000..c903450c526 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/buildKotlinTargetHierarchy.kt @@ -0,0 +1,116 @@ +/* + * 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.mpp.targetHierarchy + +import org.gradle.api.InvalidUserCodeException +import org.jetbrains.kotlin.gradle.plugin.* +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.gradle.targets.js.KotlinJsTarget +import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget +import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget +import org.jetbrains.kotlin.konan.target.Family +import org.jetbrains.kotlin.tooling.core.closure + +internal fun KotlinTargetHierarchyDescriptor.buildKotlinTargetHierarchy(compilation: KotlinCompilation<*>): KotlinTargetHierarchy { + val context = KotlinTargetHierarchyBuilderImplContext(compilation) + this(context.getOrCreateBuilder(KotlinTargetHierarchy.Node.Root)) + return context.build(KotlinTargetHierarchy.Node.Root) +} + +private class KotlinTargetHierarchyBuilderImplContext(val compilation: KotlinCompilation<*>) { + private val builders = hashMapOf() + private val builtValues = hashMapOf() + + fun getOrCreateBuilder(node: KotlinTargetHierarchy.Node): KotlinTargetHierarchyBuilderImpl = builders.getOrPut(node) { + KotlinTargetHierarchyBuilderImpl(this, node) + } + + fun build(node: KotlinTargetHierarchy.Node): KotlinTargetHierarchy { + return builtValues.getOrPut(node) { + val builder = getOrCreateBuilder(node) + + /* + Keep the hierarchy 'deduplicated'. + e.g. + if we have two child hierarchies: + + 1) a -> b -> c + 2) b -> c + + then we can remove the duplicated 'sub hierarchy' 2) and only return + a -> b -> c + + */ + val childrenNodes = builder.children.map { it.node } - + builder.children.flatMap { child -> child.childrenClosure }.map { it.node }.toSet() + + KotlinTargetHierarchy(node, childrenNodes.map(this::build).toSet()) + } + } +} + +private class KotlinTargetHierarchyBuilderImpl( + val context: KotlinTargetHierarchyBuilderImplContext, + val node: KotlinTargetHierarchy.Node, +) : KotlinTargetHierarchyBuilder { + + override val compilation: KotlinCompilation<*> get() = context.compilation + override val target: KotlinTarget get() = compilation.target + + val children = mutableSetOf() + val childrenClosure: Set get() = closure { it.children } + + override fun group(name: String, build: KotlinTargetHierarchyBuilder.() -> Unit) { + val node = KotlinTargetHierarchy.Node.Group(name) + val child = context.getOrCreateBuilder(node) + children.add(child) + child.build() + checkCyclicHierarchy() + } + + override val isNative: Boolean get() = target is KotlinNativeTarget + override val isApple: Boolean get() = target.let { it is KotlinNativeTarget && it.konanTarget.family.isAppleFamily } + override val isIos: Boolean get() = target.let { it is KotlinNativeTarget && it.konanTarget.family == Family.IOS } + override val isWatchos: Boolean get() = target.let { it is KotlinNativeTarget && it.konanTarget.family == Family.WATCHOS } + override val isMacos: Boolean get() = target.let { it is KotlinNativeTarget && it.konanTarget.family == Family.OSX } + override val isTvos: Boolean get() = target.let { it is KotlinNativeTarget && it.konanTarget.family == Family.TVOS } + override val isWindows: Boolean get() = target.let { it is KotlinNativeTarget && it.konanTarget.family == Family.MINGW } + override val isLinux: Boolean get() = target.let { it is KotlinNativeTarget && it.konanTarget.family == Family.LINUX } + override val isAndroidNative: Boolean get() = target.let { it is KotlinNativeTarget && it.konanTarget.family == Family.ANDROID } + override val isJvm: Boolean get() = target is KotlinJvmTarget + override val isAndroidJvm: Boolean get() = target is KotlinAndroidTarget + override val isJsLegacy: Boolean get() = target is KotlinJsTarget + override val isJsIr: Boolean get() = target is KotlinJsIrTarget + override val isJs: Boolean get() = isJsIr || isJsLegacy + + override fun toString(): String { + return "KotlinTargetHierarchyBuilder($node)" + } +} + + +/* Cycle Detection: Provide feedback for users when a KotlinTargetHierarchy cycle is declared */ + +private fun KotlinTargetHierarchyBuilderImpl.checkCyclicHierarchy(): Nothing? { + val stack = mutableListOf(node) + val visited = hashSetOf() + + fun checkChild(child: KotlinTargetHierarchyBuilderImpl) { + if (!visited.add(child)) return + stack += child.node + if (this == child) throw CyclicKotlinTargetHierarchyException(stack) + child.children.forEach { next -> checkChild(next) } + stack -= child.node + } + + children.forEach { child -> checkChild(child) } + return null +} + +internal class CyclicKotlinTargetHierarchyException(val cycle: List) : InvalidUserCodeException( + "KotlinTargetHierarchy cycle detected: ${cycle.joinToString(" -> ")}" +) diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/naturalKotlinTargetHierarchy.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/naturalKotlinTargetHierarchy.kt new file mode 100644 index 00000000000..7ced3014918 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/naturalKotlinTargetHierarchy.kt @@ -0,0 +1,36 @@ +/* + * 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.mpp.targetHierarchy + +import org.jetbrains.kotlin.gradle.plugin.KotlinTargetHierarchyDescriptor +import org.jetbrains.kotlin.gradle.plugin.mpp.isMain +import org.jetbrains.kotlin.gradle.plugin.mpp.isTest + +internal val naturalKotlinTargetHierarchy = KotlinTargetHierarchyDescriptor { + if (!compilation.isMain() && !compilation.isTest()) { + /* This hierarchy is only defined for default 'main' and 'test' compilations */ + return@KotlinTargetHierarchyDescriptor + } + + common { + if (isNative) { + group("native") { + if (isApple) { + group("apple") { + if (isIos) group("ios") + if (isTvos) group("tvos") + if (isWatchos) group("watchos") + if (isMacos) group("macos") + } + } + + if (isLinux) group("linux") + if (isWindows) group("windows") + if (isAndroidNative) group("androidNative") + } + } + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/KotlinTargetHierarchyDescriptorTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/KotlinTargetHierarchyDescriptorTest.kt new file mode 100644 index 00000000000..3e2218702b9 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/KotlinTargetHierarchyDescriptorTest.kt @@ -0,0 +1,200 @@ +/* + * 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 + +import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinTargetHierarchyDescriptor +import org.jetbrains.kotlin.gradle.plugin.mpp.targetHierarchy.CyclicKotlinTargetHierarchyException +import org.jetbrains.kotlin.gradle.plugin.mpp.targetHierarchy.KotlinTargetHierarchy +import org.jetbrains.kotlin.gradle.plugin.mpp.targetHierarchy.KotlinTargetHierarchy.Node +import org.jetbrains.kotlin.gradle.plugin.mpp.targetHierarchy.buildKotlinTargetHierarchy +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertSame + +class KotlinTargetHierarchyDescriptorTest { + + private val project = buildProjectWithMPP() + private val kotlin = project.multiplatformExtension + + @Test + fun `test - simple descriptor`() { + val descriptor = KotlinTargetHierarchyDescriptor { + common { + if (target.name == "a") group("groupA") + if (target.name == "b") group("groupB") + } + } + + val targetA = kotlin.linuxX64("a") + val targetB = kotlin.linuxArm64("b") + val targetC = kotlin.jvm() + + assertEquals( + hierarchy { + group("common") { + group("groupA") + } + }, + descriptor.buildKotlinTargetHierarchy(targetA.compilations.main) + ) + + assertEquals( + hierarchy { + group("common") { + group("groupB") + } + }, + descriptor.buildKotlinTargetHierarchy(targetB.compilations.main) + ) + + assertEquals( + hierarchy { group("common") }, + descriptor.buildKotlinTargetHierarchy(targetC.compilations.main) + ) + } + + @Test + fun `test - extend`() { + val descriptor = KotlinTargetHierarchyDescriptor { group("base") }.extend { + group("base") { + group("extension") + } + } + + assertEquals( + hierarchy { + group("base") { + group("extension") + } + }, + descriptor.buildKotlinTargetHierarchy(kotlin.linuxX64().compilations.getByName("main")) + ) + } + + + @Test + fun `test - extend - with new root`() { + val descriptor = KotlinTargetHierarchyDescriptor { group("base") }.extend { + group("newRoot") { + group("base") { + group("extension") + } + } + } + + assertEquals( + hierarchy { + group("newRoot") { + group("base") { + group("extension") + } + } + }, + descriptor.buildKotlinTargetHierarchy(kotlin.linuxX64().compilations.main) + ) + } + + + @Test + fun `test - extend - with two new roots and two extensions`() { + val descriptor = KotlinTargetHierarchyDescriptor { group("base") } + .extend { + group("newRoot1") { + group("base") { + group("extension1") + } + } + } + .extend { + group("newRoot2") { + group("base") { + group("extension2") + } + } + } + + val hierarchy = descriptor.buildKotlinTargetHierarchy(kotlin.linuxX64().compilations.main) + + assertEquals( + hierarchy { + group("newRoot1") { + group("base") { + group("extension1") + group("extension2") + } + } + + group("newRoot2") { + group("base") { + group("extension1") + group("extension2") + } + } + }, hierarchy + ) + + fun KotlinTargetHierarchy.collectChildren(): List { + return children.toList() + children.flatMap { it.collectChildren() } + } + + /* Check that all equal hierarchies are even the same instance */ + val allNodes = hierarchy.collectChildren() + allNodes.forEach { node -> + val equalNodes = allNodes.filter { otherNode -> otherNode == node } + equalNodes.forEach { equalNode -> + assertSame(node, equalNode, "Expected equal nodes to be the same instance") + } + } + } + + @Test + fun `test - cycle`() { + val descriptor = KotlinTargetHierarchyDescriptor { + group("x") { // decoy 1 + group("a") { + group("xx") // decoy 2 + + group("b") { + group("xxx") // decoy 3 + + group("c") { + group("a") { + group("xxxx") // decoy 4 + } + } + } + } + } + } + + val cycleStack = assertFailsWith { + descriptor.buildKotlinTargetHierarchy(kotlin.linuxX64().compilations.main) + }.cycle + + assertEquals( + listOf(Node.Group("a"), Node.Group("b"), Node.Group("c"), Node.Group("a")), + cycleStack + ) + } +} + +private class TestHierarchyBuilder(private val node: Node) { + private val children = mutableSetOf() + + fun group(name: String, builder: TestHierarchyBuilder.() -> Unit = {}) { + children.add(TestHierarchyBuilder(Node.Group(name)).also(builder)) + } + + fun build(): KotlinTargetHierarchy = KotlinTargetHierarchy(node, children.map { it.build() }.toSet()) +} + +private fun hierarchy(build: TestHierarchyBuilder.() -> Unit): KotlinTargetHierarchy { + return TestHierarchyBuilder(Node.Root).also(build).build() +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/KotlinTargetHierarchyDslTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/KotlinTargetHierarchyDslTest.kt new file mode 100644 index 00000000000..770adc3561f --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/KotlinTargetHierarchyDslTest.kt @@ -0,0 +1,268 @@ +/* + * 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 + +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.KotlinTargetHierarchyDescriptor +import kotlin.test.Test +import kotlin.test.assertEquals + +class KotlinTargetHierarchyDslTest { + + + private val project = buildProjectWithMPP() + private val kotlin = project.multiplatformExtension + + + @Test + fun `test - hierarchy default - targets from all families`() { + kotlin.apply { + targetHierarchy.default() + iosArm32() + iosArm64() + iosX64() + iosSimulatorArm64() + + tvosArm64() + tvosX64() + + watchosArm32() + watchosArm64() + + macosX64() + macosArm64() + + linuxX64() + linuxArm32Hfp() + + mingwX64() + mingwX86() + + androidNativeArm32() + androidNativeArm64() + } + + assertEquals( + stringSetOf( + "androidNativeArm32Main", + "androidNativeArm64Main", + "iosArm32Main", + "iosArm64Main", + "iosSimulatorArm64Main", + "iosX64Main", + "linuxArm32HfpMain", + "linuxX64Main", + "macosArm64Main", + "macosX64Main", + "mingwX64Main", + "mingwX86Main", + "nativeMain", + "tvosArm64Main", + "tvosX64Main", + "watchosArm32Main", + "watchosArm64Main" + ), + kotlin.dependingSourceSetNames("commonMain") + ) + + assertEquals( + stringSetOf( + "androidNativeArm32Test", + "androidNativeArm64Test", + "iosArm32Test", + "iosArm64Test", + "iosSimulatorArm64Test", + "iosX64Test", + "linuxArm32HfpTest", + "linuxX64Test", + "macosArm64Test", + "macosX64Test", + "mingwX64Test", + "mingwX86Test", + "nativeTest", + "tvosArm64Test", + "tvosX64Test", + "watchosArm32Test", + "watchosArm64Test" + ), kotlin.dependingSourceSetNames("commonTest") + ) + + assertEquals( + stringSetOf("androidNativeMain", "appleMain", "linuxMain", "windowsMain"), + kotlin.dependingSourceSetNames("nativeMain") + ) + + assertEquals( + stringSetOf("androidNativeTest", "appleTest", "linuxTest", "windowsTest"), + kotlin.dependingSourceSetNames("nativeTest") + ) + + assertEquals( + stringSetOf("iosMain", "macosMain", "tvosMain", "watchosMain"), + kotlin.dependingSourceSetNames("appleMain") + ) + + assertEquals( + stringSetOf("iosTest", "macosTest", "tvosTest", "watchosTest"), + kotlin.dependingSourceSetNames("appleTest") + ) + + assertEquals( + stringSetOf("iosArm32Main", "iosArm64Main", "iosSimulatorArm64Main", "iosX64Main"), + kotlin.dependingSourceSetNames("iosMain") + ) + + assertEquals( + stringSetOf("iosArm32Test", "iosArm64Test", "iosSimulatorArm64Test", "iosX64Test"), + kotlin.dependingSourceSetNames("iosTest") + ) + + assertEquals( + stringSetOf("tvosArm64Main", "tvosX64Main"), + kotlin.dependingSourceSetNames("tvosMain") + ) + + assertEquals( + stringSetOf("tvosArm64Test", "tvosX64Test"), + kotlin.dependingSourceSetNames("tvosTest") + ) + + assertEquals( + stringSetOf("watchosArm32Main", "watchosArm64Main"), + kotlin.dependingSourceSetNames("watchosMain") + ) + + assertEquals( + stringSetOf("watchosArm32Test", "watchosArm64Test"), + kotlin.dependingSourceSetNames("watchosTest") + ) + + assertEquals( + stringSetOf("linuxArm32HfpMain", "linuxX64Main"), + kotlin.dependingSourceSetNames("linuxMain") + ) + + assertEquals( + stringSetOf("linuxArm32HfpTest", "linuxX64Test"), + kotlin.dependingSourceSetNames("linuxTest") + ) + } + + @Test + fun `test - hierarchy default - only linuxX64`() { + kotlin.apply { + targetHierarchy.default() + kotlin.linuxX64() + } + + val commonMain = kotlin.sourceSets.getByName("commonMain") + val commonTest = kotlin.sourceSets.getByName("commonTest") + val nativeMain = kotlin.sourceSets.getByName("nativeMain") + val nativeTest = kotlin.sourceSets.getByName("nativeTest") + val linuxMain = kotlin.sourceSets.getByName("linuxMain") + val linuxTest = kotlin.sourceSets.getByName("linuxTest") + val linuxX64Main = kotlin.sourceSets.getByName("linuxX64Main") + val linuxX64Test = kotlin.sourceSets.getByName("linuxX64Test") + + assertEquals( + setOf(commonMain, commonTest, nativeMain, nativeTest, linuxMain, linuxTest, linuxX64Main, linuxX64Test), + kotlin.sourceSets.toSet() + ) + } + + @Test + fun `test - hierarchy apply - extend`() { + val descriptor = KotlinTargetHierarchyDescriptor { + group("common") { + group("base") + } + } + + kotlin.apply { + targetHierarchy.apply(descriptor) { group("base") { group("extension") } } + linuxX64() + } + + assertEquals( + stringSetOf("baseMain", "linuxX64Main"), kotlin.dependingSourceSetNames("commonMain") + ) + + assertEquals( + stringSetOf("extensionMain"), kotlin.dependingSourceSetNames("baseMain") + ) + + assertEquals( + stringSetOf("linuxX64Main"), kotlin.dependingSourceSetNames("extensionMain") + ) + + assertEquals( + stringSetOf(), kotlin.dependingSourceSetNames("linuxX64Main") + ) + } + + + @Test + fun `test - hierarchy set - extend - with new root`() { + val descriptor = KotlinTargetHierarchyDescriptor { + group("common") { + group("base") + } + } + + kotlin.apply { + targetHierarchy.apply(descriptor) { + group("newRoot") { + group("base") { group("extension") } + } + } + linuxX64() + } + + assertEquals( + stringSetOf("baseMain"), kotlin.dependingSourceSetNames("newRootMain") + ) + + assertEquals( + stringSetOf("baseMain", "linuxX64Main"), kotlin.dependingSourceSetNames("commonMain") + ) + + assertEquals( + stringSetOf("extensionMain"), kotlin.dependingSourceSetNames("baseMain") + ) + + assertEquals( + stringSetOf("linuxX64Main"), kotlin.dependingSourceSetNames("extensionMain") + ) + + assertEquals( + stringSetOf(), kotlin.dependingSourceSetNames("linuxX64Main") + ) + } + +} + +private fun KotlinMultiplatformExtension.dependingSourceSetNames(sourceSetName: String) = + dependingSourceSetNames(sourceSets.getByName(sourceSetName)) + +private fun KotlinMultiplatformExtension.dependingSourceSetNames(sourceSet: KotlinSourceSet) = + sourceSets.filter { sourceSet in it.dependsOn }.map { it.name }.toStringSet() + + +/* StringSet: Special Set implementation, which makes it easy to copy and paste after assertions fail */ + +private fun stringSetOf(vararg values: String) = StringSet(values.toSet()) + +private fun Iterable.toStringSet() = StringSet(this.toSet()) + +private data class StringSet(private val set: Set) : Set by set { + override fun toString(): String { + return "stringSetOf(" + set.joinToString(", ") { "\"$it\"" } + ")" + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/compilationUtils.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/compilationUtils.kt new file mode 100644 index 00000000000..6f4c453acbf --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/compilationUtils.kt @@ -0,0 +1,12 @@ +/* + * 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 + +import org.gradle.api.NamedDomainObjectCollection +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation + +val NamedDomainObjectCollection>.main: KotlinCompilation<*> get() = getByName("main") +val NamedDomainObjectCollection>.test: KotlinCompilation<*> get() = getByName("test")