diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/FatFrameworkIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/FatFrameworkIT.kt index bea9360bec4..d68ded66386 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/FatFrameworkIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/FatFrameworkIT.kt @@ -7,12 +7,14 @@ package org.jetbrains.kotlin.gradle.native import org.jetbrains.kotlin.gradle.* import org.jetbrains.kotlin.gradle.embedProject +import org.jetbrains.kotlin.gradle.tasks.FrameworkLayout import org.jetbrains.kotlin.gradle.transformProjectWithPluginsDsl import org.jetbrains.kotlin.gradle.util.* import org.jetbrains.kotlin.konan.target.HostManager import org.junit.Assume import org.junit.BeforeClass import org.junit.Test +import java.io.File import java.util.* import kotlin.test.assertTrue @@ -71,10 +73,39 @@ class FatFrameworkIT : BaseGradleIT() { } } + @Test + fun smokeMacos() { + with(transformProjectWithPluginsDsl("smoke", directoryPrefix = "native-fat-framework")) { + + gradleBuildScript().modify { + it.checkedReplace("iosArm32()", "") + .checkedReplace("iosArm64()", "macosArm64()") + .checkedReplace("iosX64()", "macosX64()") + } + + build("fat") { + checkSmokeBuild( + archs = listOf("x64", "arm64"), + targetPrefix = "macos", + expectedPlistPlatform = "MacOSX", + true + ) + + val binary = fileInWorkingDir("build/fat-framework/smoke.framework/Versions/A/smoke") + with(runProcess(listOf("file", binary.absolutePath), projectDir)) { + assertTrue(isSuccessful) + assertTrue(output.contains("\\(for architecture x86_64\\):\\s+Mach-O 64-bit dynamically linked shared library x86_64".toRegex())) + assertTrue(output.contains("\\(for architecture arm64\\):\\s+Mach-O 64-bit dynamically linked shared library arm64".toRegex())) + } + } + } + } + private fun CompiledProject.checkSmokeBuild( archs: List, targetPrefix: String, - expectedPlistPlatform: String + expectedPlistPlatform: String, + isMacosFramework: Boolean = false ) { assertSuccessful() val linkTasks = archs.map { @@ -89,17 +120,21 @@ class FatFrameworkIT : BaseGradleIT() { 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 frameworkLayout = FrameworkLayout(fileInWorkingDir("build/fat-framework/smoke.framework"), isMacosFramework) - val headerContent = fileInWorkingDir("build/fat-framework/smoke.framework/Headers/smoke.h").readText() + fun File.projectRelative() = relativeTo(File(workingDir, project.projectName)).path + + assertFileExists(frameworkLayout.binary.projectRelative()) + assertFileExists(frameworkLayout.header.projectRelative()) + assertFileExists(frameworkLayout.dSYM.binary.projectRelative()) + + val headerContent = frameworkLayout.header.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") + val plistContent = frameworkLayout.infoPlist .readLines() .joinToString(separator = "\n") { it.trim() } diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/FatFrameworkTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/FatFrameworkTask.kt index 736f9800074..a90c9073d55 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/FatFrameworkTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/tasks/FatFrameworkTask.kt @@ -20,8 +20,11 @@ import org.jetbrains.kotlin.konan.target.HostManager import org.jetbrains.kotlin.konan.target.KonanTarget import org.jetbrains.kotlin.konan.target.KonanTarget.* import org.jetbrains.kotlin.konan.util.visibleName +import org.jetbrains.kotlin.utils.PathUtil import java.io.ByteArrayOutputStream import java.io.File +import java.nio.file.Files +import java.nio.file.Path class FrameworkDsymLayout(val rootDir: File) { init { @@ -41,27 +44,43 @@ class FrameworkDsymLayout(val rootDir: File) { fun exists() = rootDir.exists() } -class FrameworkLayout(val rootDir: File) { +class FrameworkLayout( + val rootDir: File, + val isMacosFramework: Boolean +) { init { require(rootDir.extension == "framework") } private val frameworkName = rootDir.nameWithoutExtension + private val macosVersionsDir = rootDir.resolve("Versions") + private val macosADir = macosVersionsDir.resolve("A") + private val macosResourcesDir = macosADir.resolve("Resources") + private val contentDir = if (isMacosFramework) macosADir else rootDir - val headerDir = rootDir.resolve("Headers") - val modulesDir = rootDir.resolve("Modules") + val headerDir = contentDir.resolve("Headers") + val modulesDir = contentDir.resolve("Modules") - val binary = rootDir.resolve(frameworkName) + val binary = contentDir.resolve(frameworkName) val header = headerDir.resolve("$frameworkName.h") val moduleFile = modulesDir.resolve("module.modulemap") - val infoPlist = rootDir.resolve("Info.plist") + val infoPlist = (if (isMacosFramework) macosResourcesDir else rootDir).resolve("Info.plist") val dSYM = FrameworkDsymLayout(rootDir.parentFile.resolve("$frameworkName.framework.dSYM")) fun mkdirs() { rootDir.mkdirs() - headerDir.mkdir() - modulesDir.mkdir() + headerDir.mkdirs() + modulesDir.mkdirs() + if (isMacosFramework) { + macosResourcesDir.mkdirs() + val root = rootDir.toPath() + Files.createSymbolicLink(root.resolve("Headers"), headerDir.toPath()) + Files.createSymbolicLink(root.resolve("Modules"), modulesDir.toPath()) + Files.createSymbolicLink(root.resolve("Resources"), macosResourcesDir.toPath()) + Files.createSymbolicLink(root.resolve(frameworkName), binary.toPath()) + Files.createSymbolicLink(macosVersionsDir.toPath().resolve("Current"), macosADir.toPath()) + } } fun exists() = rootDir.exists() @@ -83,7 +102,7 @@ class FrameworkDescriptor( } val name = file.nameWithoutExtension - val files = FrameworkLayout(file) + val files = FrameworkLayout(file, target.family == Family.OSX) } /** @@ -124,9 +143,6 @@ open class FatFrameworkTask : DefaultTask() { val fatFramework: File get() = destinationDir.resolve(fatFrameworkName + ".framework") - private val fatFrameworkLayout: FrameworkLayout - get() = FrameworkLayout(fatFramework) - @get:PathSensitive(PathSensitivity.ABSOLUTE) @get:IgnoreEmptyDirectories @get:InputFiles @@ -212,6 +228,7 @@ open class FatFrameworkTask : DefaultTask() { private val FrameworkDescriptor.plistPlatform: String get() = when (target) { + MACOS_X64, MACOS_ARM64 -> "MacOSX" IOS_ARM32, IOS_ARM64, IOS_X64, IOS_SIMULATOR_ARM64 -> "iPhoneOS" TVOS_ARM64, TVOS_X64, TVOS_SIMULATOR_ARM64 -> "AppleTVOS" WATCHOS_ARM32, WATCHOS_ARM64, WATCHOS_X86, WATCHOS_X64, WATCHOS_SIMULATOR_ARM64 -> "WatchOS" @@ -358,7 +375,7 @@ open class FatFrameworkTask : DefaultTask() { } } - private fun mergeDSYM() { + private fun mergeDSYM(fatDsym: FrameworkDsymLayout) { val dsymInputs = archToFramework.mapValues { (_, framework) -> framework.files.dSYM }.filterValues { @@ -369,7 +386,6 @@ open class FatFrameworkTask : DefaultTask() { return } - val fatDsym = fatFrameworkLayout.dSYM fatDsym.mkdirs() // Merge dSYM binary. @@ -386,21 +402,22 @@ open class FatFrameworkTask : DefaultTask() { @TaskAction protected fun createFatFramework() { - val outFramework = fatFrameworkLayout + val outFramework = FrameworkLayout(fatFramework, getFatFrameworkFamily() == Family.OSX) outFramework.mkdirs() mergeBinaries(outFramework.binary) mergeHeaders(outFramework.header) createModuleFile(outFramework.moduleFile, fatFrameworkName) mergePlists(outFramework.infoPlist, fatFrameworkName) - mergeDSYM() + mergeDSYM(outFramework.dSYM) } companion object { private val supportedTargets = listOf( IOS_ARM32, IOS_ARM64, IOS_X64, WATCHOS_ARM32, WATCHOS_ARM64, WATCHOS_X86, WATCHOS_X64, - TVOS_ARM64, TVOS_X64 + TVOS_ARM64, TVOS_X64, + MACOS_X64, MACOS_ARM64 ) fun isSupportedTarget(target: KotlinNativeTarget): Boolean {