[Gradle] Implement ideaImportDependsOn API for KTIJ-20989

This will rely on a task called `prepareKotlinIdeaImport` which,
if present, will be invoked by the IDE before import.


Co-authored-by: Sergey Igushkin <Sergey.Igushkin@jetbrains.com>

Merge-request: KT-MR-5688
Merged-by: Sebastian Sellmair <Sebastian.sellmair@jetbrains.com>
This commit is contained in:
sebastian.sellmair
2022-02-09 13:43:29 +00:00
committed by Space
parent 003cb156bf
commit 0a85f0dd34
12 changed files with 289 additions and 19 deletions
@@ -0,0 +1,68 @@
/*
* 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.gradle.api.internal.project.ProjectInternal
import org.gradle.testfixtures.ProjectBuilder
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeCompilation
import kotlin.test.Test
class CommonizerTaskTests {
private val rootProject = ProjectBuilder.builder().build() as ProjectInternal
private val subproject = ProjectBuilder.builder().withName("subproject").withParent(rootProject).build() as ProjectInternal
private val kotlin = subproject.applyMultiplatformPlugin().apply {
linuxX64()
linuxArm64()
targets.flatMap { it.compilations }
.filterIsInstance<KotlinNativeCompilation>()
.forEach { compilation -> compilation.cinterops.create("dummy") }
}
init {
rootProject.enableCInteropCommonization()
subproject.enableCInteropCommonization()
rootProject.evaluate()
subproject.evaluate()
}
@Test
fun `test runCommonizer task`() {
subproject.tasks.getByName("runCommonizer")
.assertDependsOn(subproject.tasks.getByName("commonize"))
/*
Since commonizing the native distribution is done on the root project,
we can also expect that the umbrella tasks are present there as well!
*/
rootProject.tasks.getByName("runCommonizer")
.assertDependsOn(rootProject.tasks.getByName("commonize"))
}
@Test
fun `test commonizeNativeDistributionTask`() {
val commonizeNativeDistributionTaskName = "commonizeNativeDistribution"
subproject.assertContainsNoTaskWithName(commonizeNativeDistributionTaskName)
/* Native Distribution Commonization is only done on the root project */
val rootProjectCommonizeNativeDistributionTask = rootProject.assertContainsTaskWithName(commonizeNativeDistributionTaskName)
rootProject.tasks.getByName("commonize").assertDependsOn(rootProjectCommonizeNativeDistributionTask)
subproject.tasks.getByName("commonize").assertDependsOn(rootProjectCommonizeNativeDistributionTask)
}
@Test
fun `test commonizeCInteropTask`() {
val commonizeCInteropTaskName = "commonizeCInterop"
val commonizeCInteropTask = subproject.assertContainsTaskWithName(commonizeCInteropTaskName)
subproject.tasks.getByName("commonize").assertDependsOn(commonizeCInteropTask)
rootProject.assertContainsNoTaskWithName(commonizeCInteropTaskName)
}
}
@@ -0,0 +1,47 @@
/*
* 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.Project
import org.gradle.api.Task
import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion
import kotlin.test.assertEquals
import kotlin.test.fail
fun Task.isDependsOn(other: Task): Boolean {
return other in this.taskDependencies.getDependencies(null)
}
fun Task.assertDependsOn(other: Task) {
if (!isDependsOn(other)) {
fail("Expected ${this.path} to depend on ${other.path}")
}
}
fun Task.assertNotDependsOn(other: Task) {
if (isDependsOn(other)) {
fail("Expected ${this.path} to *not* depend on ${other.path}")
}
}
fun Task.assertTaskDependenciesEquals(dependencies: Set<Task>) {
assertEquals(
dependencies, this.taskDependencies.getDependencies(null),
"Expected given set of taskDependencies for task ${this.path}"
)
}
fun Project.assertContainsTaskWithName(taskName: String): Task {
this.getKotlinPluginVersion()
return project.tasks.findByName(taskName)
?: fail("Expected task with name $taskName in project ${this.path}")
}
fun Project.assertContainsNoTaskWithName(taskName: String) {
if (taskName in tasks.names) {
fail("Expected *no* task with name $taskName in project ${this.path}")
}
}
@@ -0,0 +1,80 @@
/*
* 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")
@file:OptIn(Idea222Api::class)
package org.jetbrains.kotlin.gradle.ide
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.testfixtures.ProjectBuilder
import org.jetbrains.kotlin.gradle.applyMultiplatformPlugin
import org.jetbrains.kotlin.gradle.assertDependsOn
import org.jetbrains.kotlin.gradle.assertTaskDependenciesEquals
import org.jetbrains.kotlin.gradle.enableCInteropCommonization
import org.jetbrains.kotlin.gradle.plugin.ide.Idea222Api
import org.jetbrains.kotlin.gradle.plugin.ide.ideaImportDependsOn
import org.jetbrains.kotlin.gradle.plugin.ide.prepareKotlinIdeaImportTask
import org.jetbrains.kotlin.gradle.utils.UnsafeApi
import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
@OptIn(UnsafeApi::class)
class PrepareKotlinIdeaImportTaskTest {
private companion object {
const val prepareKotlinIdeaImportTaskName = "prepareKotlinIdeaImport"
}
@Test
fun `test prepareKotlinIdeaImport task depending on commonizer and cinterop tasks`() {
val rootProject = ProjectBuilder.builder().build() as ProjectInternal
val subproject = ProjectBuilder.builder().withParent(rootProject).withName("subproject").build() as ProjectInternal
rootProject.enableCInteropCommonization()
subproject.enableCInteropCommonization()
val kotlin = subproject.applyMultiplatformPlugin()
kotlin.linuxX64()
kotlin.linuxArm64()
rootProject.evaluate()
subproject.evaluate()
assertTrue(
prepareKotlinIdeaImportTaskName in subproject.tasks.names,
"Expected a task named '$prepareKotlinIdeaImportTaskName' to be registered"
)
subproject.prepareKotlinIdeaImportTask.get().assertDependsOn(subproject.tasks.getByName("commonize"))
subproject.prepareKotlinIdeaImportTask.get().assertDependsOn(subproject.tasks.getByName("copyCommonizeCInteropForIde"))
}
@Test
fun `test declaring dependsOnIdeaImport`() {
val project = ProjectBuilder.builder().build()
assertNull(
project.tasks.findByName(prepareKotlinIdeaImportTaskName),
"Expected task $prepareKotlinIdeaImportTaskName to not be registered when no task declares a dependency"
)
val testTaskA = project.tasks.register("testTaskA")
project.ideaImportDependsOn(testTaskA)
val prepareKotlinIdeaImportTask = assertNotNull(
project.tasks.findByName(prepareKotlinIdeaImportTaskName),
"Expected task $prepareKotlinIdeaImportTaskName to be registered after $testTaskA declared dependency"
)
prepareKotlinIdeaImportTask.assertTaskDependenciesEquals(setOf(testTaskA.get()))
val testTaskB = project.tasks.register("testTaskB")
project.ideaImportDependsOn(testTaskB)
prepareKotlinIdeaImportTask.assertTaskDependenciesEquals(setOf(testTaskA.get(), testTaskB.get()))
}
}
@@ -0,0 +1,9 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.plugin.ide
@RequiresOptIn("This API is only supported by IDEA 2022.2 or higher", level = RequiresOptIn.Level.ERROR)
internal annotation class Idea222Api
@@ -0,0 +1,28 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.plugin.ide
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.tasks.TaskProvider
import org.jetbrains.kotlin.gradle.tasks.dependsOn
import org.jetbrains.kotlin.gradle.tasks.locateOrRegisterTask
import org.jetbrains.kotlin.gradle.utils.UnsafeApi
@Idea222Api
internal fun Project.ideaImportDependsOn(task: TaskProvider<*>) {
@OptIn(UnsafeApi::class)
prepareKotlinIdeaImportTask.dependsOn(task)
}
@UnsafeApi("Use 'ideaImportDependsOn' instead")
internal val Project.prepareKotlinIdeaImportTask: TaskProvider<Task>
get() = locateOrRegisterTask(
"prepareKotlinIdeaImport",
configureTask = {
description = "Umbrella for all tasks required to run before IDEA/Gradle import"
}
)
@@ -56,4 +56,4 @@ import org.jetbrains.kotlin.gradle.plugin.getTestedVariantData
val compileJavaTaskProvider: TaskProvider<out JavaCompile>?
get() = androidVariant.getJavaTaskProvider()
}
}
@@ -84,7 +84,7 @@ abstract class AbstractKotlinNativeTargetPreset<T : KotlinNativeTarget>(
createTargetConfigurator().configureTarget(result)
SingleActionPerProject.run(project, "setUpKotlinNativePlatformDependencies") {
project.gradle.projectsEvaluated {
project.whenEvaluated {
project.setupKotlinNativePlatformDependencies()
}
}
@@ -18,7 +18,10 @@ import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.addExtension
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation.*
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation.Git
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation.Url
import org.jetbrains.kotlin.gradle.plugin.ide.Idea222Api
import org.jetbrains.kotlin.gradle.plugin.ide.ideaImportDependsOn
import org.jetbrains.kotlin.gradle.plugin.mpp.Framework
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType
@@ -541,7 +544,8 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
kotlinExtension: KotlinMultiplatformExtension
) {
val podInstallTaskProvider = project.tasks.named(POD_INSTALL_TASK_NAME, PodInstallTask::class.java)
project.tasks.register(POD_IMPORT_TASK_NAME) {
val podImportTaskProvider = project.tasks.register(POD_IMPORT_TASK_NAME) {
it.group = TASK_GROUP
it.description = "Called on Gradle sync, depends on Cinterop tasks for every used pod"
it.dependsOn(podInstallTaskProvider)
@@ -553,6 +557,10 @@ open class KotlinCocoapodsPlugin : Plugin<Project> {
}
}
}
/* Older IDEs will explicitly call 'podImport' instead */
@OptIn(Idea222Api::class)
project.ideaImportDependsOn(podImportTaskProvider)
}
private fun configureTestBinaries(project: Project, cocoapodsExtension: CocoapodsExtension) {
@@ -13,6 +13,8 @@ import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.*
import org.jetbrains.kotlin.commonizer.SharedCommonizerTarget
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.Companion.kotlinPropertiesProvider
import org.jetbrains.kotlin.gradle.plugin.ide.Idea222Api
import org.jetbrains.kotlin.gradle.plugin.ide.ideaImportDependsOn
import org.jetbrains.kotlin.gradle.plugin.mpp.GranularMetadataTransformation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinProjectStructureMetadata
import org.jetbrains.kotlin.gradle.plugin.mpp.MetadataDependencyResolution.ChooseVisibleSourceSets
@@ -57,7 +59,14 @@ internal fun Project.locateOrRegisterCInteropMetadataDependencyTransformationTas
return locateOrRegisterTask(
lowerCamelCaseName("transform", sourceSet.name, "CInteropDependenciesMetadataForIde"),
invokeWhenRegistered = { commonizeTask.dependsOn(this) },
invokeWhenRegistered = {
@OptIn(Idea222Api::class)
ideaImportDependsOn(this)
/* Older IDEs will still enqueue 'runCommonizer' task before import */
@Suppress("deprecation")
runCommonizerTask.dependsOn(this)
},
args = listOf(
sourceSet,
/* outputDirectory = */
@@ -11,6 +11,8 @@ 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.ide.Idea222Api
import org.jetbrains.kotlin.gradle.plugin.ide.ideaImportDependsOn
import org.jetbrains.kotlin.gradle.plugin.whenEvaluated
import org.jetbrains.kotlin.gradle.tasks.dependsOn
import org.jetbrains.kotlin.gradle.tasks.locateOrRegisterTask
@@ -26,7 +28,14 @@ internal val Project.isOptimisticNumberCommonizationEnabled: Boolean
internal val Project.commonizeTask: TaskProvider<Task>
get() = locateOrRegisterTask(
"commonize",
invokeWhenRegistered = { @Suppress("deprecation") runCommonizerTask.dependsOn(this) },
invokeWhenRegistered = {
@OptIn(Idea222Api::class)
ideaImportDependsOn(this)
/* 'runCommonizer' is called by older IDEs during import */
@Suppress("deprecation")
runCommonizerTask.dependsOn(this)
},
configureTask = {
group = "interop"
description = "Aggregator task for all c-interop & Native distribution commonizer tasks"
@@ -40,7 +49,6 @@ internal val Project.commonizeTask: TaskProvider<Task>
internal val Project.runCommonizerTask: TaskProvider<Task>
get() = locateOrRegisterTask(
"runCommonizer",
invokeWhenRegistered = { dependsOn(commonizeTask) },
configureTask = {
group = "interop"
description = "[Deprecated: Use 'commonize' instead]"
@@ -74,7 +82,15 @@ internal val Project.copyCommonizeCInteropForIdeTask: TaskProvider<CopyCommonize
if (commonizeCInteropTask != null) {
return locateOrRegisterTask(
"copyCommonizeCInteropForIde",
invokeWhenRegistered = { if (isInIdeaSync) commonizeTask.dependsOn(this) },
invokeWhenRegistered = {
@OptIn(Idea222Api::class)
ideaImportDependsOn(this)
/* Older IDEs will still call 'runCommonizer' -> 'commonize' tasks */
if (isInIdeaSync) {
commonizeTask.dependsOn(this)
}
},
configureTask = {
group = "interop"
description = "Copies the output of ${commonizeCInteropTask.get().name} into " +
@@ -90,7 +106,11 @@ internal val Project.commonizeNativeDistributionTask: TaskProvider<NativeDistrib
if (!isAllowCommonizer()) return null
return rootProject.locateOrRegisterTask(
"commonizeNativeDistribution",
invokeWhenRegistered = { rootProject.commonizeTask.dependsOn(this); cleanNativeDistributionCommonizerTask },
invokeWhenRegistered = {
commonizeTask.dependsOn(this)
rootProject.commonizeTask.dependsOn(this)
cleanNativeDistributionCommonizerTask
},
configureTask = {
group = "interop"
description = "Invokes the commonizer on platform libraries provided by the Kotlin/Native distribution"
@@ -12,6 +12,7 @@ import org.jetbrains.kotlin.compilerRunner.konanHome
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.ide.ideaImportDependsOn
import org.jetbrains.kotlin.gradle.plugin.sources.DefaultKotlinSourceSet
import org.jetbrains.kotlin.gradle.targets.metadata.getMetadataCompilationForSourceSet
import org.jetbrains.kotlin.gradle.targets.metadata.isKotlinGranularMetadataEnabled
@@ -87,20 +88,18 @@ private fun File.listLibraryFiles(): List<File> = listFiles().orEmpty()
internal val Project.isNativeDependencyPropagationEnabled: Boolean
get() = PropertiesProvider(this).nativeDependencyPropagation ?: true
//for reflection call from KotlinCommonizerModelBuilder
// DO NOT REFACTOR THIS FUNCTION!
// TODO SELLMAIR: Resolve fragile reflection call from IDE plugin
@JvmOverloads
/**
* Function signature needs to be kept stable since this is used during import
* in IDEs (KotlinCommonizerModelBuilder) < 222
*
* IDEs >= will use the [ideaImportDependsOn] infrastructure
*/
@JvmName("isAllowCommonizer")
internal fun Project.isAllowCommonizer(
kotlinVersion: String = getKotlinPluginVersion()
): Boolean {
internal fun Project.isAllowCommonizer(): 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
return compareVersionNumbers(kotlinVersion, "1.4") >= 0
&& multiplatformExtension.targets.any { it.platformType == KotlinPlatformType.native }
return multiplatformExtension.targets.any { it.platformType == KotlinPlatformType.native }
&& isKotlinGranularMetadataEnabled
&& !isNativeDependencyPropagationEnabled // temporary fix: turn on commonizer only when native deps propagation is disabled
}
@@ -58,6 +58,8 @@ internal fun <T : Task> Project.registerTask(
internal fun TaskProvider<*>.dependsOn(other: TaskProvider<*>) = configure { it.dependsOn(other) }
internal fun TaskProvider<*>.dependsOn(other: Task) = configure { it.dependsOn(other) }
internal inline fun <reified S : Task> TaskCollection<in S>.withType(): TaskCollection<S> = withType(S::class.java)
/**