diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/FatFrameworkTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/FatFrameworkTask.kt new file mode 100644 index 00000000000..8680bb6a16a --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/FatFrameworkTask.kt @@ -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 + 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 = 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 + get() = frameworks.map { project.fileTree(it.outputFile) } + + @get:InputFiles + protected val inputDsymFiles: Iterable + 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) { + 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, 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) + } +}