Fat frameworks: Support for watchOS and tvOS

Issue #KT-34259 fixed
This commit is contained in:
Ilya Matveev
2019-10-11 13:46:02 +03:00
parent 37e828d4aa
commit 919c09d849
2 changed files with 97 additions and 59 deletions
@@ -5,6 +5,7 @@
package org.jetbrains.kotlin.gradle
import org.jetbrains.kotlin.gradle.util.checkedReplace
import org.jetbrains.kotlin.gradle.util.modify
import org.jetbrains.kotlin.gradle.util.runProcess
import org.junit.Test
@@ -13,39 +14,12 @@ import kotlin.test.assertTrue
class FatFrameworkIT : BaseGradleIT() {
@Test
fun smoke() = with(transformProjectWithPluginsDsl("smoke", directoryPrefix = "new-mpp-fat-framework")) {
fun smokeIos() = with(transformProjectWithPluginsDsl("smoke", directoryPrefix = "new-mpp-fat-framework")) {
build("fat") {
assertSuccessful()
val archs = listOf("x64", "arm64", "arm32")
val linkTasks = archs.map { ":linkDebugFrameworkIos${it.capitalize()}" }
assertTasksExecuted(linkTasks)
assertTasksExecuted(":fat")
assertFileExists("build/fat-framework/smoke.framework/smoke")
assertFileExists("build/fat-framework/smoke.framework/Headers/smoke.h")
assertFileExists("build/fat-framework/smoke.framework.dSYM/Contents/Resources/DWARF/smoke")
val headerContent = fileInWorkingDir("build/fat-framework/smoke.framework/Headers/smoke.h").readText()
assertTrue(
headerContent.contains("+ (int32_t)foo __attribute__((swift_name(\"foo()\")));"),
"Unexpected header content:\n$headerContent"
)
val plistContent = fileInWorkingDir("build/fat-framework/smoke.framework/Info.plist")
.readLines()
.joinToString(separator = "\n") { it.trim() }
assertTrue(
plistContent.contains(
"""
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
</array>
""".trimIndent()
),
"Unexpected Info.plist content:\n$plistContent"
checkSmokeBuild(
archs = listOf("x64", "arm64", "arm32"),
targetPrefix = "ios",
expectedPlistPlatform = "iPhoneOS"
)
val binary = fileInWorkingDir("build/fat-framework/smoke.framework/smoke")
@@ -58,6 +32,70 @@ class FatFrameworkIT : BaseGradleIT() {
}
}
@Test
fun smokeWatchos() = with(transformProjectWithPluginsDsl("smoke", directoryPrefix = "new-mpp-fat-framework")) {
gradleBuildScript().modify {
it.checkedReplace("iosArm32()", "watchosArm32()")
.checkedReplace("iosArm64()", "watchosArm64()")
.checkedReplace("iosX64()", "watchosX86()")
}
build("fat") {
checkSmokeBuild(
archs = listOf("x86", "arm64", "arm32"),
targetPrefix = "watchos",
expectedPlistPlatform = "WatchOS"
)
val binary = fileInWorkingDir("build/fat-framework/smoke.framework/smoke")
with(runProcess(listOf("file", binary.absolutePath), projectDir)) {
assertTrue(isSuccessful)
assertTrue(output.contains("\\(for architecture i386\\):\\s+Mach-O dynamically linked shared library i386".toRegex()))
assertTrue(output.contains("\\(for architecture armv7k\\):\\s+Mach-O dynamically linked shared library arm_v7k".toRegex()))
assertTrue(output.contains("\\(for architecture arm64_32\\):\\s+Mach-O dynamically linked shared library arm64_32_v8".toRegex()))
}
}
}
private fun CompiledProject.checkSmokeBuild(
archs: List<String>,
targetPrefix: String,
expectedPlistPlatform: String
) {
assertSuccessful()
val linkTasks = archs.map { ":linkDebugFramework${targetPrefix.capitalize()}${it.capitalize()}" }
assertTasksExecuted(linkTasks)
assertTasksExecuted(":fat")
assertFileExists("build/fat-framework/smoke.framework/smoke")
assertFileExists("build/fat-framework/smoke.framework/Headers/smoke.h")
assertFileExists("build/fat-framework/smoke.framework.dSYM/Contents/Resources/DWARF/smoke")
val headerContent = fileInWorkingDir("build/fat-framework/smoke.framework/Headers/smoke.h").readText()
assertTrue(
headerContent.contains("+ (int32_t)foo __attribute__((swift_name(\"foo()\")));"),
"Unexpected header content:\n$headerContent"
)
val plistContent = fileInWorkingDir("build/fat-framework/smoke.framework/Info.plist")
.readLines()
.joinToString(separator = "\n") { it.trim() }
assertTrue(
plistContent.contains(
"""
<key>CFBundleSupportedPlatforms</key>
<array>
<string>$expectedPlistPlatform</string>
</array>
""".trimIndent()
),
"Unexpected Info.plist content:\n$plistContent"
)
}
@Test
fun testDuplicatedArchitecture()= with(
transformProjectWithPluginsDsl("smoke", directoryPrefix = "new-mpp-fat-framework")
@@ -77,7 +115,7 @@ class FatFrameworkIT : BaseGradleIT() {
}
@Test
fun testIncorrectTarget() = with(
fun testIncorrectFamily() = with(
transformProjectWithPluginsDsl("smoke", directoryPrefix = "new-mpp-fat-framework")
) {
gradleBuildScript().modify {
@@ -90,7 +128,7 @@ class FatFrameworkIT : BaseGradleIT() {
}
build("fat") {
assertFailed()
assertContains("Cannot add a framework with target 'macos_x64' to the fat framework")
assertContains("Cannot add a binary with platform family 'osx' to the fat framework")
}
}
@@ -13,8 +13,8 @@ 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 org.jetbrains.kotlin.konan.target.KonanTarget.*
import org.jetbrains.kotlin.konan.util.visibleName
import java.io.ByteArrayOutputStream
import java.io.File
@@ -131,12 +131,15 @@ open class FatFrameworkTask: DefaultTask() {
*/
fun from(frameworks: Iterable<Framework>) {
frameworks.forEach {
val konanTarget = it.konanTarget
val arch = konanTarget.architecture
require(konanTarget.family == Family.IOS) {
"Cannot add a framework with target '${konanTarget.visibleName}' to the fat framework: " +
"fat frameworks are available only for iOS binaries."
val arch = it.konanTarget.architecture
val family = it.konanTarget.family
val fatFrameworkFamily = getFatFrameworkFamily()
require(fatFrameworkFamily == null || family == fatFrameworkFamily) {
"Cannot add a binary with platform family '${family.visibleName}' to the fat framework:\n" +
"A fat framework must include binaries with the same platform family " +
"while this framework already includes binaries with family '${fatFrameworkFamily!!.visibleName}'"
}
require(!archToFramework.containsKey(arch)) {
val alreadyAdded = archToFramework.getValue(arch)
"This fat framework already has a binary for architecture `${arch.name.toLowerCase()}` " +
@@ -144,21 +147,19 @@ open class FatFrameworkTask: DefaultTask() {
}
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 fun getFatFrameworkFamily(): Family? {
assert(archToFramework.values.distinctBy { it.konanTarget.family }.size <= 1)
return archToFramework.values.firstOrNull()?.konanTarget?.family
}
private val Architecture.clangMacro: String
get() = when(this) {
Architecture.X86 -> "__i386__"
Architecture.X64 -> "__x86_64__"
Architecture.ARM32 -> "__arm__"
Architecture.ARM64 -> "__aarch64__"
@@ -203,19 +204,18 @@ open class FatFrameworkTask: DefaultTask() {
fun delete(entry: String) = commands.add("Delete \"$entry\"")
}
private fun runLipo(inputFiles: Map<Architecture, File>, outputFile: File) =
project.exec {
it.executable = "/usr/bin/lipo"
it.args = mutableListOf("-create").apply {
inputFiles.forEach { (arch, binary) ->
addAll(listOf("-arch", arch.lipoArg, binary.absolutePath))
}
addArg("-output", outputFile.absolutePath)
}
private fun runLipo(inputFiles: Collection<File>, outputFile: File) =
project.exec { exec ->
exec.executable = "/usr/bin/lipo"
exec.args = listOf(
"-create",
*inputFiles.map { it.absolutePath }.toTypedArray(),
"-output", outputFile.absolutePath
)
}
private fun mergeBinaries(outputFile: File) =
runLipo(archToFramework.mapValues { (_, framework) -> framework.files.binary }, outputFile)
runLipo(archToFramework.values.map { it.files.binary }, outputFile)
private fun mergeHeaders(outputFile: File) = outputFile.writer().use { writer ->
@@ -311,7 +311,7 @@ open class FatFrameworkTask: DefaultTask() {
fatDsym.mkdirs()
// Merge dSYM binary.
runLipo(dsymInputs.mapValues { (_, dsym) -> dsym.binary }, fatDsym.binary)
runLipo(dsymInputs.values.map { it.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.