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:
Ilya Matveev
2019-03-19 20:41:10 +07:00
parent 538512fa2a
commit 6621414d72
@@ -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)
}
}