Add 'XCFramework' extension for creating XCFrameworks.
#KT-42667
This commit is contained in:
committed by
Space
parent
9fa55a6809
commit
84ae3df8b6
+92
@@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
kotlin.mpp.enableGranularSourceSetsMetadata=true
|
||||
kotlin.native.enableDependencyPropagation=false
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
}
|
||||
val kotlin_version: String by settings
|
||||
plugins {
|
||||
kotlin("multiplatform").version(kotlin_version)
|
||||
}
|
||||
}
|
||||
|
||||
include(":shared")
|
||||
+29
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package com.github.jetbrains.myapplication
|
||||
|
||||
class Greeting {
|
||||
fun greeting() = "Hello!"
|
||||
}
|
||||
+234
@@ -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) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user