diff --git a/CODE_COVERAGE.md b/CODE_COVERAGE.md index e31ccbb757b..2f7b5e0bff4 100644 --- a/CODE_COVERAGE.md +++ b/CODE_COVERAGE.md @@ -3,10 +3,9 @@ Kotlin/Native has a code coverage support that is based on Clang's [Source-based Code Coverage](https://clang.llvm.org/docs/SourceBasedCodeCoverage.html). **Please note**: -1. Coverage support is in it's very early days and is in active development. Known issues and restrictions are: +1. Coverage support is in it's very early days and is in active development. Known issues are: * Coverage information may be inaccurate. * Line execution counts may be wrong. - * Only macOS and iOS simulator binaries are supported. 2. Most of described functionality will be incorporated into Gradle plugin. diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/Linker.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/Linker.kt index 276308640f7..8b888565449 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/Linker.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/Linker.kt @@ -79,6 +79,8 @@ internal class Linker(val context: Context) { executable = dylibPath.absolutePath } + val needsProfileLibrary = context.coverage.enabled + try { File(executable).delete() linker.linkCommands(objectFiles = objectFiles, executable = executable, @@ -87,7 +89,8 @@ internal class Linker(val context: Context) { BitcodeEmbedding.getLinkerOptions(context.config) + libraryProvidedLinkerFlags + frameworkLinkerArgs, optimize = optimize, debug = debug, kind = linkerOutput, - outputDsymBundle = context.config.outputFile + ".dSYM").forEach { + outputDsymBundle = context.config.outputFile + ".dSYM", + needsProfileLibrary = needsProfileLibrary).forEach { it.logWith(context::log) it.execute() } diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/CoverageManager.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/CoverageManager.kt index 1f897c44730..d215ce6b966 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/CoverageManager.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/CoverageManager.kt @@ -13,7 +13,7 @@ import org.jetbrains.kotlin.ir.declarations.IrFile import org.jetbrains.kotlin.ir.declarations.IrFunction import org.jetbrains.kotlin.ir.declarations.IrModuleFragment import org.jetbrains.kotlin.konan.file.File -import org.jetbrains.kotlin.konan.target.KonanTarget +import org.jetbrains.kotlin.konan.target.supportsCodeCoverage import org.jetbrains.kotlin.resolve.descriptorUtil.module /** @@ -43,14 +43,14 @@ internal class CoverageManager(val context: Context) { init { if (enabled && !checkRestrictions()) { - context.reportCompilationError("Coverage is only supported for macOS and iOS simulator binaries for now.") + context.reportCompilationError("Coverage is not supported for ${context.config.target}.") } } private fun checkRestrictions(): Boolean { val isKindAllowed = context.config.produce.involvesBitcodeGeneration val target = context.config.target - val isTargetAllowed = target == KonanTarget.MACOS_X64 || target == KonanTarget.IOS_X64 + val isTargetAllowed = target.supportsCodeCoverage() return isKindAllowed && isTargetAllowed } diff --git a/backend.native/tests/build.gradle b/backend.native/tests/build.gradle index f5d139faa51..2452567d945 100644 --- a/backend.native/tests/build.gradle +++ b/backend.native/tests/build.gradle @@ -3756,7 +3756,7 @@ KotlinNativeTestKt.createTest(project, 'harmonyRegexTest', KonanGTest) { task -> standaloneTest("coverage_basic_program") { - disabled = PlatformInfo.getTarget(project).name != "ios_x64" && PlatformInfo.getTarget(project).name != "macos_x64" + disabled = !UtilsKt.testTargetSupportsCodeCoverage(project) description = "Test that `-Xcoverage` generates correct __llvm_coverage information" @@ -3783,7 +3783,7 @@ standaloneTest("coverage_basic_program") { standaloneTest("coverage_basic_library") { - disabled = PlatformInfo.getTarget(project).name != "ios_x64" && PlatformInfo.getTarget(project).name != "macos_x64" + disabled = !UtilsKt.testTargetSupportsCodeCoverage(project) description = "Test that `-Xlibrary-to-cover` generates correct __llvm_coverage information" diff --git a/build-tools/src/main/kotlin/org/jetbrains/kotlin/Utils.kt b/build-tools/src/main/kotlin/org/jetbrains/kotlin/Utils.kt index 50d18c04986..e93a23d96d1 100644 --- a/build-tools/src/main/kotlin/org/jetbrains/kotlin/Utils.kt +++ b/build-tools/src/main/kotlin/org/jetbrains/kotlin/Utils.kt @@ -7,10 +7,7 @@ package org.jetbrains.kotlin import org.gradle.api.Project import org.gradle.api.Task -import org.jetbrains.kotlin.konan.target.AppleConfigurables -import org.jetbrains.kotlin.konan.target.HostManager -import org.jetbrains.kotlin.konan.target.KonanTarget -import org.jetbrains.kotlin.konan.target.PlatformManager +import org.jetbrains.kotlin.konan.target.* import java.io.FileInputStream import java.io.IOException import java.io.File @@ -54,6 +51,9 @@ val Project.globalTestArgs: List fun Project.platformManager() = findProperty("platformManager") as PlatformManager fun Project.testTarget() = findProperty("target") as KonanTarget +fun Project.testTargetSupportsCodeCoverage(): Boolean = + this.testTarget.supportsCodeCoverage() + /** * Ad-hoc signing of the specified path. */ diff --git a/shared/src/main/kotlin/org/jetbrains/kotlin/konan/target/KonanTargetExtenstions.kt b/shared/src/main/kotlin/org/jetbrains/kotlin/konan/target/KonanTargetExtenstions.kt new file mode 100644 index 00000000000..f736d61e3d8 --- /dev/null +++ b/shared/src/main/kotlin/org/jetbrains/kotlin/konan/target/KonanTargetExtenstions.kt @@ -0,0 +1,7 @@ +package org.jetbrains.kotlin.konan.target + +fun KonanTarget.supportsCodeCoverage(): Boolean = + this == KonanTarget.MINGW_X64 || + this == KonanTarget.LINUX_X64 || + this == KonanTarget.MACOS_X64 || + this == KonanTarget.IOS_X64 \ No newline at end of file diff --git a/shared/src/main/kotlin/org/jetbrains/kotlin/konan/target/Linker.kt b/shared/src/main/kotlin/org/jetbrains/kotlin/konan/target/Linker.kt index b056123a14a..cd7b87969f8 100644 --- a/shared/src/main/kotlin/org/jetbrains/kotlin/konan/target/Linker.kt +++ b/shared/src/main/kotlin/org/jetbrains/kotlin/konan/target/Linker.kt @@ -75,10 +75,12 @@ abstract class LinkerFlags(val configurables: Configurables) open val useCompilerDriverAsLinker: Boolean get() = false // TODO: refactor. + // TODO: Number of arguments is quite big. Better to pass args via object. abstract fun linkCommands(objectFiles: List, executable: ExecutableFile, libraries: List, linkerArgs: List, optimize: Boolean, debug: Boolean, - kind: LinkerOutputKind, outputDsymBundle: String): List + kind: LinkerOutputKind, outputDsymBundle: String, + needsProfileLibrary: Boolean): List abstract fun filterStaticLibraries(binaries: List): List @@ -87,6 +89,16 @@ abstract class LinkerFlags(val configurables: Configurables) // Let's just pass them as absolute paths. return libraries } + + protected open fun provideCompilerRtLibrary(libraryName: String): String? { + System.err.println("Can't provide $libraryName.") + return null + } + + // Code coverage requires this library. + protected val profileLibrary: String? by lazy { + provideCompilerRtLibrary("profile") + } } open class AndroidLinker(targetProperties: AndroidConfigurables) @@ -107,7 +119,8 @@ open class AndroidLinker(targetProperties: AndroidConfigurables) override fun linkCommands(objectFiles: List, executable: ExecutableFile, libraries: List, linkerArgs: List, optimize: Boolean, debug: Boolean, - kind: LinkerOutputKind, outputDsymBundle: String): List { + kind: LinkerOutputKind, outputDsymBundle: String, + needsProfileLibrary: Boolean): List { if (kind == LinkerOutputKind.STATIC_LIBRARY) return staticGnuArCommands(ar, executable, objectFiles, libraries) @@ -153,7 +166,7 @@ open class MacOSBasedLinker(targetProperties: AppleConfigurables) get() = this == KonanTarget.TVOS_X64 || this == KonanTarget.IOS_X64 || this == KonanTarget.WATCHOS_X86 || this == KonanTarget.WATCHOS_X64 - private fun provideCompilerRtLibrary(libraryName: String): String? { + override fun provideCompilerRtLibrary(libraryName: String): String? { val prefix = when (target.family) { Family.IOS -> "ios" Family.WATCHOS -> "watchos" @@ -173,15 +186,6 @@ open class MacOSBasedLinker(targetProperties: AppleConfigurables) return if (dir != null) "$dir/lib/darwin/libclang_rt.$mangledLibraryName$prefix$suffix.a" else null } - private val compilerRtLibrary: String? by lazy { - provideCompilerRtLibrary("") - } - - // Code coverage requires this library. - private val profileLibrary: String? by lazy { - provideCompilerRtLibrary("profile") - } - private val osVersionMinFlags: List by lazy { listOf(osVersionMinFlagLd, osVersionMin + ".0") } @@ -191,7 +195,8 @@ open class MacOSBasedLinker(targetProperties: AppleConfigurables) override fun linkCommands(objectFiles: List, executable: ExecutableFile, libraries: List, linkerArgs: List, optimize: Boolean, debug: Boolean, kind: LinkerOutputKind, - outputDsymBundle: String): List { + outputDsymBundle: String, + needsProfileLibrary: Boolean): List { if (kind == LinkerOutputKind.STATIC_LIBRARY) return listOf(Command(libtool).apply { +"-static" @@ -214,7 +219,7 @@ open class MacOSBasedLinker(targetProperties: AppleConfigurables) if (dynamic) +linkerDynamicFlags +linkerKonanFlags if (compilerRtLibrary != null) +compilerRtLibrary!! - if (profileLibrary != null) +profileLibrary!! + if (needsProfileLibrary) +profileLibrary!! +libraries +linkerArgs +rpath(dynamic) @@ -231,6 +236,10 @@ open class MacOSBasedLinker(targetProperties: AppleConfigurables) return result } + private val compilerRtLibrary: String? by lazy { + provideCompilerRtLibrary("") + } + private fun rpath(dynamic: Boolean): List = listOfNotNull( when (target.family) { Family.OSX -> "@executable_path/../Frameworks" @@ -295,12 +304,22 @@ open class LinuxBasedLinker(targetProperties: LinuxBasedConfigurables) private val linker = "$absoluteLlvmHome/bin/ld.lld" private val specificLibs = abiSpecificLibraries.map { "-L${absoluteTargetSysRoot}/$it" } + override fun provideCompilerRtLibrary(libraryName: String): String? { + val targetSuffix = when (target) { + KonanTarget.LINUX_X64 -> "x86_64" + else -> error("$target is not supported.") + } + val dir = File("$absoluteLlvmHome/lib/clang/").listFiles.firstOrNull()?.absolutePath + return if (dir != null) "$dir/lib/linux/libclang_rt.$libraryName-$targetSuffix.a" else null + } + override fun filterStaticLibraries(binaries: List) = binaries.filter { it.isUnixStaticLib } override fun linkCommands(objectFiles: List, executable: ExecutableFile, libraries: List, linkerArgs: List, optimize: Boolean, debug: Boolean, - kind: LinkerOutputKind, outputDsymBundle: String): List { + kind: LinkerOutputKind, outputDsymBundle: String, + needsProfileLibrary: Boolean): List { if (kind == LinkerOutputKind.STATIC_LIBRARY) return staticGnuArCommands(ar, executable, objectFiles, libraries) @@ -331,6 +350,9 @@ open class LinuxBasedLinker(targetProperties: LinuxBasedConfigurables) if (!debug) +linkerNoDebugFlags if (dynamic) +linkerDynamicFlags +objectFiles + // See explanation about `-u__llvm_profile_runtime` here: + // https://github.com/llvm/llvm-project/blob/21e270a479a24738d641e641115bce6af6ed360a/llvm/lib/Transforms/Instrumentation/InstrProfiling.cpp#L930 + if (needsProfileLibrary) +listOf("-u__llvm_profile_runtime", profileLibrary!!) +linkerKonanFlags +listOf("-lgcc", "--as-needed", "-lgcc_s", "--no-as-needed", "-lc", "-lgcc", "--as-needed", "-lgcc_s", "--no-as-needed") @@ -352,10 +374,20 @@ open class MingwLinker(targetProperties: MingwConfigurables) override fun filterStaticLibraries(binaries: List) = binaries.filter { it.isWindowsStaticLib || it.isUnixStaticLib } + override fun provideCompilerRtLibrary(libraryName: String): String? { + val targetSuffix = when (target) { + KonanTarget.MINGW_X64 -> "x86_64" + else -> error("$target is not supported.") + } + val dir = File("$absoluteLlvmHome/lib/clang/").listFiles.firstOrNull()?.absolutePath + return if (dir != null) "$dir/lib/windows/libclang_rt.$libraryName-$targetSuffix.a" else null + } + override fun linkCommands(objectFiles: List, executable: ExecutableFile, libraries: List, linkerArgs: List, optimize: Boolean, debug: Boolean, - kind: LinkerOutputKind, outputDsymBundle: String): List { + kind: LinkerOutputKind, outputDsymBundle: String, + needsProfileLibrary: Boolean): List { if (kind == LinkerOutputKind.STATIC_LIBRARY) return staticGnuArCommands(ar, executable, objectFiles, libraries) @@ -366,10 +398,14 @@ open class MingwLinker(targetProperties: MingwConfigurables) }.apply { +listOf("-o", executable) +objectFiles - if (optimize) +linkerOptimizationFlags + // --gc-sections flag may affect profiling. + // See https://clang.llvm.org/docs/SourceBasedCodeCoverage.html#drawbacks-and-limitations. + // TODO: switching to lld may help. + if (optimize && !needsProfileLibrary) +linkerOptimizationFlags if (!debug) +linkerNoDebugFlags if (dynamic) +linkerDynamicFlags +libraries + if (needsProfileLibrary) +profileLibrary!! +linkerArgs +linkerKonanFlags }) @@ -386,7 +422,8 @@ open class WasmLinker(targetProperties: WasmConfigurables) override fun linkCommands(objectFiles: List, executable: ExecutableFile, libraries: List, linkerArgs: List, optimize: Boolean, debug: Boolean, - kind: LinkerOutputKind, outputDsymBundle: String): List { + kind: LinkerOutputKind, outputDsymBundle: String, + needsProfileLibrary: Boolean): List { if (kind != LinkerOutputKind.EXECUTABLE) throw Error("Unsupported linker output kind") // TODO(horsh): maybe rethink it. @@ -433,7 +470,8 @@ open class ZephyrLinker(targetProperties: ZephyrConfigurables) override fun linkCommands(objectFiles: List, executable: ExecutableFile, libraries: List, linkerArgs: List, optimize: Boolean, debug: Boolean, - kind: LinkerOutputKind, outputDsymBundle: String): List { + kind: LinkerOutputKind, outputDsymBundle: String, + needsProfileLibrary: Boolean): List { if (kind != LinkerOutputKind.EXECUTABLE) throw Error("Unsupported linker output kind: $kind") return listOf(Command(linker).apply { +listOf("-r", "--gc-sections", "--entry", "main")