From 1ad0a662fd9ecaeaa8a1efb80cd46e4369004ebd Mon Sep 17 00:00:00 2001 From: Pavel Punegov Date: Thu, 22 Jun 2023 13:23:17 +0000 Subject: [PATCH] [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 --- .../kotlin/backend/konan/CompilerOutput.kt | 1 + .../jetbrains/kotlin/backend/konan/Linker.kt | 58 ++++++++++++------- .../konan/driver/DynamicCompilerDriver.kt | 21 +++++++ .../konan/driver/phases/BackendPhases.kt | 11 ++++ .../backend/konan/objcexport/BundleBuilder.kt | 40 +++++++++++++ .../backend/konan/objcexport/ObjCExport.kt | 12 +++- .../kotlin/konan/target/CompilerOutputKind.kt | 3 + 7 files changed, 123 insertions(+), 23 deletions(-) create mode 100644 kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/BundleBuilder.kt diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CompilerOutput.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CompilerOutput.kt index 8feb2501628..938e2b397f6 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CompilerOutput.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CompilerOutput.kt @@ -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}") } 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 cb42d81493e..1022509802f 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 @@ -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 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() diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/DynamicCompilerDriver.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/DynamicCompilerDriver.kt index 83dc743bec7..e7e949f7b7d 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/DynamicCompilerDriver.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/DynamicCompilerDriver.kt @@ -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, 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, diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/phases/BackendPhases.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/phases/BackendPhases.kt index bba319ecc4f..44bab12f156 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/phases/BackendPhases.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/phases/BackendPhases.kt @@ -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( + "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) } \ No newline at end of file diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/BundleBuilder.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/BundleBuilder.kt new file mode 100644 index 00000000000..797efb41176 --- /dev/null +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/BundleBuilder.kt @@ -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()) + } + } +} \ No newline at end of file diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExport.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExport.kt index eee2e184b4e..00730c18a9b 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExport.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExport.kt @@ -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, diff --git a/native/utils/src/org/jetbrains/kotlin/konan/target/CompilerOutputKind.kt b/native/utils/src/org/jetbrains/kotlin/konan/target/CompilerOutputKind.kt index e1f38b0602c..0a13ac121d5 100644 --- a/native/utils/src/org/jetbrains/kotlin/konan/target/CompilerOutputKind.kt +++ b/native/utils/src/org/jetbrains/kotlin/konan/target/CompilerOutputKind.kt @@ -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}"