From 905823fb1d6fdc276f6bd9df0551d5085e4192a1 Mon Sep 17 00:00:00 2001 From: Dmitriy Dolovov Date: Fri, 18 Oct 2019 19:32:55 +0700 Subject: [PATCH] Fix: Notify user in IDE on K/N KLIBs with incompatible ABI version Issue #KT-34159 --- .../KotlinNativeABICompatibilityChecker.kt | 212 ++++++++++++++++++ .../idea/configuration/nativeLibrariesUtil.kt | 28 ++- .../KotlinNativeLibraryNameUtilTest.kt | 54 +++++ ...rtedAbiVersionNotificationPanelProvider.kt | 1 + .../ide/konan/NativePlatformKindResolution.kt | 26 +-- idea/resources/META-INF/native.xml | 5 + 6 files changed, 310 insertions(+), 16 deletions(-) create mode 100644 idea/idea-gradle-native/src/org/jetbrains/kotlin/ide/konan/KotlinNativeABICompatibilityChecker.kt create mode 100644 idea/idea-gradle/tests/org/jetbrains/kotlin/idea/configuration/KotlinNativeLibraryNameUtilTest.kt 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 new file mode 100644 index 00000000000..78f579b26d4 --- /dev/null +++ b/idea/idea-gradle-native/src/org/jetbrains/kotlin/ide/konan/KotlinNativeABICompatibilityChecker.kt @@ -0,0 +1,212 @@ +/* + * 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.* +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +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 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 +import org.jetbrains.kotlin.library.KotlinAbiVersion + +/** 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() + + 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 + + val librariesToNotify = getLibrariesToNotifyAbout() + if (librariesToNotify.isEmpty()) + return + + val librariesByGroups = HashMap, MutableList>>() + librariesToNotify.forEach { (libraryRoot, libraryInfo) -> + val isOldAbi = (libraryInfo.safeAbiVersion?.version ?: 0) < KotlinAbiVersion.CURRENT.version + val (libraryName, libraryGroup) = parseIDELibraryName(libraryInfo) + librariesByGroups.computeIfAbsent(libraryGroup to isOldAbi) { mutableListOf() } += libraryName to libraryRoot + } + + librariesByGroups.keys.sortedWith( + compareBy( + { (libraryGroup, _) -> libraryGroup }, + { (_, isOldAbi) -> isOldAbi } + ) + ).forEach { key -> + + val (libraryGroup, isOldAbi) = key + val libraries = + librariesByGroups.getValue(key).sortedWith(compareBy(LIBRARY_NAME_COMPARATOR) { (libraryName, _) -> libraryName }) + + val compilerVersionText = if (isOldAbi) "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 + ).notify(project) + } + } + + private fun getLibrariesToNotifyAbout(): Map = synchronized(this) { + val incompatibleLibraries = getModuleInfosFromIdeaModel(project).asSequence() + .filterIsInstance() + .filter { it.safeAbiVersion != KotlinAbiVersion.CURRENT } + .associateBy { it.libraryRoot } + + val newEntries = if (cachedIncompatibleLibraries.isNotEmpty()) + incompatibleLibraries.filterKeys { it !in cachedIncompatibleLibraries } + else + incompatibleLibraries + + cachedIncompatibleLibraries.clear() + cachedIncompatibleLibraries.addAll(incompatibleLibraries.keys) + + return newEntries + } + + // 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() = synchronized(this) { + 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 + } +} diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/nativeLibrariesUtil.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/nativeLibrariesUtil.kt index 518f17a80c6..b1c21d38c98 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/nativeLibrariesUtil.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/nativeLibrariesUtil.kt @@ -24,6 +24,7 @@ import org.gradle.tooling.model.idea.IdeaModule import org.jetbrains.kotlin.gradle.KotlinMPPGradleModel import org.jetbrains.kotlin.idea.configuration.DependencySubstitute.NoSubstitute import org.jetbrains.kotlin.idea.configuration.DependencySubstitute.YesSubstitute +import org.jetbrains.kotlin.idea.configuration.KotlinNativeLibraryNameUtil.buildIDELibraryName import org.jetbrains.kotlin.idea.inspections.gradle.findKotlinPluginVersion import org.jetbrains.kotlin.idea.versions.bundledRuntimeVersion import org.jetbrains.kotlin.konan.library.KONAN_STDLIB_NAME @@ -155,8 +156,7 @@ internal class KotlinNativeLibrariesDependencySubstitutor( val library = libraryProvider.getLibrary(libraryFile) ?: return NoSubstitute val nonNullKotlinVersion = kotlinVersion ?: return NoSubstitute - val platformNamePart = library.platform?.let { " [$it]" }.orEmpty() - val newLibraryName = "$KOTLIN_NATIVE_LIBRARY_PREFIX_PLUS_SPACE$nonNullKotlinVersion - ${library.name}$platformNamePart" + val newLibraryName = buildIDELibraryName(nonNullKotlinVersion, library.name, library.platform) val substitute = DefaultExternalMultiLibraryDependency().apply { classpathOrder = if (library.name == KONAN_STDLIB_NAME) -1 else 0 // keep stdlib upper @@ -240,6 +240,30 @@ private sealed class DependencySubstitute { class YesSubstitute(val substitute: ExternalMultiLibraryDependency) : DependencySubstitute() } +object KotlinNativeLibraryNameUtil { + private val IDE_LIBRARY_NAME_REGEX = Regex("^$KOTLIN_NATIVE_LIBRARY_PREFIX_PLUS_SPACE([^\\s]+) - ([^\\s]+)( \\[([\\w]+)\\])?$") + + // Builds the name of Kotlin/Native library that is a part of Kotlin/Native distribution + // as it will be displayed in IDE UI. + fun buildIDELibraryName(kotlinVersion: String, libraryName: String, platform: String?): String { + val platformNamePart = platform?.let { " [$it]" }.orEmpty() + return "$KOTLIN_NATIVE_LIBRARY_PREFIX_PLUS_SPACE$kotlinVersion - $libraryName$platformNamePart" + } + + // N.B. Returns null if this is not IDE name of Kotlin/Native library. + fun parseIDELibraryName(ideLibraryName: String): Triple? { + val match = IDE_LIBRARY_NAME_REGEX.matchEntire(ideLibraryName) ?: return null + + val kotlinVersion = match.groups[1]!!.value + val libraryName = match.groups[2]!!.value + val platform = match.groups[4]?.value + + return Triple(kotlinVersion, libraryName, platform) + } + + fun isGradleLibraryName(ideLibraryName: String) = ideLibraryName.startsWith(GRADLE_LIBRARY_PREFIX) +} + internal const val KOTLIN_NATIVE_LIBRARY_PREFIX = "Kotlin/Native" private const val KOTLIN_NATIVE_LIBRARY_PREFIX_PLUS_SPACE = "$KOTLIN_NATIVE_LIBRARY_PREFIX " private const val KOTLIN_NATIVE_LEGACY_GROUP_ID = KOTLIN_NATIVE_LIBRARY_PREFIX diff --git a/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/configuration/KotlinNativeLibraryNameUtilTest.kt b/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/configuration/KotlinNativeLibraryNameUtilTest.kt new file mode 100644 index 00000000000..a64f086fcda --- /dev/null +++ b/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/configuration/KotlinNativeLibraryNameUtilTest.kt @@ -0,0 +1,54 @@ +/* + * 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.idea.configuration + +import junit.framework.TestCase +import org.jetbrains.kotlin.idea.configuration.KotlinNativeLibraryNameUtil.buildIDELibraryName +import org.jetbrains.kotlin.idea.configuration.KotlinNativeLibraryNameUtil.isGradleLibraryName +import org.jetbrains.kotlin.idea.configuration.KotlinNativeLibraryNameUtil.parseIDELibraryName + +class KotlinNativeLibraryNameUtilTest : TestCase() { + + fun testBuildIDELibraryName() { + assertEquals( + "Kotlin/Native 1.3.60 - stdlib", + buildIDELibraryName("1.3.60", "stdlib", null) + ) + + assertEquals( + "Kotlin/Native 1.3.60-eap-23 - Accelerate [macos_x64]", + buildIDELibraryName("1.3.60-eap-23", "Accelerate", "macos_x64") + ) + } + + fun testParseIDELibraryName() { + assertEquals( + Triple("1.3.60", "stdlib", null), + parseIDELibraryName("Kotlin/Native 1.3.60 - stdlib") + ) + + assertEquals( + Triple("1.3.60-eap-23", "Accelerate", "macos_x64"), + parseIDELibraryName("Kotlin/Native 1.3.60-eap-23 - Accelerate [macos_x64]") + ) + + assertNull(parseIDELibraryName("Kotlin/Native - something unexpected")) + + assertNull(parseIDELibraryName("foo.klib")) + + assertNull(parseIDELibraryName("Gradle: some:third-party-library:1.2")) + } + + fun testIsGradleLibraryName() { + assertFalse(isGradleLibraryName("Kotlin/Native 1.3.60 - stdlib")) + + assertFalse(isGradleLibraryName("Kotlin/Native 1.3.60-eap-23 - Accelerate [macos_x64]")) + + assertFalse(isGradleLibraryName("foo.klib")) + + assertTrue(isGradleLibraryName("Gradle: some:third-party-library:1.2")) + } +} diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/versions/UnsupportedAbiVersionNotificationPanelProvider.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/versions/UnsupportedAbiVersionNotificationPanelProvider.kt index fd68039dc34..eed2375f14e 100644 --- a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/versions/UnsupportedAbiVersionNotificationPanelProvider.kt +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/versions/UnsupportedAbiVersionNotificationPanelProvider.kt @@ -324,6 +324,7 @@ class UnsupportedAbiVersionNotificationPanelProvider(private val project: Projec val badRoots = when { platform.isJvm() -> getLibraryRootsWithAbiIncompatibleKotlinClasses(module) platform.isJs() -> getLibraryRootsWithAbiIncompatibleForKotlinJs(module) + // TODO: also check it for Native KT-34525 else -> return emptyList() } diff --git a/idea/idea-native/src/org/jetbrains/kotlin/ide/konan/NativePlatformKindResolution.kt b/idea/idea-native/src/org/jetbrains/kotlin/ide/konan/NativePlatformKindResolution.kt index fe771b0efa1..a1c27751979 100644 --- a/idea/idea-native/src/org/jetbrains/kotlin/ide/konan/NativePlatformKindResolution.kt +++ b/idea/idea-native/src/org/jetbrains/kotlin/ide/konan/NativePlatformKindResolution.kt @@ -27,7 +27,8 @@ import org.jetbrains.kotlin.descriptors.PackageFragmentProvider import org.jetbrains.kotlin.descriptors.impl.CompositePackageFragmentProvider import org.jetbrains.kotlin.descriptors.konan.DeserializedKlibModuleOrigin import org.jetbrains.kotlin.descriptors.konan.KlibModuleOrigin -import org.jetbrains.kotlin.ide.konan.NativeLibraryInfo.Companion.isAbiCompatible +import org.jetbrains.kotlin.ide.konan.NativeLibraryInfo.Companion.safeAbiVersion +import org.jetbrains.kotlin.ide.konan.NativeLibraryInfo.Companion.isCompatible import org.jetbrains.kotlin.ide.konan.analyzer.NativeResolverForModuleFactory import org.jetbrains.kotlin.idea.caches.project.LibraryInfo import org.jetbrains.kotlin.idea.caches.project.lazyClosure @@ -47,7 +48,6 @@ import org.jetbrains.kotlin.resolve.TargetEnvironment import org.jetbrains.kotlin.serialization.konan.impl.KlibMetadataModuleDescriptorFactoryImpl import org.jetbrains.kotlin.storage.StorageManager import java.io.IOException -import kotlin.LazyThreadSafetyMode.PUBLICATION fun KotlinLibrary.createPackageFragmentProvider( storageManager: StorageManager, @@ -55,7 +55,7 @@ fun KotlinLibrary.createPackageFragmentProvider( moduleDescriptor: ModuleDescriptor ): PackageFragmentProvider? { - if (!isAbiCompatible) return null + if (!safeAbiVersion.isCompatible) return null val libraryProto = CachingIdeKonanLibraryMetadataLoader.loadModuleHeader(this) @@ -78,7 +78,7 @@ class NativePlatformKindResolution : IdePlatformKindResolution { return library.getFiles(OrderRootType.CLASSES).mapNotNull { file -> if (!isLibraryFileForPlatform(file)) return@createLibraryInfo emptyList() val path = PathUtil.getLocalPath(file) ?: return@createLibraryInfo emptyList() - NativeLibraryInfo(project, library, File(path)) + NativeLibraryInfo(project, library, path) } } @@ -185,19 +185,16 @@ private fun createKotlinNativeBuiltIns(moduleInfo: ModuleInfo, projectContext: P private fun ModuleInfo.findNativeStdlib(): NativeLibraryInfo? = dependencies().lazyClosure { it.dependencies() } .filterIsInstance() - .firstOrNull { it.isStdlib && it.isAbiCompatible } + .firstOrNull { it.isStdlib && it.safeAbiVersion.isCompatible } -class NativeLibraryInfo(project: Project, library: Library, root: File) : LibraryInfo(project, library) { +class NativeLibraryInfo(project: Project, library: Library, val libraryRoot: String) : LibraryInfo(project, library) { - private val nativeLibrary = createKotlinLibrary(root) + private val nativeLibrary = createKotlinLibrary(File(libraryRoot)) - private val roots = listOf(root.absolutePath) + val isStdlib get() = libraryRoot.endsWith(KONAN_STDLIB_NAME) + val safeAbiVersion get() = nativeLibrary.safeAbiVersion - val isStdlib by lazy(PUBLICATION) { roots.first().endsWith(KONAN_STDLIB_NAME) } - - val isAbiCompatible get() = nativeLibrary.isAbiCompatible - - override fun getLibraryRoots() = roots + override fun getLibraryRoots() = listOf(libraryRoot) override val capabilities: Map, Any?> get() { @@ -216,7 +213,8 @@ class NativeLibraryInfo(project: Project, library: Library, root: File) : Librar companion object { val NATIVE_LIBRARY_CAPABILITY = ModuleDescriptor.Capability("KotlinNativeLibrary") - val KotlinLibrary.isAbiCompatible get() = this.readSafe(null) { versions.abiVersion } == KotlinAbiVersion.CURRENT + internal val KotlinLibrary.safeAbiVersion get() = this.readSafe(null) { versions.abiVersion } + internal val KotlinAbiVersion?.isCompatible get() = this == KotlinAbiVersion.CURRENT private fun KotlinLibrary.readSafe(defaultValue: T, action: KotlinLibrary.() -> T) = try { action() diff --git a/idea/resources/META-INF/native.xml b/idea/resources/META-INF/native.xml index 7745fd5418c..7f4509f092f 100644 --- a/idea/resources/META-INF/native.xml +++ b/idea/resources/META-INF/native.xml @@ -1,4 +1,9 @@ + + + org.jetbrains.kotlin.ide.konan.KotlinNativeABICompatibilityChecker + + org.jetbrains.kotlin.ide.konan.decompiler.KotlinNativeLoadingMetadataCache