diff --git a/.gitignore b/.gitignore index 01e2413a384..ea71f7cef8a 100644 --- a/.gitignore +++ b/.gitignore @@ -55,4 +55,7 @@ backend.native/tests/teamcity-test.property # Sample output samples/**/*.kt.bc-build -samples/androidNativeActivity/Polyhedron \ No newline at end of file +samples/androidNativeActivity/Polyhedron + +# CMake +llvmCoverageMappingC/CMakeLists.txt diff --git a/CODE_COVERAGE.md b/CODE_COVERAGE.md new file mode 100644 index 00000000000..e31ccbb757b --- /dev/null +++ b/CODE_COVERAGE.md @@ -0,0 +1,68 @@ +# Code Coverage +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: + * 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. + +### Usage + +#### TL;DR +```bash +kotlinc-native main.kt -Xcoverage +./program.kexe +llvm-profdata merge program.kexe.profraw -o program.profdata +llvm-cov report program.kexe -instr-profile program.profdata +``` + +#### Compiling with coverage enabled + +There are 2 compiler flags that allows to generate coverage information: +* `-Xcoverage`. Generate coverage for immediate sources. +* `-Xlibrary-to-cover=`. Generate coverage for specified `klib`. +Note that library also should be either linked via `-library/-l` compiler option or be a transitive dependency. + +#### Running covered executable + +After the execution of the compiled binary (ex. `program.kexe`) `program.kexe.profraw` will be generated. +By default it will be generated in the same location where binary was created. The are two ways to override this behavior: + * `-Xcoverage-file=` compiler flag. + * `LLVM_PROFILE_FILE` environment variable. So if you run your program like this: +``` +LLVM_PROFILE_FILE=build/program.profraw ./program.kexe +``` +Then the coverage information will be stored to the `build` dir as `program.profraw`. + +#### Parsing `*.profraw` + +Generated file can be parsed with `llvm-profdata` utility. Basic usage: +``` +llvm-profdata merge default.profraw -o program.profdata +``` +See [command guide](http://llvm.org/docs/CommandGuide/llvm-profdata.html) for more options. + +#### Creating reports + +The last step is to create a report from the `program.profdata` file. +It can be done with `llvm-cov` utility (refer to [command guide](http://llvm.org/docs/CommandGuide/llvm-cov.html) for detailed usage). +For example, we can see a basic report using: +``` +llvm-cov report program.kexe -instr-profile program.profdata +``` +Or show a line-by-line coverage information in html: +``` +llvm-cov show program.kexe -instr-profile program.profdata -format=html > report.html +``` + +### Sample +Usually coverage information is collected during running of the tests. +Please refer to `samples/coverage` to see how it can be done. + + +### Useful links +* [LLVM Code Coverage Mapping Format](https://llvm.org/docs/CoverageMappingFormat.html) \ No newline at end of file diff --git a/backend.native/build.gradle b/backend.native/build.gradle index 3be8ce2d15c..9f30a6fcc13 100644 --- a/backend.native/build.gradle +++ b/backend.native/build.gradle @@ -80,11 +80,12 @@ task renamePackage { kotlinNativeInterop { llvm { dependsOn ":llvmDebugInfoC:debugInfoStaticLibrary" + dependsOn ":llvmCoverageMappingC:coverageMappingStaticLibrary" defFile 'llvm.def' if (!project.parent.convention.plugins.platformInfo.isWindows()) compilerOpts "-fPIC" - compilerOpts "-I$llvmDir/include", "-I${project(':llvmDebugInfoC').projectDir}/src/main/include" - linkerOpts "-L$llvmDir/lib", "-L${project(':llvmDebugInfoC').buildDir}/libs/debugInfo/static" + compilerOpts "-I$llvmDir/include", "-I${project(':llvmDebugInfoC').projectDir}/src/main/include", "-I${project(':llvmCoverageMappingC').projectDir}/src/main/include" + linkerOpts "-L$llvmDir/lib", "-L${project(':llvmDebugInfoC').buildDir}/libs/debugInfo/static", "-L${project(':llvmCoverageMappingC').buildDir}/libs/coverageMapping/static" } hash { // TODO: copy-pasted from ':common:compileHash' diff --git a/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2Native.kt b/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2Native.kt index c84d843c4b4..340514e5ac7 100644 --- a/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2Native.kt +++ b/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2Native.kt @@ -195,6 +195,9 @@ class K2Native : CLICompiler() { put(BITCODE_EMBEDDING_MODE, selectBitcodeEmbeddingMode(this, arguments, outputKind)) put(DEBUG_INFO_VERSION, arguments.debugInfoFormatVersion.toInt()) + put(COVERAGE, arguments.coverage) + put(LIBRARIES_TO_COVER, arguments.coveredLibraries.toNonNullList()) + arguments.coverageFile?.let { put(PROFRAW_PATH, it) } } } } diff --git a/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2NativeCompilerArguments.kt b/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2NativeCompilerArguments.kt index 9043303587b..dd38cca08de 100644 --- a/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2NativeCompilerArguments.kt +++ b/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2NativeCompilerArguments.kt @@ -162,6 +162,19 @@ class K2NativeCompilerArguments : CommonCompilerArguments() { @Argument(value = "-Xdebug-info-version", description = "generate debug info of given version (1, 2)") var debugInfoFormatVersion: String = "1" /* command line parser doesn't accept kotlin.Int type */ + @Argument(value = "-Xcoverage", description = "emit coverage") + var coverage: Boolean = false + + @Argument( + value = "-Xlibrary-to-cover", + valueDescription = "", + description = "Path to library that should be covered." + ) + var coveredLibraries: Array? = null + + @Argument(value = "-Xcoverage-file", valueDescription = "", description = "Save coverage information to the given file") + var coverageFile: String? = null + override fun configureAnalysisFlags(collector: MessageCollector): MutableMap, Any> = super.configureAnalysisFlags(collector).also { val useExperimental = it[AnalysisFlags.useExperimental] as List<*> diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CompilerOutput.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CompilerOutput.kt index 04593f1bb0a..42584898ae5 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CompilerOutput.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CompilerOutput.kt @@ -4,9 +4,7 @@ */ package org.jetbrains.kotlin.backend.konan -import llvm.LLVMLinkModules2 -import llvm.LLVMModuleRef -import llvm.LLVMWriteBitcodeToFile +import llvm.* import org.jetbrains.kotlin.backend.konan.library.impl.buildLibrary import org.jetbrains.kotlin.backend.konan.llvm.parseBitcodeFile import org.jetbrains.kotlin.konan.KonanAbiVersion @@ -27,6 +25,22 @@ internal fun produceCStubs(context: Context) { } } +private fun shouldRunBitcodePasses(context: Context): Boolean = + context.coverage.enabled + +internal fun runBitcodePasses(context: Context) { + if (!shouldRunBitcodePasses(context)) { + return + } + val llvmModule = context.llvmModule!! + val passManager = LLVMCreatePassManager()!! + val targetLibraryInfo = LLVMGetTargetLibraryInfo(llvmModule) + LLVMAddTargetLibraryInfo(targetLibraryInfo, passManager) + context.coverage.addLlvmPasses(passManager) + LLVMRunPassManager(passManager, llvmModule) + LLVMDisposePassManager(passManager) +} + internal fun produceOutput(context: Context) { val config = context.config.configuration @@ -44,15 +58,15 @@ internal fun produceOutput(context: Context) { val generatedBitcodeFiles = if (produce == CompilerOutputKind.DYNAMIC || produce == CompilerOutputKind.STATIC) { produceCAdapterBitcode( - context.config.clang, - tempFiles.cAdapterCppName, + context.config.clang, + tempFiles.cAdapterCppName, tempFiles.cAdapterBitcodeName) listOf(tempFiles.cAdapterBitcodeName) } else emptyList() - val nativeLibraries = + val nativeLibraries = context.config.nativeLibraries + - context.config.defaultNativeLibraries + + context.config.defaultNativeLibraries + generatedBitcodeFiles for (library in nativeLibraries) { @@ -75,14 +89,14 @@ internal fun produceOutput(context: Context) { val library = buildLibrary( - context.config.nativeLibraries, + context.config.nativeLibraries, context.config.includeBinaries, neededLibraries, context.serializedLinkData!!, versions, target, output, - libraryName, + libraryName, null, nopack, manifestProperties, diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/Context.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/Context.kt index 1494f03da53..5428271c765 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/Context.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/Context.kt @@ -42,7 +42,6 @@ import org.jetbrains.kotlin.builtins.konan.KonanBuiltIns import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment import org.jetbrains.kotlin.ir.symbols.impl.IrSimpleFunctionSymbolImpl import org.jetbrains.kotlin.konan.target.CompilerOutputKind -import org.jetbrains.kotlin.metadata.konan.KonanProtoBuf import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.psi2ir.generators.GeneratorContext @@ -50,13 +49,12 @@ import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.descriptorUtil.module import org.jetbrains.kotlin.resolve.scopes.MemberScope import org.jetbrains.kotlin.resolve.scopes.receivers.ImplicitClassReceiver -import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedClassDescriptor -import org.jetbrains.kotlin.serialization.deserialization.getName import java.lang.System.out import kotlin.LazyThreadSafetyMode.PUBLICATION import kotlin.reflect.KProperty import org.jetbrains.kotlin.backend.common.ir.copyTo import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExport +import org.jetbrains.kotlin.backend.konan.llvm.coverage.CoverageManager import org.jetbrains.kotlin.ir.symbols.impl.IrTypeParameterSymbolImpl /** @@ -324,6 +322,8 @@ internal class Context(config: KonanConfig) : KonanBackendContext(config) { val cStubsManager = CStubsManager() + val coverage = CoverageManager(this) + lateinit var privateFunctions: List> lateinit var privateClasses: List> diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfigurationKeys.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfigurationKeys.kt index 3be8d039e87..4604acabbe9 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfigurationKeys.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfigurationKeys.kt @@ -106,7 +106,12 @@ class KonanConfigKeys { = CompilerConfigurationKey.create("verbose backend phases") val DEBUG_INFO_VERSION: CompilerConfigurationKey = CompilerConfigurationKey.create("debug info format version") - + val COVERAGE: CompilerConfigurationKey + = CompilerConfigurationKey.create("emit coverage info for sources") + val LIBRARIES_TO_COVER: CompilerConfigurationKey> + = CompilerConfigurationKey.create>("libraries that should be covered") + val PROFRAW_PATH: CompilerConfigurationKey + = CompilerConfigurationKey.create("path to *.profraw coverage output") } } diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/LinkStage.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/LinkStage.kt index 03f5f1d3c79..fa8f74bdc46 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/LinkStage.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/LinkStage.kt @@ -45,6 +45,15 @@ internal class LinkStage(val context: Context) { addAll(elements.filter { !it.isEmpty() }) } + private val exportedSymbols = context.coverage.addExportedSymbols() + + private fun mangleSymbol(symbol: String) = + if (target.family == Family.IOS || target.family == Family.OSX) { + "_$symbol" + } else { + symbol + } + private fun runTool(command: List) = runTool(*command.toTypedArray()) private fun runTool(vararg command: String) = Command(*command) @@ -65,6 +74,8 @@ internal class LinkStage(val context: Context) { } command.addNonEmpty(platform.llvmLtoDynamicFlags) command.addNonEmpty(files) + // Prevent symbols from being deleted by DCE. + command.addNonEmpty(exportedSymbols.map { "-exported-symbol=${mangleSymbol(it)}"} ) runTool(command) return combined @@ -208,6 +219,7 @@ internal class LinkStage(val context: Context) { val objectFiles = mutableListOf() fun makeObjectFiles() { + val bitcodeFiles = listOf(emitted) + libraries.map { it.bitcodePaths }.flatten().filter { it.isBitcode } diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ToplevelPhases.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ToplevelPhases.kt index 64f57c164b9..525eee2c9b7 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ToplevelPhases.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ToplevelPhases.kt @@ -275,6 +275,7 @@ internal val bitcodePhase = namedIrModulePhase( escapeAnalysisPhase then codegenPhase then finalizeDebugInfoPhase then + bitcodePassesPhase then cStubsPhase ) diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/BitcodePhases.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/BitcodePhases.kt index 0977c2628e6..d9554ad903d 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/BitcodePhases.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/BitcodePhases.kt @@ -151,6 +151,16 @@ internal val cStubsPhase = makeKonanModuleOpPhase( op = { context, _ -> produceCStubs(context) } ) +/** + * Runs specific passes over context.llvmModule. The main compilation pipeline + * is performed by [linkPhase]. + */ +internal val bitcodePassesPhase = makeKonanModuleOpPhase( + name = "BitcodePasses", + description = "Run custom LLVM passes over bitcode", + op = { context, _ -> runBitcodePasses(context) } +) + internal val produceOutputPhase = makeKonanModuleOpPhase( name = "ProduceOutput", description = "Produce output", diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt index d26717302a0..733ebb0e101 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt @@ -12,6 +12,7 @@ import org.jetbrains.kotlin.backend.common.ir.ir2string import org.jetbrains.kotlin.backend.konan.* import org.jetbrains.kotlin.backend.konan.descriptors.* import org.jetbrains.kotlin.backend.konan.ir.* +import org.jetbrains.kotlin.backend.konan.llvm.coverage.* import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExport import org.jetbrains.kotlin.backend.konan.optimizations.* import org.jetbrains.kotlin.builtins.KotlinBuiltIns @@ -308,6 +309,8 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map functionGenerationContext.call(function, args) } + private var name:String? = declaration?.name?.asString() override fun genReturn(target: IrSymbolOwner, value: LLVMValueRef?) { @@ -666,6 +673,7 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map body.statements.forEach { generateStatement(it) } is IrExpressionBody -> generateStatement(body.expression) @@ -746,10 +754,18 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map return evaluateTypeOperator (value) is IrCall -> return evaluateCall (value) diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/CoverageInformation.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/CoverageInformation.kt new file mode 100644 index 00000000000..0129b681faa --- /dev/null +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/CoverageInformation.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ +package org.jetbrains.kotlin.backend.konan.llvm.coverage + +import org.jetbrains.kotlin.backend.common.ir.ir2string +import org.jetbrains.kotlin.backend.konan.llvm.column +import org.jetbrains.kotlin.backend.konan.llvm.line +import org.jetbrains.kotlin.backend.konan.llvm.symbolName +import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.declarations.IrFile +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.declarations.name + +/** + * The most important class in the coverage package. + * It describes textual region of the code that is associated with IrElement. + * Besides the obvious [file] and line/column borders, it has [RegionKind] which is described later. + */ +class Region( + val file: IrFile, + val startLine: Int, + val startColumn: Int, + val endLine: Int, + val endColumn: Int, + val kind: RegionKind +) { + + companion object { + fun fromIr(irElement: IrElement, irFile: IrFile, kind: RegionKind) = + fromOffset(irElement.startOffset, irElement.endOffset, irFile, kind) + + fun fromOffset(startOffset: Int, endOffset: Int, irFile: IrFile, kind: RegionKind) = + Region( + irFile, + irFile.fileEntry.line(startOffset), + irFile.fileEntry.column(startOffset), + irFile.fileEntry.line(endOffset), + irFile.fileEntry.column(endOffset), + kind + ) + } + + override fun toString(): String { + val expansion = (kind as? RegionKind.Expansion)?.let { " expand to " + it.expandedFile.name } ?: "" + return "${file.name}$expansion: ${kind::class.simpleName} $startLine, $startColumn -> $endLine, $endColumn" + } +} + +/** + * Describes what is the given code region. + * Based on llvm::coverage::CounterMappingRegion. + * Currently only [RegionKind.Code] is used. + */ +sealed class RegionKind { + /** + * Regular peace of code. + */ + object Code : RegionKind() + /** + * Empty line. + */ + object Gap : RegionKind() + /** + * Region of code that is an expansion of another source file. + * Used for inline function. + */ + class Expansion(val expandedFile: IrFile) : RegionKind() +} + +/** + * "Regional" description of the [function]. + */ +class FunctionRegions( + val function: IrFunction, + val regions: Map +) { + // Enumeration is required for serialization and instrumentation calls. + val regionEnumeration = regions.values.mapIndexed { index, region -> region to index }.toMap() + // Actually, it should be computed. + // But since we don't support PGO structural hash doesn't really matter for now. + val structuralHash: Long = 0 + + override fun toString(): String = buildString { + appendln("${function.symbolName} regions:") + regions.forEach { (irElem, region) -> appendln("${ir2string(irElem)} -> ($region)") } + } +} + +/** + * Since file is the biggest unit in terms of the code coverage + * we aggregate [FunctionRegions] per [file]. + */ +class FileRegionInfo( + val file: IrFile, + val functions: List +) \ No newline at end of file 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 new file mode 100644 index 00000000000..89cc0d07550 --- /dev/null +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/CoverageManager.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ +package org.jetbrains.kotlin.backend.konan.llvm.coverage + +import llvm.LLVMAddInstrProfPass +import llvm.LLVMPassManagerRef +import llvm.LLVMValueRef +import org.jetbrains.kotlin.backend.konan.Context +import org.jetbrains.kotlin.backend.konan.KonanConfigKeys +import org.jetbrains.kotlin.backend.konan.isNativeBinary +import org.jetbrains.kotlin.backend.konan.reportCompilationError +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +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.CompilerOutputKind +import org.jetbrains.kotlin.konan.target.KonanTarget +import org.jetbrains.kotlin.konan.util.removeSuffixIfPresent +import org.jetbrains.kotlin.resolve.descriptorUtil.module + +/** + * "Umbrella" class of all the of the code coverage related logic. + */ +internal class CoverageManager(val context: Context) { + + private val shouldCoverProgram: Boolean = + context.config.configuration.getBoolean(KonanConfigKeys.COVERAGE) + + private val librariesToCover: Set = + context.config.configuration.getList(KonanConfigKeys.LIBRARIES_TO_COVER) + .map { File(it).absolutePath.removeSuffixIfPresent(".klib") } + .toSet() + + private val llvmProfileFilenameGlobal = "__llvm_profile_filename" + + private val defaultOutputFilePath: String by lazy { + "${context.config.outputFile}.profraw" + } + + private val outputFileName: String = + context.config.configuration.get(KonanConfigKeys.PROFRAW_PATH) + ?.let { File(it).absolutePath } + ?: defaultOutputFilePath + + val enabled: Boolean = + shouldCoverProgram || librariesToCover.isNotEmpty() + + init { + if (enabled && !checkRestrictions()) { + context.reportCompilationError("Coverage is only supported for macOS and iOS simulator binaries for now.") + } + } + + private fun checkRestrictions(): Boolean { + val isKindAllowed = with(context.config.produce) { isNativeBinary || this == CompilerOutputKind.BITCODE } + val target = context.config.target + val isTargetAllowed = target == KonanTarget.MACOS_X64 || target == KonanTarget.IOS_X64 + return isKindAllowed && isTargetAllowed + } + + private val filesRegionsInfo = mutableListOf() + + private fun getFunctionRegions(irFunction: IrFunction) = + filesRegionsInfo.flatMap { it.functions }.firstOrNull { it.function == irFunction } + + private val coveredModules: Set by lazy { + val coveredUserCode = if (shouldCoverProgram) setOf(context.moduleDescriptor) else emptySet() + val coveredLibs = context.irModules.filter { it.key in librariesToCover }.values + .map { it.descriptor }.toSet() + coveredLibs + coveredUserCode + } + + private fun fileCoverageFilter(file: IrFile) = + file.packageFragmentDescriptor.module in coveredModules + + /** + * Walk [irModuleFragment] subtree and collect [FileRegionInfo] for files that are part of [coveredModules]. + */ + fun collectRegions(irModuleFragment: IrModuleFragment) { + if (enabled) { + val regions = CoverageRegionCollector(this::fileCoverageFilter).collectFunctionRegions(irModuleFragment) + filesRegionsInfo += regions + } + } + + /** + * @return [LLVMCoverageInstrumentation] instance if [irFunction] should be covered. + */ + fun tryGetInstrumentation(irFunction: IrFunction?, callSitePlacer: (function: LLVMValueRef, args: List) -> Unit) = + if (enabled && irFunction != null) { + getFunctionRegions(irFunction)?.let { LLVMCoverageInstrumentation(context, it, callSitePlacer) } + } else { + null + } + + /** + * Add __llvm_coverage_mapping to the LLVM module. + */ + fun writeRegionInfo() { + if (enabled) { + LLVMCoverageWriter(context, filesRegionsInfo).write() + } + } + + /** + * Add InstrProfilingLegacyPass to the list of llvm passes + */ + fun addLlvmPasses(passManager: LLVMPassManagerRef) { + if (enabled) { + LLVMAddInstrProfPass(passManager, outputFileName) + } + } + + /** + * Since we performing instruction profiling before internalization and global dce + * __llvm_profile_filename need to be added to exported symbols. + */ + fun addExportedSymbols(): List = + if (enabled) { + listOf(llvmProfileFilenameGlobal) + } else { + emptyList() + } +} \ No newline at end of file diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/CoverageRegionCollector.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/CoverageRegionCollector.kt new file mode 100644 index 00000000000..406260646ec --- /dev/null +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/CoverageRegionCollector.kt @@ -0,0 +1,239 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ +package org.jetbrains.kotlin.backend.konan.llvm.coverage + +import org.jetbrains.kotlin.backend.common.pop +import org.jetbrains.kotlin.backend.common.push +import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET +import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.expressions.* +import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid +import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid +import org.jetbrains.kotlin.ir.visitors.acceptVoid + +/** + * Collect all regions in the module. + * @param fileFilter filters files that should be processed. + */ +internal class CoverageRegionCollector(private val fileFilter: (IrFile) -> Boolean) { + + fun collectFunctionRegions(irModuleFragment: IrModuleFragment): List = + irModuleFragment.files + .filter(fileFilter) + .map { file -> + val collector = FunctionsCollector(file) + collector.visitFile(file) + FileRegionInfo(file, collector.functionRegions) + } + + private inner class FunctionsCollector(val file: IrFile) : IrElementVisitorVoid { + + val functionRegions = mutableListOf() + + override fun visitElement(element: IrElement) { + element.acceptChildrenVoid(this) + } + + override fun visitFunction(declaration: IrFunction) { + if (!declaration.isInline && !declaration.isExternal) { + declaration.body?.let { + val regionsCollector = IrFunctionRegionsCollector(fileFilter, file) + regionsCollector.visitBody(it) + if (regionsCollector.regions.isNotEmpty()) { + functionRegions += FunctionRegions(declaration, regionsCollector.regions) + } + } + } + // TODO: Decide how to work with local functions. Should they be process separately? + declaration.acceptChildrenVoid(this) + } + } +} + +/** + * Very similar to [org.jetbrains.kotlin.backend.konan.llvm.CodeGeneratorVisitor] but instead of bitcode generation we collect regions. + * [fileFilter]: specify which files should be processed by code coverage. Here it is required + * for checking calls to inline functions from other files. + * TODO: for now it is very inaccurate. + */ +private class IrFunctionRegionsCollector( + val fileFilter: (IrFile) -> Boolean, + val irFile: IrFile +) : IrElementVisitorVoid { + + private val irFileStack = mutableListOf(irFile) + + private val currentFile: IrFile + get() = irFileStack.last() + + val regions = mutableMapOf() + + override fun visitElement(element: IrElement) { + element.acceptChildrenVoid(this) + } + + override fun visitFunction(declaration: IrFunction) { + } + + override fun visitExpression(expression: IrExpression) { + collectRegions(expression) + } + + override fun visitVariable(declaration: IrVariable) { + declaration.initializer?.let { collectRegions(it) } + } + + override fun visitBody(body: IrBody) = when (body) { + is IrExpressionBody -> body.acceptChildrenVoid(this) + is IrBlockBody -> body.acceptChildrenVoid(this) + else -> error("Unexpected function body type: $body") + } + + fun collectRegions(value: IrExpression): Unit = when (value) { + is IrTypeOperatorCall -> collectTypeOperator(value) + is IrCall -> collectCall(value) + is IrDelegatingConstructorCall -> collectCall(value) + is IrInstanceInitializerCall -> collectInstanceInitializerCall(value) + is IrGetValue -> collectGetValue(value) + is IrSetVariable -> collectSetVariable(value) + is IrGetField -> collectGetField(value) + is IrSetField -> collectSetField(value) + is IrConst<*> -> collectConst(value) + is IrReturn -> collectReturn(value) + is IrWhen -> collectWhen(value) + is IrThrow -> collectThrow(value) + is IrTry -> collectTry(value) + is IrReturnableBlock -> collectReturnableBlock(value) + is IrContainerExpression -> collectContainerExpression(value) + is IrWhileLoop -> collectWhileLoop(value) + is IrDoWhileLoop -> collectDoWhileLoop(value) + is IrVararg -> collectVararg(value) + is IrBreak -> collectBreak(value) + is IrContinue -> collectContinue(value) + is IrGetObjectValue -> collectGetObjectValue(value) + is IrFunctionReference -> collectFunctionReference(value) + is IrSuspendableExpression -> collectSuspendableExpression(value) + is IrSuspensionPoint -> collectSuspensionPoint(value) + else -> { + } + } + + private fun collectInstanceInitializerCall(instanceInitializerCall: IrInstanceInitializerCall) { + + } + + private fun collectGetValue(getValue: IrGetValue) { + recordRegion(getValue) + } + + private fun collectSetVariable(setVariable: IrSetVariable) { + setVariable.value.acceptVoid(this) + } + + private fun collectGetField(getField: IrGetField) { + getField.receiver?.let { collectRegions(it) } + } + + private fun collectSetField(setField: IrSetField) { + collectRegions(setField.value) + setField.receiver?.let { collectRegions(it) } + } + + private fun collectConst(const: IrConst<*>) { + recordRegion(const) + } + + private fun collectReturn(irReturn: IrReturn) { + collectRegions(irReturn.value) + } + + private fun collectWhen(irWhen: IrWhen) { + irWhen.branches.forEach { branch -> + // Do not record location for else branch since it doesn't look correct. + if (branch.condition !is IrConst<*>) { + collectRegions(branch.condition) + } + collectRegions(branch.result) + } + } + + private fun collectThrow(irThrow: IrThrow) { + collectRegions(irThrow.value) + recordRegion(irThrow) + } + + private fun collectTry(irTry: IrTry) { + } + + private fun collectReturnableBlock(returnableBlock: IrReturnableBlock) { + val file = (returnableBlock.sourceFileSymbol?.owner) + if (file != null && file != currentFile && fileFilter(file)) { + recordRegion(returnableBlock) + irFileStack.push(file) + returnableBlock.acceptChildrenVoid(this) + irFileStack.pop() + } + } + + private fun collectContainerExpression(containerExpression: IrContainerExpression) { + containerExpression.acceptChildrenVoid(this) + } + + private fun collectWhileLoop(whileLoop: IrWhileLoop) { + collectRegions(whileLoop.condition) + whileLoop.body?.let { collectRegions(it) } + } + + private fun collectDoWhileLoop(doWhileLoop: IrDoWhileLoop) { + collectRegions(doWhileLoop.condition) + doWhileLoop.body?.let { collectRegions(it) } + } + + private fun collectVararg(vararg: IrVararg) { + vararg.elements.forEach { it.acceptVoid(this) } + } + + private fun collectBreak(irBreak: IrBreak) { + recordRegion(irBreak) + } + + private fun collectContinue(irContinue: IrContinue) { + recordRegion(irContinue) + } + + private fun collectGetObjectValue(getObjectValue: IrGetObjectValue) { + + } + + private fun collectFunctionReference(functionReference: IrFunctionReference) { + + } + + + private fun collectSuspendableExpression(suspendableExpression: IrSuspendableExpression) { + + } + + private fun collectSuspensionPoint(suspensionPoint: IrSuspensionPoint) { + + } + + private fun collectTypeOperator(typeOperatorCall: IrTypeOperatorCall) { + + } + + private fun collectCall(call: IrFunctionAccessExpression) { + recordRegion(call, RegionKind.Code) + call.acceptChildrenVoid(this) + } + + private fun recordRegion(irElement: IrElement, kind: RegionKind = RegionKind.Code) { + if (irElement.startOffset == UNDEFINED_OFFSET || irElement.endOffset == UNDEFINED_OFFSET) { + return + } + regions[irElement] = Region.fromIr(irElement, currentFile, kind) + } +} \ No newline at end of file diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/LLVMCoverageInstrumentation.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/LLVMCoverageInstrumentation.kt new file mode 100644 index 00000000000..be7b6c80fcf --- /dev/null +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/LLVMCoverageInstrumentation.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ +package org.jetbrains.kotlin.backend.konan.llvm.coverage + +import llvm.LLVMConstBitCast +import llvm.LLVMCreatePGOFunctionNameVar +import llvm.LLVMInstrProfIncrement +import llvm.LLVMValueRef +import org.jetbrains.kotlin.backend.konan.Context +import org.jetbrains.kotlin.backend.konan.llvm.* +import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.declarations.IrFunction + +/** + * Places calls to `llvm.instrprof.increment` in the beginning of the each + * region in [functionRegions]. + */ +internal class LLVMCoverageInstrumentation( + override val context: Context, + private val functionRegions: FunctionRegions, + private val callSitePlacer: (function: LLVMValueRef, args: List) -> Unit +) : ContextUtils { + + private val functionNameGlobal = createFunctionNameGlobal(functionRegions.function) + + private val functionHash = Int64(functionRegions.structuralHash).llvm + + fun instrumentIrElement(element: IrElement) { + functionRegions.regions[element]?.let { + placeRegionIncrement(it) + } + } + + /** + * See https://llvm.org/docs/LangRef.html#llvm-instrprof-increment-intrinsic + */ + private fun placeRegionIncrement(region: Region) { + val numberOfRegions = Int32(functionRegions.regions.size).llvm + val regionNumber = Int32(functionRegions.regionEnumeration.getValue(region)).llvm + val args = listOf(functionNameGlobal, functionHash, numberOfRegions, regionNumber) + callSitePlacer(LLVMInstrProfIncrement(context.llvmModule)!!, args) + } + + // Each profiled function should have a global with its name in a specific format. + private fun createFunctionNameGlobal(function: IrFunction): LLVMValueRef { + val name = context.llvmDeclarations.forFunction(function).llvmFunction.name + val pgoFunctionName = LLVMCreatePGOFunctionNameVar(function.llvmFunction, name)!! + return LLVMConstBitCast(pgoFunctionName, int8TypePtr)!! + } +} \ No newline at end of file diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/LLVMCoverageWriter.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/LLVMCoverageWriter.kt new file mode 100644 index 00000000000..4e28b9f5fea --- /dev/null +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/LLVMCoverageWriter.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ +package org.jetbrains.kotlin.backend.konan.llvm.coverage + +import kotlinx.cinterop.* +import llvm.* +import org.jetbrains.kotlin.backend.konan.Context +import org.jetbrains.kotlin.backend.konan.llvm.name +import org.jetbrains.kotlin.backend.konan.llvm.symbolName +import org.jetbrains.kotlin.ir.declarations.IrFile +import org.jetbrains.kotlin.ir.declarations.name +import org.jetbrains.kotlin.ir.declarations.path +import org.jetbrains.kotlin.konan.file.File + +private fun RegionKind.toLLVMCoverageRegionKind(): LLVMCoverageRegionKind = when (this) { + RegionKind.Code -> LLVMCoverageRegionKind.CODE + RegionKind.Gap -> LLVMCoverageRegionKind.GAP + is RegionKind.Expansion -> LLVMCoverageRegionKind.EXPANSION +} + +private fun LLVMCoverageRegion.populateFrom(region: Region, regionId: Int, filesIndex: Map) = apply { + fileId = filesIndex.getValue(region.file) + lineStart = region.startLine + columnStart = region.startColumn + lineEnd = region.endLine + columnEnd = region.endColumn + counterId = regionId + kind = region.kind.toLLVMCoverageRegionKind() + expandedFileId = if (region.kind is RegionKind.Expansion) filesIndex.getValue(region.kind.expandedFile) else 0 +} + +/** + * Writes all of the coverage information to the [org.jetbrains.kotlin.backend.konan.Context.llvmModule]. + * See http://llvm.org/docs/CoverageMappingFormat.html for the format description. + */ +internal class LLVMCoverageWriter( + private val context: Context, + private val filesRegionsInfo: List +) { + fun write() { + if (filesRegionsInfo.isEmpty()) return + + val module = context.llvmModule + ?: error("LLVM module should be initialized.") + val filesIndex = filesRegionsInfo.mapIndexed { index, fileRegionInfo -> fileRegionInfo.file to index }.toMap() + + val coverageGlobal = memScoped { + val (functionMappingRecords, functionCoverages) = filesRegionsInfo.flatMap { it.functions }.map { functionRegions -> + val regions = (functionRegions.regions.values).map { region -> + alloc().populateFrom(region, functionRegions.regionEnumeration.getValue(region), filesIndex).ptr + } + val fileIds = functionRegions.regions.map { filesIndex.getValue(it.value.file) }.toSet().toIntArray() + val functionCoverage = LLVMWriteCoverageRegionMapping( + fileIds.toCValues(), fileIds.size.signExtend(), + regions.toCValues(), regions.size.signExtend()) + + val functionName = context.llvmDeclarations.forFunction(functionRegions.function).llvmFunction.name + val functionMappingRecord = LLVMAddFunctionMappingRecord(LLVMGetModuleContext(context.llvmModule), + functionName, functionRegions.structuralHash, functionCoverage)!! + + Pair(functionMappingRecord, functionCoverage) + }.unzip() + val (filenames, fileIds) = filesIndex.entries.toList().map { File(it.key.path).absolutePath to it.value }.unzip() + val retval = LLVMCoverageEmit(module, functionMappingRecords.toCValues(), functionMappingRecords.size.signExtend(), + filenames.toCStringArray(this), fileIds.toIntArray().toCValues(), fileIds.size.signExtend(), + functionCoverages.map { it }.toCValues(), functionCoverages.size.signExtend())!! + + // TODO: Is there a better way to cleanup fields of T* type in `memScoped`? + functionCoverages.forEach { LLVMFunctionCoverageDispose(it) } + + retval + } + context.llvm.usedGlobals.add(coverageGlobal) + } +} diff --git a/backend.native/llvm.def b/backend.native/llvm.def index 03850b14fbc..541c27faabc 100644 --- a/backend.native/llvm.def +++ b/backend.native/llvm.def @@ -1,7 +1,7 @@ headers = llvm-c/Core.h llvm-c/Target.h llvm-c/Analysis.h llvm-c/BitWriter.h \ - llvm-c/BitReader.h llvm-c/Linker.h DebugInfoC.h + llvm-c/BitReader.h llvm-c/Linker.h DebugInfoC.h CoverageMappingC.h -headerFilter = llvm-c/* DebugInfoC.h +headerFilter = llvm-c/* DebugInfoC.h CoverageMappingC.h compilerOpts = -std=c99 \ -Wall -W -Wno-unused-parameter -Wwrite-strings -Wmissing-field-initializers \ @@ -16,11 +16,12 @@ linkerOpts = -fvisibility-inlines-hidden \ -pedantic -Wno-long-long -Wcovered-switch-default -Wnon-virtual-dtor -Wdelete-non-virtual-dtor \ -std=c++11 \ -DNDEBUG -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS \ - -ldebugInfo -lLLVMTarget -lLLVMMC -lLLVMLinker -lLLVMTransformUtils -lLLVMBitWriter \ + -ldebugInfo -lcoverageMapping -lLLVMCoverage -lLLVMTarget -lLLVMInstrumentation \ + -lLLVMMC -lLLVMLinker -lLLVMTransformUtils -lLLVMBitWriter \ -lLLVMBitReader -lLLVMAnalysis -lLLVMProfileData -lLLVMObject -lLLVMMCParser -lLLVMMC \ -lLLVMCore -lLLVMBinaryFormat -lLLVMSupport -lLLVMDemangle -# ./llvm-config --libs analysis bitreader bitwriter core linker target +# ./llvm-config --libs analysis bitreader bitwriter core linker target coverage linkerOpts.osx = -fPIC \ diff --git a/backend.native/tests/build.gradle b/backend.native/tests/build.gradle index e6b9239c192..de4ad83a8dd 100644 --- a/backend.native/tests/build.gradle +++ b/backend.native/tests/build.gradle @@ -5,6 +5,7 @@ import groovy.json.JsonOutput import org.jetbrains.kotlin.* +import org.jetbrains.kotlin.konan.target.Family import org.jetbrains.kotlin.konan.target.KonanTarget import java.nio.file.Paths @@ -3211,6 +3212,70 @@ task runKonanRegexTests(type: RunKonanTest) { runnerLogger = RunKonanTest.Logger.GTEST } +task coverage_basic_program(type: RunStandaloneKonanTest) { + + disabled = project.testTarget != "ios_x64" && project.testTarget != "macos_x64" + + description = "Test that `-Xcoverage` generates correct __llvm_coverage information" + + def dir = buildDir.absolutePath + createOutputDirectory() + + flags = ["-Xcoverage-file=$dir/program.profraw", "-Xcoverage", "-entry", "coverage.basic.program.main"] + source = "$projectDir/coverage/basic/program/main.kt" + + doLast { + execLlvm("llvm-profdata") { + args "merge" + args "$dir/program.profraw" + args "-o", "$dir/program.profdata" + } + // Should fail in case of corrupted __llvm_coverage. + execLlvm("llvm-cov") { + def program = buildExePath() + def suffix = target.family.exeSuffix + args "report" + args "$program.$suffix" + args "-instr-profile", "$dir/program.profdata" + } + } +} + +task coverage_basic_library(type: RunStandaloneKonanTest) { + + disabled = project.testTarget != "ios_x64" && project.testTarget != "macos_x64" + + description = "Test that `-Xlibrary-to-cover` generates correct __llvm_coverage information" + + def dir = buildDir.absolutePath + createOutputDirectory() + + doFirst { + def konancScript = isWindows() ? "konanc.bat" : "konanc" + def konanc = "$dist/bin/$konancScript" + "$konanc $projectDir/coverage/basic/library/library.kt -p library -o $dir/lib_to_cover".execute().waitFor() + } + + flags = ["-Xcoverage-file=$dir/program.profraw", "-l", "$dir/lib_to_cover", "-Xlibrary-to-cover=$dir/lib_to_cover.klib", "-entry", "coverage.basic.library.main"] + source = "$projectDir/coverage/basic/library/main.kt" + + doLast { + execLlvm("llvm-profdata") { + args "merge" + args "$dir/program.profraw" + args "-o", "$dir/program.profdata" + } + // Should fail in case of corrupted __llvm_coverage. + execLlvm("llvm-cov") { + def program = buildExePath() + def suffix = target.family.exeSuffix + args "report" + args "$program.$suffix" + args "-instr-profile", "$dir/program.profdata" + } + } +} + task buildKonanStdlibTests(type: BuildKonanTest) { outputSourceSetName = "testOutputStdlib" flags = [ "-Xmulti-platform", "-friend-modules", project.rootProject.file("${project.properties['konan.home']}/klib/common/stdlib") ] diff --git a/backend.native/tests/coverage/basic/library/library.kt b/backend.native/tests/coverage/basic/library/library.kt new file mode 100644 index 00000000000..1e47ead0a75 --- /dev/null +++ b/backend.native/tests/coverage/basic/library/library.kt @@ -0,0 +1,3 @@ +package coverage_library + +fun foo() = "foo" \ No newline at end of file diff --git a/backend.native/tests/coverage/basic/library/main.kt b/backend.native/tests/coverage/basic/library/main.kt new file mode 100644 index 00000000000..c62f3636f90 --- /dev/null +++ b/backend.native/tests/coverage/basic/library/main.kt @@ -0,0 +1,5 @@ +package coverage.basic.library + +fun main() { + println(coverage_library.foo()) +} \ No newline at end of file diff --git a/backend.native/tests/coverage/basic/program/main.kt b/backend.native/tests/coverage/basic/program/main.kt new file mode 100644 index 00000000000..f06f146bc65 --- /dev/null +++ b/backend.native/tests/coverage/basic/program/main.kt @@ -0,0 +1,5 @@ +package coverage.basic.program + +fun main() { + println("OK") +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 854c1221ad6..8cbc4a4a0b1 100644 --- a/build.gradle +++ b/build.gradle @@ -92,6 +92,7 @@ void setupClang(Project project) { project.convention.plugins.platformManager = project.rootProject.ext.platformManager project.convention.plugins.execClang = new org.jetbrains.kotlin.ExecClang(project) + project.convention.plugins.execLlvm = new org.jetbrains.kotlin.ExecLlvm(project) project.plugins.withType(NativeComponentPlugin) { project.model { @@ -256,6 +257,10 @@ task distCompiler(type: Copy) { into('konan/nativelib') } + from(project(':llvmCoverageMappingC').file('build/libs/coverageMapping/shared')) { + into('konan/nativelib') + } + from(project(':llvmDebugInfoC').file('build/libs/debugInfo/shared')) { into('konan/nativelib') } diff --git a/buildSrc/plugins/src/main/kotlin/org/jetbrains/kotlin/ExecLlvm.kt b/buildSrc/plugins/src/main/kotlin/org/jetbrains/kotlin/ExecLlvm.kt new file mode 100644 index 00000000000..592ea7e9c2e --- /dev/null +++ b/buildSrc/plugins/src/main/kotlin/org/jetbrains/kotlin/ExecLlvm.kt @@ -0,0 +1,28 @@ +package org.jetbrains.kotlin + +import groovy.lang.Closure +import org.gradle.api.Action +import org.gradle.api.Project +import org.gradle.process.ExecResult +import org.gradle.process.ExecSpec +import org.gradle.util.ConfigureUtil + +internal class ExecLlvm(private val project: Project) { + + fun execLlvm(tool: String, closure: Closure): ExecResult = + this.execLlvm(tool, ConfigureUtil.configureUsing(closure)) + + fun execLlvm(tool: String, action: Action): ExecResult { + val extendedAction = Action { execSpec -> + action.execute(execSpec) + + execSpec.apply { + if (executable == null) { + val llvmDir = project.findProperty("llvmDir") + executable = "$llvmDir/bin/$tool" + } + } + } + return project.exec(extendedAction) + } +} \ No newline at end of file diff --git a/llvmCoverageMappingC/build.gradle b/llvmCoverageMappingC/build.gradle new file mode 100644 index 00000000000..cbf0db0ac7d --- /dev/null +++ b/llvmCoverageMappingC/build.gradle @@ -0,0 +1,56 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply plugin: "cpp" +apply plugin: "c" + +buildscript { + ext.rootBuildDirectory = file('..') + + apply from: "$rootBuildDirectory/gradle/kotlinGradlePlugin.gradle" + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-stdlib:$buildKotlinVersion" + classpath "org.jetbrains.kotlin:kotlin-native-shared:$konanVersion" + } +} +import org.jetbrains.kotlin.konan.target.ClangArgs + +model { + components { + coverageMapping(NativeLibrarySpec) { + sources.cpp.source.srcDirs "src/main/cpp" + + binaries.withType(StaticLibraryBinarySpec) { binary -> + if (!project.parent.convention.plugins.platformInfo.isWindows()) + cppCompiler.args "-fPIC" + cppCompiler.args "--std=c++11", "-I${llvmDir}/include", "-I${projectDir}/src/main/include" + linker.args "-L${llvmDir}/lib", "-lLLVMCore", "-lLLVMSupport" + } + binaries.withType(SharedLibraryBinarySpec) { binary -> + buildable = false + } + } + } + + toolChains { + clang(Clang) { + eachPlatform { + cppCompiler.withArguments(ClangArgs.&filterGradleNativeSoftwareFlags) + } + } + } +} diff --git a/llvmCoverageMappingC/src/main/cpp/CoverageMappingC.cpp b/llvmCoverageMappingC/src/main/cpp/CoverageMappingC.cpp new file mode 100644 index 00000000000..d6ce53c3c7b --- /dev/null +++ b/llvmCoverageMappingC/src/main/cpp/CoverageMappingC.cpp @@ -0,0 +1,230 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +#include "CoverageMappingC.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace llvm; +using namespace llvm::coverage; + +namespace llvm { + DEFINE_SIMPLE_CONVERSION_FUNCTIONS(TargetLibraryInfoImpl, LLVMTargetLibraryInfoRef) +} + +struct LLVMFunctionCoverage { + explicit LLVMFunctionCoverage(std::string coverageData) : coverageData(std::move(coverageData)) {} + + std::string coverageData; +}; + +static coverage::CounterMappingRegion::RegionKind determineRegionKind(const struct LLVMCoverageRegion& region) { + switch (region.kind) { + case LLVMCoverageRegionKind::CODE: + return coverage::CounterMappingRegion::RegionKind::CodeRegion; + case LLVMCoverageRegionKind::GAP: + return coverage::CounterMappingRegion::RegionKind::GapRegion; + case LLVMCoverageRegionKind::EXPANSION: + return coverage::CounterMappingRegion::RegionKind::ExpansionRegion; + } +} + +static coverage::CounterMappingRegion createCounterMappingRegion(struct LLVMCoverageRegion& region) { + auto regionKind = determineRegionKind(region); + int expandedFileId = 0; + if (regionKind == coverage::CounterMappingRegion::RegionKind::ExpansionRegion) { + expandedFileId = region.expandedFileId; + } + const Counter &counter = coverage::Counter::getCounter(region.counterId); + return coverage::CounterMappingRegion(counter, region.fileId, expandedFileId, region.lineStart, + region.columnStart, region.lineEnd, region.columnEnd, regionKind); +} + +LLVMFunctionCoverage* LLVMWriteCoverageRegionMapping(unsigned int *fileIdMapping, size_t fileIdMappingSize, + struct LLVMCoverageRegion **mappingRegions, size_t mappingRegionsSize) { + std::vector counterMappingRegions; + for (size_t i = 0; i < mappingRegionsSize; ++i) { + struct LLVMCoverageRegion region = *mappingRegions[i]; + counterMappingRegions.emplace_back(createCounterMappingRegion(region)); + } + CoverageMappingWriter writer(ArrayRef(fileIdMapping, fileIdMappingSize), None, counterMappingRegions); + std::string CoverageMapping; + raw_string_ostream OS(CoverageMapping); + writer.write(OS); + OS.flush(); + // Should be disposed with `LLVMFunctionCoverageDispose`. + return new LLVMFunctionCoverage(CoverageMapping); +} + +void LLVMFunctionCoverageDispose(struct LLVMFunctionCoverage* functionCoverage) { + delete functionCoverage; +} + +static StructType *getFunctionRecordTy(LLVMContext &Ctx) { +#define COVMAP_FUNC_RECORD(Type, LLVMType, Name, Init) LLVMType, + Type *FunctionRecordTypes[] = { +#include "llvm/ProfileData/InstrProfData.inc" + }; + StructType *FunctionRecordTy = StructType::get(Ctx, makeArrayRef(FunctionRecordTypes), true); + return FunctionRecordTy; +} + +static llvm::Constant *addFunctionMappingRecord(llvm::LLVMContext &Ctx, StringRef NameValue, uint64_t FuncHash, + const std::string &CoverageMapping) { + llvm::StructType *FunctionRecordTy = getFunctionRecordTy(Ctx); + +#define COVMAP_FUNC_RECORD(Type, LLVMType, Name, Init) Init, + llvm::Constant *FunctionRecordVals[] = { +#include "llvm/ProfileData/InstrProfData.inc" + }; + return llvm::ConstantStruct::get(FunctionRecordTy, makeArrayRef(FunctionRecordVals)); +} + +// See https://github.com/llvm/llvm-project/blob/fa8fa044ec46b94e64971efa8852df0d58114062/clang/lib/CodeGen/CoverageMappingGen.cpp#L1284. +LLVMValueRef LLVMAddFunctionMappingRecord(LLVMContextRef context, const char *name, uint64_t hash, + struct LLVMFunctionCoverage *coverageMapping) { + return llvm::wrap(addFunctionMappingRecord(*llvm::unwrap(context), name, hash, coverageMapping->coverageData)); +} + +// See https://github.com/llvm/llvm-project/blob/fa8fa044ec46b94e64971efa8852df0d58114062/clang/lib/CodeGen/CoverageMappingGen.cpp#L1335. +// Please note that llvm/ProfileData/InstrProfData.inc refers to variable names of the function that includes it. So be careful with renaming. +static llvm::GlobalVariable* emitCoverageGlobal( + llvm::LLVMContext &Ctx, + llvm::Module &module, + std::vector &FunctionRecords, + const llvm::SmallVector &FilenameRefs, + const std::string &RawCoverageMappings) { + + auto *Int32Ty = llvm::Type::getInt32Ty(Ctx); + + std::string FilenamesAndCoverageMappings; + llvm::raw_string_ostream outputStream(FilenamesAndCoverageMappings); + CoverageFilenamesSectionWriter(FilenameRefs).write(outputStream); + outputStream << RawCoverageMappings; + size_t CoverageMappingSize = RawCoverageMappings.size(); + size_t FilenamesSize = outputStream.str().size() - CoverageMappingSize; + + // See https://llvm.org/docs/CoverageMappingFormat.html#llvm-ir-representation + // + // > Coverage mapping data which is an array of bytes. Zero paddings are added at the end to force 8 byte alignment. + // + if (size_t rem = outputStream.str().size() % 8) { + CoverageMappingSize += 8 - rem; + for (size_t i = 0; i < 8 - rem; ++i) { + outputStream << '\0'; + } + } + + StructType *functionRecordTy = getFunctionRecordTy(Ctx); + // Create the deferred function records array + auto functionRecordsTy = llvm::ArrayType::get(functionRecordTy, FunctionRecords.size()); + auto functionRecordsVal = llvm::ConstantArray::get(functionRecordsTy, FunctionRecords); + + llvm::Type *CovDataHeaderTypes[] = { +#define COVMAP_HEADER(Type, LLVMType, Name, Init) LLVMType, + +#include "llvm/ProfileData/InstrProfData.inc" + }; + auto CovDataHeaderTy = llvm::StructType::get(Ctx, makeArrayRef(CovDataHeaderTypes)); + llvm::Constant *CovDataHeaderVals[] = { +#define COVMAP_HEADER(Type, LLVMType, Name, Init) Init, + +#include "llvm/ProfileData/InstrProfData.inc" + }; + auto covDataHeaderVal = llvm::ConstantStruct::get(CovDataHeaderTy, makeArrayRef(CovDataHeaderVals)); + + auto *filenamesAndMappingsVal = llvm::ConstantDataArray::getString(Ctx, outputStream.str(), false); + // Create the coverage data record + llvm::Type *covDataTypes[] = {CovDataHeaderTy, functionRecordsTy, filenamesAndMappingsVal->getType()}; + auto covDataTy = llvm::StructType::get(Ctx, makeArrayRef(covDataTypes)); + + llvm::Constant *TUDataVals[] = {covDataHeaderVal, functionRecordsVal, filenamesAndMappingsVal}; + auto covDataVal = llvm::ConstantStruct::get(covDataTy, makeArrayRef(TUDataVals)); + // Will be deleted when module is disposed. + return new llvm::GlobalVariable(module, covDataTy, true, llvm::GlobalValue::InternalLinkage, + covDataVal, llvm::getCoverageMappingVarName()); +} + +static std::string createRawCoverageMapping(struct LLVMFunctionCoverage **functionCoverages, size_t functionCoveragesSize) { + std::vector coverageMappings; + for (size_t i = 0; i < functionCoveragesSize; ++i) { + coverageMappings.emplace_back(functionCoverages[i]->coverageData); + } + return llvm::join(coverageMappings.begin(), coverageMappings.end(), ""); +} + +LLVMValueRef LLVMCoverageEmit(LLVMModuleRef moduleRef, + LLVMValueRef *records, size_t recordsSize, + const char **filenames, int *filenamesIndices, size_t filenamesSize, + struct LLVMFunctionCoverage **functionCoverages, size_t functionCoveragesSize) { + LLVMContext &ctx = *unwrap(LLVMGetModuleContext(moduleRef)); + Module &module = *unwrap(moduleRef); + + std::vector functionRecords; + for (size_t i = 0; i < recordsSize; ++i) { + functionRecords.push_back(dyn_cast(unwrap(records[i]))); + } + llvm::SmallVector filenameRefs; + filenameRefs.resize(filenamesSize); + for (size_t i = 0; i < filenamesSize; ++i) { + if (sys::path::is_absolute(filenames[i])) { + filenameRefs[filenamesIndices[i]] = filenames[i]; + } else { + SmallString<256> path(filenames[i]); + sys::fs::make_absolute(path); + sys::path::remove_dots(path, true); + filenameRefs[filenamesIndices[i]] = path; + } + } + const std::string &rawCoverageMappings = createRawCoverageMapping(functionCoverages, functionCoveragesSize); + GlobalVariable *coverageGlobal = emitCoverageGlobal(ctx, module, functionRecords, filenameRefs, rawCoverageMappings); + + const std::string §ion = getInstrProfSectionName(IPSK_covmap, Triple(module.getTargetTriple()).getObjectFormat()); + coverageGlobal->setSection(section); + coverageGlobal->setAlignment(8); + return wrap(coverageGlobal); +} + +LLVMValueRef LLVMInstrProfIncrement(LLVMModuleRef moduleRef) { + Module &module = *unwrap(moduleRef); + return wrap(Intrinsic::getDeclaration(&module, Intrinsic::instrprof_increment, None)); +} + +LLVMValueRef LLVMCreatePGOFunctionNameVar(LLVMValueRef llvmFunction, const char *pgoFunctionName) { + auto *fnPtr = cast(unwrap(llvmFunction)); + return wrap(createPGOFuncNameVar(*fnPtr, pgoFunctionName)); +} + +void LLVMAddInstrProfPass(LLVMPassManagerRef passManagerRef, const char* outputFileName) { + legacy::PassManagerBase *passManager = unwrap(passManagerRef); + InstrProfOptions options; + options.InstrProfileOutput = outputFileName; + passManager->add(createInstrProfilingLegacyPass(options)); +} + +LLVMTargetLibraryInfoRef LLVMGetTargetLibraryInfo(LLVMModuleRef moduleRef) { + auto* libraryInfo = new TargetLibraryInfoImpl(Triple(unwrap(moduleRef)->getTargetTriple())); + return llvm::wrap(libraryInfo); +} \ No newline at end of file diff --git a/llvmCoverageMappingC/src/main/include/CoverageMappingC.h b/llvmCoverageMappingC/src/main/include/CoverageMappingC.h new file mode 100644 index 00000000000..5cfaf09032b --- /dev/null +++ b/llvmCoverageMappingC/src/main/include/CoverageMappingC.h @@ -0,0 +1,81 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +#ifndef __COVERAGE_MAPPING_C_H__ +# define __COVERAGE_MAPPING_C_H__ + +#include +#include + + +# ifdef __cplusplus +extern "C" { +# endif + +/** + * See org.jetbrains.kotlin.backend.konan.llvm.coverage.RegionKind. + */ +enum LLVMCoverageRegionKind { + CODE, + GAP, + EXPANSION +}; + +/** + * See org.jetbrains.kotlin.backend.konan.llvm.coverage.Region. + */ +struct LLVMCoverageRegion { + int fileId; + int lineStart; + int columnStart; + int lineEnd; + int columnEnd; + int counterId; + int expandedFileId; + enum LLVMCoverageRegionKind kind; +}; + +struct LLVMFunctionCoverage; + +/** + * Add record in the following format: https://llvm.org/docs/CoverageMappingFormat.html#function-record. + */ +LLVMValueRef +LLVMAddFunctionMappingRecord(LLVMContextRef context, const char *name, uint64_t hash, struct LLVMFunctionCoverage* coverageMapping); + +/** + * Wraps creation of coverage::CoverageMappingWriter and call to coverage::CoverageMappingWriter::write. + */ +struct LLVMFunctionCoverage* LLVMWriteCoverageRegionMapping(unsigned int *fileIdMapping, size_t fileIdMappingSize, + struct LLVMCoverageRegion **mappingRegions, size_t mappingRegionsSize); + +void LLVMFunctionCoverageDispose(struct LLVMFunctionCoverage* functionCoverage); + +/** + * Create __llvm_coverage_mapping global. + */ +LLVMValueRef LLVMCoverageEmit(LLVMModuleRef moduleRef, LLVMValueRef *records, size_t recordsSize, + const char **filenames, int *filenamesIndices, size_t filenamesSize, + struct LLVMFunctionCoverage** functionCoverages, size_t functionCoveragesSize); + +/** + * Wrapper for `llvm.instrprof.increment` declaration. + */ +LLVMValueRef LLVMInstrProfIncrement(LLVMModuleRef moduleRef); + +/** + * Wrapper for llvm::createPGOFuncNameVar. + */ +LLVMValueRef LLVMCreatePGOFunctionNameVar(LLVMValueRef llvmFunction, const char *pgoFunctionName); + +void LLVMAddInstrProfPass(LLVMPassManagerRef passManagerRef, const char* outputFileName); + +LLVMTargetLibraryInfoRef LLVMGetTargetLibraryInfo(LLVMModuleRef moduleRef); + +# ifdef __cplusplus +} + +# endif +#endif diff --git a/llvmDebugInfoC/build.gradle b/llvmDebugInfoC/build.gradle index 9c0fae26f74..58009e11305 100644 --- a/llvmDebugInfoC/build.gradle +++ b/llvmDebugInfoC/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright 2010-2017 JetBrains s.r.o. + * Copyright 2010-2019 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/samples/coverage/README.md b/samples/coverage/README.md new file mode 100644 index 00000000000..25155d97f49 --- /dev/null +++ b/samples/coverage/README.md @@ -0,0 +1,12 @@ +# Code Coverage usage sample + +This example shows how to collect coverage information during execution of the test suite. +Please note that this functionality will be incorporated into Gradle plugin so you won't need to do it by hand in the nearest future. + +### Prerequisites +`createCoverageReport` task requires `llvm-profdata` and `llvm-cov` to be added to the `$PATH`. +They can be found in the Kotlin/Native dependencies dir. By default it should look like +`$HOME/.konan/dependencies/clang-llvm-6.0.1-darwin-macos/bin` + +### Usage +Just run `createCoverageReport` task. diff --git a/samples/coverage/build.gradle.kts b/samples/coverage/build.gradle.kts new file mode 100644 index 00000000000..e5645c2113d --- /dev/null +++ b/samples/coverage/build.gradle.kts @@ -0,0 +1,50 @@ +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + +plugins { + id("org.jetbrains.kotlin.multiplatform") +} + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(kotlin("stdlib-common")) + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + } + + macosX64("macos") { + binaries { + executable(listOf(DEBUG)) { + entryPoint = "coverage.main" + } + } + binaries.getExecutable("test", DEBUG).apply { + freeCompilerArgs = mutableListOf( + "-Xlibrary-to-cover=${compilations["main"].output.classesDirs.singleFile.absolutePath}" + ) + } + } +} + +tasks.create("createCoverageReport") { + dependsOn("macosTest") + + description = "Create coverage report" + + doLast { + val testDebugBinary = kotlin.targets["macos"].let { it as KotlinNativeTarget }.binaries.getExecutable("test", "debug").outputFile + exec { + commandLine("llvm-profdata", "merge", "$testDebugBinary.profraw", "-o", "$testDebugBinary.profdata") + } + exec { + commandLine("llvm-cov", "show", "$testDebugBinary", "-instr-profile", "$testDebugBinary.profdata") + } + } +} \ No newline at end of file diff --git a/samples/coverage/gradle.properties b/samples/coverage/gradle.properties new file mode 100644 index 00000000000..29e08e8ca88 --- /dev/null +++ b/samples/coverage/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official \ No newline at end of file diff --git a/samples/coverage/src/commonMain/kotlin/bar.kt b/samples/coverage/src/commonMain/kotlin/bar.kt new file mode 100644 index 00000000000..c15d0ce71bc --- /dev/null +++ b/samples/coverage/src/commonMain/kotlin/bar.kt @@ -0,0 +1,17 @@ +fun baz(args: Array) { + if (args.size > 4) { + println("Too many") + } else { + println("Fine") + } +} + +class A { + init { + println("A::init") + } + + fun f() { + println("A::f") + } +} \ No newline at end of file diff --git a/samples/coverage/src/commonMain/kotlin/main.kt b/samples/coverage/src/commonMain/kotlin/main.kt new file mode 100644 index 00000000000..546840df5bc --- /dev/null +++ b/samples/coverage/src/commonMain/kotlin/main.kt @@ -0,0 +1,3 @@ +fun main() { + baz(arrayOf("abc")) +} \ No newline at end of file diff --git a/samples/coverage/src/commonTest/kotlin/Tests.kt b/samples/coverage/src/commonTest/kotlin/Tests.kt new file mode 100644 index 00000000000..fd7d78c0bdb --- /dev/null +++ b/samples/coverage/src/commonTest/kotlin/Tests.kt @@ -0,0 +1,15 @@ +import kotlin.test.Test +import kotlin.test.assertTrue + +class CoverageTests { + @Test + fun testHello() { + main() + } + + @Test + fun testA() { + val a = A() + a.f() + } +} \ No newline at end of file diff --git a/samples/settings.gradle b/samples/settings.gradle index 20a09b39051..989491952c4 100644 --- a/samples/settings.gradle +++ b/samples/settings.gradle @@ -44,6 +44,7 @@ if (MPPTools.isMacos()) { include ':opengl' include ':symbolication' include ':uikit' + include ':coverage' } if (MPPTools.isWindows()) { diff --git a/settings.gradle b/settings.gradle index 1b53d891f0e..fb8f025ee24 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,6 +19,7 @@ include ':Interop:Indexer' include ':Interop:JsRuntime' include ':Interop:StubGenerator' include ':Interop:Runtime' +include ':llvmCoverageMappingC' include ':llvmDebugInfoC' include ':libclangext' include ':klib' diff --git a/shared/src/main/kotlin/org/jetbrains/kotlin/konan/exec/ExecuteCommand.kt b/shared/src/main/kotlin/org/jetbrains/kotlin/konan/exec/ExecuteCommand.kt index b40392f83e3..6d085468442 100644 --- a/shared/src/main/kotlin/org/jetbrains/kotlin/konan/exec/ExecuteCommand.kt +++ b/shared/src/main/kotlin/org/jetbrains/kotlin/konan/exec/ExecuteCommand.kt @@ -106,6 +106,6 @@ open class Command(initialCommand: List) { } private fun log() { - if (logger != null) logger!! { command.toList().joinToString(" ") } + if (logger != null) logger!! { command.joinToString(" ") } } } 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 e5ab5570056..be8818ad682 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 @@ -133,15 +133,32 @@ open class MacOSBasedLinker(targetProperties: AppleConfigurables) private val libtool = "$absoluteTargetToolchain/usr/bin/libtool" private val linker = "$absoluteTargetToolchain/usr/bin/ld" private val dsymutil = "$absoluteLlvmHome/bin/llvm-dsymutil" - private val compilerRtLibrary: String? by lazy { - val suffix = when (configurables.target.family) { + + private fun provideCompilerRtLibrary(libraryName: String): String? { + val suffix = if (libraryName.isNotEmpty() && target == KonanTarget.IOS_X64) { + "iossim" + } else { + when (val family = configurables.target.family) { Family.OSX -> "osx" Family.IOS -> "ios" - else -> TODO() + else -> error("Family $family is unsupported") } - val dir = File("$absoluteTargetToolchain/usr/lib/clang/").listFiles.firstOrNull()?.absolutePath - if (dir != null) "$dir/lib/darwin/libclang_rt.$suffix.a" else null } + val dir = File("$absoluteTargetToolchain/usr/lib/clang/").listFiles.firstOrNull()?.absolutePath + val mangledName = if (libraryName.isEmpty()) "" else "${libraryName}_" + + return if (dir != null) "$dir/lib/darwin/libclang_rt.$mangledName$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") } @@ -171,6 +188,7 @@ open class MacOSBasedLinker(targetProperties: AppleConfigurables) if (dynamic) +linkerDynamicFlags +linkerKonanFlags if (compilerRtLibrary != null) +compilerRtLibrary!! + if (profileLibrary != null) +profileLibrary!! +libraries +linkerArgs +rpath(dynamic)