diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/FeaturedLibraries.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/FeaturedLibraries.kt index e1bb3d29abb..15288ec38eb 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/FeaturedLibraries.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/FeaturedLibraries.kt @@ -16,11 +16,20 @@ import org.jetbrains.kotlin.library.SearchPathResolver import org.jetbrains.kotlin.library.isInterop import org.jetbrains.kotlin.library.toUnresolvedLibraries -internal fun Context.getExportedDependencies(): List = getDescriptorsFromLibraries((config.resolve.exportedLibraries + config.resolve.includedLibraries).toSet()) -internal fun Context.getIncludedLibraryDescriptors(): List = getDescriptorsFromLibraries(config.resolve.includedLibraries.toSet()) +internal fun Context.getExportedDependencies(): List = + moduleDescriptor.getExportedDependencies(config) -private fun Context.getDescriptorsFromLibraries(libraries: Set) = - moduleDescriptor.allDependencyModules.filter { +internal fun ModuleDescriptor.getExportedDependencies(konanConfig: KonanConfig): List = + getDescriptorsFromLibraries((konanConfig.resolve.exportedLibraries + konanConfig.resolve.includedLibraries).toSet()) + +internal fun Context.getIncludedLibraryDescriptors(): List = + moduleDescriptor.getIncludedLibraryDescriptors(config) + +internal fun ModuleDescriptor.getIncludedLibraryDescriptors(konanConfig: KonanConfig): List = + getDescriptorsFromLibraries(konanConfig.resolve.includedLibraries.toSet()) + +private fun ModuleDescriptor.getDescriptorsFromLibraries(libraries: Set) = + allDependencyModules.filter { when (val origin = it.klibModuleOrigin) { CurrentKlibModuleOrigin, SyntheticModulesOrigin -> false is DeserializedKlibModuleOrigin -> origin.library in libraries diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/FrameworkBuilder.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/FrameworkBuilder.kt new file mode 100644 index 00000000000..8098723a98e --- /dev/null +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/FrameworkBuilder.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2010-2022 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 + +/** + * Constructs an Apple framework without a binary. + */ +internal class FrameworkBuilder( + private val config: KonanConfig, + private val infoPListBuilder: InfoPListBuilder, + private val moduleMapBuilder: ModuleMapBuilder, + private val objCHeaderWriter: ObjCHeaderWriter, + private val mainPackageGuesser: MainPackageGuesser, +) { + fun build( + moduleDescriptor: ModuleDescriptor, + frameworkDirectory: File, + frameworkName: String, + headerLines: List, + moduleDependencies: Set, + ) { + val target = config.target + val frameworkContents = when (target.family) { + Family.IOS, + Family.WATCHOS, + Family.TVOS -> frameworkDirectory + + Family.OSX -> frameworkDirectory.child("Versions/A") + else -> error(target) + } + + val headers = frameworkContents.child("Headers") + + headers.mkdirs() + objCHeaderWriter.write("$frameworkName.h", headerLines, headers) + + val modules = frameworkContents.child("Modules") + modules.mkdirs() + + val moduleMap = moduleMapBuilder.build(frameworkName, moduleDependencies) + + modules.child("module.modulemap").writeBytes(moduleMap.toByteArray()) + + val directory = when (target.family) { + Family.IOS, + Family.WATCHOS, + Family.TVOS -> frameworkContents + + Family.OSX -> frameworkContents.child("Resources").also { it.mkdirs() } + else -> error(target) + } + + val infoPlistFile = directory.child("Info.plist") + val infoPlistContents = infoPListBuilder.build(frameworkName, mainPackageGuesser, moduleDescriptor) + infoPlistFile.writeBytes(infoPlistContents.toByteArray()) + if (target.family == Family.OSX) { + frameworkDirectory.child("Versions/Current").createAsSymlink("A") + for (child in listOf(frameworkName, "Headers", "Modules", "Resources")) { + frameworkDirectory.child(child).createAsSymlink("Versions/Current/$child") + } + } + } +} \ No newline at end of file diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/InfoPListBuilder.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/InfoPListBuilder.kt new file mode 100644 index 00000000000..6a75e229716 --- /dev/null +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/InfoPListBuilder.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2010-2022 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.* +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.konan.target.AppleConfigurables +import org.jetbrains.kotlin.konan.target.Family +import org.jetbrains.kotlin.konan.target.KonanTarget +import org.jetbrains.kotlin.konan.target.platformName +import org.jetbrains.kotlin.name.Name + +/** + * Creates an Info.plist file for an Apple framework. + * + * Some documentation about its contents can be found here: + * https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Introduction/Introduction.html + */ +internal class InfoPListBuilder( + private val config: KonanConfig, +) { + private val configuration = config.configuration + + fun build( + name: String, + mainPackageGuesser: MainPackageGuesser, + moduleDescriptor: ModuleDescriptor, + ): String { + val bundleId = computeBundleID(name, mainPackageGuesser, moduleDescriptor) + + val bundleShortVersionString = configuration[BinaryOptions.bundleShortVersionString] ?: "1.0" + val bundleVersion = configuration[BinaryOptions.bundleVersion] ?: "1" + val properties = config.platform.configurables as AppleConfigurables + val platform = properties.platformName() + val minimumOsVersion = properties.osVersionMin + + val contents = StringBuilder() + contents.append(""" + + + + + CFBundleExecutable + $name + CFBundleIdentifier + $bundleId + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $name + CFBundlePackageType + FMWK + CFBundleShortVersionString + $bundleShortVersionString + CFBundleSupportedPlatforms + + $platform + + CFBundleVersion + $bundleVersion + + """.trimIndent()) + + fun addUiDeviceFamilies(vararg values: Int) { + val xmlValues = values.joinToString(separator = "\n") { + " $it" + } + contents.append(""" + | MinimumOSVersion + | $minimumOsVersion + | UIDeviceFamily + | + |$xmlValues + | + + """.trimMargin()) + } + val target = config.target + // UIDeviceFamily mapping: + // 1 - iPhone + // 2 - iPad + // 3 - AppleTV + // 4 - Apple Watch + when (target.family) { + Family.IOS -> addUiDeviceFamilies(1, 2) + Family.TVOS -> addUiDeviceFamilies(3) + Family.WATCHOS -> addUiDeviceFamilies(4) + else -> {} + } + + if (target == KonanTarget.IOS_ARM64) { + contents.append(""" + | UIRequiredDeviceCapabilities + | + | arm64 + | + + """.trimMargin() + ) + } + + if (target == KonanTarget.IOS_ARM32) { + contents.append(""" + | UIRequiredDeviceCapabilities + | + | armv7 + | + + """.trimMargin() + ) + } + + contents.append(""" + + + """.trimIndent()) + + // TODO: Xcode also add some number of DT* keys. + return contents.toString() + } + + private fun computeBundleID( + bundleName: String, + mainPackageGuesser: MainPackageGuesser, + moduleDescriptor: ModuleDescriptor, + ): String { + val deprecatedBundleIdOption = configuration[KonanConfigKeys.BUNDLE_ID] + val bundleIdOption = configuration[BinaryOptions.bundleId] + if (deprecatedBundleIdOption != null && bundleIdOption != null && deprecatedBundleIdOption != bundleIdOption) { + configuration.report( + CompilerMessageSeverity.ERROR, + "Both the deprecated -Xbundle-id= and the new -Xbinary=bundleId= options supplied with different values: " + + "'$deprecatedBundleIdOption' and '$bundleIdOption'. " + + "Please use only one of the options or make sure they have the same value." + ) + } + deprecatedBundleIdOption?.let { return it } ?: bundleIdOption?.let { return it } + + val mainPackage = mainPackageGuesser.guess( + moduleDescriptor, + moduleDescriptor.getIncludedLibraryDescriptors(config), + moduleDescriptor.getExportedDependencies(config), + ) + val bundleID = mainPackage.child(Name.identifier(bundleName)).asString() + + if (mainPackage.isRoot) { + configuration.report( + CompilerMessageSeverity.STRONG_WARNING, + "Cannot infer a bundle ID from packages of source files and exported dependencies, " + + "use the bundle name instead: $bundleName. " + + "Please specify the bundle ID explicitly using the -Xbinary=bundleId= compiler flag." + ) + } + return bundleID + } +} \ No newline at end of file diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/MainPackageGuesser.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/MainPackageGuesser.kt new file mode 100644 index 00000000000..c16d8f6d105 --- /dev/null +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/MainPackageGuesser.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2010-2022 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.descriptors.getPackageFragments +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.isSubpackageOf + +/** + * Tries to infer a main package name that can be used + * for bundle ID of a framework. + */ +internal class MainPackageGuesser { + fun guess( + moduleDescriptor: ModuleDescriptor, + includedLibraryDescriptors: List, + exportedDependencies: List, + ): FqName { + // Consider exported libraries only if we cannot infer the package from sources or included libs. + return guessMainPackage(includedLibraryDescriptors + moduleDescriptor) + ?: guessMainPackage(exportedDependencies) + ?: FqName.ROOT + } + + private fun guessMainPackage(modules: List): FqName? { + if (modules.isEmpty()) { + return null + } + + val allPackages = modules.flatMap { + it.getPackageFragments() // Includes also all parent packages, e.g. the root one. + } + + val nonEmptyPackages = allPackages + .filter { it.getMemberScope().getContributedDescriptors().isNotEmpty() } + .map { it.fqName }.distinct() + + return allPackages.map { it.fqName }.distinct() + .filter { candidate -> nonEmptyPackages.all { it.isSubpackageOf(candidate) } } + // Now there are all common ancestors of non-empty packages. Longest of them is the least common accessor: + .maxByOrNull { it.asString().length } + } +} \ No newline at end of file diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ModuleMapBuilder.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ModuleMapBuilder.kt new file mode 100644 index 00000000000..87a60653f6e --- /dev/null +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ModuleMapBuilder.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2010-2022 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 + +/** + * Creates a module map for a framework. + * + * Reference: https://clang.llvm.org/docs/Modules.html + */ +class ModuleMapBuilder { + fun build(frameworkName: String, moduleDependencies: Set): String = buildString { + appendLine("framework module $frameworkName {") + appendLine(" umbrella header \"$frameworkName.h\"") + appendLine() + appendLine(" export *") + appendLine(" module * { export * }") + appendLine() + moduleDependencies.forEach { + appendLine(" use $it") + } + appendLine("}") + } +} \ 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 dd9720e4c06..c89d1273e64 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 @@ -6,24 +6,17 @@ package org.jetbrains.kotlin.backend.konan.objcexport import org.jetbrains.kotlin.backend.konan.* -import org.jetbrains.kotlin.backend.konan.KonanConfigKeys.Companion.BUNDLE_ID -import org.jetbrains.kotlin.backend.konan.descriptors.getPackageFragments import org.jetbrains.kotlin.backend.konan.descriptors.isInterface import org.jetbrains.kotlin.backend.konan.llvm.CodeGenerator import org.jetbrains.kotlin.backend.konan.llvm.objcexport.ObjCExportBlockCodeGenerator import org.jetbrains.kotlin.backend.konan.llvm.objcexport.ObjCExportCodeGenerator -import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor import org.jetbrains.kotlin.descriptors.ClassDescriptor -import org.jetbrains.kotlin.descriptors.ModuleDescriptor import org.jetbrains.kotlin.descriptors.SourceFile import org.jetbrains.kotlin.konan.exec.Command import org.jetbrains.kotlin.konan.file.File import org.jetbrains.kotlin.konan.file.createTempFile -import org.jetbrains.kotlin.konan.target.* -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.name.Name -import org.jetbrains.kotlin.name.isSubpackageOf +import org.jetbrains.kotlin.konan.target.CompilerOutputKind internal class ObjCExportedInterface( val generatedClasses: Set, @@ -108,150 +101,22 @@ internal class ObjCExport( } private fun produceFrameworkSpecific(headerLines: List) { - val framework = File(context.generationState.outputFile) - val frameworkContents = when(target.family) { - Family.IOS, - Family.WATCHOS, - Family.TVOS -> framework - Family.OSX -> framework.child("Versions/A") - else -> error(target) - } - - val headers = frameworkContents.child("Headers") - - val frameworkName = framework.name.removeSuffix(".framework") - val headerName = frameworkName + ".h" - val header = headers.child(headerName) - headers.mkdirs() - header.writeLines(headerLines) - - val modules = frameworkContents.child("Modules") - modules.mkdirs() - - val moduleMap = """ - |framework module $frameworkName { - | umbrella header "$headerName" - | - | export * - | module * { export * } - | - | use Foundation - |} - """.trimMargin() - - modules.child("module.modulemap").writeBytes(moduleMap.toByteArray()) - - emitInfoPlist(frameworkContents, frameworkName) - if (target.family == Family.OSX) { - framework.child("Versions/Current").createAsSymlink("A") - for (child in listOf(frameworkName, "Headers", "Modules", "Resources")) { - framework.child(child).createAsSymlink("Versions/Current/$child") - } - } - } - - private fun emitInfoPlist(frameworkContents: File, name: String) { - val properties = context.config.platform.configurables as AppleConfigurables - - val directory = when (target.family) { - Family.IOS, - Family.WATCHOS, - Family.TVOS -> frameworkContents - Family.OSX -> frameworkContents.child("Resources").also { it.mkdirs() } - else -> error(target) - } - - val file = directory.child("Info.plist") - val bundleId = guessBundleID(name) - val bundleShortVersionString = context.configuration[BinaryOptions.bundleShortVersionString] ?: "1.0" - val bundleVersion = context.configuration[BinaryOptions.bundleVersion] ?: "1" - val platform = properties.platformName() - val minimumOsVersion = properties.osVersionMin - - val contents = StringBuilder() - contents.append(""" - - - - - CFBundleExecutable - $name - CFBundleIdentifier - $bundleId - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $name - CFBundlePackageType - FMWK - CFBundleShortVersionString - $bundleShortVersionString - CFBundleSupportedPlatforms - - $platform - - CFBundleVersion - $bundleVersion - - """.trimIndent()) - - fun addUiDeviceFamilies(vararg values: Int) { - val xmlValues = values.joinToString(separator = "\n") { - " $it" - } - contents.append(""" - | MinimumOSVersion - | $minimumOsVersion - | UIDeviceFamily - | - |$xmlValues - | - - """.trimMargin()) - } - - // UIDeviceFamily mapping: - // 1 - iPhone - // 2 - iPad - // 3 - AppleTV - // 4 - Apple Watch - when (target.family) { - Family.IOS -> addUiDeviceFamilies(1, 2) - Family.TVOS -> addUiDeviceFamilies(3) - Family.WATCHOS -> addUiDeviceFamilies(4) - else -> {} - } - - if (target == KonanTarget.IOS_ARM64) { - contents.append(""" - | UIRequiredDeviceCapabilities - | - | arm64 - | - - """.trimMargin() - ) - } - - if (target == KonanTarget.IOS_ARM32) { - contents.append(""" - | UIRequiredDeviceCapabilities - | - | armv7 - | - - """.trimMargin() - ) - } - - contents.append(""" - - - """.trimIndent()) - - // TODO: Xcode also add some number of DT* keys. - - file.writeBytes(contents.toString().toByteArray()) + val frameworkDirectory = File(context.generationState.outputFile) + val frameworkName = frameworkDirectory.name.removeSuffix(".framework") + val frameworkBuilder = FrameworkBuilder( + context.config, + infoPListBuilder = InfoPListBuilder(context.config), + moduleMapBuilder = ModuleMapBuilder(), + objCHeaderWriter = ObjCHeaderWriter(), + mainPackageGuesser = MainPackageGuesser(), + ) + frameworkBuilder.build( + context.moduleDescriptor, + frameworkDirectory, + frameworkName, + headerLines, + moduleDependencies = emptySet() + ) } // See https://bugs.swift.org/browse/SR-10177 @@ -295,55 +160,4 @@ internal class ObjCExport( // In this case resulting framework will likely be unusable due to compile errors when importing it. } } - - private fun guessMainPackage(modules: List): FqName? { - if (modules.isEmpty()) { - return null - } - - val allPackages = modules.flatMap { - it.getPackageFragments() // Includes also all parent packages, e.g. the root one. - } - - val nonEmptyPackages = allPackages - .filter { it.getMemberScope().getContributedDescriptors().isNotEmpty() } - .map { it.fqName }.distinct() - - return allPackages.map { it.fqName }.distinct() - .filter { candidate -> nonEmptyPackages.all { it.isSubpackageOf(candidate) } } - // Now there are all common ancestors of non-empty packages. Longest of them is the least common accessor: - .maxByOrNull { it.asString().length } - } - - private fun guessBundleID(bundleName: String): String { - val configuration = context.configuration - val deprecatedBundleIdOption = configuration[BUNDLE_ID] - val bundleIdOption = configuration[BinaryOptions.bundleId] - if (deprecatedBundleIdOption != null && bundleIdOption != null && deprecatedBundleIdOption != bundleIdOption) { - configuration.report( - CompilerMessageSeverity.ERROR, - "Both the deprecated -Xbundle-id= and the new -Xbinary=bundleId= options supplied with different values: " + - "'$deprecatedBundleIdOption' and '$bundleIdOption'. " + - "Please use only one of the options or make sure they have the same value." - ) - } - deprecatedBundleIdOption?.let { return it } ?: bundleIdOption?.let { return it } - - // Consider exported libraries only if we cannot infer the package from sources or included libs. - val mainPackage = guessMainPackage(context.getIncludedLibraryDescriptors() + context.moduleDescriptor) - ?: guessMainPackage(context.getExportedDependencies()) - ?: FqName.ROOT - - val bundleID = mainPackage.child(Name.identifier(bundleName)).asString() - - if (mainPackage.isRoot) { - configuration.report( - CompilerMessageSeverity.STRONG_WARNING, - "Cannot infer a bundle ID from packages of source files and exported dependencies, " + - "use the bundle name instead: $bundleName. " + - "Please specify the bundle ID explicitly using the -Xbinary=bundleId= compiler flag." - ) - } - return bundleID - } } diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCHeaderWriter.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCHeaderWriter.kt new file mode 100644 index 00000000000..3ee3471db43 --- /dev/null +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCHeaderWriter.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2010-2022 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.konan.file.File + +// For now, the object is pretty dumb. +// Later it will accept an object with ObjC declarations instead of lines. +class ObjCHeaderWriter { + fun write( + headerName: String, + headerLines: List, + headersDirectory: File, + ) { + headersDirectory.child(headerName).writeLines(headerLines) + } +} \ No newline at end of file