From 919c09d849cf3dec0915cc6464c460290b03437f Mon Sep 17 00:00:00 2001 From: Ilya Matveev Date: Fri, 11 Oct 2019 13:46:02 +0300 Subject: [PATCH] Fat frameworks: Support for watchOS and tvOS Issue #KT-34259 fixed --- .../jetbrains/kotlin/gradle/FatFrameworkIT.kt | 106 ++++++++++++------ .../targets/native/tasks/FatFrameworkTask.kt | 50 ++++----- 2 files changed, 97 insertions(+), 59 deletions(-) diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/FatFrameworkIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/FatFrameworkIT.kt index d5594146125..5c4831f8ee4 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/FatFrameworkIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/FatFrameworkIT.kt @@ -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( - """ - CFBundleSupportedPlatforms - - iPhoneOS - - """.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, + 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( + """ + CFBundleSupportedPlatforms + + $expectedPlistPlatform + + """.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") } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/FatFrameworkTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/FatFrameworkTask.kt index c7ae776569b..2401c49d86f 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/FatFrameworkTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/FatFrameworkTask.kt @@ -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) { 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, 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, 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.