diff --git a/aether-dependency-resolver/src/org/jetbrains/idea/maven/aether/ArtifactKind.java b/aether-dependency-resolver/src/org/jetbrains/idea/maven/aether/ArtifactKind.java index 3e51311ff1f2..9e6745856794 100644 --- a/aether-dependency-resolver/src/org/jetbrains/idea/maven/aether/ArtifactKind.java +++ b/aether-dependency-resolver/src/org/jetbrains/idea/maven/aether/ArtifactKind.java @@ -23,7 +23,7 @@ public enum ArtifactKind { ARTIFACT("", "jar"), SOURCES("sources", "jar"), JAVADOC("javadoc", "jar"), ANNOTATIONS("annotations", "zip"), AAR_ARTIFACT("", "aar"), POM("", "pom"), ALL("all", "jar"), HTTP("http", "jar"), DLL("", "dll"), - ZIP("", "zip"); + ZIP("", "zip"), KLIB("", "klib"); private final String myClassifier; private final String myExtension; diff --git a/plugins/kotlin/base/plugin/test/org/jetbrains/kotlin/idea/artifacts/KmpLightFixtureDependencyDownloader.kt b/plugins/kotlin/base/plugin/test/org/jetbrains/kotlin/idea/artifacts/KmpLightFixtureDependencyDownloader.kt new file mode 100644 index 000000000000..6b371b32ad54 --- /dev/null +++ b/plugins/kotlin/base/plugin/test/org/jetbrains/kotlin/idea/artifacts/KmpLightFixtureDependencyDownloader.kt @@ -0,0 +1,160 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + +package org.jetbrains.kotlin.idea.artifacts + +import com.intellij.openapi.util.io.FileUtilRt +import com.intellij.openapi.util.io.JarUtil +import com.intellij.util.SystemProperties +import org.eclipse.aether.repository.RemoteRepository +import org.jetbrains.idea.maven.aether.ArtifactKind +import org.jetbrains.idea.maven.aether.ArtifactRepositoryManager +import org.jetbrains.idea.maven.aether.ProgressConsumer +import org.jetbrains.kotlin.konan.file.unzipTo +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths +import java.util.EnumSet + +/** + * Utility test downloader of KMP library parts for manual module configuration without Gradle import. + * The possible kinds of dependencies are described in [DependencyKind]. + * Use factory methods defined in [KmpAwareLibraryDependency] to specify KMP dependencies for downloading. + */ +object KmpLightFixtureDependencyDownloader { + private const val MAVEN_CENTRAL_CACHE_REDIRECTOR_URL = + "https://cache-redirector.jetbrains.com/repo.maven.apache.org/maven2/" + + /** + * Download or resolve from local cache a part of a KMP library and transform if necessary. + * + * @param kmpDependency description of dependency kind and coordinates. + * @param directoryForTransformedDependencies an optional output directory for the extracted common metadata library part. + * If the directory is not specified, a temporary directory will be used. + * @return a path to a downloaded platform artifact or a downloaded and transformed common metadata artifact; `null` in case of errors. + */ + fun resolveDependency(kmpDependency: KmpAwareLibraryDependency, directoryForTransformedDependencies: Path? = null): Path? { + val coordinates = kmpDependency.coordinates + val kmpDependencyKind = kmpDependency.kind + + val resolvedDependencyArtifact = resolveArtifact(coordinates, kmpDependencyKind.artifactKind) ?: return null + + return when (kmpDependencyKind) { + DependencyKind.PLATFORM_KLIB, DependencyKind.JAR -> resolvedDependencyArtifact + DependencyKind.COMMON_METADATA_JAR, DependencyKind.ALL_METADATA_JAR -> { + check(coordinates.sourceSet != null) { "Unable to resolve a metadata KLIB without a source set, coordinates: $coordinates" } + resolveSourceSetKlib(resolvedDependencyArtifact, coordinates, directoryForTransformedDependencies) + } + } + } + + private fun resolveArtifact(coordinates: KmpCoordinates, artifactKind: ArtifactKind): Path? { + val mavenLocalDir = File(SystemProperties.getUserHome(), ".m2/repository") + + val repositories = listOf( + RemoteRepository.Builder("mavenLocal", "default", "file://" + mavenLocalDir.absolutePath).build(), + RemoteRepository.Builder("mavenCentral", "default", MAVEN_CENTRAL_CACHE_REDIRECTOR_URL).build(), + ) + + val resolvedDependencyArtifact = ArtifactRepositoryManager( + mavenLocalDir, repositories, ProgressConsumer.DEAF + ).resolveDependencyAsArtifact( + /* groupId = */ coordinates.group, + /* artifactId = */ coordinates.artifact, + /* versionConstraint = */ coordinates.version, + /* artifactKinds = */ EnumSet.of(artifactKind), + /* includeTransitiveDependencies = */ false, + /* excludedDependencies = */ emptyList() + ).singleOrNull()?.file?.toPath() ?: return null + + return resolvedDependencyArtifact + } + + private fun resolveSourceSetKlib( + resolvedArtifactPath: Path, + kmpCoordinates: KmpCoordinates, + directoryForTransformedDependencies: Path? + ): Path? { + val sourceSet = kmpCoordinates.sourceSet + check(sourceSet != null) + if (!JarUtil.containsEntry(resolvedArtifactPath.toFile(), sourceSet)) return null + + val transformedLibrariesRoot = directoryForTransformedDependencies + ?: FileUtilRt.createTempDirectory("kotlinTransformedMetadataLibraries", "").toPath() + val destination = transformedLibrariesRoot.resolve(kmpCoordinates.toString()) + + resolvedArtifactPath.unzipTo(destination, fromSubdirectory = Paths.get("$sourceSet/")) + return destination + } +} + +enum class DependencyKind(val artifactKind: ArtifactKind) { + /** + * JVM JAR with .class files. + */ + JAR(ArtifactKind.ARTIFACT), + + /** + * Platform KLIB with target-specific binaries. + */ + PLATFORM_KLIB(ArtifactKind.KLIB), + + /** + * Fat composite Kotlin metadata artifact, published by KMP libraries. + * Contains unpacked common metadata KLIBs with bodiless declaration headers serialized in .knm format + * One inner KLIB corresponds to one shared source set. + * Platform (target-specific) binaries and platform source sets' content are not included in this artifact. + */ + COMMON_METADATA_JAR(ArtifactKind.ARTIFACT), + + /** + * Same as metadata KLIB, but published as a special `-all` variant. + * The default variant in such cases is used for a compatibility artifact in a legacy format. + * This format is used by older KMP libraries, such as kotlin-stdlib and a few others. + */ + ALL_METADATA_JAR(ArtifactKind.ALL), +} + +class KmpCoordinates( + val group: String, + val artifact: String, + val version: String, + val sourceSet: String?, +) { + override fun toString(): String { + val sourceSetIfNotNull = sourceSet?.let { ":$sourceSet" }.orEmpty() + return "$group:$artifact$sourceSetIfNotNull:$version" + } +} + +class KmpAwareLibraryDependency private constructor( + val coordinates: KmpCoordinates, + val kind: DependencyKind, +) { + companion object { + // expected format: org.example:some:1.2.3 + fun klib(coordinates: String) = createFromCoordinates(coordinates, DependencyKind.PLATFORM_KLIB) + + // expected format: org.example:some:1.2.3 + fun jar(coordinates: String) = createFromCoordinates(coordinates, DependencyKind.JAR) + + // expected format: org.example:some:commonMain:1.2.3 + fun metadataKlib(coordinates: String) = createFromCoordinates(coordinates, DependencyKind.COMMON_METADATA_JAR) + + // expected format: org.example:some:commonMain:1.2.3 + fun allMetadataJar(coordinates: String) = createFromCoordinates(coordinates, DependencyKind.ALL_METADATA_JAR) + + private fun createFromCoordinates(coordinatesString: String, kind: DependencyKind): KmpAwareLibraryDependency { + return when (kind) { + DependencyKind.JAR, DependencyKind.PLATFORM_KLIB -> { + val (group, artifact, version) = coordinatesString.split(":").also { check(it.size == 3) } + KmpAwareLibraryDependency(KmpCoordinates(group, artifact, version, null), kind) + } + + DependencyKind.COMMON_METADATA_JAR, DependencyKind.ALL_METADATA_JAR -> { + val (group, artifact, sourceSet, version) = coordinatesString.split(":").also { check(it.size == 4) } + KmpAwareLibraryDependency(KmpCoordinates(group, artifact, version, sourceSet), kind) + } + } + } + } +} diff --git a/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/kmp/K2KmpLightFixtureHighlightingTestGenerated.java b/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/kmp/K2KmpLightFixtureHighlightingTestGenerated.java index 3c49fccd7cc9..64f73f21e090 100644 --- a/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/kmp/K2KmpLightFixtureHighlightingTestGenerated.java +++ b/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/kmp/K2KmpLightFixtureHighlightingTestGenerated.java @@ -28,6 +28,11 @@ public class K2KmpLightFixtureHighlightingTestGenerated extends AbstractK2KmpLig runTest("testData/kmp/highlighting/collectionBuilders.kt"); } + @TestMetadata("coroutines.kt") + public void testCoroutines() throws Exception { + runTest("testData/kmp/highlighting/coroutines.kt"); + } + @TestMetadata("expectActualFunctions.kt") public void testExpectActualFunctions() throws Exception { runTest("testData/kmp/highlighting/expectActualFunctions.kt"); diff --git a/plugins/kotlin/fir/tests/testData/kmp/highlighting/collectionBuilders.kt b/plugins/kotlin/fir/tests/testData/kmp/highlighting/collectionBuilders.kt index 2a9cf3aea3c1..310b9625d71e 100644 --- a/plugins/kotlin/fir/tests/testData/kmp/highlighting/collectionBuilders.kt +++ b/plugins/kotlin/fir/tests/testData/kmp/highlighting/collectionBuilders.kt @@ -1,5 +1,5 @@ // PLATFORM: Common -// FILE: A.kt +// FILE: common.kt fun commonThings() { val c = listOf(1, 2, 3) @@ -8,7 +8,7 @@ fun commonThings() { } // PLATFORM: Jvm -// FILE: B.kt +// FILE: jvm.kt fun jvmThings() { val c = listOf(1, 2, 3) @@ -17,7 +17,7 @@ fun jvmThings() { } // PLATFORM: Js -// FILE: C.kt +// FILE: js.kt fun jsThings() { val c = listOf(1, 2, 3) diff --git a/plugins/kotlin/fir/tests/testData/kmp/highlighting/coroutines.kt b/plugins/kotlin/fir/tests/testData/kmp/highlighting/coroutines.kt new file mode 100644 index 000000000000..aaa32a3b2584 --- /dev/null +++ b/plugins/kotlin/fir/tests/testData/kmp/highlighting/coroutines.kt @@ -0,0 +1,38 @@ +// PLATFORM: Common +// FILE: common.kt + +import kotlinx.coroutines.* + +suspend fun commonCoroutines() { + coroutineScope { + launch { + delay(1000) + } + } +} + +// PLATFORM: Jvm +// FILE: jvm.kt + +import kotlinx.coroutines.* + +suspend fun jvmCoroutines() { + coroutineScope { + launch { + delay(1000) + } + } +} + +// PLATFORM: Js +// FILE: js.kt + +import kotlinx.coroutines.* + +suspend fun jsCoroutines() { + coroutineScope { + launch { + delay(1000) + } + } +} diff --git a/plugins/kotlin/fir/tests/testData/kmp/highlighting/expectActualFunctions.kt b/plugins/kotlin/fir/tests/testData/kmp/highlighting/expectActualFunctions.kt index b75ee5de47d5..ecbad8748606 100644 --- a/plugins/kotlin/fir/tests/testData/kmp/highlighting/expectActualFunctions.kt +++ b/plugins/kotlin/fir/tests/testData/kmp/highlighting/expectActualFunctions.kt @@ -1,5 +1,5 @@ // PLATFORM: Common -// FILE: A.kt +// FILE: common.kt expect fun foo(): String @@ -8,7 +8,7 @@ fun useFoo() { } // PLATFORM: Jvm -// FILE: B.kt +// FILE: jvm.kt actual fun foo(): String = "JVM" @@ -17,7 +17,7 @@ fun useFoo() { } // PLATFORM: Js -// FILE: C.kt +// FILE: js.kt actual fun foo(): String = "JS" diff --git a/plugins/kotlin/fir/tests/testData/kmp/highlighting/literals.kt b/plugins/kotlin/fir/tests/testData/kmp/highlighting/literals.kt index 2c144ebb126c..f8a5340b624d 100644 --- a/plugins/kotlin/fir/tests/testData/kmp/highlighting/literals.kt +++ b/plugins/kotlin/fir/tests/testData/kmp/highlighting/literals.kt @@ -1,5 +1,5 @@ // PLATFORM: Common -// FILE: A.kt +// FILE: common.kt fun common() { val f: Float = 0.0f @@ -14,7 +14,7 @@ fun common() { } // PLATFORM: Jvm -// FILE: B.kt +// FILE: jvm.kt fun jvm() { val f: Float = 0.0f @@ -29,7 +29,7 @@ fun jvm() { } // PLATFORM: Js -// FILE: C.kt +// FILE: js.kt fun js() { val f: Float = 0.0f diff --git a/plugins/kotlin/test-framework/test/org/jetbrains/kotlin/idea/test/KotlinMultiPlatformProjectDescriptor.kt b/plugins/kotlin/test-framework/test/org/jetbrains/kotlin/idea/test/KotlinMultiPlatformProjectDescriptor.kt index 718e1840e487..0ea834d6eb5c 100644 --- a/plugins/kotlin/test-framework/test/org/jetbrains/kotlin/idea/test/KotlinMultiPlatformProjectDescriptor.kt +++ b/plugins/kotlin/test-framework/test/org/jetbrains/kotlin/idea/test/KotlinMultiPlatformProjectDescriptor.kt @@ -8,14 +8,16 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.ProjectJdkTable import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.roots.* +import com.intellij.openapi.roots.libraries.Library import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.ex.temp.TempFileSystem import com.intellij.pom.java.LanguageLevel import com.intellij.testFramework.IdeaTestUtil import com.intellij.testFramework.IndexingTestUtil -import com.intellij.testFramework.fixtures.MavenDependencyUtil import org.jetbrains.jps.model.java.JavaSourceRootType +import org.jetbrains.kotlin.idea.artifacts.KmpAwareLibraryDependency +import org.jetbrains.kotlin.idea.artifacts.KmpLightFixtureDependencyDownloader import org.jetbrains.kotlin.idea.framework.KotlinSdkType import org.jetbrains.kotlin.platform.TargetPlatform import org.jetbrains.kotlin.platform.js.JsPlatforms @@ -24,50 +26,56 @@ import org.jetbrains.kotlin.platform.jvm.JvmPlatforms /** * The project is created with three modules: Common, Jvm -> Common, Js -> Common. * - * Standard library dependency is added to all modules. + * Standard library and kotlinx-coroutines-core of fixed versions are added to all modules. + * + * Since we can't use Gradle in light fixture tests due to performance reasons, correct libraries should be mapped to modules manually. */ object KotlinMultiPlatformProjectDescriptor : KotlinLightProjectDescriptor() { enum class PlatformDescriptor( val moduleName: String, - val sourceRootName: String? = null, val targetPlatform: TargetPlatform, val isKotlinSdkUsed: Boolean = true, val refinementDependencies: List = emptyList(), - val dependencyCoordinates: List = emptyList(), + val libraryDependencies: List = emptyList(), ) { COMMON( moduleName = "Common", - sourceRootName = "src_common", targetPlatform = TargetPlatform( setOf( JvmPlatforms.jvm8.single(), JsPlatforms.defaultJsPlatform.single() ) ), - dependencyCoordinates = listOf( - "org.jetbrains.kotlin:kotlin-stdlib-common:1.9.23", // TODO (KTIJ-29725): make stdlib version dynamic + libraryDependencies = listOf( + KmpAwareLibraryDependency.allMetadataJar("org.jetbrains.kotlin:kotlin-stdlib:commonMain:1.9.23"), // TODO (KTIJ-29725): sliding version + KmpAwareLibraryDependency.metadataKlib("org.jetbrains.kotlinx:kotlinx-coroutines-core:commonMain:1.8.0") ), ), JVM( moduleName = "Jvm", - sourceRootName = "src_jvm", targetPlatform = JvmPlatforms.jvm8, isKotlinSdkUsed = false, refinementDependencies = listOf(COMMON), - dependencyCoordinates = listOf( - "org.jetbrains.kotlin:kotlin-stdlib:1.9.23", + libraryDependencies = listOf( + KmpAwareLibraryDependency.jar("org.jetbrains.kotlin:kotlin-stdlib:1.9.23"), + KmpAwareLibraryDependency.jar("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0"), + KmpAwareLibraryDependency.jar("org.jetbrains:annotations:23.0.0"), ), ), JS( moduleName = "Js", - sourceRootName = "src_js", targetPlatform = JsPlatforms.defaultJsPlatform, refinementDependencies = listOf(COMMON), - dependencyCoordinates = listOf( - "org.jetbrains.kotlin:kotlin-stdlib-js:1.9.23", + libraryDependencies = listOf( + KmpAwareLibraryDependency.klib("org.jetbrains.kotlin:kotlin-stdlib-js:1.9.23"), + KmpAwareLibraryDependency.klib("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.8.0"), + KmpAwareLibraryDependency.klib("org.jetbrains.kotlinx:atomicfu-js:0.23.1"), ), ); + val sourceRootName: String + get() = "src_${moduleName.lowercase()}" + fun sourceRoot(): VirtualFile? = findRoot(sourceRootName) private fun findRoot(rootName: String?): VirtualFile? = @@ -102,10 +110,8 @@ object KotlinMultiPlatformProjectDescriptor : KotlinLightProjectDescriptor() { private fun configureModule(module: Module, model: ModifiableRootModel, descriptor: PlatformDescriptor) { model.getModuleExtension(LanguageLevelModuleExtension::class.java).languageLevel = LanguageLevel.HIGHEST - if (descriptor.sourceRootName != null) { - val sourceRoot = createSourceRoot(module, descriptor.sourceRootName) - model.addContentEntry(sourceRoot).addSourceFolder(sourceRoot, JavaSourceRootType.SOURCE) - } + val sourceRoot = createSourceRoot(module, descriptor.sourceRootName) + model.addContentEntry(sourceRoot).addSourceFolder(sourceRoot, JavaSourceRootType.SOURCE) setUpSdk(module, model, descriptor) @@ -115,9 +121,18 @@ object KotlinMultiPlatformProjectDescriptor : KotlinLightProjectDescriptor() { dependsOnModuleNames = descriptor.refinementDependencies.map(PlatformDescriptor::moduleName), pureKotlinSourceFolders = listOf(descriptor.sourceRoot()!!.path), ) + for (libraryCoordinates in descriptor.libraryDependencies) { + val library = setUpLibraryFromCoordinates(module.project, libraryCoordinates) + model.addLibraryEntry(library) + } + } - for (libraryCoordinates in descriptor.dependencyCoordinates) { - MavenDependencyUtil.addFromMaven(model, libraryCoordinates) + private fun setUpLibraryFromCoordinates(project: Project, dependency: KmpAwareLibraryDependency): Library { + val dependencyRoot = KmpLightFixtureDependencyDownloader.resolveDependency(dependency)?.toFile() + ?: error("Unable to download library ${dependency.coordinates}") + return ConfigLibraryUtil.addProjectLibrary(project = project, name = dependency.coordinates.toString()) { + addRoot(dependencyRoot, OrderRootType.CLASSES) + commit() } }