Add 'XCFramework' extension for creating XCFrameworks.

#KT-42667
This commit is contained in:
Konstantin Tskhovrebov
2021-06-21 17:10:47 +03:00
committed by Space
parent 9fa55a6809
commit 84ae3df8b6
6 changed files with 375 additions and 0 deletions
@@ -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"
)
}
}
}
}
@@ -0,0 +1,2 @@
kotlin.mpp.enableGranularSourceSetsMetadata=true
kotlin.native.enableDependencyPropagation=false
@@ -0,0 +1,13 @@
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
mavenLocal()
}
val kotlin_version: String by settings
plugins {
kotlin("multiplatform").version(kotlin_version)
}
}
include(":shared")
@@ -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)
}
}
}
}
@@ -0,0 +1,5 @@
package com.github.jetbrains.myapplication
class Greeting {
fun greeting() = "Hello!"
}
@@ -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<KonanTarget>
) {
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<XCFrameworkTask>,
val fatTasks: Map<AppleTarget, TaskProvider<FatFrameworkTask>>
) {
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<XCFrameworkTaskHolder>
) {
/**
* 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<Task> =
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<XCFrameworkTask> {
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<FatFrameworkTask> {
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<Framework> = mutableSetOf()
@get:Internal // We take it into account as an input in the inputFrameworkFiles property.
val frameworks: List<Framework>
get() = allFrameworks.filter { it.buildType == buildType }
@get:InputFiles
@get:PathSensitive(PathSensitivity.ABSOLUTE)
@get:SkipWhenEmpty
protected val inputFrameworkFiles: Iterable<FileTree>
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<File>, 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) }
}
}