Fat frameworks: Support for watchOS and tvOS
Issue #KT-34259 fixed
This commit is contained in:
+72
-34
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-25
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user