diff --git a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/CommandLine.kt b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/CommandLine.kt index eec96fc8923..1dc5e740a2f 100644 --- a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/CommandLine.kt +++ b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/CommandLine.kt @@ -34,6 +34,7 @@ const val SHORT_MODULE_NAME = "Xshort-module-name" const val FOREIGN_EXCEPTION_MODE = "Xforeign-exception-mode" const val DUMP_BRIDGES = "Xdump-bridges" const val DISABLE_EXCEPTION_PRETTIFIER = "Xdisable-exception-prettifier" +const val USER_SETUP_HINT = "Xuser-setup-hint" // TODO: unify camel and snake cases. // Possible solution is to accept both cases @@ -138,6 +139,9 @@ open class CInteropArguments(argParser: ArgParser = val disableExceptionPrettifier by argParser.option(ArgType.Boolean, DISABLE_EXCEPTION_PRETTIFIER, description = "Don't hide exceptions with user-friendly ones").default(false) + + val userSetupHint by argParser.option(ArgType.String, USER_SETUP_HINT, + description = "A suggestion that is displayed to the user if produced lib fails to link") } class JSInteropArguments(argParser: ArgParser = ArgParser("jsinterop", diff --git a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/main.kt b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/main.kt index 46ad0212907..1ddbba56713 100644 --- a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/main.kt +++ b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/main.kt @@ -388,6 +388,12 @@ private fun processCLib( ForeignExceptionMode.byValue(it).value // may throw IllegalArgumentException } + cinteropArguments.userSetupHint?.let { + def.manifestAddendProperties.put("userSetupHint", it)?.also { + warn("User setup hint provided in .def file will be shadowed by command line argument") + } + } + manifestAddend?.parentFile?.mkdirs() manifestAddend?.let { def.manifestAddendProperties.storeProperties(it) } diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/Linker.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/Linker.kt index d796253767e..cb42d81493e 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/Linker.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/Linker.kt @@ -9,6 +9,8 @@ import org.jetbrains.kotlin.konan.target.CompilerOutputKind import org.jetbrains.kotlin.konan.target.Family import org.jetbrains.kotlin.konan.target.LinkerOutputKind import org.jetbrains.kotlin.konan.target.presetName +import org.jetbrains.kotlin.library.isInterop +import org.jetbrains.kotlin.library.uniqueName internal fun determineLinkerOutput(context: PhaseContext): LinkerOutputKind = when (context.config.produce) { @@ -144,15 +146,33 @@ internal fun runLinkerCommands(context: PhaseContext, commands: List, c it.execute() } } catch (e: KonanExternalToolFailure) { - val extraUserInfo = - if (cachingInvolved) - """ - Please try to disable compiler caches and rerun the build. To disable compiler caches, add the following line to the gradle.properties file in the project's root directory: - - kotlin.native.cacheKind.${context.config.target.presetName}=none - - Also, consider filing an issue with full Gradle log here: https://kotl.in/issue - """.trimIndent() - else "" - context.reportCompilationError("${e.toolName} invocation reported errors\n$extraUserInfo\n${e.message}") + val extraUserInfo = if (cachingInvolved) + """ + Please try to disable compiler caches and rerun the build. To disable compiler caches, add the following line to the gradle.properties file in the project's root directory: + + kotlin.native.cacheKind.${context.config.target.presetName}=none + + Also, consider filing an issue with full Gradle log here: https://kotl.in/issue + """.trimIndent() + else null + + val extraUserSetupInfo = run { + context.config.resolvedLibraries.getFullResolvedList() + .filter { it.library.isInterop } + .mapNotNull { library -> + library.library.manifestProperties["userSetupHint"]?.let { + "From ${library.library.uniqueName}:\n$it".takeIf { it.isNotEmpty() } + } + } + .mapIndexed { index, message -> "$index. $message" } + .takeIf { it.isNotEmpty() } + ?.joinToString(separator = "\n\n") + ?.let { + "It seems your project produced link errors.\nProposed solutions:\n\n$it\n" + } + } + + val extraInfo = listOfNotNull(extraUserInfo, extraUserSetupInfo).joinToString(separator = "\n") + + context.reportCompilationError("${e.toolName} invocation reported errors\n$extraInfo\n${e.message}") } \ No newline at end of file diff --git a/native/native.tests/testData/CInterop/KT-55578/userSetupFancyHint.def b/native/native.tests/testData/CInterop/KT-55578/userSetupFancyHint.def new file mode 100644 index 00000000000..923005ba37e --- /dev/null +++ b/native/native.tests/testData/CInterop/KT-55578/userSetupFancyHint.def @@ -0,0 +1,10 @@ +headerFilter = **/userSetupHint.h +userSetupHint = ๐ŸคŒ\t\u2115ever put ketchup on-a ๐Ÿ\ +\nโ„• = `\u2115` = "\u2115" = \\u2115\ +\n๐Ÿ‡ฎ๐Ÿ‡น +--- +void foo(); + +void test() { + foo(); +} \ No newline at end of file diff --git a/native/native.tests/testData/CInterop/KT-55578/userSetupFancyHint.kt b/native/native.tests/testData/CInterop/KT-55578/userSetupFancyHint.kt new file mode 100644 index 00000000000..35a9e0107d3 --- /dev/null +++ b/native/native.tests/testData/CInterop/KT-55578/userSetupFancyHint.kt @@ -0,0 +1,10 @@ +/* + * 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. + */ + +import userSetupFancyHint.* + +fun userSetupHint(args: Array) { + test() +} \ No newline at end of file diff --git a/native/native.tests/testData/CInterop/KT-55578/userSetupHint.h b/native/native.tests/testData/CInterop/KT-55578/userSetupHint.h new file mode 100644 index 00000000000..a9a7b415174 --- /dev/null +++ b/native/native.tests/testData/CInterop/KT-55578/userSetupHint.h @@ -0,0 +1,7 @@ +#include + +#ifdef __cplusplus +#define EXPORT extern "C" +#else +#define EXPORT +#endif \ No newline at end of file diff --git a/native/native.tests/testData/CInterop/KT-55578/userSetupHint.kt b/native/native.tests/testData/CInterop/KT-55578/userSetupHint.kt new file mode 100644 index 00000000000..7b23e1d9bbb --- /dev/null +++ b/native/native.tests/testData/CInterop/KT-55578/userSetupHint.kt @@ -0,0 +1,12 @@ +/* + * 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. + */ + +import userSetupHint1.* +import userSetupHint2.* + +fun userSetupHint(args: Array) { + testFoo() + testBar() +} \ No newline at end of file diff --git a/native/native.tests/testData/CInterop/KT-55578/userSetupHint1.c b/native/native.tests/testData/CInterop/KT-55578/userSetupHint1.c new file mode 100644 index 00000000000..083ea6459a7 --- /dev/null +++ b/native/native.tests/testData/CInterop/KT-55578/userSetupHint1.c @@ -0,0 +1,5 @@ +#import "userSetupHint.h" + +EXPORT void foo() { + // this just needs to exist +} diff --git a/native/native.tests/testData/CInterop/KT-55578/userSetupHint1.def b/native/native.tests/testData/CInterop/KT-55578/userSetupHint1.def new file mode 100644 index 00000000000..abb1745611d --- /dev/null +++ b/native/native.tests/testData/CInterop/KT-55578/userSetupHint1.def @@ -0,0 +1,8 @@ +headerFilter = **/userSetupHint.h +userSetupHint = <> +--- +void foo(void); + +void testFoo() { + foo(); +} \ No newline at end of file diff --git a/native/native.tests/testData/CInterop/KT-55578/userSetupHint2.c b/native/native.tests/testData/CInterop/KT-55578/userSetupHint2.c new file mode 100644 index 00000000000..2be04460318 --- /dev/null +++ b/native/native.tests/testData/CInterop/KT-55578/userSetupHint2.c @@ -0,0 +1,5 @@ +#import "userSetupHint.h" + +EXPORT void bar() { + // this just needs to exist +} \ No newline at end of file diff --git a/native/native.tests/testData/CInterop/KT-55578/userSetupHint2.def b/native/native.tests/testData/CInterop/KT-55578/userSetupHint2.def new file mode 100644 index 00000000000..0ee9580c92d --- /dev/null +++ b/native/native.tests/testData/CInterop/KT-55578/userSetupHint2.def @@ -0,0 +1,8 @@ +headerFilter = **/userSetupHint.h +userSetupHint = <> +--- +void bar(void); + +void testBar() { + bar(); +} \ No newline at end of file diff --git a/native/native.tests/testData/CInterop/KT-55578/userSetupHintLinkingMissingLibrary.def b/native/native.tests/testData/CInterop/KT-55578/userSetupHintLinkingMissingLibrary.def new file mode 100644 index 00000000000..88e61f0c179 --- /dev/null +++ b/native/native.tests/testData/CInterop/KT-55578/userSetupHintLinkingMissingLibrary.def @@ -0,0 +1,5 @@ +headerFilter = **/userSetupHint.h +userSetupHint = <> +linkerOpts = -lNonExistant +--- +void test() { } \ No newline at end of file diff --git a/native/native.tests/testData/CInterop/KT-55578/userSetupHintLinkingMissingLibrary.kt b/native/native.tests/testData/CInterop/KT-55578/userSetupHintLinkingMissingLibrary.kt new file mode 100644 index 00000000000..b1210504906 --- /dev/null +++ b/native/native.tests/testData/CInterop/KT-55578/userSetupHintLinkingMissingLibrary.kt @@ -0,0 +1,5 @@ +import userSetupHintLinkingMissingLibrary.* + +fun userSetupHint(args: Array) { + test() +} \ No newline at end of file diff --git a/native/native.tests/testData/CInterop/KT-55578/userSetupNoHint.def b/native/native.tests/testData/CInterop/KT-55578/userSetupNoHint.def new file mode 100644 index 00000000000..35d7516d240 --- /dev/null +++ b/native/native.tests/testData/CInterop/KT-55578/userSetupNoHint.def @@ -0,0 +1,7 @@ +headerFilter = **/userSetupHint.h +--- +void foo(void); + +void test() { + foo(); +} \ No newline at end of file diff --git a/native/native.tests/testData/CInterop/KT-55578/userSetupNoHint.kt b/native/native.tests/testData/CInterop/KT-55578/userSetupNoHint.kt new file mode 100644 index 00000000000..a9f010480ae --- /dev/null +++ b/native/native.tests/testData/CInterop/KT-55578/userSetupNoHint.kt @@ -0,0 +1,10 @@ +/* + * 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. + */ + +import userSetupNoHint.* + +fun userSetupHint(args: Array) { + test() +} \ No newline at end of file diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/AbstractNativeLinkerOutputTest.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/AbstractNativeLinkerOutputTest.kt new file mode 100644 index 00000000000..f4206899ed9 --- /dev/null +++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/AbstractNativeLinkerOutputTest.kt @@ -0,0 +1,54 @@ +/* + * 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.blackboxtest + +import org.jetbrains.kotlin.konan.blackboxtest.support.* +import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.* +import org.jetbrains.kotlin.konan.blackboxtest.support.runner.TestRunChecks +import org.jetbrains.kotlin.konan.blackboxtest.support.settings.KotlinNativeTargets +import org.jetbrains.kotlin.konan.blackboxtest.support.settings.Timeouts + +@EnforcedProperty(ClassLevelProperty.COMPILER_OUTPUT_INTERCEPTOR, "NONE") +abstract class AbstractNativeLinkerOutputTest : AbstractNativeCInteropBaseTest() { + private fun createTestCaseNoTestRun(module: TestModule.Exclusive, compilerArgs: TestCompilerArgs) = TestCase( + id = TestCaseId.Named(module.name), + kind = TestKind.STANDALONE_NO_TR, + modules = setOf(module), + freeCompilerArgs = compilerArgs, + nominalPackageName = PackageName.EMPTY, + checks = TestRunChecks.Default(testRunSettings.get().executionTimeout), + extras = TestCase.NoTestRunnerExtras(".${module.name}") + ).apply { + initialize(null, null) + } + + internal fun compileToExecutable( + module: TestModule.Exclusive, + dependencies: List>, + args: List = emptyList() + ) = compileToExecutable( + createTestCaseNoTestRun(module, TestCompilerArgs(args)), + dependencies + ) + + internal fun compileToExecutable( + testCase: TestCase, + dependencies: List> + ): TestCompilationResult { + val compilation = ExecutableCompilation( + settings = testRunSettings, + freeCompilerArgs = testCase.freeCompilerArgs, + sourceModules = testCase.modules, + extras = TestCase.NoTestRunnerExtras(".${testCase.modules.singleOrNull()!!.name}"), + dependencies = dependencies, + expectedArtifact = getExecutableArtifact() + ) + return compilation.result + } + + private fun getExecutableArtifact() = + TestCompilationArtifact.Executable(buildDir.resolve("app." + testRunSettings.get().testTarget.family.exeSuffix)) +} \ No newline at end of file diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/LinkerOutputTestKT55578.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/LinkerOutputTestKT55578.kt new file mode 100644 index 00000000000..a22f0f3792e --- /dev/null +++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/LinkerOutputTestKT55578.kt @@ -0,0 +1,177 @@ +/* + * 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.blackboxtest + +import com.intellij.testFramework.TestDataPath +import org.jetbrains.kotlin.konan.blackboxtest.support.* +import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilationArtifact +import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilationArtifact.KLIB +import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilationResult +import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilationResult.Companion.assertSuccess +import org.junit.jupiter.api.Test +import java.io.File +import kotlin.test.assertContains +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@TestDataPath("\$PROJECT_ROOT") +@EnforcedProperty(ClassLevelProperty.COMPILER_OUTPUT_INTERCEPTOR, "NONE") +class LinkerOutputTestKT55578 : AbstractNativeLinkerOutputTest() { + private val testDir = File("native/native.tests/testData/CInterop/KT-55578/") + + private val hint1 = "<>" + private val hint2 = "<>" + private val cliHint = "<>" + private val hintMissingLibrary = "<>" + private val hintFancy = "\uD83E\uDD0C\tโ„•ever put ketchup on-a \uD83C\uDF5D\nโ„• = `โ„•` = \"\\u2115\" = \\u2115\n\uD83C\uDDEE\uD83C\uDDF9\n" + + @Test + fun `should print hints on failed linkage`() { + val targetLibrary1 = compileKlib(testDir.resolve("userSetupHint1.def")) + val targetLibrary2 = compileKlib(testDir.resolve("userSetupHint2.def")) + + val compilationResult = compileExecutable(testDir.resolve("userSetupHint.kt"), targetLibrary1, targetLibrary2) + assertTrue(compilationResult is TestCompilationResult.Failure, "Compilation is expected to fail with linkage errors") + val compilationOutput = compilationResult.loggedData.toString() + + for (hint in arrayOf(hint1, hint2)) { + assertContains(compilationOutput, hint, false, """ + |Error output should contain provided hint "$hint" + |Actual output: + |$compilationOutput + """.trimMargin()) + } + } + + @Test + fun `should print fancy hints`() { + val targetLibrary = compileKlib(testDir.resolve("userSetupFancyHint.def")) + + val compilationResult = compileExecutable(testDir.resolve("userSetupFancyHint.kt"), targetLibrary) + assertTrue(compilationResult is TestCompilationResult.Failure, "Compilation is expected to fail with linkage errors") + val compilationOutput = compilationResult.loggedData.toString() + + assertContains(compilationOutput, hintFancy, false, """ + |Error output should contain provided hint + |Actual output: + |$compilationOutput + """.trimMargin()) + + } + + @Test + fun `should print hints on missing library`() { + val targetLibrary = compileKlib(testDir.resolve("userSetupHintLinkingMissingLibrary.def")) + + val compilationResult = compileExecutable(testDir.resolve("userSetupHintLinkingMissingLibrary.kt"), targetLibrary) + assertTrue(compilationResult is TestCompilationResult.Failure, "Compilation is expected to fail with linkage errors") + val compilationOutput = compilationResult.loggedData.toString() + + for (hint in arrayOf(hintMissingLibrary)) { + assertContains(compilationOutput, hint, false, """ + |Error output should contain provided hint "$hint" + |Actual output: + |$compilationOutput + """.trimMargin()) + } + } + @Test + fun `should not print hints on successful compilation`() { + val targetLibrary1 = compileKlib( + testDir.resolve("userSetupHint1.def"), + testDir.resolve("userSetupHint1.c") + ) + val targetLibrary2 = compileKlib( + testDir.resolve("userSetupHint2.def"), + testDir.resolve("userSetupHint2.c") + ) + + val compilationResult = compileExecutable( + testDir.resolve("userSetupHint.kt"), + targetLibrary1, targetLibrary2 + ) + val compilationOutput = compilationResult.assertSuccess().loggedData.toString() + + for (hint in arrayOf(hint1, hint2)) { + assertFalse(compilationOutput.contains(hint), """ + |Error output should *not* contain provided hint "$hint" + |Actual output: + |$compilationOutput + """.trimMargin()) + } + } + + @Test + fun `should not print hints on compilation failed without linker errors`() { + val targetLibrary1 = compileKlib( + testDir.resolve("userSetupHint1.def"), + testDir.resolve("userSetupHint1.c") + ) + + val compilationResult = compileExecutable(testDir.resolve("userSetupHint.kt"), targetLibrary1) + assertTrue(compilationResult is TestCompilationResult.Failure, "Compilation is expected to fail") + val compilationOutput = compilationResult.loggedData.toString() + + for (hint in arrayOf(hint1, hint2)) { + assertFalse(compilationOutput.contains(hint), """ + |Error output should *not* contain provided hint "$hint" + |Actual output: + |$compilationOutput + """.trimMargin()) + } + } + + @Test + fun `should shadow hint by cli argument`() { + val targetLibrary1 = compileKlib(testDir.resolve("userSetupHint1.def"), extraArgs = listOf("-Xuser-setup-hint", cliHint)) + val targetLibrary2 = compileKlib(testDir.resolve("userSetupHint2.def")) + + val compilationResult = compileExecutable(testDir.resolve("userSetupHint.kt"), targetLibrary1, targetLibrary2) + assertTrue(compilationResult is TestCompilationResult.Failure, "Compilation is expected to fail") + val compilationOutput = compilationResult.loggedData.toString() + + for (hint in arrayOf(cliHint, hint2)) { + assertContains(compilationOutput, hint, false, """ + |Error output should contain provided hint "$hint" + |Actual output: + |$compilationOutput + """.trimMargin()) + } + } + + @Test + fun `should not print hints on compilation failed with no provided hints`() { + val targetLibrary = compileKlib(testDir.resolve("userSetupNoHint.def")) + + val compilationResult = compileExecutable(testDir.resolve("userSetupNoHint.kt"), targetLibrary) + assertTrue(compilationResult is TestCompilationResult.Failure, "Compilation is expected to fail") + val compilationOutput = compilationResult.loggedData.toString() + + assertFalse(compilationOutput.contains("It seems your project produced link errors."), """ + |Error output should *not* contain any hints + |Actual output: + |$compilationOutput + """.trimMargin()) + } + + private fun compileExecutable( + file: File, + vararg libraries: KLIB, + extraArgs: List = emptyList() + ): TestCompilationResult { + val module = TestModule.Exclusive("userSetupHint", emptySet(), emptySet(), emptySet()).apply { + files += TestFile.createCommitted(file, this) + } + + return compileToExecutable(module, libraries.asList().map { it.asLibraryDependency() }, extraArgs) + } + + private fun compileKlib(defFile: File, sourceFile: File? = null, extraArgs: List = emptyList()): KLIB { + val sourceArguments = sourceFile?.let { listOf("-Xcompile-source", sourceFile.absolutePath) } ?: emptyList() + val libraryTestCase: TestCase = generateCInteropTestCaseWithSingleDef(defFile, extraArgs + sourceArguments) + return libraryTestCase.cinteropToLibrary().assertSuccess().resultingArtifact + } +} diff --git a/native/utils/src/org/jetbrains/kotlin/konan/util/DefFile.kt b/native/utils/src/org/jetbrains/kotlin/konan/util/DefFile.kt index a9da3309f79..066cc875862 100644 --- a/native/utils/src/org/jetbrains/kotlin/konan/util/DefFile.kt +++ b/native/utils/src/org/jetbrains/kotlin/konan/util/DefFile.kt @@ -135,6 +135,10 @@ class DefFile(val file:File?, val config:DefFileConfig, val manifestAddendProper val objcClassesIncludingCategories by lazy { properties.getSpaceSeparated("objcClassesIncludingCategories") } + + val userSetupHint by lazy { + properties.getProperty("userSetupHint") + } } }