diff --git a/idea/idea-gradle-native/src/org/jetbrains/kotlin/ide/konan/KotlinNativeABICompatibilityChecker.kt b/idea/idea-gradle-native/src/org/jetbrains/kotlin/ide/konan/KotlinNativeABICompatibilityChecker.kt index aa38d7f29ce..321f0d454a7 100644 --- a/idea/idea-gradle-native/src/org/jetbrains/kotlin/ide/konan/KotlinNativeABICompatibilityChecker.kt +++ b/idea/idea-gradle-native/src/org/jetbrains/kotlin/ide/konan/KotlinNativeABICompatibilityChecker.kt @@ -9,20 +9,17 @@ import com.intellij.ProjectTopics import com.intellij.notification.Notification import com.intellij.notification.NotificationType import com.intellij.openapi.Disposable -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.runInEdt -import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.application.* +import com.intellij.openapi.application.ReadAction.nonBlocking import com.intellij.openapi.components.ProjectComponent -import com.intellij.openapi.progress.EmptyProgressIndicator -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.roots.ModuleRootEvent import com.intellij.openapi.roots.ModuleRootListener import com.intellij.openapi.util.text.StringUtilRt import com.intellij.util.PathUtilRt +import com.intellij.util.concurrency.AppExecutorUtil +import org.jetbrains.concurrency.CancellablePromise import org.jetbrains.kotlin.idea.caches.project.getModuleInfosFromIdeaModel import org.jetbrains.kotlin.idea.configuration.KotlinNativeLibraryNameUtil import org.jetbrains.kotlin.idea.versions.UnsupportedAbiVersionNotificationPanelProvider @@ -47,6 +44,9 @@ class KotlinNativeABICompatibilityChecker(private val project: Project) : Projec private val cachedIncompatibleLibraries = HashSet() + @Volatile + private var backgroundJob: CancellablePromise<*>? = null + init { project.messageBus.connect().subscribe(ProjectTopics.PROJECT_ROOTS, object : ModuleRootListener { override fun rootsChanged(event: ModuleRootEvent) { @@ -65,29 +65,25 @@ class KotlinNativeABICompatibilityChecker(private val project: Project) : Projec if (ApplicationManager.getApplication().isUnitTestMode || project.isDisposed) return - ProgressManager.getInstance().runProcessWithProgressAsynchronously( - object : Task.Backgroundable(project, BG_TASK_NAME) { - override fun run(indicator: ProgressIndicator) { - val librariesToNotify = runReadAction { - if (project.isDisposed) return@runReadAction emptyMap() - - getLibrariesToNotifyAbout() - } - if (project.isDisposed) return - val notifications = prepareNotifications(librariesToNotify) - - notifications.forEach { - runInEdt { - it.notify(project) - } - } + backgroundJob = nonBlocking> { + val librariesToNotify = getLibrariesToNotifyAbout() + prepareNotifications(librariesToNotify) + } + .finishOnUiThread(ModalityState.defaultModalityState()) { notifications -> + notifications.forEach { + it.notify(project) } - }, - EmptyProgressIndicator() - ) + } + .expireWith(project) // cancel job when project is disposed + .coalesceBy(this@KotlinNativeABICompatibilityChecker) // cancel previous job when new one is submitted + .withDocumentsCommitted(project) + .submit(AppExecutorUtil.getAppExecutorService()) + .onProcessed { + backgroundJob = null + } } - private fun getLibrariesToNotifyAbout(): Map = synchronized(this) { + private fun getLibrariesToNotifyAbout(): Map { val incompatibleLibraries = getModuleInfosFromIdeaModel(project).asSequence() .filterIsInstance() .filter { !it.metadataInfo.isCompatible } @@ -219,7 +215,10 @@ class KotlinNativeABICompatibilityChecker(private val project: Project) : Projec return (ideLibraryName ?: PathUtilRt.getFileName(libraryInfo.libraryRoot)) to LibraryGroup.User } - override fun dispose() = synchronized(this) { + override fun dispose() { + backgroundJob?.let(CancellablePromise<*>::cancel) + backgroundJob = null + cachedIncompatibleLibraries.clear() } @@ -237,7 +236,5 @@ class KotlinNativeABICompatibilityChecker(private val project: Project) : Projec private const val NOTIFICATION_TITLE = "Incompatible Kotlin/Native libraries" private const val NOTIFICATION_GROUP_ID = NOTIFICATION_TITLE - - private const val BG_TASK_NAME = "Finding incompatible Kotlin/Native libraries" } } diff --git a/idea/idea-gradle-native/src/org/jetbrains/kotlin/ide/konan/KotlinNativeABICompatibilityChecker.kt.192 b/idea/idea-gradle-native/src/org/jetbrains/kotlin/ide/konan/KotlinNativeABICompatibilityChecker.kt.192 new file mode 100644 index 00000000000..00124f6a205 --- /dev/null +++ b/idea/idea-gradle-native/src/org/jetbrains/kotlin/ide/konan/KotlinNativeABICompatibilityChecker.kt.192 @@ -0,0 +1,241 @@ +/* + * Copyright 2010-2019 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.ide.konan + +import com.intellij.ProjectTopics +import com.intellij.notification.Notification +import com.intellij.notification.NotificationType +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.* +import com.intellij.openapi.application.ReadAction.nonBlocking +import com.intellij.openapi.components.ProjectComponent +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.guessProjectDir +import com.intellij.openapi.roots.ModuleRootEvent +import com.intellij.openapi.roots.ModuleRootListener +import com.intellij.openapi.util.text.StringUtilRt +import com.intellij.util.PathUtilRt +import com.intellij.util.concurrency.AppExecutorUtil +import org.jetbrains.concurrency.CancellablePromise +import org.jetbrains.kotlin.idea.caches.project.getModuleInfosFromIdeaModel +import org.jetbrains.kotlin.idea.configuration.KotlinNativeLibraryNameUtil +import org.jetbrains.kotlin.idea.versions.UnsupportedAbiVersionNotificationPanelProvider +import org.jetbrains.kotlin.idea.versions.bundledRuntimeVersion +import org.jetbrains.kotlin.konan.library.KONAN_STDLIB_NAME + +/** TODO: merge [KotlinNativeABICompatibilityChecker] in the future with [UnsupportedAbiVersionNotificationPanelProvider], KT-34525 */ +class KotlinNativeABICompatibilityChecker(private val project: Project) : ProjectComponent, Disposable { + + private sealed class LibraryGroup(private val ordinal: Int) : Comparable { + + override fun compareTo(other: LibraryGroup) = when { + this == other -> 0 + this is FromDistribution && other is FromDistribution -> kotlinVersion.compareTo(other.kotlinVersion) + else -> ordinal.compareTo(other.ordinal) + } + + data class FromDistribution(val kotlinVersion: String) : LibraryGroup(0) + object ThirdParty : LibraryGroup(1) + object User : LibraryGroup(2) + } + + private val cachedIncompatibleLibraries = HashSet() + + @Volatile + private var backgroundJob: CancellablePromise<*>? = null + + init { + project.messageBus.connect().subscribe(ProjectTopics.PROJECT_ROOTS, object : ModuleRootListener { + override fun rootsChanged(event: ModuleRootEvent) { + // run when project roots are changes, e.g. on project import + validateKotlinNativeLibraries() + } + }) + } + + override fun projectOpened() { + // run when project is opened + validateKotlinNativeLibraries() + } + + private fun validateKotlinNativeLibraries() { + if (ApplicationManager.getApplication().isUnitTestMode || project.isDisposed) + return + + backgroundJob = nonBlocking> { + val librariesToNotify = getLibrariesToNotifyAbout() + prepareNotifications(librariesToNotify) + } + .finishOnUiThread(ModalityState.defaultModalityState()) { notifications -> + notifications.forEach { + it.notify(project) + } + } + .expireWith(project) // cancel job when project is disposed + .withDocumentsCommitted(project) + .submit(AppExecutorUtil.getAppExecutorService()) + .also { + it.onProcessed { + backgroundJob = null + } + } + } + + private fun getLibrariesToNotifyAbout(): Map { + val incompatibleLibraries = getModuleInfosFromIdeaModel(project).asSequence() + .filterIsInstance() + .filter { !it.metadataInfo.isCompatible } + .associateBy { it.libraryRoot } + + val newEntries = if (cachedIncompatibleLibraries.isNotEmpty()) + incompatibleLibraries.filterKeys { it !in cachedIncompatibleLibraries } + else + incompatibleLibraries + + cachedIncompatibleLibraries.clear() + cachedIncompatibleLibraries.addAll(incompatibleLibraries.keys) + + return newEntries + } + + private fun prepareNotifications(librariesToNotify: Map): List { + if (librariesToNotify.isEmpty()) + return emptyList() + + val librariesByGroups = HashMap, MutableList>>() + librariesToNotify.forEach { (libraryRoot, libraryInfo) -> + val isOldMetadata = (libraryInfo.metadataInfo as? NativeLibraryInfo.MetadataInfo.Incompatible)?.isOlder ?: true + val (libraryName, libraryGroup) = parseIDELibraryName(libraryInfo) + librariesByGroups.computeIfAbsent(libraryGroup to isOldMetadata) { mutableListOf() } += libraryName to libraryRoot + } + + return librariesByGroups.keys.sortedWith( + compareBy( + { (libraryGroup, _) -> libraryGroup }, + { (_, isOldMetadata) -> isOldMetadata } + ) + ).map { key -> + + val (libraryGroup, isOldMetadata) = key + val libraries = + librariesByGroups.getValue(key).sortedWith(compareBy(LIBRARY_NAME_COMPARATOR) { (libraryName, _) -> libraryName }) + + val compilerVersionText = if (isOldMetadata) "an older" else "a newer" + + val message = when (libraryGroup) { + is LibraryGroup.FromDistribution -> { + val libraryNamesInOneLine = + libraries.joinToString(limit = MAX_LIBRARY_NAMES_IN_ONE_LINE) { (libraryName, _) -> libraryName } + + """ + |There are ${libraries.size} libraries from the Kotlin/Native ${libraryGroup.kotlinVersion} distribution attached to the project: $libraryNamesInOneLine + | + |These libraries were compiled with $compilerVersionText Kotlin/Native compiler and can't be read in IDE. Please edit Gradle buildfile(s) to use Kotlin Gradle plugin version ${bundledRuntimeVersion()}. Then re-import the project in IDE. + """.trimMargin() + } + is LibraryGroup.ThirdParty -> { + if (libraries.size == 1) { + """ + |There is a third-party library attached to the project that was compiled with $compilerVersionText Kotlin/Native compiler and can't be read in IDE: ${libraries.single() + .first} + | + |Please edit Gradle buildfile(s) and specify library version compatible with Kotlin/Native ${bundledRuntimeVersion()}. Then re-import the project in IDE. + """.trimMargin() + } else { + val librariesLineByLine = libraries.joinToString(separator = "\n") { (libraryName, _) -> libraryName } + + """ + |There are ${libraries.size} third-party libraries attached to the project that were compiled with $compilerVersionText Kotlin/Native compiler and can't be read in IDE: + |$librariesLineByLine + | + |Please edit Gradle buildfile(s) and specify library versions compatible with Kotlin/Native ${bundledRuntimeVersion()}. Then re-import the project in IDE. + """.trimMargin() + } + } + is LibraryGroup.User -> { + val projectRoot = project.guessProjectDir()?.canonicalPath + + fun getLibraryTextToPrint(libraryNameAndRoot: Pair): String { + val (libraryName, libraryRoot) = libraryNameAndRoot + + val relativeRoot = projectRoot?.let { + libraryRoot.substringAfter(projectRoot) + .takeIf { it != libraryRoot } + ?.trimStart('/', '\\') + ?.let { "${'$'}project/$it" } + } ?: libraryRoot + + return "\"$libraryName\" at $relativeRoot" + } + + if (libraries.size == 1) { + """ + |There is a library attached to the project that was compiled with $compilerVersionText Kotlin/Native compiler and can't be read in IDE: + |${getLibraryTextToPrint(libraries.single())} + | + |Please edit Gradle buildfile(s) to use Kotlin Gradle plugin version ${bundledRuntimeVersion()}. Then rebuild the project and re-import it in IDE. + """.trimMargin() + } else { + val librariesLineByLine = libraries.joinToString(separator = "\n", transform = ::getLibraryTextToPrint) + + """ + |There are ${libraries.size} libraries attached to the project that were compiled with $compilerVersionText Kotlin/Native compiler and can't be read in IDE: + |$librariesLineByLine + | + |Please edit Gradle buildfile(s) to use Kotlin Gradle plugin version ${bundledRuntimeVersion()}. Then rebuild the project and re-import it in IDE. + """.trimMargin() + } + } + } + + Notification( + NOTIFICATION_GROUP_ID, + NOTIFICATION_TITLE, + StringUtilRt.convertLineSeparators(message, "
"), + NotificationType.ERROR, + null + ) + } + } + + // returns pair of library name and library group + private fun parseIDELibraryName(libraryInfo: NativeLibraryInfo): Pair { + val ideLibraryName = libraryInfo.library.name?.takeIf(String::isNotEmpty) + if (ideLibraryName != null) { + KotlinNativeLibraryNameUtil.parseIDELibraryName(ideLibraryName)?.let { (kotlinVersion, libraryName) -> + return libraryName to LibraryGroup.FromDistribution(kotlinVersion) + } + + if (KotlinNativeLibraryNameUtil.isGradleLibraryName(ideLibraryName)) + return ideLibraryName to LibraryGroup.ThirdParty + } + + return (ideLibraryName ?: PathUtilRt.getFileName(libraryInfo.libraryRoot)) to LibraryGroup.User + } + + override fun dispose() { + backgroundJob?.let(CancellablePromise<*>::cancel) + backgroundJob = null + + cachedIncompatibleLibraries.clear() + } + + companion object { + private val LIBRARY_NAME_COMPARATOR = Comparator { libraryName1, libraryName2 -> + when { + libraryName1 == libraryName2 -> 0 + libraryName1 == KONAN_STDLIB_NAME -> -1 // stdlib must go the first + libraryName2 == KONAN_STDLIB_NAME -> 1 + else -> libraryName1.compareTo(libraryName2) + } + } + + private const val MAX_LIBRARY_NAMES_IN_ONE_LINE = 5 + + private const val NOTIFICATION_TITLE = "Incompatible Kotlin/Native libraries" + private const val NOTIFICATION_GROUP_ID = NOTIFICATION_TITLE + } +}