[Kotlin] Resolve parts of KMP libraries in multiplatform descriptor

KTIJ-29725

GitOrigin-RevId: c7f8cb5e25abd7db02e9d3404283d6b3db04a96c
This commit is contained in:
Pavel Kirpichenkov
2024-05-13 21:24:22 +03:00
committed by intellij-monorepo-bot
parent 1f8598525c
commit a05c97a762
8 changed files with 247 additions and 29 deletions

View File

@@ -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;

View File

@@ -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)
}
}
}
}
}

View File

@@ -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");

View File

@@ -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)

View File

@@ -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)
}
}
}

View File

@@ -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"

View File

@@ -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

View File

@@ -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<PlatformDescriptor> = emptyList(),
val dependencyCoordinates: List<String> = emptyList(),
val libraryDependencies: List<KmpAwareLibraryDependency> = 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()
}
}