[KLIB Resolver] Add a heuristic to look up for KLIB file given only unique name
This fix adds a workaround to allow Kotlin/Native compiler to resolve
KLIB library if only library name was passed via `-l` CLI argument.
^KT-63931
diff --git a/compiler/util-klib/src/org/jetbrains/kotlin/library/SearchPathResolver.kt b/compiler/util-klib/src/org/jetbrains/kotlin/library/SearchPathResolver.kt
index 005b850..6b2024b 100644
--- a/compiler/util-klib/src/org/jetbrains/kotlin/library/SearchPathResolver.kt
+++ b/compiler/util-klib/src/org/jetbrains/kotlin/library/SearchPathResolver.kt
@@ -31,13 +31,28 @@
*/
class SearchRoot(val searchRootPath: File, val allowLookupByRelativePath: Boolean = false, val isDeprecated: Boolean = false) {
fun lookUp(libraryPath: File): LookupResult {
- if (libraryPath.isAbsolute)
+ if (libraryPath.isAbsolute) {
+ // Look up by the absolute path if it is indeed an absolute path.
return LookupResult.Found(lookUpByAbsolutePath(libraryPath) ?: return LookupResult.NotFound)
+ }
- if (!allowLookupByRelativePath && libraryPath.nameSegments.size > 1)
+ val isDefinitelyRelativePath = libraryPath.nameSegments.size > 1
+ if (isDefinitelyRelativePath && !allowLookupByRelativePath) {
+ // Lookup by the relative path is disallowed, but the path is definitely a relative path.
return LookupResult.NotFound
+ }
- val resolvedLibrary = lookUpByAbsolutePath(File(searchRootPath, libraryPath)) ?: return LookupResult.NotFound
+ // First, try to resolve by the relative path.
+ val resolvedLibrary = lookUpByAbsolutePath(File(searchRootPath, libraryPath))
+ ?: run {
+ if (!isDefinitelyRelativePath && libraryPath.extension.isEmpty()) {
+ // If the path actually looks like an unique name of the library, try to guess the name of the KLIB file.
+ // TODO: This logic is unreliable and needs to be replaced by the new KLIB resolver in the future.
+ lookUpByAbsolutePath(File(searchRootPath, "${libraryPath.path}.$KLIB_FILE_EXTENSION"))
+ } else null
+ }
+ ?: return LookupResult.NotFound
+
return if (isDeprecated)
LookupResult.FoundWithWarning(
library = resolvedLibrary,
@@ -54,7 +69,7 @@
when {
absoluteLibraryPath.isFile -> {
// It's a really existing file.
- when (absoluteLibraryPath.extension.toLowerCase()) {
+ when (absoluteLibraryPath.extension) {
KLIB_FILE_EXTENSION -> absoluteLibraryPath
"jar" -> {
// A special workaround for old JS stdlib, that was packed in a JAR file.
diff --git a/compiler/util-klib/src/org/jetbrains/kotlin/library/UnresolvedLibrary.kt b/compiler/util-klib/src/org/jetbrains/kotlin/library/UnresolvedLibrary.kt
index 76afe9f..dcf0653 100644
--- a/compiler/util-klib/src/org/jetbrains/kotlin/library/UnresolvedLibrary.kt
+++ b/compiler/util-klib/src/org/jetbrains/kotlin/library/UnresolvedLibrary.kt
@@ -8,6 +8,15 @@
fun UnresolvedLibrary(path: String, libraryVersion: String?, lenient: Boolean): UnresolvedLibrary =
if (lenient) LenientUnresolvedLibrary(path, libraryVersion) else RequiredUnresolvedLibrary(path, libraryVersion)
+/**
+ * Representation of a Kotlin library that has not been yet resolved.
+ *
+ * TODO: This class has a major design flaw and needs to be replaced by the new KLIB resolver in the future.
+ * - In certain situations [path] represents a path to the library, would it be relative or absolute.
+ * - In certain situations [path] represents an `unique_name` of the library.
+ * - In general, `unique_name` needs not be equal to the file name of the library. And this adds some mess to the classes
+ * that implement the "resolver" logic, e.g. [SearchPathResolver].
+ */
sealed class UnresolvedLibrary {
abstract val path: String
abstract val libraryVersion: String?
diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/KlibResolverTest.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/KlibResolverTest.kt
new file mode 100644
index 0000000..410352a
--- /dev/null
+++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/KlibResolverTest.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2010-2023 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.konan.test.blackbox
+
+import org.jetbrains.kotlin.konan.test.blackbox.support.TestCompilerArgs
+import org.jetbrains.kotlin.konan.test.blackbox.support.compilation.LibraryCompilation
+import org.jetbrains.kotlin.konan.test.blackbox.support.compilation.TestCompilationArtifact.KLIB
+import org.jetbrains.kotlin.konan.test.blackbox.support.compilation.TestCompilationResult.Companion.assertSuccess
+import org.jetbrains.kotlin.library.SearchPathResolver
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Tag
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.parallel.Execution
+import org.junit.jupiter.api.parallel.ExecutionMode
+import org.junit.jupiter.api.parallel.Isolated
+import java.io.File
+
+/**
+ * This test class needs to set up a custom working directory in the JVM process. This is necessary to trigger
+ * the special behavior inside the [SearchPathResolver] to start looking for KLIBs by relative path (or just
+ * by library name aka `unique_name`) inside the working directory.
+ *
+ * In order to make this possible and in order to avoid side effects for other tests two special annotations
+ * are added: `@Isolated` and `@Execution(ExecutionMode.SAME_THREAD)`.
+ *
+ * The control over the working directory in performed in the [runWithCustomWorkingDir] function.
+ */
+@Tag("klib")
+@Isolated // Run this test class in isolation from other test classes.
+@Execution(ExecutionMode.SAME_THREAD) // Run all test functions sequentially in the same thread.
+class KlibResolverTest : AbstractNativeSimpleTest() {
+ private data class Module(val name: String, val dependencyNames: List<String>) {
+ constructor(name: String, vararg dependencyNames: String) : this(name, dependencyNames.asList())
+
+ lateinit var dependencies: List<Module>
+ lateinit var sourceFile: File
+
+ fun initDependencies(resolveDependency: (String) -> Module) {
+ dependencies = dependencyNames.map(resolveDependency)
+ }
+ }
+
+ @Test
+ @DisplayName("Test resolving all dependencies recorded in `depends` / `dependency_version` properties (KT-63931)")
+ fun testResolvingDependenciesRecordedInManifest() {
+ val modules = createModules(
+ Module("a"),
+ Module("b", "a"),
+ Module("c", "a"),
+ Module("d", "b", "c", "a"),
+ )
+
+ listOf(
+ false to false,
+ true to false,
+ true to true,
+ false to true,
+ ).forEach { (produceUnpackedKlibs, useLibraryNamesInCliArguments) ->
+ modules.compileModules(produceUnpackedKlibs, useLibraryNamesInCliArguments)
+ }
+ }
+
+ private fun createModules(vararg modules: Module): List<Module> {
+ val mapping: Map<String, Module> = modules.groupBy(Module::name).mapValues {
+ it.value.singleOrNull() ?: error("Duplicated modules: ${it.value}")
+ }
+
+ modules.forEach { it.initDependencies(mapping::getValue) }
+
+ val generatedSourcesDir = buildDir.resolve("generated-sources")
+ generatedSourcesDir.mkdirs()
+
+ modules.forEach { module ->
+ module.sourceFile = generatedSourcesDir.resolve(module.name + ".kt")
+ module.sourceFile.writeText(
+ buildString {
+ appendLine("package ${module.name}")
+ appendLine()
+ appendLine("fun ${module.name}(indent: Int) {")
+ appendLine(" repeat(indent) { print(\" \") }")
+ appendLine(" println(\"${module.name}\")")
+ module.dependencyNames.forEach { dependencyName ->
+ appendLine(" $dependencyName.$dependencyName(indent + 1)")
+ }
+ appendLine("}")
+ }
+ )
+ }
+
+ return modules.asList()
+ }
+
+ private fun List<Module>.compileModules(
+ produceUnpackedKlibs: Boolean,
+ useLibraryNamesInCliArguments: Boolean
+ ) {
+ val klibFilesDir = buildDir.resolve(
+ listOf(
+ "klib-files",
+ if (produceUnpackedKlibs) "unpacked" else "packed",
+ if (useLibraryNamesInCliArguments) "names" else "paths"
+ ).joinToString(".")
+ )
+ klibFilesDir.mkdirs()
+
+ fun Module.computeArtifactPath(): String {
+ val basePath: String = if (useLibraryNamesInCliArguments) name else klibFilesDir.resolve(name).path
+ return if (produceUnpackedKlibs) basePath else "$basePath.klib"
+ }
+
+ runWithCustomWorkingDir(klibFilesDir) {
+ forEach { module ->
+ val testCase = generateTestCaseWithSingleFile(
+ sourceFile = module.sourceFile,
+ moduleName = module.name,
+ TestCompilerArgs(
+ buildList {
+ if (produceUnpackedKlibs) add("-nopack")
+ module.dependencies.forEach { dependency ->
+ add("-l")
+ add(dependency.computeArtifactPath())
+ }
+ }
+ )
+ )
+
+ val compilation = LibraryCompilation(
+ settings = testRunSettings,
+ freeCompilerArgs = testCase.freeCompilerArgs,
+ sourceModules = testCase.modules,
+ dependencies = emptySet(),
+ expectedArtifact = KLIB(klibFilesDir.resolve(module.computeArtifactPath()))
+ )
+
+ compilation.result.assertSuccess()
+ }
+ }
+ }
+
+ private inline fun runWithCustomWorkingDir(customWorkingDir: File, block: () -> Unit) {
+ val previousWorkingDir: String = System.getProperty(USER_DIR)
+ try {
+ System.setProperty(USER_DIR, customWorkingDir.absolutePath)
+ block()
+ } finally {
+ System.setProperty(USER_DIR, previousWorkingDir)
+ }
+ }
+
+ companion object {
+ private const val USER_DIR = "user.dir"
+ }
+}