KMM — Publish iOS/ watchOS / tvOS SDK Binaries
KMM has been getting attention of developers and it , obviously, deserves that.
We have different dependency managers for iOS , and KMM binaries are required to be made available on same for easy integration process of KMM Library consumers.
In this article — I am going to explain the steps needed for publishing KMM libraries via
- CocoaPods
- SPM — Swift Package Manager
- Carthage
I have opted for below formats of SDKs — to be shared
- XCFramework — iOS/WatchOS/TvOS
Lets start with supporting files setup
CocoaPods — Podspec file SetUp — Create SDK_NAME.podspec at project root with below contents
Pod::Spec.new do |spec|
spec.name = '<SDK_NAME>'
spec.version = "1.0.0"
spec.homepage = '<GIT_REPO_URL>'
spec.source = { :http => "<GIT_REPO_URL>/releases/download/1.1.0/RCCachingManager.xcframework.zip"}
spec.authors = '<EMAIL_ID>'
spec.license = 'https://opensource.org/licenses/Apache-2.0'
spec.summary = '<DESCRIPTION>'
spec.vendored_frameworks = "<SDK_NAME>.xcframework"
spec.ios.deployment_target = '12.0'
spec.watchos.deployment_target = '7.0'
spec.tvos.deployment_target = '13.0'
spec.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=*simulator*]' => 'arm64' }
spec.pod_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=*simulator*]' => 'arm64' }
end
Register your machine & email as publisher on CocoaPods
pod trunk register <GIT_EMAIL_HAVING_ACCESS_TO_REPO> '<NAME>' --description='<DESCRIPTION>
SPM — Package.swift file SetUp — Create Package.swift at project root with below contents
// swift-tools-version:5.4
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "<SDK_NAME>",
platforms: [
.iOS(.v12), .tvOS(.v13), .watchOS(.v7)
],
products: [
.library(
name: "<SDK_NAME>",
targets: ["<SDK_NAME>"]),
],
dependencies: [
// no dependencies
],
targets: [
.binaryTarget(
name: "<SDK_NAME>",
url: "<GIT_REPO_URL>/releases/download/1.0.0/<SDK_NAME>.xcframework.zip",
checksum: "c8982f3e5e56adea5355396a68fb09c37f9223d09738ed1037c643b590e38573"
),
]
)
Carthage — JSON file SetUp — Create Folder with Carthage name — having file <SDK_NAME>.json — at project root with below contents
{
"1.0.0":"<GIT_REPO_URL>/releases/download/1.0.0/<SDK_NAME>.xcframework.zip"
}
Lets get started for build.gradle.kts setup
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework
Create a global variable
version = "1.1.0"
val iOSBinaryName = "<SDK_NAME>"
Under kotlin section, add below
val xcf = XCFramework()
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "${iOSBinaryName}"
xcf.add(this)
}
}
watchos {
binaries.framework {
baseName = "${iOSBinaryName}"
xcf.add(this)
}
}
tvos {
binaries.framework {
baseName = "${iOSBinaryName}"
xcf.add(this)
}
}
Under sourceSets sub-section of kotlin section, add below —
val iosX64Main by getting
val iosArm64Main by getting
val iosSimulatorArm64Main by getting
val watchosMain by getting
val tvosMain by getting
val iosMain by creating {
dependsOn(commonMain)
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
iosSimulatorArm64Main.dependsOn(this)
watchosMain.dependsOn(this)
tvosMain.dependsOn(this)
}
val iosX64Test by getting
val iosArm64Test by getting
val iosSimulatorArm64Test by getting
val iosTest by creating {
dependsOn(commonTest)
iosX64Test.dependsOn(this)
iosArm64Test.dependsOn(this)
iosSimulatorArm64Test.dependsOn(this)
}
Add below in build.gradle.kts — module level
/**
* This task prepares Release Artefact for iOS XCFramework
* Updates Podspec, Package.swift, Carthage contents with version and checksum
*/
tasks.register("prepareReleaseOfiOSXCFramework") {
description = "Publish iOS framework to the Cocoa Repo"
// Create Release Framework for Xcode
dependsOn("assembleXCFramework", "packageDistribution")
doLast {
// Update Podspec Version
val poddir = File("$rootDir/$iOSBinaryName.podspec")
val podtempFile = File("$rootDir/$iOSBinaryName.podspec.new")
val podreader = poddir.bufferedReader()
val podwriter = podtempFile.bufferedWriter()
var podcurrentLine: String?
while (podreader.readLine().also { currLine -> podcurrentLine = currLine } != null) {
if (podcurrentLine?.trim()?.startsWith("spec.version") == true) {
podwriter.write(" spec.version = \"${version}\"" + System.lineSeparator())
} else if (podcurrentLine?.trim()?.startsWith("spec.source") == true) {
podwriter.write(" spec.source = { :http => \"https://github.com/rakeshchander/CachingLibrary-KMM/releases/download/${version}/${iOSBinaryName}.xcframework.zip\"}" + System.lineSeparator())
} else {
podwriter.write(podcurrentLine + System.lineSeparator())
}
}
podwriter.close()
podreader.close()
podtempFile.renameTo(poddir)
// Update Cartfile Version
val cartdir = File("$rootDir/Carthage/$iOSBinaryName.json")
val carttempFile = File("$rootDir/Carthage/$iOSBinaryName.json.new")
val cartreader = cartdir.bufferedReader()
val cartwriter = carttempFile.bufferedWriter()
var cartcurrentLine: String?
while (cartreader.readLine().also { currLine -> cartcurrentLine = currLine } != null) {
if (cartcurrentLine?.trim()?.startsWith("{") == true) {
cartwriter.write("{"+ System.lineSeparator())
cartwriter.write(" \"${version}\":\"https://github.com/rakeshchander/CachingLibrary-KMM/releases/download/${version}/${iOSBinaryName}.xcframework.zip\","+ System.lineSeparator())
} else if (cartcurrentLine?.trim()?.startsWith("\"${version}\"") == true) {
continue
} else {
cartwriter.write(cartcurrentLine + System.lineSeparator())
}
}
cartwriter.close()
cartreader.close()
carttempFile.renameTo(cartdir)
// Update Package.swift Version
// Calculate Checksum
val checksumValue: String = org.apache.commons.io.output.ByteArrayOutputStream()
.use { outputStream ->
// Calculate checksum
project.exec {
workingDir = File("$rootDir")
commandLine("swift", "package", "compute-checksum", "${iOSBinaryName}.xcframework.zip")
standardOutput = outputStream
}
outputStream.toString()
}
val spmdir = File("$rootDir/Package.swift")
val spmtempFile = File("$rootDir/Package.swift.new")
val spmreader = spmdir.bufferedReader()
val spmwriter = spmtempFile.bufferedWriter()
var spmcurrentLine: String?
while (spmreader.readLine().also { currLine -> spmcurrentLine = currLine } != null) {
if (spmcurrentLine?.trim()?.startsWith("url") == true) {
spmwriter.write(" url: \"<GIT_REPO_URL>/releases/download/${version}/${iOSBinaryName}.xcframework.zip\"," + System.lineSeparator())
} else if (spmcurrentLine?.trim()?.startsWith("checksum") == true) {
spmwriter.write(" checksum: \"${checksumValue.trim()}\"" + System.lineSeparator())
} else {
spmwriter.write(spmcurrentLine + System.lineSeparator())
}
}
spmwriter.close()
spmreader.close()
spmtempFile.renameTo(spmdir)
}
}**
* Task to create zip for XCFramework
* To be used by Carthage for Distribution as Binary
*/
tasks.create<Zip>("packageDistribution") {
// Delete existing XCFramework
delete("$rootDir/XCFramework")
// Replace XCFramework File at root from Build Directory
copy {
from("$buildDir/XCFrameworks/release")
into("$rootDir/XCFramework")
}
// Delete existing ZIP, if any
delete("$rootDir/${iOSBinaryName}.xcframework.zip")
// ZIP File Name - as per Carthage Nomenclature
archiveFileName.set("${iOSBinaryName}.xcframework.zip")
// Destination for ZIP File
destinationDirectory.set(layout.projectDirectory.dir("../"))
// Source Directory for ZIP
from(layout.projectDirectory.dir("../XCFramework"))
}
Distribution SetUp
Sync your gradle file. You will find below task added to your Project
- prepareReleaseOfiOSXCFramework
You can execute this command now via Terminal OR double click from task list.
Save <SDK_NAME>.xcframework.zip (from project root) to some other location.
Push your Code and Merge to master.
Create a GIT Release with same version as that of build.gradle.kts and upload <SDK_NAME>.xcframework.zip as release artefacts.
For CocoaPods Publish, execute below command-
pod trunk push <SDK_NAME>.podspec --allow-warnings
Now you can import in your iOS/ watchOS / tvOS projects following below —
CocoaPods
- Add below in podfile — in respective target block
source 'https://github.com/CocoaPods/Specs.git'pod '<SDK_NAME>'
Swift Package Manager — SPM
Once you have your Swift package set up, adding SDK as a dependency is as easy as adding it to the dependencies
value of your Package.swift
.
dependencies: [
.package(url: "<GIT_REPO_URL>")
]
Carthage (min Carthage version 0.38.0)
To integrate SDK into your Xcode project using Carthage, specify it in your Cartfile
:
binary "<GIT_REPO_URL>/blob/main/Carthage/GrowthBook.json"
Execute below command -
source 'https://github.com/CocoaPods/Specs.git'
All Set!! Distribute SDKs