diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/XCFrameworkIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/XCFrameworkIT.kt new file mode 100644 index 00000000000..a354be3422f --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/XCFrameworkIT.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.gradle.native + +import org.jetbrains.kotlin.gradle.BaseGradleIT +import org.jetbrains.kotlin.gradle.GradleVersionRequired +import org.jetbrains.kotlin.gradle.util.AGPVersion +import org.jetbrains.kotlin.konan.target.HostManager +import org.jetbrains.kotlin.test.util.KtTestUtil +import org.junit.Assume +import org.junit.BeforeClass +import kotlin.test.Test + +class XCFrameworkIT : BaseGradleIT() { + companion object { + @BeforeClass + @JvmStatic + fun assumeItsMac() { + Assume.assumeTrue(HostManager.hostIsMac) + } + } + + override val defaultGradleVersion = GradleVersionRequired.FOR_MPP_SUPPORT + override fun defaultBuildOptions() = super.defaultBuildOptions().copy( + androidHome = KtTestUtil.findAndroidSdk(), + androidGradlePluginVersion = AGPVersion.v3_6_0 + ) + + @Test + fun `assemble XCFramework for all available ios and watchos targets`() { + with(Project("appleXCFramework")) { + build("assembleSdkDebugXCFramework") { + assertSuccessful() + assertTasksExecuted(":shared:linkDebugFrameworkIosArm64") + assertTasksExecuted(":shared:linkDebugFrameworkIosX64") + assertTasksExecuted(":shared:linkDebugFrameworkWatchosArm32") + assertTasksExecuted(":shared:linkDebugFrameworkWatchosArm64") + assertTasksExecuted(":shared:linkDebugFrameworkWatchosX64") + assertTasksExecuted(":shared:assembleSdkDebugWatchosFatFrameworkForXCFramework") + assertTasksExecuted(":shared:assembleSdkDebugXCFramework") + assertFileExists("/shared/build/XCFrameworks/debug/sdk.xcframework") + assertFileExists("/shared/build/fat-framework/debug/sdk/watchos/sdk.framework") + assertFileExists("/shared/build/fat-framework/debug/sdk/watchos/sdk.framework.dSYM") + } + + build("assembleSdkDebugXCFramework") { + assertSuccessful() + assertTasksUpToDate(":shared:linkDebugFrameworkIosArm64") + assertTasksUpToDate(":shared:linkDebugFrameworkIosX64") + assertTasksUpToDate(":shared:linkDebugFrameworkWatchosArm32") + assertTasksUpToDate(":shared:linkDebugFrameworkWatchosArm64") + assertTasksUpToDate(":shared:linkDebugFrameworkWatchosX64") + assertTasksUpToDate(":shared:assembleSdkDebugWatchosFatFrameworkForXCFramework") + assertTasksUpToDate(":shared:assembleSdkDebugXCFramework") + } + } + } + + @Test + fun `assemble other XCFramework for ios targets`() { + with(Project("appleXCFramework")) { + build("assembleSharedXCFramework") { + assertSuccessful() + assertTasksExecuted(":shared:linkReleaseFrameworkIosArm64") + assertTasksExecuted(":shared:linkReleaseFrameworkIosX64") + assertTasksExecuted(":shared:assembleSharedReleaseXCFramework") + assertFileExists("/shared/build/XCFrameworks/release/shared.xcframework") + assertTasksExecuted(":shared:linkDebugFrameworkIosArm64") + assertTasksExecuted(":shared:linkDebugFrameworkIosX64") + assertTasksExecuted(":shared:assembleSharedDebugXCFramework") + assertFileExists("/shared/build/XCFrameworks/debug/shared.xcframework") + } + } + } + + @Test + fun `check there aren't XCFramework tasks without declaration in build script`() { + with(Project("sharedAppleFramework")) { + build("tasks") { + assertSuccessful() + assertTasksNotRegistered( + ":shared:assembleSharedDebugXCFramework", + ":shared:assembleSharedReleaseXCFramework", + ":shared:assembleSharedXCFramework" + ) + } + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/appleXCFramework/gradle.properties b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/appleXCFramework/gradle.properties new file mode 100644 index 00000000000..1d0d44f1d8e --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/appleXCFramework/gradle.properties @@ -0,0 +1,2 @@ +kotlin.mpp.enableGranularSourceSetsMetadata=true +kotlin.native.enableDependencyPropagation=false diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/appleXCFramework/settings.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/appleXCFramework/settings.gradle.kts new file mode 100644 index 00000000000..ec6ea15d7f0 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/appleXCFramework/settings.gradle.kts @@ -0,0 +1,13 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + mavenLocal() + } + val kotlin_version: String by settings + plugins { + kotlin("multiplatform").version(kotlin_version) + } +} + +include(":shared") \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/appleXCFramework/shared/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/appleXCFramework/shared/build.gradle.kts new file mode 100644 index 00000000000..581db9f1219 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/appleXCFramework/shared/build.gradle.kts @@ -0,0 +1,29 @@ +import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework + +plugins { + kotlin("multiplatform") +} + +kotlin { + val sdkXCFramework = XCFramework("sdk") + val otherXCFramework = XCFramework() + + ios { + binaries { + framework { + baseName = "shared" + sdkXCFramework.add(this) + otherXCFramework.add(this) + } + } + } + + watchos { + binaries { + framework { + baseName = "shared" + sdkXCFramework.add(this) + } + } + } +} diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/appleXCFramework/shared/src/commonMain/kotlin/com/github/jetbrains/myapplication/Greeting.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/appleXCFramework/shared/src/commonMain/kotlin/com/github/jetbrains/myapplication/Greeting.kt new file mode 100644 index 00000000000..758320432cf --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/appleXCFramework/shared/src/commonMain/kotlin/com/github/jetbrains/myapplication/Greeting.kt @@ -0,0 +1,5 @@ +package com.github.jetbrains.myapplication + +class Greeting { + fun greeting() = "Hello!" +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/XCFrameworkTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/XCFrameworkTask.kt new file mode 100644 index 00000000000..80497e8d6c8 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/XCFrameworkTask.kt @@ -0,0 +1,234 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.gradle.plugin.mpp.apple + +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.file.FileTree +import org.gradle.api.tasks.* +import org.jetbrains.kotlin.gradle.plugin.cocoapods.asValidFrameworkName +import org.jetbrains.kotlin.gradle.plugin.mpp.Framework +import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType +import org.jetbrains.kotlin.gradle.tasks.* +import org.jetbrains.kotlin.gradle.tasks.dependsOn +import org.jetbrains.kotlin.gradle.tasks.locateOrRegisterTask +import org.jetbrains.kotlin.gradle.tasks.registerTask +import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName +import org.jetbrains.kotlin.konan.target.KonanTarget +import java.io.File + +internal enum class AppleTarget( + val targetName: String, + val targets: List +) { + MACOS_DEVICE("macos", listOf(KonanTarget.MACOS_X64, KonanTarget.MACOS_ARM64)), + IPHONE_DEVICE("ios", listOf(KonanTarget.IOS_ARM32, KonanTarget.IOS_ARM64)), + IPHONE_SIMULATOR("iosSimulator", listOf(KonanTarget.IOS_X64, KonanTarget.IOS_SIMULATOR_ARM64)), + WATCHOS_DEVICE("watchos", listOf(KonanTarget.WATCHOS_ARM32, KonanTarget.WATCHOS_ARM64)), + WATCHOS_SIMULATOR("watchosSimulator", listOf(KonanTarget.WATCHOS_X64, KonanTarget.WATCHOS_SIMULATOR_ARM64)), + TVOS_DEVICE("tvos", listOf(KonanTarget.TVOS_ARM64)), + TVOS_SIMULATOR("tvosSimulator", listOf(KonanTarget.TVOS_X64, KonanTarget.TVOS_SIMULATOR_ARM64)) +} + +internal class XCFrameworkTaskHolder( + val buildType: NativeBuildType, + val task: TaskProvider, + val fatTasks: Map> +) { + companion object { + fun create(project: Project, xcFrameworkName: String, buildType: NativeBuildType): XCFrameworkTaskHolder { + + val parentTask = project.parentAssembleXCFrameworkTask(xcFrameworkName) + val task = project.registerAssembleXCFrameworkTask(xcFrameworkName, buildType) + parentTask.dependsOn(task) + + val fatTasks = AppleTarget.values().associate { fatTarget -> + val fatTask = project.registerAssembleFatForXCFrameworkTask(xcFrameworkName, buildType, fatTarget) + task.dependsOn(fatTask) + fatTarget to fatTask + } + + return XCFrameworkTaskHolder(buildType, task, fatTasks) + } + } +} + +class XCFrameworkConfig internal constructor( + private val taskHolders: List +) { + /** + * Adds the specified frameworks in this XCFramework. + */ + fun add(framework: Framework) { + taskHolders.forEach { holder -> + if (framework.buildType == holder.buildType) { + holder.task.configure { task -> task.from(framework) } + AppleTarget.values() + .firstOrNull { it.targets.contains(framework.konanTarget) } + ?.also { appleTarget -> + holder.fatTasks[appleTarget]?.configure { fatTask -> + fatTask.from(framework) + } + } + } + } + } +} + +fun Project.XCFramework( + xcFrameworkName: String = name +) = XCFrameworkConfig( + NativeBuildType.values().map { buildType -> + XCFrameworkTaskHolder.create(this, xcFrameworkName, buildType) + } +) + +private fun Project.parentAssembleXCFrameworkTask(xcFrameworkName: String): TaskProvider = + locateOrRegisterTask(lowerCamelCaseName("assemble", xcFrameworkName, "XCFramework")) { + it.group = "build" + it.description = "Assemble all types of registered '$xcFrameworkName' XCFramework" + } + +private fun Project.registerAssembleXCFrameworkTask( + xcFrameworkName: String, + buildType: NativeBuildType +): TaskProvider { + require(xcFrameworkName.isNotBlank()) + val taskName = lowerCamelCaseName( + "assemble", + xcFrameworkName, + buildType.getName(), + "XCFramework" + ) + return registerTask(taskName) { task -> + task.baseName = xcFrameworkName + task.buildType = buildType + } +} + +//see: https://developer.apple.com/forums/thread/666335 +private fun Project.registerAssembleFatForXCFrameworkTask( + xcFrameworkName: String, + buildType: NativeBuildType, + appleTarget: AppleTarget +): TaskProvider { + val taskName = lowerCamelCaseName( + "assemble", + xcFrameworkName, + buildType.getName(), + appleTarget.targetName, + "FatFrameworkForXCFramework" + ) + + return registerTask(taskName) { task -> + task.destinationDir = fatFrameworkDir(xcFrameworkName, buildType).resolve(appleTarget.targetName) + task.baseName = xcFrameworkName.asValidFrameworkName() + task.onlyIf { + task.frameworks.size > 1 + } + } +} + +private fun Project.fatFrameworkDir( + xcFrameworkName: String, + buildType: NativeBuildType +) = buildDir + .resolve("fat-framework") + .resolve(buildType.getName()) + .resolve(xcFrameworkName) + +abstract class XCFrameworkTask : DefaultTask() { + /** + * A base name for the XCFramework. + */ + @Input + var baseName: String = project.name + + private val xcFrameworkName: String + get() = baseName.asValidFrameworkName() + + /** + * A build type of the XCFramework. + */ + @Input + var buildType: NativeBuildType = NativeBuildType.RELEASE + + /** + * A collection of frameworks used ot build the XCFramework. + */ + private val allFrameworks: MutableSet = mutableSetOf() + + @get:Internal // We take it into account as an input in the inputFrameworkFiles property. + val frameworks: List + get() = allFrameworks.filter { it.buildType == buildType } + + @get:InputFiles + @get:PathSensitive(PathSensitivity.ABSOLUTE) + @get:SkipWhenEmpty + protected val inputFrameworkFiles: Iterable + get() = frameworks.map { project.fileTree(it.outputFile) } + + /** + * A parent directory for the XCFramework. + */ + @get:Internal // We take it into account as an output in the outputXCFrameworkFile property. + var outputDir: File = project.buildDir.resolve("XCFrameworks") + + /** + * A parent directory for the fat frameworks. + */ + @get:Internal // We take it into account as an input in the buildType and baseName properties. + protected val fatFrameworksDir: File + get() = project.fatFrameworkDir(xcFrameworkName, buildType) + + @get:OutputDirectory + protected val outputXCFrameworkFile: File + get() = outputDir.resolve(buildType.getName()).resolve("$xcFrameworkName.xcframework") + + /** + * Adds the specified frameworks in this XCFramework. + */ + fun from(vararg frameworks: Framework) { + frameworks.forEach { framework -> + require(framework.konanTarget.family.isAppleFamily) { + "XCFramework supports Apple frameworks only" + } + allFrameworks.add(framework) + dependsOn(framework.linkTask) + } + } + + @TaskAction + fun assemble() { + val frameworksForXCFramework = AppleTarget.values().mapNotNull { appleTarget -> + val group = frameworks.filter { it.konanTarget in appleTarget.targets } + when { + group.size == 1 -> { + group.first().outputFile + } + group.size > 1 -> { + fatFrameworksDir.resolve(appleTarget.targetName).resolve("$xcFrameworkName.framework") + } + else -> null + } + } + createXCFramework(frameworksForXCFramework, outputXCFrameworkFile) + } + + private fun createXCFramework(frameworks: List, output: File) { + if (output.exists()) output.deleteRecursively() + + val cmdArgs = mutableListOf("xcodebuild", "-create-xcframework") + frameworks.forEach { framework -> + cmdArgs.add("-framework") + cmdArgs.add(framework.path) + } + cmdArgs.add("-output") + cmdArgs.add(output.path) + project.exec { it.commandLine(cmdArgs) } + } +} \ No newline at end of file