Skip to content

Instantly share code, notes, and snippets.

@LouisCAD
Forked from Ribesg/!README.md
Created February 28, 2019 11:31
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save LouisCAD/3a9d8fc1f8dc118b9ae5fb8f98abe118 to your computer and use it in GitHub Desktop.
Save LouisCAD/3a9d8fc1f8dc118b9ae5fb8f98abe118 to your computer and use it in GitHub Desktop.
How to use an iOS Framework in a Kotlin MPP library published to Maven

This gist demonstrates how to build a Kotlin MPP library so that the iOS sourceSet depends on and uses an iOS Framework as a dependency.

Key ideas:

  • We use Carthage to retrieve/build the iOS Framework.
  • We use cinterop to create bindings allowing us to use the iOS Framework from Kotlin
  • We build and publish the library using ./gradlew publishToMavenLocal
  • We build and publish klib artifacts for both the arm64 and x86_64 architectures, you can easily add arm32 if you need.
  • Note that the publish process also publishes a cinterop klib artifact, allowing dependents to also know about the iOS Framework headers.

You can find a gist explaining how to use such library in an iOS app here.

More notes:

  • The .def file's name contains its path with / replaced with -.
  • I have no idea what I'm doing in this .def file, but it works. Half the entries are probably useless.
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTargetPreset
import org.jetbrains.kotlin.gradle.tasks.CInteropProcess
group = "com.example.lib"
version = "1.2.3"
repositories {
google()
jcenter()
}
plugins {
id("maven-publish")
id("com.android.library") version "3.2.1"
id("org.jetbrains.kotlin.multiplatform") version "1.3.21"
}
android {
compileSdkVersion(28)
defaultConfig {
minSdkVersion(16)
targetSdkVersion(28)
}
sourceSets {
val main by getting {
setRoot("src/androidMain")
}
val test by getting {
setRoot("src/androidTest")
}
}
lintOptions {
isAbortOnError = false
}
packagingOptions {
exclude("META-INF/*.kotlin_module")
}
}
kotlin {
data class IosTarget(val name: String, val preset: String, val id: String)
val iosTargets = listOf(
IosTarget("ios", "iosArm64", "ios-arm64"),
IosTarget("iosSim", "iosX64", "ios-x64")
)
android {
publishAllLibraryVariants()
}
for ((targetName, presetName, id) in iosTargets) {
targetFromPreset(presets.getByName<KotlinNativeTargetPreset>(presetName), targetName) {
compilations {
val main by getting {
val carthageBuildDir = "$projectDir/Carthage/Build/iOS"
cinterops(Action {
val bugsnag by creating {
defFile("src/iosMain/cinterop/bugsnag.def")
includeDirs.allHeaders("$carthageBuildDir/Bugsnag.framework/Headers")
}
})
linkerOpts("-F$carthageBuildDir")
}
}
mavenPublication {
artifactId = "${project.name}-$id"
}
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(kotlin("stdlib-common"))
}
}
val androidMain by getting {
dependencies {
implementation(kotlin("stdlib"))
implementation(kotlin("reflect"))
implementation("com.bugsnag:bugsnag-android:4.11.0")
}
}
val iosMain by getting {
}
val iosSimMain by getting {
dependsOn(iosMain)
}
}
}
// Create Carthage tasks
listOf("bootstrap", "update").forEach { type ->
task<Exec>("carthage${type.capitalize()}") {
group = "carthage"
executable = "carthage"
args(
type,
"--platform", "iOS",
"--no-use-binaries", // Provided binaries are sometimes problematic, remove this to speedup process
"--cache-builds"
)
}
}
// Make CInterop tasks depend on Carthage
tasks.withType(CInteropProcess::class) {
dependsOn("carthageBootstrap")
}
// Delete build directory on clean
tasks.named<Delete>("clean") {
delete(buildDir)
}
// Temporary workaround for https://youtrack.jetbrains.com/issue/KT-27170
configurations.create("compileClasspath")
github "bugsnag/bugsnag-cocoa" ~> 5.0
rootProject.name = "my-mpp-library"
enableFeaturePreview("GRADLE_METADATA")
pluginManagement {
repositories {
google()
gradlePluginPortal()
}
resolutionStrategy {
eachPlugin {
if (requested.id.id == "com.android.library") {
useModule("com.android.tools.build:gradle:${requested.version}")
}
}
}
}
depends = Foundation
package = framework.Bugsnag
language = Objective-C
headers = Bugsnag.h
headersFilter = **
compilerOpts = -framework Bugsnag -fmodules
linkerOpts = -framework Bugsnag
@jonasbark
Copy link

jonasbark commented Sep 5, 2019

Thanks for the helpful infos!
It looks like with Kotlin 1.3.50 linkerOpts doesn't exist anymore on the KotlinNativeCompilation target. It exsist in the DefaultCInteropSettings, but that prints out a warning that it's not used anymore.
Is target.binaries.forEach { it.linkerOpts ... } the correct way to handle this? I've tried it but so far I failed at Reason: image not found

Edit: for unit tests and their dynamic frameworks: copy the frameworks to the location of the kexe in a directory called Frameworks and voilà, it works.

@LouisCAD
Copy link
Author

LouisCAD commented Sep 5, 2019

This is a just a fork of another gist. All the credit goes to Gael Ribes. Please ask him in his gist. I haven't played much with CInterop yet to be helpful on that right now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment