[K/N] XCTest support: Make compiler produce bundles

XCTest test binary is a bundle plug-in that is similar to a framework.
This is a part of ^KT-58928

Merge-request: KT-MR-10662
Merged-by: Pavel Punegov <Pavel.Punegov@jetbrains.com>
This commit is contained in:
Pavel Punegov
2023-06-22 13:23:17 +00:00
committed by Space Team
parent 92bcf3b2d5
commit 1ad0a662fd
7 changed files with 123 additions and 23 deletions
@@ -25,6 +25,7 @@ val KonanConfig.isFinalBinary: Boolean get() = when (this.produce) {
CompilerOutputKind.DYNAMIC_CACHE, CompilerOutputKind.STATIC_CACHE,
CompilerOutputKind.LIBRARY, CompilerOutputKind.BITCODE -> false
CompilerOutputKind.FRAMEWORK -> !omitFrameworkBinary
CompilerOutputKind.TEST_BUNDLE -> true
else -> error("not supported: ${this.produce}")
}
@@ -18,6 +18,7 @@ internal fun determineLinkerOutput(context: PhaseContext): LinkerOutputKind =
val staticFramework = context.config.produceStaticFramework
if (staticFramework) LinkerOutputKind.STATIC_LIBRARY else LinkerOutputKind.DYNAMIC_LIBRARY
}
CompilerOutputKind.TEST_BUNDLE,
CompilerOutputKind.DYNAMIC_CACHE,
CompilerOutputKind.DYNAMIC -> LinkerOutputKind.DYNAMIC_LIBRARY
CompilerOutputKind.STATIC_CACHE,
@@ -91,31 +92,44 @@ internal class Linker(
val additionalLinkerArgs: List<String>
val executable: String
if (config.produce != CompilerOutputKind.FRAMEWORK) {
additionalLinkerArgs = if (target.family.isAppleFamily) {
when (config.produce) {
CompilerOutputKind.DYNAMIC_CACHE ->
listOf("-install_name", outputFiles.dynamicCacheInstallName)
else -> listOf("-dead_strip")
when (config.produce) {
CompilerOutputKind.TEST_BUNDLE -> {
val bundleDir = File(outputFile)
val name = bundleDir.name.removeSuffix(config.produce.suffix())
require(target.family.isAppleFamily)
val bundleRelativePath = if (target.family == Family.OSX) "Contents/MacOS/$name" else name
additionalLinkerArgs = listOf("-bundle")
val bundlePath = bundleDir.child(bundleRelativePath)
bundlePath.parentFile.mkdirs()
executable = bundlePath.absolutePath
}
CompilerOutputKind.FRAMEWORK -> {
val framework = File(outputFile)
val dylibName = framework.name.removeSuffix(".framework")
val dylibRelativePath = when (target.family) {
Family.IOS,
Family.TVOS,
Family.WATCHOS -> dylibName
Family.OSX -> "Versions/A/$dylibName"
else -> error(target)
}
} else {
emptyList()
additionalLinkerArgs = listOf("-dead_strip", "-install_name", "@rpath/${framework.name}/$dylibRelativePath")
val dylibPath = framework.child(dylibRelativePath)
dylibPath.parentFile.mkdirs()
executable = dylibPath.absolutePath
}
executable = outputFiles.nativeBinaryFile
} else {
val framework = File(outputFile)
val dylibName = framework.name.removeSuffix(".framework")
val dylibRelativePath = when (target.family) {
Family.IOS,
Family.TVOS,
Family.WATCHOS -> dylibName
Family.OSX -> "Versions/A/$dylibName"
else -> error(target)
else -> {
additionalLinkerArgs = if (target.family.isAppleFamily) {
when (config.produce) {
CompilerOutputKind.DYNAMIC_CACHE ->
listOf("-install_name", outputFiles.dynamicCacheInstallName)
else -> listOf("-dead_strip")
}
} else {
emptyList()
}
executable = outputFiles.nativeBinaryFile
}
additionalLinkerArgs = listOf("-dead_strip", "-install_name", "@rpath/${framework.name}/$dylibRelativePath")
val dylibPath = framework.child(dylibRelativePath)
dylibPath.parentFile.mkdirs()
executable = dylibPath.absolutePath
}
File(executable).delete()
@@ -45,6 +45,7 @@ internal class DynamicCompilerDriver : CompilerDriver() {
CompilerOutputKind.DYNAMIC_CACHE -> produceBinary(engine, config, environment)
CompilerOutputKind.STATIC_CACHE -> produceBinary(engine, config, environment)
CompilerOutputKind.PRELIMINARY_CACHE -> TODO()
CompilerOutputKind.TEST_BUNDLE -> produceBundle(engine, config, environment)
}
}
}
@@ -154,6 +155,26 @@ internal class DynamicCompilerDriver : CompilerDriver() {
}
}
/**
* Produce a bundle that is a directory with code and resources.
* It consists of
* - Info.plist
* - Binary without an entry point.
*
* See https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/AboutBundles/AboutBundles.html
*/
private fun produceBundle(engine: PhaseEngine<PhaseContext>, config: KonanConfig, environment: KotlinCoreEnvironment) {
require(config.target.family.isAppleFamily)
require(config.produce == CompilerOutputKind.TEST_BUNDLE)
val frontendOutput = engine.runFrontend(config, environment) ?: return
engine.runPhase(CreateTestBundlePhase, frontendOutput)
val psiToIrOutput = engine.runPsiToIr(frontendOutput, isProducingLibrary = false)
require(psiToIrOutput is PsiToIrOutput.ForBackend)
val backendContext = createBackendContext(config, frontendOutput, psiToIrOutput)
engine.runBackend(backendContext, psiToIrOutput.irModule)
}
private fun createBackendContext(
config: KonanConfig,
frontendOutput: FrontendPhaseOutput.Full,
@@ -7,6 +7,7 @@ package org.jetbrains.kotlin.backend.konan.driver.phases
import org.jetbrains.kotlin.backend.common.lower
import org.jetbrains.kotlin.backend.konan.NativeGenerationState
import org.jetbrains.kotlin.backend.konan.OutputFiles
import org.jetbrains.kotlin.backend.konan.driver.PhaseContext
import org.jetbrains.kotlin.backend.konan.driver.PhaseEngine
import org.jetbrains.kotlin.backend.konan.driver.utilities.KotlinBackendIrHolder
@@ -17,6 +18,7 @@ import org.jetbrains.kotlin.backend.konan.lower.SpecialBackendChecksTraversal
import org.jetbrains.kotlin.backend.konan.makeEntryPoint
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.backend.konan.objcexport.createTestBundle
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
import org.jetbrains.kotlin.ir.util.NaiveSourceBasedFileEntryImpl
import org.jetbrains.kotlin.ir.util.addChild
@@ -87,4 +89,13 @@ internal val EntryPointPhase = createSimpleNamedCompilerPhase<NativeGenerationSt
}
file.addChild(makeEntryPoint(context))
}
internal val CreateTestBundlePhase = createSimpleNamedCompilerPhase<PhaseContext, FrontendPhaseOutput.Full>(
"CreateTestBundlePhase",
"Create XCTest bundle"
) { context, input ->
val config = context.config
val output = OutputFiles(config.outputPath, config.target, config.produce).mainFile
createTestBundle(config, input.moduleDescriptor, output)
}
@@ -0,0 +1,40 @@
/*
* 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.backend.konan.objcexport
import org.jetbrains.kotlin.backend.konan.KonanConfig
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.konan.file.File
import org.jetbrains.kotlin.konan.target.Family
/**
* Builds Apple bundle directory.
*/
internal class BundleBuilder(
private val config: KonanConfig,
private val infoPListBuilder: InfoPListBuilder,
private val mainPackageGuesser: MainPackageGuesser,
) {
fun build(
moduleDescriptor: ModuleDescriptor,
bundleDirectory: File,
name: String,
) {
val target = config.target
val bundleContents = when (target.family) {
Family.IOS,
Family.WATCHOS,
Family.TVOS -> bundleDirectory
Family.OSX -> bundleDirectory.child("Contents")
else -> error(target)
}.apply { mkdirs() }
bundleContents.child("Info.plist").run {
val infoPlistContents = infoPListBuilder.build(name, mainPackageGuesser, moduleDescriptor)
writeBytes(infoPlistContents.toByteArray())
}
}
}
@@ -74,7 +74,7 @@ internal fun createObjCFramework(
exportedInterface: ObjCExportedInterface,
frameworkDirectory: File
) {
val frameworkName = frameworkDirectory.name.removeSuffix(".framework")
val frameworkName = frameworkDirectory.name.removeSuffix(CompilerOutputKind.FRAMEWORK.suffix())
val frameworkBuilder = FrameworkBuilder(
config,
infoPListBuilder = InfoPListBuilder(config),
@@ -91,6 +91,16 @@ internal fun createObjCFramework(
)
}
internal fun createTestBundle(
config: KonanConfig,
moduleDescriptor: ModuleDescriptor,
bundleDirectory: File
) {
val name = bundleDirectory.name.removeSuffix(CompilerOutputKind.TEST_BUNDLE.suffix())
BundleBuilder(config, infoPListBuilder = InfoPListBuilder(config), mainPackageGuesser = MainPackageGuesser())
.build(moduleDescriptor, bundleDirectory, name)
}
// TODO: No need for such class in dynamic driver.
internal class ObjCExport(
private val generationState: NativeGenerationState,
@@ -26,6 +26,9 @@ enum class CompilerOutputKind {
BITCODE {
override fun suffix(target: KonanTarget?) = ".bc"
},
TEST_BUNDLE {
override fun suffix(target: KonanTarget?): String = ".xctest"
},
DYNAMIC_CACHE {
override fun suffix(target: KonanTarget?) = ".${target!!.family.dynamicSuffix}"