Provide a task for building a fat framework
This patch adds a task taking several iOS frameworks and building one fat framework on their basis. Issue #KT-24184 Fixed
This commit is contained in:
+285
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* 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/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.gradle.tasks
|
||||
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.FileTree
|
||||
import org.gradle.api.tasks.*
|
||||
import org.jetbrains.kotlin.gradle.plugin.cocoapods.asValidFrameworkName
|
||||
import org.jetbrains.kotlin.gradle.plugin.mpp.Framework
|
||||
import org.jetbrains.kotlin.konan.target.Architecture
|
||||
import org.jetbrains.kotlin.konan.target.Family
|
||||
import org.jetbrains.kotlin.konan.target.KonanTarget
|
||||
import java.io.File
|
||||
|
||||
private val Framework.files: IosFrameworkFiles
|
||||
get() = IosFrameworkFiles(outputFile)
|
||||
|
||||
private class IosDsymFiles(val rootDir: File) {
|
||||
|
||||
val frameworkName = rootDir.name.removeSuffix(".framework.dSYM")
|
||||
|
||||
val binaryDir = rootDir.resolve("Contents/Resources/DWARF")
|
||||
val binary = binaryDir.resolve(frameworkName)
|
||||
val infoPlist = rootDir.resolve("Contents/Info.plist")
|
||||
|
||||
fun mkdirs() {
|
||||
binaryDir.mkdirs()
|
||||
}
|
||||
|
||||
fun exists(): Boolean = rootDir.exists()
|
||||
}
|
||||
|
||||
private class IosFrameworkFiles(val rootDir: File) {
|
||||
|
||||
constructor(parentDir: File, frameworkName: String):
|
||||
this(parentDir.resolve("$frameworkName.framework"))
|
||||
|
||||
val frameworkName: String = rootDir.nameWithoutExtension
|
||||
|
||||
val headerDir = rootDir.resolve("Headers")
|
||||
val modulesDir = rootDir.resolve("Modules")
|
||||
|
||||
val binary = rootDir.resolve(frameworkName)
|
||||
val header = headerDir.resolve("$frameworkName.h")
|
||||
val moduleFile = modulesDir.resolve("module.modulemap")
|
||||
val infoPlist = rootDir.resolve("Info.plist")
|
||||
|
||||
val dSYM: IosDsymFiles = IosDsymFiles(rootDir.parentFile.resolve("$frameworkName.framework.dSYM"))
|
||||
|
||||
fun mkdirs() {
|
||||
rootDir.mkdirs()
|
||||
headerDir.mkdir()
|
||||
modulesDir.mkdir()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task running lipo to create a fat framework from several simple frameworks. It also merges headers, plists and module files.
|
||||
*/
|
||||
open class FatFrameworkTask: DefaultTask() {
|
||||
|
||||
//region DSL properties.
|
||||
/**
|
||||
* A collection of frameworks used ot build the fat framework.
|
||||
*/
|
||||
@get:Internal // We take it into account as an input in the inputFiles property.
|
||||
val frameworks: Collection<Framework>
|
||||
get() = archToFramework.values
|
||||
|
||||
/**
|
||||
* A base name for the fat framework.
|
||||
*/
|
||||
@Input
|
||||
var baseName: String = project.name
|
||||
|
||||
/**
|
||||
* A parent directory for the fat framework..
|
||||
*/
|
||||
@OutputDirectory
|
||||
var destinationDir: File = project.buildDir.resolve("fat-framework")
|
||||
|
||||
@get:Internal // We take it into account as an input in the destinationDir property.
|
||||
val fatFrameworkDir: File
|
||||
get() = fatFramework.rootDir
|
||||
|
||||
@get:Internal // We take it into account as an input in the destinationDir property.
|
||||
val fatDsymDir: File
|
||||
get() = fatDsym.rootDir
|
||||
// endregion.
|
||||
|
||||
private val archToFramework: MutableMap<Architecture, Framework> = mutableMapOf()
|
||||
|
||||
private val fatFrameworkName: String
|
||||
get() = baseName.asValidFrameworkName()
|
||||
|
||||
private val fatFramework: IosFrameworkFiles
|
||||
get() = IosFrameworkFiles(destinationDir, fatFrameworkName)
|
||||
|
||||
private val fatDsym: IosDsymFiles
|
||||
get() = fatFramework.dSYM
|
||||
|
||||
@get:InputFiles
|
||||
@get:SkipWhenEmpty
|
||||
protected val inputFrameworkFiles: Iterable<FileTree>
|
||||
get() = frameworks.map { project.fileTree(it.outputFile) }
|
||||
|
||||
@get:InputFiles
|
||||
protected val inputDsymFiles: Iterable<FileTree>
|
||||
get() = frameworks.mapNotNull { framework ->
|
||||
framework.files.dSYM.takeIf {
|
||||
it.exists()
|
||||
}?.rootDir?.let {
|
||||
project.fileTree(it)
|
||||
}
|
||||
}
|
||||
|
||||
// region DSL methods.
|
||||
/**
|
||||
* Adds the specified frameworks in this fat framework.
|
||||
*/
|
||||
fun from(vararg frameworks: Framework) = from(frameworks.toList())
|
||||
|
||||
/**
|
||||
* Adds the specified frameworks in this fat framework.
|
||||
*/
|
||||
fun from(frameworks: Iterable<Framework>) {
|
||||
frameworks.forEach {
|
||||
val konanTarget = it.target.konanTarget
|
||||
val arch = konanTarget.architecture
|
||||
require(konanTarget.family == Family.IOS) {
|
||||
"Cannot add a framework with target '${konanTarget.visibleName}' in the fat framework: " +
|
||||
"fat frameworks are available only for iOS binaries."
|
||||
}
|
||||
require(!archToFramework.containsKey(arch)) {
|
||||
"This fat framework already has a binary for architecture `${arch.name.toLowerCase()}`"
|
||||
}
|
||||
archToFramework[arch] = it
|
||||
dependsOn(it.linkTask)
|
||||
// Framework generating task may stop with NO-SOURCE result. We should track it.
|
||||
}
|
||||
}
|
||||
// endregion.
|
||||
|
||||
private val Architecture.lipoArg: String
|
||||
get() = when(this) {
|
||||
Architecture.X64 -> "x86_64"
|
||||
Architecture.ARM32 -> "armv7"
|
||||
Architecture.ARM64 -> "arm64"
|
||||
else -> error("Fat frameworks are not supported for architecture `$name`")
|
||||
}
|
||||
|
||||
private val Architecture.clangMacro: String
|
||||
get() = when(this) {
|
||||
Architecture.X64 -> "__x86_64__"
|
||||
Architecture.ARM32 -> "__arm__"
|
||||
Architecture.ARM64 -> "__aarch64__"
|
||||
else -> error("Fat frameworks are not supported for architecture `$name`")
|
||||
}
|
||||
|
||||
private val Framework.plistPlatform: String
|
||||
get() = when(target.konanTarget) {
|
||||
KonanTarget.IOS_ARM32, KonanTarget.IOS_ARM64 -> "iPhoneOS"
|
||||
KonanTarget.IOS_X64 -> "iPhoneSimulator"
|
||||
else -> error("Fat frameworks are not supported for target `${target.konanTarget.visibleName}`")
|
||||
}
|
||||
|
||||
// Runs the PlistBuddy utility with the given commands to configure the given plist file.
|
||||
private fun processPlist(plist: File, commands: PlistBuddyRunner.() -> Unit) =
|
||||
PlistBuddyRunner(plist).commands()
|
||||
|
||||
private inner class PlistBuddyRunner(val plist: File) {
|
||||
fun run(command: String) = project.exec {
|
||||
it.executable = "/usr/libexec/PlistBuddy"
|
||||
it.args("-c", command, plist.absolutePath)
|
||||
}
|
||||
|
||||
fun add(entry: String, value: String) = run("Add \"$entry\" string \"$value\"")
|
||||
fun set(entry: String, value: String) = run("Set \"$entry\" \"$value\"")
|
||||
fun delete(entry: String) = run("Delete \"$entry\"")
|
||||
}
|
||||
|
||||
private fun runLipo(inputFiles: Map<Architecture, File>, outputFile: File) =
|
||||
project.exec {
|
||||
it.executable = "lipo"
|
||||
it.args = mutableListOf("-create").apply {
|
||||
inputFiles.forEach { (arch, binary) ->
|
||||
addAll(listOf("-arch", arch.lipoArg, binary.absolutePath))
|
||||
}
|
||||
addArg("-output", outputFile.absolutePath)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mergeBinaries(outputFile: File) =
|
||||
runLipo(archToFramework.mapValues { (_, framework) -> framework.files.binary }, outputFile)
|
||||
|
||||
private fun mergeHeaders(outputFile: File) = outputFile.writer().use { writer ->
|
||||
archToFramework.forEach { (arch, framework) ->
|
||||
writer.appendln("#ifdef ${arch.clangMacro}\n")
|
||||
writer.appendln(framework.files.header.readText())
|
||||
writer.appendln("#endif\n")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createModuleFile(outputFile: File, frameworkName: String) {
|
||||
outputFile.writeText("""
|
||||
framework module $frameworkName {
|
||||
umbrella header "$frameworkName.h"
|
||||
|
||||
export *
|
||||
module * { export * }
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
private fun mergePlists(outputFile: File, frameworkName: String) {
|
||||
// The list of frameworks isn't empty because Gradle skips the task in this case
|
||||
// (corresponding task input is annotated with @SkipWhenEmpty).
|
||||
assert(frameworks.isNotEmpty())
|
||||
|
||||
// Use Info.plist of one of the frameworks to get basic data.
|
||||
val baseInfo = frameworks.first().files.infoPlist
|
||||
project.copy {
|
||||
it.from(baseInfo)
|
||||
it.into(outputFile.parentFile)
|
||||
}
|
||||
|
||||
// TODO: What should we do with bundle id?
|
||||
processPlist(outputFile) {
|
||||
// Set framework name in the plist file.
|
||||
set(":CFBundleExecutable", frameworkName)
|
||||
set(":CFBundleName", frameworkName)
|
||||
|
||||
// Remove all platform-specific sections.
|
||||
delete(":UIRequiredDeviceCapabilities")
|
||||
delete(":CFBundleSupportedPlatforms:0")
|
||||
|
||||
// Add supported platforms.
|
||||
frameworks
|
||||
.map { it.plistPlatform }
|
||||
.distinct()
|
||||
.forEachIndexed { index, platform ->
|
||||
add(":CFBundleSupportedPlatforms:$index", platform)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mergeDSYM() {
|
||||
val dsymInputs = archToFramework.mapValues { (_, framework) ->
|
||||
framework.files.dSYM
|
||||
}.filterValues {
|
||||
it.exists()
|
||||
}
|
||||
|
||||
if (dsymInputs.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
fatDsym.mkdirs()
|
||||
|
||||
// Merge dSYM binary.
|
||||
runLipo(dsymInputs.mapValues { (_, dsym) -> dsym.binary }, fatDsym.binary)
|
||||
|
||||
// Copy dSYM's Info.plist.
|
||||
// It doesn't contain target-specific info or framework names except the bundle id so there is no need to edit it.
|
||||
// TODO: handle bunde id.
|
||||
project.copy {
|
||||
it.from(dsymInputs.values.first().infoPlist)
|
||||
it.into(fatDsym.infoPlist)
|
||||
}
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
protected fun createFatFramework() {
|
||||
fatFramework.mkdirs()
|
||||
|
||||
val frameworkName = fatFramework.frameworkName
|
||||
mergeBinaries(fatFramework.binary)
|
||||
mergeHeaders(fatFramework.header)
|
||||
createModuleFile(fatFramework.moduleFile, frameworkName)
|
||||
mergePlists(fatFramework.infoPlist, frameworkName)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user