From 02d4c866ca7a3d5788d6db7b5e71280713e1caca Mon Sep 17 00:00:00 2001 From: "sebastian.sellmair" Date: Mon, 19 Jul 2021 15:08:21 +0200 Subject: [PATCH] [Gradle] CInterop commonization: Improve grouping and dependency management ^KT-47775 Verification Pending This implementation will be lenient toward the common *Test.dependsOn(*Main) mistake. ^KT-47053 Verification Pending Support source sets that do not have a dedicated shared native compilation. Also support additional visible source sets coming from associate compilations. --- .../jetbrains/kotlin/gradle/CommonizerIT.kt | 2 +- .../gradle/CInteropCommonizerTaskTest.kt | 428 ++++++++++++++++-- .../gradle/MultiplatformExtensionTest.kt | 79 ++++ .../KotlinMetadataTargetConfigurator.kt | 18 - .../native/KotlinNativeTargetPreset.kt | 5 + .../AbstractCInteropCommonizerTask.kt | 29 +- .../CInteropCommonizerDependencies.kt | 77 ++++ .../internal/CInteropCommonizerDependent.kt | 117 +++++ .../internal/CInteropCommonizerGroup.kt | 32 ++ .../native/internal/CInteropCommonizerTask.kt | 192 +++----- .../native/internal/CommonizerTasks.kt | 6 +- ...yCInteropCommonizerTaskOutputForIdeTask.kt | 11 +- .../KotlinNativePlatformDependencies.kt | 1 + .../commonizer/util/transitiveClosure.kt | 8 +- 14 files changed, 777 insertions(+), 228 deletions(-) create mode 100644 libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/MultiplatformExtensionTest.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CInteropCommonizerDependencies.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CInteropCommonizerDependent.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CInteropCommonizerGroup.kt diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/CommonizerIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/CommonizerIT.kt index a5363d950f9..cdf91f8396d 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/CommonizerIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/CommonizerIT.kt @@ -135,10 +135,10 @@ class CommonizerIT : BaseGradleIT() { val expectedOutputDirectoryForBuild = projectDir.resolve("build/classes/kotlin/commonizer") build(":copyCommonizeCInteropForIde") { + assertSuccessful() assertTasksExecuted(":cinteropCurlTargetA") assertTasksExecuted(":cinteropCurlTargetB") assertTasksExecuted(":commonizeCInterop") - assertSuccessful() assertTrue(expectedOutputDirectoryForIde.isDirectory, "Missing output directory for IDE") assertTrue(expectedOutputDirectoryForBuild.isDirectory, "Missing output directory for build") diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/CInteropCommonizerTaskTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/CInteropCommonizerTaskTest.kt index d4c56a32e42..0a8bcf2da5e 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/CInteropCommonizerTaskTest.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/CInteropCommonizerTaskTest.kt @@ -8,36 +8,27 @@ package org.jetbrains.kotlin.gradle -import org.gradle.api.internal.project.ProjectInternal -import org.gradle.api.plugins.ExtraPropertiesExtension -import org.gradle.testfixtures.ProjectBuilder import org.jetbrains.kotlin.commonizer.CommonizerTarget -import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.commonizer.SharedCommonizerTarget +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinSharedNativeCompilation -import org.jetbrains.kotlin.gradle.targets.native.internal.CInteropCommonizationParameters import org.jetbrains.kotlin.gradle.targets.native.internal.CInteropCommonizerTask +import org.jetbrains.kotlin.gradle.targets.native.internal.CInteropCommonizerGroup import org.jetbrains.kotlin.gradle.targets.native.internal.commonizeCInteropTask -import org.jetbrains.kotlin.gradle.targets.native.internal.supports import org.jetbrains.kotlin.konan.target.KonanTarget.* import kotlin.test.* -class CInteropCommonizerTaskTest { +class CInteropCommonizerTaskTest : MultiplatformExtensionTest() { - private lateinit var project: ProjectInternal - private lateinit var kotlin: KotlinMultiplatformExtension private val task: CInteropCommonizerTask get() = project.commonizeCInteropTask?.get() ?: fail("Missing commonizeCInteropTask") @BeforeTest - fun setup() { - project = ProjectBuilder.builder().build() as ProjectInternal - project.extensions.getByType(ExtraPropertiesExtension::class.java).set("kotlin.mpp.enableGranularSourceSetsMetadata", "true") - project.extensions.getByType(ExtraPropertiesExtension::class.java).set("kotlin.mpp.enableCInteropCommonization", "true") - project.plugins.apply("kotlin-multiplatform") - kotlin = project.extensions.getByName("kotlin") as KotlinMultiplatformExtension + override fun setup() { + enableGranularSourceSetsMetadata() + enableCInteropCommonization() + super.setup() } - @Test fun `nativeMain linux macos`() { val linuxInterop = kotlin.linuxX64("linux").compilations.getByName("main").cinterops.create("anyInteropName") @@ -53,15 +44,18 @@ class CInteropCommonizerTaskTest { macosMain.dependsOn(nativeMain) project.evaluate() - val nativeMainCompilation = kotlin.targets.flatMap { it.compilations } - .filterIsInstance() - .single { it.defaultSourceSet == nativeMain } + + val groups = task.getAllInteropsGroups() + assertEquals(1, groups.size, "Expected only one InteropsGroup") + + assertCInteropDependentEqualsForSourceSetAndCompilation(nativeMain) assertEquals( - CInteropCommonizationParameters( - setOf(CommonizerTarget(LINUX_X64, MACOS_X64)), setOf(linuxInterop.identifier, macosInterop.identifier) + CInteropCommonizerGroup( + setOf(CommonizerTarget(LINUX_X64, MACOS_X64)), + setOf(linuxInterop.identifier, macosInterop.identifier) ), - task.getCommonizationParameters(nativeMainCompilation) + task.findInteropsGroup(expectCInteropCommonizerDependent(nativeMain)) ) } @@ -80,17 +74,18 @@ class CInteropCommonizerTaskTest { macosMain.dependsOn(nativeMain) project.evaluate() - val nativeMainCompilation = kotlin.targets.flatMap { it.compilations } - .filterIsInstance() - .single { it.defaultSourceSet == nativeMain } assertNull( - task.getCommonizationParameters(nativeMainCompilation), + findCInteropCommonizerDependent(nativeMain), + "Expected no CInteropCommonizerTarget from nativeMain, since one target has not defined any cinterop" + ) + + assertNull( + findCInteropCommonizerDependent(expectSharedNativeCompilation(nativeMain)), "Expected no CInteropCommonizerTarget from nativeMain, since one target has not defined any cinterop" ) } - @Test fun `nativeMain iosMain linux macos iosX64 iosArm64`() { val linuxInterop = kotlin.linuxX64("linux").compilations.getByName("main").cinterops.create("anyInteropName").identifier @@ -116,33 +111,370 @@ class CInteropCommonizerTaskTest { project.evaluate() assertEquals( - CInteropCommonizationParameters( - setOf( - CommonizerTarget(IOS_X64, IOS_ARM64), - CommonizerTarget(IOS_X64, IOS_ARM64, MACOS_X64, LINUX_X64) - ), - setOf(linuxInterop, macosInterop, iosX64Interop, iosArm64Interop) - ), task.getCommonizationParameters(sharedNativeCompilation(nativeMain)) + 1, task.getAllInteropsGroups().size, + "Expected exactly one InteropsGroup for task" + ) + + val group = CInteropCommonizerGroup( + setOf( + CommonizerTarget(IOS_X64, IOS_ARM64, MACOS_X64, LINUX_X64), + CommonizerTarget(IOS_X64, IOS_ARM64) + ), + setOf( + linuxInterop, macosInterop, iosX64Interop, iosArm64Interop + ) + ) + + assertCInteropDependentEqualsForSourceSetAndCompilation(nativeMain) + assertCInteropDependentEqualsForSourceSetAndCompilation(iosMain) + + assertEquals(group, task.findInteropsGroup(expectCInteropCommonizerDependent(nativeMain))) + assertEquals(group, task.findInteropsGroup(expectCInteropCommonizerDependent(iosMain))) + } + + @Test + fun `nativeTest nativeMain linux macos`() { + `nativeTest nativeMain linux macos`(false) + } + + @Test + fun `nativeTest nativeMain linux macos - nativeTest dependsOn nativeMain`() { + `nativeTest nativeMain linux macos`(true) + } + + private fun `nativeTest nativeMain linux macos`( + nativeTestDependsOnNativeMain: Boolean + ) { + val linuxInterop = kotlin.linuxX64("linux").compilations.getByName("main").cinterops.create("anyInteropName").identifier + val macosInterop = kotlin.macosX64("macos").compilations.getByName("main").cinterops.create("anyInteropName").identifier + + val commonMain = kotlin.sourceSets.getByName("commonMain") + val nativeMain = kotlin.sourceSets.create("nativeMain") + val linuxMain = kotlin.sourceSets.getByName("linuxMain") + val macosMain = kotlin.sourceSets.getByName("macosMain") + + val nativeTest = kotlin.sourceSets.create("nativeTest") + val linuxTest = kotlin.sourceSets.getByName("linuxTest") + val macosTest = kotlin.sourceSets.getByName("macosTest") + + linuxTest.dependsOn(nativeTest) + macosTest.dependsOn(nativeTest) + + linuxMain.dependsOn(nativeMain) + macosMain.dependsOn(nativeMain) + + nativeMain.dependsOn(commonMain) + + if (nativeTestDependsOnNativeMain) { + nativeTest.dependsOn(nativeMain) + } + + project.evaluate() + + assertEquals( + 1, task.getAllInteropsGroups().size, + "Expected exactly 1 'SharedInteropsGroup' for task" + ) + + val group = CInteropCommonizerGroup( + setOf(CommonizerTarget(LINUX_X64, MACOS_X64)), + setOf(linuxInterop, macosInterop) ) assertEquals( - CInteropCommonizationParameters( - setOf( - CommonizerTarget(IOS_X64, IOS_ARM64), - CommonizerTarget(IOS_X64, IOS_ARM64, MACOS_X64, LINUX_X64) - ), - setOf(linuxInterop, macosInterop, iosX64Interop, iosArm64Interop) - ), task.getCommonizationParameters(sharedNativeCompilation(iosMain)) + expectCInteropCommonizerDependent(nativeMain), + expectCInteropCommonizerDependent(expectSharedNativeCompilation(nativeMain)), + "Expected same dependent from 'nativeMain' source set and 'nativeMain' compilation" ) - assertTrue( - task.getCommonizationParameters(sharedNativeCompilation(nativeMain))!!.supports(sharedNativeCompilation(nativeMain)), - "Expected CInteropCommonizerTarget of nativeMain to support iosMain" + assertEquals( + group, task.findInteropsGroup(expectCInteropCommonizerDependent(nativeMain)) + ) + + assertNull( + findCInteropCommonizerDependent(nativeTest), + "Expected nativeTest to not depend on CInteropCommonizer" ) } - private fun sharedNativeCompilation(sourceSet: KotlinSourceSet): KotlinSharedNativeCompilation { - return kotlin.targets.flatMap { it.compilations }.filterIsInstance() - .single { it.defaultSourceSet == sourceSet } + @Test + fun `nativeTest nativeMain linux macos - test compilation defines custom cinterop`() { + `nativeTest nativeMain linux macos - test compilation defines custom cinterop`(false) + } + + @Test + fun `nativeTest nativeMain linux macos - test compilation defines custom cinterop - nativeTest dependsOn nativeMain`() { + `nativeTest nativeMain linux macos - test compilation defines custom cinterop`(true) + } + + private fun `nativeTest nativeMain linux macos - test compilation defines custom cinterop`( + nativeTestDependsOnNativeMain: Boolean + ) { + val linuxInterop = kotlin.linuxX64("linux").compilations.getByName("main").cinterops.create("anyInteropName").identifier + val macosInterop = kotlin.macosX64("macos").compilations.getByName("main").cinterops.create("anyInteropName").identifier + kotlin.linuxX64("linux").compilations.getByName("test").cinterops.create("anyOtherName").identifier + + val commonMain = kotlin.sourceSets.getByName("commonMain") + val nativeMain = kotlin.sourceSets.create("nativeMain") + val linuxMain = kotlin.sourceSets.getByName("linuxMain") + val macosMain = kotlin.sourceSets.getByName("macosMain") + + val nativeTest = kotlin.sourceSets.create("nativeTest") + val linuxTest = kotlin.sourceSets.getByName("linuxTest") + val macosTest = kotlin.sourceSets.getByName("macosTest") + + linuxTest.dependsOn(nativeTest) + macosTest.dependsOn(nativeTest) + + linuxMain.dependsOn(nativeMain) + macosMain.dependsOn(nativeMain) + + nativeMain.dependsOn(commonMain) + + if (nativeTestDependsOnNativeMain) { + nativeTest.dependsOn(nativeMain) + } + + project.evaluate() + + assertEquals( + 1, task.getAllInteropsGroups().size, + "Expected exactly 1 'SharedInteropsGroup' for task" + ) + + val group = CInteropCommonizerGroup( + setOf(CommonizerTarget(LINUX_X64, MACOS_X64)), + setOf(linuxInterop, macosInterop) + ) + + assertEquals( + group, task.findInteropsGroup(expectCInteropCommonizerDependent(nativeMain)) + ) + + assertEquals( + group, task.findInteropsGroup(expectCInteropCommonizerDependent(expectSharedNativeCompilation(nativeMain))) + ) + + assertNull( + findCInteropCommonizerDependent(nativeTest), + "Expected 'nativeTest' to not be CInteropCommonizer dependent" + ) + } + + @Test + fun `hierarchical project - testSourceSetsDependOnMainSourceSets = true`() { + `hierarchical project`(testSourceSetsDependOnMainSourceSets = true) + } + + @Test + fun `hierarchical project - testSourceSetsDependOnMainSourceSets = false`() { + `hierarchical project`(testSourceSetsDependOnMainSourceSets = false) + } + + private fun `hierarchical project`(testSourceSetsDependOnMainSourceSets: Boolean) { + /* Define targets */ + val linux = kotlin.linuxX64("linux") + val macos = kotlin.macosX64("macos") + val iosX64 = kotlin.iosX64() + val iosArm64 = kotlin.iosArm64() + val windows64 = kotlin.mingwX64("windows64") + val windows32 = kotlin.mingwX86("windows32") + kotlin.jvm() + kotlin.js().browser() + + val nativeTargets = listOf(linux, macos, iosX64, iosArm64, windows64, windows32) + val windowsTargets = listOf(windows64, windows32) + val unixLikeTargets = listOf(linux, macos, iosX64, iosArm64) + val appleTargets = listOf(macos, iosX64, iosArm64) + val iosTargets = listOf(iosX64, iosArm64) + + /* Define interops */ + nativeTargets.map { target -> + target.compilations.getByName("main").cinterops.create("nativeHelper").identifier + } + + nativeTargets.map { target -> + target.compilations.getByName("test").cinterops.create("nativeTestHelper").identifier + } + + windowsTargets.map { target -> + target.compilations.getByName("main").cinterops.create("windowsHelper").identifier + } + + unixLikeTargets.map { target -> + target.compilations.getByName("main").cinterops.create("unixHelper").identifier + } + + appleTargets.map { target -> + target.compilations.getByName("main").cinterops.create("appleHelper").identifier + } + + appleTargets.map { target -> + target.compilations.getByName("test").cinterops.create("appleTestHelper").identifier + } + + iosTargets.map { target -> + target.compilations.getByName("main").cinterops.create("iosHelper").identifier + } + + iosX64.compilations.getByName("main").cinterops.create("iosX64Helper").identifier + iosX64.compilations.getByName("test").cinterops.create("iosX64TestHelper").identifier + + /* Define source set hierarchy */ + val commonMain = kotlin.sourceSets.getByName("commonMain") + val commonTest = kotlin.sourceSets.getByName("commonTest") + val nativeMain = kotlin.sourceSets.create("nativeMain") + val nativeTest = kotlin.sourceSets.create("nativeTest") + val unixMain = kotlin.sourceSets.create("unixMain") + val appleMain = kotlin.sourceSets.create("appleMain") + val appleTest = kotlin.sourceSets.create("appleTest") + val windowsMain = kotlin.sourceSets.create("windowsMain") + val iosMain = kotlin.sourceSets.create("iosMain") + val iosTest = kotlin.sourceSets.create("iosTest") + + nativeMain.dependsOn(commonMain) + nativeTest.dependsOn(commonTest) + unixMain.dependsOn(nativeMain) + appleMain.dependsOn(nativeMain) + appleTest.dependsOn(nativeTest) + windowsMain.dependsOn(nativeMain) + iosMain.dependsOn(appleMain) + + if (testSourceSetsDependOnMainSourceSets) { + nativeTest.dependsOn(nativeMain) + appleTest.dependsOn(appleMain) + iosTest.dependsOn(iosMain) + } + + windowsTargets.forEach { target -> + target.compilations.getByName("main").defaultSourceSet.dependsOn(windowsMain) + } + + iosTargets.forEach { target -> + target.compilations.getByName("main").defaultSourceSet.dependsOn(iosMain) + target.compilations.getByName("test").defaultSourceSet.dependsOn(iosTest) + } + + appleTargets.forEach { target -> + target.compilations.getByName("main").defaultSourceSet.dependsOn(appleMain) + target.compilations.getByName("test").defaultSourceSet.dependsOn(appleTest) + } + + unixLikeTargets.forEach { target -> + target.compilations.getByName("main").defaultSourceSet.dependsOn(unixMain) + } + + nativeTargets.forEach { target -> + target.compilations.getByName("main").defaultSourceSet.dependsOn(nativeMain) + target.compilations.getByName("test").defaultSourceSet.dependsOn(nativeTest) + } + + project.evaluate() + + assertCInteropDependentEqualsForSourceSetAndCompilation(nativeMain) + assertCInteropDependentEqualsForSourceSetAndCompilation(unixMain) + assertCInteropDependentEqualsForSourceSetAndCompilation(windowsMain) + assertCInteropDependentEqualsForSourceSetAndCompilation(appleMain) + assertCInteropDependentEqualsForSourceSetAndCompilation(iosMain) + + val groups = task.getAllInteropsGroups() + assertEquals(2, groups.size, "Expected exactly two interop groups: main and test") + + val nativeCommonizerTarget = SharedCommonizerTarget(nativeTargets.map { it.konanTarget }) + val unixLikeCommonizerTarget = SharedCommonizerTarget(unixLikeTargets.map { it.konanTarget }) + val windowsCommonizerTarget = SharedCommonizerTarget(windowsTargets.map { it.konanTarget }) + val appleCommonizerTarget = SharedCommonizerTarget(appleTargets.map { it.konanTarget }) + val iosCommonizerTarget = SharedCommonizerTarget(iosTargets.map { it.konanTarget }) + + val expectedMainGroup = CInteropCommonizerGroup( + targets = setOf( + nativeCommonizerTarget, unixLikeCommonizerTarget, windowsCommonizerTarget, + appleCommonizerTarget, iosCommonizerTarget + ), + interops = nativeTargets.map { target -> target.mainCinteropIdentifier("nativeHelper") }.toSet() + + unixLikeTargets.map { target -> target.mainCinteropIdentifier("unixHelper") } + + windowsTargets.map { target -> target.mainCinteropIdentifier("windowsHelper") } + + appleTargets.map { target -> target.mainCinteropIdentifier("appleHelper") } + + iosTargets.map { target -> target.mainCinteropIdentifier("iosHelper") } + + iosX64.mainCinteropIdentifier("iosX64Helper") + ) + + val expectedTestGroup = CInteropCommonizerGroup( + targets = setOf(nativeCommonizerTarget, appleCommonizerTarget, iosCommonizerTarget), + interops = nativeTargets.map { target -> target.testCinteropIdentifier("nativeTestHelper") }.toSet() + + appleTargets.map { target -> target.testCinteropIdentifier("appleTestHelper") } + + iosX64.testCinteropIdentifier("iosX64TestHelper") + ) + + val mainGroup = groups.maxBy { it.targets.size }!! + val testGroup = groups.minBy { it.targets.size }!! + + assertEquals( + expectedMainGroup, mainGroup, + "mainGroup does not match" + ) + + assertEquals( + expectedTestGroup, testGroup, + "testGroup does not match" + ) + + assertEquals( + mainGroup, task.findInteropsGroup(expectCInteropCommonizerDependent(nativeMain)), + "Expected nativeMain being part of the mainGroup" + ) + + assertEquals( + mainGroup, task.findInteropsGroup(expectCInteropCommonizerDependent(unixMain)), + "Expected unixMain being part of the mainGroup" + ) + + assertEquals( + mainGroup, task.findInteropsGroup(expectCInteropCommonizerDependent(windowsMain)), + "Expected windowsMain being part of the mainGroup" + ) + + assertEquals( + mainGroup, task.findInteropsGroup(expectCInteropCommonizerDependent(appleMain)), + "Expected appleMain being part of the mainGroup" + ) + + assertEquals( + mainGroup, task.findInteropsGroup(expectCInteropCommonizerDependent(iosMain)), + "Expected iosMain being part of the mainGroup" + ) + + assertEquals( + testGroup, task.findInteropsGroup(expectCInteropCommonizerDependent(nativeTest)), + "Expected nativeTest being part of the testGroup" + ) + + assertEquals( + testGroup, task.findInteropsGroup(expectCInteropCommonizerDependent(appleTest)), + "Expected appleTest being part of the testGroup" + ) + + assertEquals( + testGroup, task.findInteropsGroup(expectCInteropCommonizerDependent(iosTest)), + "Expected iosTest being part of the testGroup" + ) + + kotlin.targets + /* Shared K/N targets still are considered type common */ + .filter { it.platformType != KotlinPlatformType.common } + .flatMap { it.compilations }.map { it.defaultSourceSet }.forEach { targetDefaultSourceSet -> + assertNull( + findCInteropCommonizerDependent(targetDefaultSourceSet), + "Expected target source set ${targetDefaultSourceSet.name} not be CInteropCommonizerDependent" + ) + } + } + + private fun assertCInteropDependentEqualsForSourceSetAndCompilation(sourceSet: KotlinSourceSet) { + assertEquals( + expectCInteropCommonizerDependent(sourceSet), + expectCInteropCommonizerDependent(expectSharedNativeCompilation(sourceSet)), + "Expected found CInteropCommonizerDependent for source set '$sourceSet and it's shared native compilation to be equal" + ) } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/MultiplatformExtensionTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/MultiplatformExtensionTest.kt new file mode 100644 index 00000000000..0eeeeacb04e --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/MultiplatformExtensionTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2010-2021 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. + */ + +/* Associate compilations are not yet supported by the IDE. KT-34102 */ +@file:Suppress("invisible_reference", "invisible_member", "FunctionName", "DuplicatedCode") + +package org.jetbrains.kotlin.gradle + +import org.gradle.api.internal.project.ProjectInternal +import org.gradle.api.plugins.ExtraPropertiesExtension +import org.gradle.testfixtures.ProjectBuilder +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinSharedNativeCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.KotlinCompilationData +import org.jetbrains.kotlin.gradle.targets.native.internal.CInteropCommonizerDependent +import org.jetbrains.kotlin.gradle.targets.native.internal.CInteropIdentifier +import org.jetbrains.kotlin.gradle.targets.native.internal.from +import kotlin.test.BeforeTest +import kotlin.test.assertNotNull + +abstract class MultiplatformExtensionTest { + protected val project: ProjectInternal = ProjectBuilder.builder().build() as ProjectInternal + protected lateinit var kotlin: KotlinMultiplatformExtension + + @BeforeTest + open fun setup() { + project.plugins.apply("kotlin-multiplatform") + kotlin = project.extensions.getByName("kotlin") as KotlinMultiplatformExtension + } + + protected fun enableGranularSourceSetsMetadata() { + project.extensions.getByType(ExtraPropertiesExtension::class.java).set("kotlin.mpp.enableGranularSourceSetsMetadata", "true") + } + + protected fun enableCInteropCommonization() { + project.extensions.getByType(ExtraPropertiesExtension::class.java).set("kotlin.mpp.enableCInteropCommonization", "true") + } + + internal fun expectCInteropCommonizerDependent(compilation: KotlinSharedNativeCompilation): CInteropCommonizerDependent { + return assertNotNull( + CInteropCommonizerDependent.from(compilation), "Can't find SharedInterops for ${compilation.name} compilation" + ) + } + + internal fun expectCInteropCommonizerDependent(sourceSet: KotlinSourceSet): CInteropCommonizerDependent { + return assertNotNull( + CInteropCommonizerDependent.from(project, sourceSet), "Can't find SharedInterops for ${sourceSet.name} source set" + ) + } + + internal fun findCInteropCommonizerDependent(compilation: KotlinSharedNativeCompilation): CInteropCommonizerDependent? { + return CInteropCommonizerDependent.from(compilation) + } + + internal fun findCInteropCommonizerDependent(sourceSet: KotlinSourceSet): CInteropCommonizerDependent? { + return CInteropCommonizerDependent.from(project, sourceSet) + } + + internal fun expectSharedNativeCompilation(sourceSet: KotlinSourceSet): KotlinSharedNativeCompilation { + return kotlin.targets.flatMap { it.compilations }.filterIsInstance() + .single { it.defaultSourceSet == sourceSet } + } + + internal fun KotlinNativeTarget.mainCinteropIdentifier(name: String): CInteropIdentifier { + return compilations.getByName("main").cinteropIdentifier(name) + } + + internal fun KotlinNativeTarget.testCinteropIdentifier(name: String): CInteropIdentifier { + return compilations.getByName("test").cinteropIdentifier(name) + } + + internal fun KotlinCompilationData<*>.cinteropIdentifier(name: String): CInteropIdentifier { + return CInteropIdentifier(CInteropIdentifier.Scope.create(this), name) + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/metadata/KotlinMetadataTargetConfigurator.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/metadata/KotlinMetadataTargetConfigurator.kt index 4b99444fd11..0001db940ee 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/metadata/KotlinMetadataTargetConfigurator.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/metadata/KotlinMetadataTargetConfigurator.kt @@ -27,9 +27,6 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.KotlinPm20ProjectExtension import org.jetbrains.kotlin.gradle.plugin.sources.* import org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildStatsService import org.jetbrains.kotlin.gradle.targets.native.internal.* -import org.jetbrains.kotlin.gradle.targets.native.internal.commonizeCInteropTask -import org.jetbrains.kotlin.gradle.targets.native.internal.copyCommonizeCInteropForIdeTask -import org.jetbrains.kotlin.gradle.targets.native.internal.getLibraries import org.jetbrains.kotlin.gradle.tasks.* import org.jetbrains.kotlin.gradle.utils.addExtendsFromRelation import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName @@ -318,21 +315,6 @@ class KotlinMetadataTargetConfigurator : compileDependencyFiles = project.files() } } - - if (this is KotlinSharedNativeCompilation) { - project.commonizeCInteropTask?.let { task -> - compileDependencyFiles = compileDependencyFiles.plus(task.getLibraries(this)) - } - project.copyCommonizeCInteropForIdeTask?.let { task -> - val libraries = task.getLibraries(this) - kotlinSourceSetsIncludingDefault.filterIsInstance().forEach { sourceSet -> - val dependencyConfigurationName = - if (project.isIntransitiveMetadataConfigurationEnabled) sourceSet.intransitiveMetadataConfigurationName - else sourceSet.implementationMetadataConfigurationName - project.dependencies.add(dependencyConfigurationName, libraries) - } - } - } } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/KotlinNativeTargetPreset.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/KotlinNativeTargetPreset.kt index 061eb3579a6..d1931643cb8 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/KotlinNativeTargetPreset.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/KotlinNativeTargetPreset.kt @@ -16,6 +16,7 @@ import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider import org.jetbrains.kotlin.gradle.targets.native.DisabledNativeTargetsReporter import org.jetbrains.kotlin.gradle.targets.native.internal.NativeDistributionTypeProvider import org.jetbrains.kotlin.gradle.targets.native.internal.PlatformLibrariesGenerator +import org.jetbrains.kotlin.gradle.targets.native.internal.setupCInteropCommonizerDependencies import org.jetbrains.kotlin.gradle.targets.native.internal.setupKotlinNativePlatformDependencies import org.jetbrains.kotlin.gradle.utils.NativeCompilerDownloader import org.jetbrains.kotlin.gradle.utils.SingleActionPerProject @@ -91,6 +92,10 @@ abstract class AbstractKotlinNativeTargetPreset( } } + SingleActionPerProject.run(project, "setupCInteropCommonizerDependencies") { + project.setupCInteropCommonizerDependencies() + } + if (!konanTarget.enabledOnCurrentHost) { with(HostManager()) { val supportedHosts = enabledByHost.filterValues { konanTarget in it }.keys diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/AbstractCInteropCommonizerTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/AbstractCInteropCommonizerTask.kt index 7b8d4945a7d..52d935cf8ea 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/AbstractCInteropCommonizerTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/AbstractCInteropCommonizerTask.kt @@ -8,10 +8,10 @@ package org.jetbrains.kotlin.gradle.targets.native.internal import org.gradle.api.DefaultTask import org.gradle.api.file.FileCollection import org.gradle.api.tasks.OutputDirectory -import org.gradle.api.tasks.TaskProvider import org.jetbrains.kotlin.commonizer.CommonizerOutputFileLayout -import org.jetbrains.kotlin.commonizer.CommonizerOutputFileLayout.fileName -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinSharedNativeCompilation +import org.jetbrains.kotlin.commonizer.CommonizerOutputFileLayout.base64Hash +import org.jetbrains.kotlin.commonizer.CommonizerOutputFileLayout.ensureMaxFileNameLength +import org.jetbrains.kotlin.commonizer.identityString import org.jetbrains.kotlin.gradle.utils.filesProvider import java.io.File @@ -19,27 +19,26 @@ internal abstract class AbstractCInteropCommonizerTask : DefaultTask() { @get:OutputDirectory abstract val outputDirectory: File - internal fun outputDirectory(parameters: CInteropCommonizationParameters): File { + internal fun outputDirectory(group: CInteropCommonizerGroup): File { + val interopsDirectoryName = group.interops.map { it.interopName }.toSet().joinToString(";") + val groupDisambiguation = group.targets.joinToString { it.identityString } + + group.interops.joinToString { it.uniqueName } + return outputDirectory - .resolve(parameters.targets.fileName) - .resolve(parameters.interops.map { it.interopName }.distinct().joinToString("-")) + .resolve(ensureMaxFileNameLength(interopsDirectoryName)) + .resolve(base64Hash(groupDisambiguation)) } - internal abstract fun getCommonizationParameters(compilation: KotlinSharedNativeCompilation): CInteropCommonizationParameters? + internal abstract fun findInteropsGroup(dependent: CInteropCommonizerDependent): CInteropCommonizerGroup? - internal fun getLibraries(compilation: KotlinSharedNativeCompilation): FileCollection { - val compilationCommonizerTarget = project.getCommonizerTarget(compilation) ?: return project.files() + internal fun commonizedOutputLibraries(dependent: CInteropCommonizerDependent): FileCollection { val fileProvider = project.filesProvider { - val parameters = getCommonizationParameters(compilation) ?: return@filesProvider emptySet() + val group = findInteropsGroup(dependent) ?: return@filesProvider emptySet() CommonizerOutputFileLayout - .resolveCommonizedDirectory(outputDirectory(parameters), compilationCommonizerTarget) + .resolveCommonizedDirectory(outputDirectory(group), dependent.target) .listFiles().orEmpty().toSet() } return fileProvider.builtBy(this) } } - -internal fun TaskProvider.getLibraries(compilation: KotlinSharedNativeCompilation): FileCollection { - return compilation.target.project.files(map { it.getLibraries(compilation) }) -} diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CInteropCommonizerDependencies.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CInteropCommonizerDependencies.kt new file mode 100644 index 00000000000..db0e371e011 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CInteropCommonizerDependencies.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2010-2021 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.targets.native.internal + +import org.gradle.api.Project +import org.gradle.api.file.FileCollection +import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMetadataTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinSharedNativeCompilation +import org.jetbrains.kotlin.gradle.plugin.sources.DefaultKotlinSourceSet +import org.jetbrains.kotlin.gradle.utils.filesProvider +import java.io.File + +// TODO NOW: Integration tests! + +internal fun Project.setupCInteropCommonizerDependencies() { + val kotlin = this.multiplatformExtensionOrNull ?: return + + kotlin.targets.withType(KotlinMetadataTarget::class.java).all { target -> + target.compilations.withType(KotlinSharedNativeCompilation::class.java).all { compilation -> + setupCInteropCommonizerDependenciesForCompilation(compilation) + } + } + + kotlin.sourceSets.withType(DefaultKotlinSourceSet::class.java).all { sourceSet -> + setupCInteropCommonizerDependenciesForIde(sourceSet) + } +} + +private fun Project.setupCInteropCommonizerDependenciesForCompilation(compilation: KotlinSharedNativeCompilation) { + val cinteropCommonizerTask = project.commonizeCInteropTask ?: return + + compilation.compileDependencyFiles += filesProvider { + val cinteropCommonizerDependent = CInteropCommonizerDependent.from(compilation) ?: return@filesProvider emptySet() + cinteropCommonizerTask.get().commonizedOutputLibraries(cinteropCommonizerDependent) + } +} + +/** + * IDE will resolve the dependencies provided on source sets. + * This will use the [Project.copyCommonizeCInteropForIdeTask] over the regular cinterop commonization task. + * The copying task prevent red code within the IDE after cleaning the build output. + */ +private fun Project.setupCInteropCommonizerDependenciesForIde(sourceSet: DefaultKotlinSourceSet) { + val cinteropCommonizerTask = project.copyCommonizeCInteropForIdeTask ?: return + + addDependency(sourceSet, filesProvider files@{ + val sourceSetCommonizerTarget = getCommonizerTarget(sourceSet) + + val additionalVisibleSourceSets = sourceSet.getAdditionalVisibleSourceSets() + .filter { sourceSet -> getCommonizerTarget(sourceSet) == sourceSetCommonizerTarget } + + val cinteropCommonizerDependents = (additionalVisibleSourceSets + sourceSet).toSet() + .mapNotNull { sourceSet -> CInteropCommonizerDependent.from(this, sourceSet) } + .toSet() + + cinteropCommonizerDependents.map { cinteropCommonizerDependent -> + cinteropCommonizerTask.get().commonizedOutputLibraries(cinteropCommonizerDependent) + } + }) +} + +/** + * Dependencies here are using a special configuration called 'intransitiveMetadataConfiguration'. + * This special configuration can tell the IDE that this dependencies shall *not* be transitively be visible + * to dependsOn edges. This is necessary for the way the commonizer handles it's "expect refinement" approach. + * In this mode, every source set will receive exactly one commonized library to analyze its source code with. + */ +private fun Project.addDependency(sourceSet: DefaultKotlinSourceSet, dependency: FileCollection) { + val dependencyConfigurationName = + if (project.isIntransitiveMetadataConfigurationEnabled) sourceSet.intransitiveMetadataConfigurationName + else sourceSet.implementationMetadataConfigurationName + project.dependencies.add(dependencyConfigurationName, dependency) +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CInteropCommonizerDependent.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CInteropCommonizerDependent.kt new file mode 100644 index 00000000000..568bbb1c00a --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CInteropCommonizerDependent.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2010-2021 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.targets.native.internal + +import org.gradle.api.Project +import org.jetbrains.kotlin.commonizer.SharedCommonizerTarget +import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.mpp.* +import org.jetbrains.kotlin.gradle.plugin.mpp.CompilationSourceSetUtil.compilationsBySourceSets +import org.jetbrains.kotlin.gradle.plugin.sources.resolveAllDependsOnSourceSets +import org.jetbrains.kotlin.gradle.targets.native.internal.CInteropIdentifier.Scope +import org.jetbrains.kotlin.gradle.utils.UnsafeApi + +internal class CInteropCommonizerDependent @UnsafeApi constructor( + val target: SharedCommonizerTarget, + val scopes: Set, + val interops: Set +) { + + override fun equals(other: Any?): Boolean { + if (other !is CInteropCommonizerDependent) return false + if (this.target != other.target) return false + if (this.scopes != other.scopes) return false + if (this.interops != other.interops) return false + return true + } + + override fun hashCode(): Int { + var result = target.hashCode() + result = 31 * result + scopes.hashCode() + result = 31 * result + interops.hashCode() + return result + } + + init { + require(target.targets.isNotEmpty()) { "CInteropCommonizerDependent.target.targets.size can't be empty" } + require(scopes.isNotEmpty()) { "CInteropCommonizerDependent.scopes can't be empty" } + require(interops.isNotEmpty()) { "CInteropCommonizerDependent.interops can't be empty" } + } + + companion object Factory +} + +@OptIn(UnsafeApi::class) +internal fun CInteropCommonizerDependent.Factory.from( + target: SharedCommonizerTarget, + compilations: Set +): CInteropCommonizerDependent? { + target.targets.ifEmpty { return null } + + /* + Filter out compilations that have their associate also in the set of compilations + e.g. do not include '*test' if their main counterpart is also present. + *test and *main compilations will be included when build authors declare a *Test dependsOn *Main source set relationship. + This relationship should not be declared, but we try to be lenient towards it here. + */ + val filteredCompilations = compilations.filter { compilation -> + compilation.associateWithTransitiveClosure.none { associateCompilation -> associateCompilation in compilations } + }.ifEmpty { return null }.toSet() + + val scopes: Set = filteredCompilations + .map { compilation -> Scope.create(compilation) }.toSet() + .ifEmpty { return null } + + val interops: Set = filteredCompilations + .flatMap { compilation -> compilation.cinterops.ifEmpty { return null } } + .map { cinterop -> cinterop.identifier }.toSet() + + return CInteropCommonizerDependent(target, scopes, interops) +} + +internal fun CInteropCommonizerDependent.Factory.from(compilation: KotlinSharedNativeCompilation): CInteropCommonizerDependent? { + return from( + compilation.project.getCommonizerTarget(compilation) as? SharedCommonizerTarget ?: return null, + compilation.findDependingNativeCompilations() + ) +} + +internal fun CInteropCommonizerDependent.Factory.from(project: Project, sourceSet: KotlinSourceSet): CInteropCommonizerDependent? { + val target = project.getCommonizerTarget(sourceSet) as? SharedCommonizerTarget ?: return null + val compilations = compilationsBySourceSets(project)[sourceSet] ?: return null + + /* Non-native or non 'shared native' source sets can return eagerly */ + if (compilations.any { compilation -> compilation !is AbstractKotlinNativeCompilation }) { + return null + } + + return from( + target = target, + compilations = compilations.filterIsInstance().toSet() + ) +} + +private fun KotlinSharedNativeCompilation.findDependingNativeCompilations(): Set { + val multiplatformExtension = project.multiplatformExtensionOrNull ?: return emptySet() + val allParticipatingSourceSetsOfCompilation = allParticipatingSourceSets() + + return multiplatformExtension.targets + .flatMap { target -> target.compilations } + .filterIsInstance() + .filter { nativeCompilation -> nativeCompilation.allParticipatingSourceSets().containsAll(allParticipatingSourceSetsOfCompilation) } + .toSet() +} + +/** + * Some implementations of [KotlinCompilation] do not contain the default source set in + * [KotlinCompilation.kotlinSourceSets] or [KotlinCompilation.allKotlinSourceSets] + * see KT-45412 + */ +private fun KotlinCompilation<*>.allParticipatingSourceSets(): Set { + return kotlinSourceSetsIncludingDefault + kotlinSourceSetsIncludingDefault.resolveAllDependsOnSourceSets() +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CInteropCommonizerGroup.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CInteropCommonizerGroup.kt new file mode 100644 index 00000000000..6e17176db36 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CInteropCommonizerGroup.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2010-2021 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.targets.native.internal + +import org.gradle.api.tasks.Input +import org.jetbrains.kotlin.commonizer.SharedCommonizerTarget +import org.jetbrains.kotlin.gradle.utils.appendLine + +internal data class CInteropCommonizerGroup( + @get:Input val targets: Set, + @get:Input val interops: Set +) { + @Suppress("deprecation") + override fun toString(): String { + return buildString { + appendln("InteropsGroup {") + appendln("targets: ") + targets.sortedBy { it.targets.size }.forEach { target -> + appendln(" $target") + } + appendln() + appendln("interops: ") + interops.sortedBy { it.toString() }.forEach { interop -> + appendln(" $interop") + } + appendLine("}") + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CInteropCommonizerTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CInteropCommonizerTask.kt index 85c0160b94d..98dfd6fdb5b 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CInteropCommonizerTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CInteropCommonizerTask.kt @@ -5,19 +5,18 @@ package org.jetbrains.kotlin.gradle.targets.native.internal -import org.gradle.api.Project import org.gradle.api.file.FileCollection import org.gradle.api.provider.Provider import org.gradle.api.tasks.* -import org.jetbrains.kotlin.commonizer.* +import org.jetbrains.kotlin.commonizer.CommonizerDependency +import org.jetbrains.kotlin.commonizer.NonTargetedCommonizerDependency +import org.jetbrains.kotlin.commonizer.TargetedCommonizerDependency +import org.jetbrains.kotlin.commonizer.allLeaves import org.jetbrains.kotlin.compilerRunner.GradleCliCommonizer import org.jetbrains.kotlin.compilerRunner.konanHome import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull -import org.jetbrains.kotlin.gradle.plugin.CInteropSettings import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet -import org.jetbrains.kotlin.gradle.plugin.mpp.DefaultCInteropSettings -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeCompilation import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinSharedNativeCompilation import org.jetbrains.kotlin.gradle.plugin.mpp.kotlinSourceSetsIncludingDefault import org.jetbrains.kotlin.gradle.plugin.sources.resolveAllDependsOnSourceSets @@ -48,12 +47,12 @@ internal open class CInteropCommonizerTask : AbstractCInteropCommonizerTask() { @get:OutputDirectories val allOutputDirectories: Set - get() = getCommonizationParameters().map { outputDirectory(it) }.toSet() + get() = getAllInteropsGroups().map { outputDirectory(it) }.toSet() @Suppress("unused") // Used for UP-TO-DATE check @get:Classpath val commonizedNativeDistributionDependencies: Set - get() = getCommonizationParameters().flatMap { parameters -> parameters.targets } + get() = getAllInteropsGroups().flatMap { group -> group.targets } .flatMap { target -> project.getNativeDistributionDependencies(target) } .toSet() @@ -71,102 +70,80 @@ internal open class CInteropCommonizerTask : AbstractCInteropCommonizerTask() { this.cinterops += cinterops } - fun exclude(vararg tasks: CInteropProcess) { - exclude(tasks.map { it.settings.identifier }) - } - - fun exclude(vararg settings: CInteropSettings) { - exclude(settings.mapNotNull { (it as? DefaultCInteropSettings)?.identifier }) - } - - internal fun exclude(interopIdentifiers: List) { - this.cinterops = this.cinterops.filterTo(mutableSetOf()) { it.identifier !in interopIdentifiers } - } - @TaskAction internal fun commonizeCInteropLibraries() { - getCommonizationParameters().forEach(::commonize) + getAllInteropsGroups().forEach(::commonize) } - private fun commonize(parameters: CInteropCommonizationParameters) { - val cinteropsForTarget = cinterops.filter { cinterop -> cinterop.identifier in parameters.interops } - outputDirectory(parameters).deleteRecursively() + private fun commonize(group: CInteropCommonizerGroup) { + val cinteropsForTarget = cinterops.filter { cinterop -> cinterop.identifier in group.interops } + outputDirectory(group).deleteRecursively() if (cinteropsForTarget.isEmpty()) return GradleCliCommonizer(project).commonizeLibraries( konanHome = project.file(project.konanHome), - outputTargets = parameters.targets, + outputTargets = group.targets, inputLibraries = cinteropsForTarget.map { it.libraryFile.get() }.filter { it.exists() }.toSet(), - dependencyLibraries = getNativeDistributionDependencies(parameters), - outputDirectory = outputDirectory(parameters), + dependencyLibraries = getNativeDistributionDependencies(group), + outputDirectory = outputDirectory(group), logLevel = project.commonizerLogLevel ) } - private fun getNativeDistributionDependencies(parameters: CInteropCommonizationParameters): Set { - return (parameters.targets + parameters.targets.allLeaves()).flatMapTo(mutableSetOf()) { target -> + private fun getNativeDistributionDependencies(group: CInteropCommonizerGroup): Set { + return (group.targets + group.targets.allLeaves()).flatMapTo(mutableSetOf()) { target -> project.getNativeDistributionDependencies(target).map { dependency -> TargetedCommonizerDependency(target, dependency) } } } - private fun getSharedNativeCInterops(): Set { - val sharedNativeCompilations = (project.multiplatformExtensionOrNull ?: return emptySet()) - .targets.flatMap { it.compilations } - .filterIsInstance() - - fun buildSharedNativeCInterops(compilation: KotlinSharedNativeCompilation): SharedNativeCInterops? { - return SharedNativeCInterops( - target = project.getCommonizerTarget(compilation) as? SharedCommonizerTarget ?: return null, - interops = project.getDependingNativeCompilations(compilation) - /* If any dependee native compilation has no interop, then commonization is useless */ - .flatMap { nativeCompilation -> nativeCompilation.cinterops.ifEmpty { return null } } - .map { interop -> interop.identifier } - .toSet() - ) - } - - return sharedNativeCompilations.mapNotNull(::buildSharedNativeCInterops).toSet() - .run(::removeNotRegisteredInterops) - .run(::removeEmptyInterops) - } - - @Nested - internal fun getCommonizationParameters(): Set { - val sharedNativeCInterops = getSharedNativeCInterops() - if (sharedNativeCInterops.isEmpty()) return emptySet() - - return sharedNativeCInterops.distinct() - .filter { potentialRoot -> sharedNativeCInterops.none { other -> potentialRoot isProperSubsetOf other } } - .map { root -> root to sharedNativeCInterops.filter { other -> other isProperSubsetOf root } } - .mapTo(mutableSetOf()) { (root, subsets) -> - CInteropCommonizationParameters( - targets = subsets.mapTo(mutableSetOf()) { it.target } + root.target, - interops = root.interops - ) - } - } - - override fun getCommonizationParameters(compilation: KotlinSharedNativeCompilation): CInteropCommonizationParameters? { - val supportedParameters = getCommonizationParameters().filter { parameters -> parameters.supports(compilation) } - if (supportedParameters.isEmpty()) return null - assert(supportedParameters.size == 1) { - "Unnecessary work detected: Multiple commonization parameters seem to be doing redundant work" + internal fun getAllInteropsGroups(): Set { + val dependents = getAllDependents() + val allScopes = dependents.map { it.scopes }.toSet() + val rootScopes = allScopes.filter { scopes -> + allScopes.none { otherScopes -> otherScopes != scopes && otherScopes.containsAll(scopes) } } - return supportedParameters.first() + + return rootScopes.map { scopes -> + val dependentsForScopes = dependents.filter { dependent -> + scopes.containsAll(dependent.scopes) + } + + CInteropCommonizerGroup( + targets = dependentsForScopes.map { it.target }.toSet(), + interops = dependentsForScopes.flatMap { it.interops }.toSet() + ) + }.toSet() } -} + override fun findInteropsGroup(dependent: CInteropCommonizerDependent): CInteropCommonizerGroup? { + val suitableGroups = getAllInteropsGroups().filter { group -> + group.interops.containsAll(dependent.interops) && group.targets.contains(dependent.target) + } -internal fun CInteropCommonizationParameters.supports( - compilation: KotlinSharedNativeCompilation -): Boolean { - val project = compilation.project - val commonizerTargetOfCompilation = project.getCommonizerTarget(compilation) ?: return false - val interopsOfCompilation = project.getDependingNativeCompilations(compilation) - .flatMap { it.cinterops }.map { it.identifier } + assert(suitableGroups.size <= 1) { + "CInteropCommonizerTask: Unnecessary work detected: More than one suitable group found for cinterop dependent." + } - return targets.contains(commonizerTargetOfCompilation) && interops.containsAll(interopsOfCompilation) + return suitableGroups.firstOrNull() + } + + @Internal + internal fun getAllDependents(): Set { + val multiplatformExtension = project.multiplatformExtensionOrNull ?: return emptySet() + + val fromSharedNativeCompilations = multiplatformExtension + .targets.flatMap { target -> target.compilations } + .filterIsInstance() + .mapNotNull { compilation -> CInteropCommonizerDependent.from(compilation) } + .toSet() + + val fromSourceSets = multiplatformExtension.sourceSets + .mapNotNull { sourceSet -> CInteropCommonizerDependent.from(project, sourceSet) } + .toSet() + + return (fromSharedNativeCompilations + fromSourceSets) + } } private fun CInteropProcess.toGist(): CInteropGist { @@ -178,58 +155,3 @@ private fun CInteropProcess.toGist(): CInteropGist { libraryFile = outputFileProvider ) } - -/** - * Represents a single shared native compilation / shared native source set - * that would rely on given [interops] - */ -internal data class SharedNativeCInterops( - val target: SharedCommonizerTarget, - val interops: Set -) - -internal infix fun SharedNativeCInterops.isProperSubsetOf(other: SharedNativeCInterops): Boolean { - return target.allLeaves() != other.target.allLeaves() && other.target.allLeaves().containsAll(target.allLeaves()) - && interops != other.interops && other.interops.containsAll(interops) -} - -/** - * Represents a single invocation to the commonizer - */ -internal data class CInteropCommonizationParameters( - @get:Input val targets: Set, - @get:Input val interops: Set -) - -private fun CInteropCommonizerTask.removeNotRegisteredInterops( - parameters: Set -): Set { - val registeredInterops = this.cinterops.map { it.identifier } - return parameters.mapTo(mutableSetOf()) { params -> - params.copy(interops = params.interops.filterTo(mutableSetOf()) { interop -> interop in registeredInterops }) - } -} - -private fun removeEmptyInterops(parameters: Set): Set { - return parameters.filterTo(mutableSetOf()) { it.interops.isNotEmpty() } -} - -private fun Project.getDependingNativeCompilations(compilation: KotlinSharedNativeCompilation): Set { - /** - * Some implementations of [KotlinCompilation] do not contain the default source set in - * [KotlinCompilation.kotlinSourceSets] or [KotlinCompilation.allKotlinSourceSets] - * see KT-45412 - */ - fun KotlinCompilation<*>.allParticipatingSourceSets(): Set { - return kotlinSourceSetsIncludingDefault + kotlinSourceSetsIncludingDefault.resolveAllDependsOnSourceSets() - } - - val multiplatformExtension = multiplatformExtensionOrNull ?: return emptySet() - val allParticipatingSourceSetsOfCompilation = compilation.allParticipatingSourceSets() - - return multiplatformExtension.targets - .flatMap { target -> target.compilations } - .filterIsInstance() - .filter { nativeCompilation -> nativeCompilation.allParticipatingSourceSets().containsAll(allParticipatingSourceSetsOfCompilation) } - .toSet() -} diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CommonizerTasks.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CommonizerTasks.kt index 87280af57f7..9aa7870dd2b 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CommonizerTasks.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CommonizerTasks.kt @@ -11,6 +11,7 @@ import org.gradle.api.tasks.Delete import org.gradle.api.tasks.TaskProvider import org.jetbrains.kotlin.gradle.internal.isInIdeaSync import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider +import org.jetbrains.kotlin.gradle.plugin.whenEvaluated import org.jetbrains.kotlin.gradle.tasks.dependsOn import org.jetbrains.kotlin.gradle.tasks.locateTask import org.jetbrains.kotlin.gradle.tasks.registerTask @@ -50,8 +51,11 @@ internal val Project.commonizeCInteropTask: TaskProvider return locateOrRegisterTask( "commonizeCInterop", invokeWhenRegistered = { + val task = this commonizeTask.dependsOn(this) - commonizeNativeDistributionTask?.let(this::dependsOn) + whenEvaluated { + commonizeNativeDistributionTask?.let(task::dependsOn) + } }, configureTask = { group = "interop" diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CopyCInteropCommonizerTaskOutputForIdeTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CopyCInteropCommonizerTaskOutputForIdeTask.kt index 8223d2958f5..88e323b5eae 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CopyCInteropCommonizerTaskOutputForIdeTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/CopyCInteropCommonizerTaskOutputForIdeTask.kt @@ -7,7 +7,6 @@ package org.jetbrains.kotlin.gradle.targets.native.internal import org.gradle.api.provider.Provider import org.gradle.api.tasks.* -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinSharedNativeCompilation import java.io.File internal open class CopyCommonizeCInteropForIdeTask : AbstractCInteropCommonizerTask() { @@ -25,17 +24,17 @@ internal open class CopyCommonizeCInteropForIdeTask : AbstractCInteropCommonizer override val outputDirectory: File = project.rootDir.resolve(".gradle/kotlin/commonizer") .resolve(project.path.removePrefix(":").replace(":", "/")) - override fun getCommonizationParameters(compilation: KotlinSharedNativeCompilation): CInteropCommonizationParameters? { - return commonizeCInteropTask.get().getCommonizationParameters(compilation) + override fun findInteropsGroup(dependent: CInteropCommonizerDependent): CInteropCommonizerGroup? { + return commonizeCInteropTask.get().findInteropsGroup(dependent) } @TaskAction protected fun copy() { outputDirectory.mkdirs() - for (parameters in commonizeCInteropTask.get().getCommonizationParameters()) { - val source = commonizeCInteropTask.get().outputDirectory(parameters) + for (group in commonizeCInteropTask.get().getAllInteropsGroups()) { + val source = commonizeCInteropTask.get().outputDirectory(group) if (!source.exists()) continue - val target = outputDirectory(parameters) + val target = outputDirectory(group) if (target.exists()) target.deleteRecursively() source.copyRecursively(target, true) } diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/KotlinNativePlatformDependencies.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/KotlinNativePlatformDependencies.kt index be344d922d3..0bcd6ea7f44 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/KotlinNativePlatformDependencies.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/internal/KotlinNativePlatformDependencies.kt @@ -85,6 +85,7 @@ private val Project.isNativeDependencyPropagationEnabled: Boolean internal fun Project.isAllowCommonizer( kotlinVersion: String = getKotlinPluginVersion() ): Boolean { + assert(state.executed) { "'isAllowCommonizer' can only be called after project evaluation" } multiplatformExtensionOrNull ?: return false //register commonizer only for 1.4+, only for HMPP projects diff --git a/native/commonizer-api/src/org/jetbrains/kotlin/commonizer/util/transitiveClosure.kt b/native/commonizer-api/src/org/jetbrains/kotlin/commonizer/util/transitiveClosure.kt index 73c3ac2a83e..241be05429d 100644 --- a/native/commonizer-api/src/org/jetbrains/kotlin/commonizer/util/transitiveClosure.kt +++ b/native/commonizer-api/src/org/jetbrains/kotlin/commonizer/util/transitiveClosure.kt @@ -18,7 +18,7 @@ public inline fun transitiveClosure(seed: T, edges: T.() -> Iterable val initialEdges = seed.edges() if (initialEdges is Collection && initialEdges.isEmpty()) return emptySet() - val queue = deque() + val queue = deque(initialEdges.count() * 2) val results = mutableSetOf() queue.addAll(initialEdges) while (queue.isNotEmpty()) { @@ -34,7 +34,7 @@ public inline fun transitiveClosure(seed: T, edges: T.() -> Iterable @OptIn(ExperimentalStdlibApi::class) @PublishedApi -internal inline fun deque(): MutableList { - return if (KotlinVersion.CURRENT.isAtLeast(1, 4)) ArrayDeque() - else mutableListOf() +internal inline fun deque(initialSize: Int): MutableList { + return if (KotlinVersion.CURRENT.isAtLeast(1, 4)) ArrayDeque(initialSize) + else ArrayList(initialSize) }