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 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.modify
import org.jetbrains.kotlin.gradle.util.runProcess import org.jetbrains.kotlin.gradle.util.runProcess
import org.junit.Test import org.junit.Test
@@ -13,39 +14,12 @@ import kotlin.test.assertTrue
class FatFrameworkIT : BaseGradleIT() { class FatFrameworkIT : BaseGradleIT() {
@Test @Test
fun smoke() = with(transformProjectWithPluginsDsl("smoke", directoryPrefix = "new-mpp-fat-framework")) { fun smokeIos() = with(transformProjectWithPluginsDsl("smoke", directoryPrefix = "new-mpp-fat-framework")) {
build("fat") { build("fat") {
assertSuccessful() checkSmokeBuild(
val archs = listOf("x64", "arm64", "arm32") archs = listOf("x64", "arm64", "arm32"),
val linkTasks = archs.map { ":linkDebugFrameworkIos${it.capitalize()}" } targetPrefix = "ios",
expectedPlistPlatform = "iPhoneOS"
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"
) )
val binary = fileInWorkingDir("build/fat-framework/smoke.framework/smoke") 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 @Test
fun testDuplicatedArchitecture()= with( fun testDuplicatedArchitecture()= with(
transformProjectWithPluginsDsl("smoke", directoryPrefix = "new-mpp-fat-framework") transformProjectWithPluginsDsl("smoke", directoryPrefix = "new-mpp-fat-framework")
@@ -77,7 +115,7 @@ class FatFrameworkIT : BaseGradleIT() {
} }
@Test @Test
fun testIncorrectTarget() = with( fun testIncorrectFamily() = with(
transformProjectWithPluginsDsl("smoke", directoryPrefix = "new-mpp-fat-framework") transformProjectWithPluginsDsl("smoke", directoryPrefix = "new-mpp-fat-framework")
) { ) {
gradleBuildScript().modify { gradleBuildScript().modify {
@@ -90,7 +128,7 @@ class FatFrameworkIT : BaseGradleIT() {
} }
build("fat") { build("fat") {
assertFailed() 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.gradle.plugin.mpp.Framework
import org.jetbrains.kotlin.konan.target.Architecture import org.jetbrains.kotlin.konan.target.Architecture
import org.jetbrains.kotlin.konan.target.Family 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.target.KonanTarget.*
import org.jetbrains.kotlin.konan.util.visibleName
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
@@ -131,12 +131,15 @@ open class FatFrameworkTask: DefaultTask() {
*/ */
fun from(frameworks: Iterable<Framework>) { fun from(frameworks: Iterable<Framework>) {
frameworks.forEach { frameworks.forEach {
val konanTarget = it.konanTarget val arch = it.konanTarget.architecture
val arch = konanTarget.architecture val family = it.konanTarget.family
require(konanTarget.family == Family.IOS) { val fatFrameworkFamily = getFatFrameworkFamily()
"Cannot add a framework with target '${konanTarget.visibleName}' to the fat framework: " + require(fatFrameworkFamily == null || family == fatFrameworkFamily) {
"fat frameworks are available only for iOS binaries." "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)) { require(!archToFramework.containsKey(arch)) {
val alreadyAdded = archToFramework.getValue(arch) val alreadyAdded = archToFramework.getValue(arch)
"This fat framework already has a binary for architecture `${arch.name.toLowerCase()}` " + "This fat framework already has a binary for architecture `${arch.name.toLowerCase()}` " +
@@ -144,21 +147,19 @@ open class FatFrameworkTask: DefaultTask() {
} }
archToFramework[arch] = it archToFramework[arch] = it
dependsOn(it.linkTask) dependsOn(it.linkTask)
// Framework generating task may stop with NO-SOURCE result. We should track it.
} }
} }
// endregion. // endregion.
private val Architecture.lipoArg: String private fun getFatFrameworkFamily(): Family? {
get() = when(this) { assert(archToFramework.values.distinctBy { it.konanTarget.family }.size <= 1)
Architecture.X64 -> "x86_64" return archToFramework.values.firstOrNull()?.konanTarget?.family
Architecture.ARM32 -> "armv7" }
Architecture.ARM64 -> "arm64"
else -> error("Fat frameworks are not supported for architecture `$name`")
}
private val Architecture.clangMacro: String private val Architecture.clangMacro: String
get() = when(this) { get() = when(this) {
Architecture.X86 -> "__i386__"
Architecture.X64 -> "__x86_64__" Architecture.X64 -> "__x86_64__"
Architecture.ARM32 -> "__arm__" Architecture.ARM32 -> "__arm__"
Architecture.ARM64 -> "__aarch64__" Architecture.ARM64 -> "__aarch64__"
@@ -203,19 +204,18 @@ open class FatFrameworkTask: DefaultTask() {
fun delete(entry: String) = commands.add("Delete \"$entry\"") fun delete(entry: String) = commands.add("Delete \"$entry\"")
} }
private fun runLipo(inputFiles: Map<Architecture, File>, outputFile: File) = private fun runLipo(inputFiles: Collection<File>, outputFile: File) =
project.exec { project.exec { exec ->
it.executable = "/usr/bin/lipo" exec.executable = "/usr/bin/lipo"
it.args = mutableListOf("-create").apply { exec.args = listOf(
inputFiles.forEach { (arch, binary) -> "-create",
addAll(listOf("-arch", arch.lipoArg, binary.absolutePath)) *inputFiles.map { it.absolutePath }.toTypedArray(),
} "-output", outputFile.absolutePath
addArg("-output", outputFile.absolutePath) )
}
} }
private fun mergeBinaries(outputFile: File) = 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 -> private fun mergeHeaders(outputFile: File) = outputFile.writer().use { writer ->
@@ -311,7 +311,7 @@ open class FatFrameworkTask: DefaultTask() {
fatDsym.mkdirs() fatDsym.mkdirs()
// Merge dSYM binary. // Merge dSYM binary.
runLipo(dsymInputs.mapValues { (_, dsym) -> dsym.binary }, fatDsym.binary) runLipo(dsymInputs.values.map { it.binary }, fatDsym.binary)
// Copy dSYM's Info.plist. // 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. // It doesn't contain target-specific info or framework names except the bundle id so there is no need to edit it.