[K/N] Decompose ObjCExport framework generator

Split framework generation into several specialized classes
to reduce coupling.
This commit is contained in:
Sergey Bogolepov
2022-10-07 14:44:13 +03:00
committed by Space Team
parent 6a491dfd0f
commit 56602290ec
7 changed files with 354 additions and 207 deletions
@@ -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<ModuleDescriptor> = getDescriptorsFromLibraries((config.resolve.exportedLibraries + config.resolve.includedLibraries).toSet())
internal fun Context.getIncludedLibraryDescriptors(): List<ModuleDescriptor> = getDescriptorsFromLibraries(config.resolve.includedLibraries.toSet())
internal fun Context.getExportedDependencies(): List<ModuleDescriptor> =
moduleDescriptor.getExportedDependencies(config)
private fun Context.getDescriptorsFromLibraries(libraries: Set<KonanLibrary>) =
moduleDescriptor.allDependencyModules.filter {
internal fun ModuleDescriptor.getExportedDependencies(konanConfig: KonanConfig): List<ModuleDescriptor> =
getDescriptorsFromLibraries((konanConfig.resolve.exportedLibraries + konanConfig.resolve.includedLibraries).toSet())
internal fun Context.getIncludedLibraryDescriptors(): List<ModuleDescriptor> =
moduleDescriptor.getIncludedLibraryDescriptors(config)
internal fun ModuleDescriptor.getIncludedLibraryDescriptors(konanConfig: KonanConfig): List<ModuleDescriptor> =
getDescriptorsFromLibraries(konanConfig.resolve.includedLibraries.toSet())
private fun ModuleDescriptor.getDescriptorsFromLibraries(libraries: Set<KonanLibrary>) =
allDependencyModules.filter {
when (val origin = it.klibModuleOrigin) {
CurrentKlibModuleOrigin, SyntheticModulesOrigin -> false
is DeserializedKlibModuleOrigin -> origin.library in libraries
@@ -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<String>,
moduleDependencies: Set<String>,
) {
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")
}
}
}
}
@@ -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("""
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>$name</string>
<key>CFBundleIdentifier</key>
<string>$bundleId</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$name</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>$bundleShortVersionString</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>$platform</string>
</array>
<key>CFBundleVersion</key>
<string>$bundleVersion</string>
""".trimIndent())
fun addUiDeviceFamilies(vararg values: Int) {
val xmlValues = values.joinToString(separator = "\n") {
" <integer>$it</integer>"
}
contents.append("""
| <key>MinimumOSVersion</key>
| <string>$minimumOsVersion</string>
| <key>UIDeviceFamily</key>
| <array>
|$xmlValues
| </array>
""".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("""
| <key>UIRequiredDeviceCapabilities</key>
| <array>
| <string>arm64</string>
| </array>
""".trimMargin()
)
}
if (target == KonanTarget.IOS_ARM32) {
contents.append("""
| <key>UIRequiredDeviceCapabilities</key>
| <array>
| <string>armv7</string>
| </array>
""".trimMargin()
)
}
contents.append("""
</dict>
</plist>
""".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=<id> and the new -Xbinary=bundleId=<id> 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=<id> compiler flag."
)
}
return bundleID
}
}
@@ -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<ModuleDescriptor>,
exportedDependencies: List<ModuleDescriptor>,
): 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<ModuleDescriptor>): 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 }
}
}
@@ -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>): String = buildString {
appendLine("framework module $frameworkName {")
appendLine(" umbrella header \"$frameworkName.h\"")
appendLine()
appendLine(" export *")
appendLine(" module * { export * }")
appendLine()
moduleDependencies.forEach {
appendLine(" use $it")
}
appendLine("}")
}
}
@@ -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<ClassDescriptor>,
@@ -108,150 +101,22 @@ internal class ObjCExport(
}
private fun produceFrameworkSpecific(headerLines: List<String>) {
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("""
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>$name</string>
<key>CFBundleIdentifier</key>
<string>$bundleId</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$name</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>$bundleShortVersionString</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>$platform</string>
</array>
<key>CFBundleVersion</key>
<string>$bundleVersion</string>
""".trimIndent())
fun addUiDeviceFamilies(vararg values: Int) {
val xmlValues = values.joinToString(separator = "\n") {
" <integer>$it</integer>"
}
contents.append("""
| <key>MinimumOSVersion</key>
| <string>$minimumOsVersion</string>
| <key>UIDeviceFamily</key>
| <array>
|$xmlValues
| </array>
""".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("""
| <key>UIRequiredDeviceCapabilities</key>
| <array>
| <string>arm64</string>
| </array>
""".trimMargin()
)
}
if (target == KonanTarget.IOS_ARM32) {
contents.append("""
| <key>UIRequiredDeviceCapabilities</key>
| <array>
| <string>armv7</string>
| </array>
""".trimMargin()
)
}
contents.append("""
</dict>
</plist>
""".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<ModuleDescriptor>): 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=<id> and the new -Xbinary=bundleId=<id> 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=<id> compiler flag."
)
}
return bundleID
}
}
@@ -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<String>,
headersDirectory: File,
) {
headersDirectory.child(headerName).writeLines(headerLines)
}
}