Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e20435e
basic objc wrapper to use kmp on ios
maerki Jan 25, 2026
3d46afc
kmp: extract mapcore module
maerki Jan 26, 2026
534004d
kmp: set group id
maerki Jan 26, 2026
26a8e5e
kmp: move sources to kmp directory
maerki Jan 26, 2026
1ea9d5a
kmp: inline plugin and dependency versions
maerki Jan 26, 2026
f1fab04
kmp: add settings with plugin repos
maerki Jan 26, 2026
f278462
kmp: define iosMain source set
maerki Jan 26, 2026
a616aad
kmp: wire iosMain and enable AndroidX
maerki Jan 26, 2026
79b0230
mapcore: update font loader bundle path
maerki Jan 26, 2026
fff55ff
mapcore: add resource bundle registry
maerki Jan 26, 2026
bf65b31
gradle + ignore
maerki Jan 26, 2026
2c69123
kmp: simplify map interop api
maerki Jan 26, 2026
e1708f5
kmp: align expect/actual for camera and raster
maerki Jan 26, 2026
1b28149
kmp: align camera parameter names
maerki Jan 26, 2026
06f33ba
kmp: fix camera expect/actual members
maerki Jan 26, 2026
1bb478b
kmp: remove redundant layer cast
maerki Jan 26, 2026
573ad12
kmp: make GpsLayerHandle public
maerki Jan 26, 2026
a4f3341
fix(kmp): add ios arm64 cinterop and spm fallback
maerki Jan 26, 2026
09becdb
fix(kmp): avoid duplicate spm task registration
maerki Jan 26, 2026
328de4c
update gradle
maerki Jan 27, 2026
0a644ec
remove dependsOn
maerki Jan 27, 2026
555bd91
targetHierarchy.default()
maerki Jan 27, 2026
c37b8e6
improve kmp setup
maerki Jan 27, 2026
7fa2487
configure tasks
maerki Jan 27, 2026
f7ba2ae
kmp: opt in to ExperimentalForeignApi for Coord
maerki Jan 27, 2026
3c6206c
kmp: opt in to ExperimentalForeignApi in iosMain
maerki Jan 27, 2026
173c8dd
kmp: drop redundant ExperimentalForeignApi opt-in
maerki Jan 27, 2026
fa80dbb
Adjust android multiplatform plugin version, adjust minSDK for android
maurhofer-ubique Jan 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,6 @@ DerivedData/
compile_commands.json

cmake-build-test
/local.properties
/.gradle
/.kotlin
15 changes: 14 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ let package = Package(
name: "MapCoreSharedModuleCpp",
targets: ["MapCoreSharedModuleCpp"]
),
.library(
name: "MapCoreObjC",
targets: ["MapCoreObjC"]
),
],
dependencies: [
.package(url: "https://github.com/UbiqueInnovation/djinni.git", .upToNextMinor(from: "1.0.9")),
Expand Down Expand Up @@ -117,7 +121,7 @@ let package = Package(
.product(name: "Atomics", package: "swift-atomics"),
],
path: "ios",
exclude: ["readme.md"],
exclude: ["readme.md", "objc"],
resources: [
.process("graphics/Shader/Metal/")
]
Expand Down Expand Up @@ -189,6 +193,15 @@ let package = Package(
// .disableWarning("reorder"),
]
),
.target(
name: "MapCoreObjC",
dependencies: [
"MapCore",
"MapCoreSharedModule",
],
path: "ios/objc",
publicHeadersPath: "include"
),
],
cxxLanguageStandard: .cxx17
)
337 changes: 337 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget
import org.jetbrains.kotlin.gradle.tasks.CInteropProcess
import java.net.URI
import javax.inject.Inject

group = "io.openmobilemaps.mapscore"

val mapCoreCheckoutPath = project.layout.projectDirectory.asFile.absolutePath
val mapCoreMetalToolchain = providers.environmentVariable("MAPCORE_METAL_TOOLCHAIN")
.orElse(providers.gradleProperty("mapCoreMetalToolchain"))
.orElse("")
val mapCoreMetallibToolchain = providers.environmentVariable("MAPCORE_METALLIB_TOOLCHAIN")
.orElse(providers.gradleProperty("mapCoreMetallibToolchain"))
.orElse("com.apple.dt.toolchain.XcodeDefault")
val mapCoreMetalTargetSimulator = providers.environmentVariable("MAPCORE_METAL_TARGET_SIMULATOR")
.orElse(providers.gradleProperty("mapCoreMetalTargetSimulator"))
.orElse("air64-apple-ios26.0-simulator")
val mapCoreMetalTargetDevice = providers.environmentVariable("MAPCORE_METAL_TARGET_DEVICE")
.orElse(providers.gradleProperty("mapCoreMetalTargetDevice"))
.orElse("air64-apple-ios26.0")

plugins {
id("org.jetbrains.kotlin.multiplatform") version "2.3.0"
id("com.android.kotlin.multiplatform.library") version "8.13.2"
id("io.github.frankois944.spmForKmp") version "1.4.6"
}

@OptIn(ExperimentalKotlinGradlePluginApi::class)
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xexpect-actual-classes") // Opt-in for expect/actual classes
}

applyDefaultHierarchyTemplate()

android {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
namespace = "io.openmobilemaps.mapscore.kmp"
compileSdk = 36
minSdk = 28
}

val mapCoreCinteropName = "MapCoreKmp"
val iosTargets = listOf(
iosArm64(),
iosSimulatorArm64()
)

iosTargets.forEach { iosTarget ->
iosTarget.compilations {
val main by getting {
cinterops.create(mapCoreCinteropName)
}
}
}

sourceSets {
val commonMain by getting {
kotlin.srcDir("kmp/commonMain/kotlin")
dependencies {
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
}
}
val androidMain by getting {
kotlin.srcDir("kmp/androidMain/kotlin")
dependencies {
api("io.openmobilemaps:mapscore:3.6.0")
api("io.openmobilemaps:layer-gps:3.6.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.3")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.3")
implementation("ch.ubique.android:djinni-support-lib:1.1.1")
}
}
val iosMain by getting {
kotlin.srcDir("kmp/iosMain/kotlin")
languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi")
}
val iosArm64Main by getting {
languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi")
}
val iosSimulatorArm64Main by getting {
languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi")
}
}
}

swiftPackageConfig {
create("MapCoreKmp") {
minIos = "14.0"
bridgeSettings {
cSetting {
headerSearchPath = listOf("Sources/djinni-objc")
}
}
dependency {
localPackage(
mapCoreCheckoutPath,
"maps-core"
) {
add("MapCoreObjC", exportToKotlin = true)
add("MapCoreSharedModule", exportToKotlin = true)
}
remotePackageVersion(
url = URI("https://github.com/openmobilemaps/layer-gps"),
packageName = "layer-gps",
version = "3.6.0",
) {
add("LayerGpsSharedModule", exportToKotlin = true)
}
}
}
}

// Avoid overlapping Package.resolved outputs between per-target SwiftPM compile tasks.
tasks
.matching { it.name.startsWith("SwiftPackageConfigAppleMapCoreKmpCompileSwiftPackage") }
.configureEach {
val packageResolveFileGetter = javaClass.methods.firstOrNull { it.name == "getPackageResolveFile" }
val packageResolveFile =
project.layout.buildDirectory.file(
"spmKmpPlugin/MapCoreKmp/package-resolved/${name}/Package.resolved",
)
val packageResolveProperty =
packageResolveFileGetter
?.invoke(this) as? RegularFileProperty
packageResolveProperty?.set(packageResolveFile)

doLast {
val sourceFile =
project.layout.buildDirectory
.file("spmKmpPlugin/MapCoreKmp/Package.resolved")
.get()
.asFile
if (!sourceFile.exists()) return@doLast
val targetFile =
project.layout.buildDirectory
.file("spmKmpPlugin/MapCoreKmp/package-resolved/${name}/Package.resolved")
.get()
.asFile
targetFile.parentFile.mkdirs()
sourceFile.copyTo(targetFile, overwrite = true)
}
}

tasks.withType<CInteropProcess>().configureEach {
if (name.contains("MapCoreKmp")) {
settings.compilerOpts("-I$mapCoreCheckoutPath/external/djinni/support-lib/objc")
settings.compilerOpts("-I$mapCoreCheckoutPath/bridging/ios")
}
}

val mapCoreSpmBuiltDir =
project.layout.buildDirectory.dir("spmKmpPlugin/MapCoreKmp/scratch/arm64 x86_64-apple-ios-simulator/release").get().asFile
mapCoreSpmBuiltDir.mkdirs()

val mapCoreSpmDeviceDir =
project.layout.buildDirectory.dir("spmKmpPlugin/MapCoreKmp/scratch/arm64-apple-ios/release")
val mapCoreSpmSimulatorDir =
project.layout.buildDirectory.dir("spmKmpPlugin/MapCoreKmp/scratch/arm64-apple-ios-simulator/release")

afterEvaluate {
val deviceTaskName = "SwiftPackageConfigAppleMapCoreKmpCompileSwiftPackageIosArm64"
val simulatorTaskName = "SwiftPackageConfigAppleMapCoreKmpCompileSwiftPackageIosSimulatorArm64"
if (tasks.findByName(deviceTaskName) != null) return@afterEvaluate
runCatching {
tasks.register(deviceTaskName) {
group = "io.github.frankois944.spmForKmp.tasks"
description = "Fallback: copy simulator SwiftPM output for iOS device metal compilation"
dependsOn(simulatorTaskName)
doLast {
val sourceDir = mapCoreSpmSimulatorDir.get().asFile
if (!sourceDir.exists()) return@doLast
val targetDir = mapCoreSpmDeviceDir.get().asFile
targetDir.mkdirs()
copy {
from(sourceDir)
into(targetDir)
}
}
}
}.onFailure { error ->
val message = error.message.orEmpty()
if (!message.contains("already exists")) {
throw error
}
}
}

abstract class CompileMapCoreMetallibTask : DefaultTask() {
@get:Input
abstract val sdk: Property<String>

@get:Input
abstract val toolchainId: Property<String>

@get:Input
abstract val metallibToolchainId: Property<String>

@get:Input
abstract val targetTriple: Property<String>

@get:InputDirectory
abstract val bundleDir: DirectoryProperty

@get:InputFiles
abstract val metalSources: ConfigurableFileCollection

@get:OutputDirectory
abstract val outputDir: DirectoryProperty

@get:OutputFile
abstract val metallibFile: RegularFileProperty

@get:Inject
abstract val execOperations: ExecOperations

@TaskAction
fun run() {
val bundleRoot = bundleDir.get().asFile
if (!bundleRoot.exists()) return
val metalFiles = metalSources.files.sortedBy { it.name }
if (metalFiles.isEmpty()) return
val toolchain = toolchainId.orNull?.takeIf { it.isNotBlank() }
val metallibToolchain = metallibToolchainId.orNull?.takeIf { it.isNotBlank() }

val outputRoot = outputDir.get().asFile
outputRoot.mkdirs()

val airFiles = metalFiles.map { file ->
File(outputRoot, "${file.nameWithoutExtension}.air")
}

metalFiles.zip(airFiles).forEach { (metalFile, airFile) ->
execOperations.exec {
val args = buildList {
add("xcrun")
if (toolchain != null) {
add("--toolchain")
add(toolchain)
}
addAll(
listOf(
"-sdk",
sdk.get(),
"metal",
"-target",
targetTriple.get(),
"-c",
metalFile.absolutePath,
"-o",
airFile.absolutePath,
),
)
}
commandLine(args)
}
}

val metallibArgs = buildList {
add("xcrun")
if (metallibToolchain != null) {
add("--toolchain")
add(metallibToolchain)
}
addAll(
listOf(
"-sdk",
sdk.get(),
"metallib",
),
)
airFiles.forEach { add(it.absolutePath) }
add("-o")
add(metallibFile.get().asFile.absolutePath)
}

execOperations.exec {
commandLine(metallibArgs)
}
}
}

val compileMapCoreMetallibIosSimulator = tasks.register<CompileMapCoreMetallibTask>("compileMapCoreMetallibIosSimulator") {
dependsOn("SwiftPackageConfigAppleMapCoreKmpCompileSwiftPackageIosSimulatorArm64")
sdk.set("iphonesimulator")
toolchainId.set(mapCoreMetalToolchain)
metallibToolchainId.set(mapCoreMetallibToolchain)
targetTriple.set(mapCoreMetalTargetSimulator)
bundleDir.set(
project.layout.buildDirectory
.dir("spmKmpPlugin/MapCoreKmp/scratch/arm64-apple-ios-simulator/release/MapCore_MapCore.bundle"),
)
outputDir.set(project.layout.buildDirectory.dir("spmKmpPlugin/MapCoreKmp/metal/iphonesimulator"))
metalSources.from(bundleDir.map { it.asFileTree.matching { include("**/*.metal") } })
metallibFile.set(bundleDir.map { it.file("default.metallib") })
}

val compileMapCoreMetallibIosArm64 = tasks.register<CompileMapCoreMetallibTask>("compileMapCoreMetallibIosArm64") {
dependsOn("SwiftPackageConfigAppleMapCoreKmpCompileSwiftPackageIosArm64")
sdk.set("iphoneos")
toolchainId.set(mapCoreMetalToolchain)
metallibToolchainId.set(mapCoreMetallibToolchain)
targetTriple.set(mapCoreMetalTargetDevice)
bundleDir.set(
project.layout.buildDirectory
.dir("spmKmpPlugin/MapCoreKmp/scratch/arm64-apple-ios/release/MapCore_MapCore.bundle"),
)
outputDir.set(project.layout.buildDirectory.dir("spmKmpPlugin/MapCoreKmp/metal/iphoneos"))
metalSources.from(bundleDir.map { it.asFileTree.matching { include("**/*.metal") } })
metallibFile.set(bundleDir.map { it.file("default.metallib") })
}

tasks.matching { it.name == "SwiftPackageConfigAppleMapCoreKmpCopyPackageResourcesIosSimulatorArm64" }
.configureEach { dependsOn(compileMapCoreMetallibIosSimulator) }
tasks.matching { it.name == "SwiftPackageConfigAppleMapCoreKmpCopyPackageResourcesIosArm64" }
.configureEach { dependsOn(compileMapCoreMetallibIosArm64) }

tasks.matching { it.name == "compileKotlinIosSimulatorArm64" }
.configureEach { dependsOn(compileMapCoreMetallibIosSimulator) }
tasks.matching { it.name == "compileKotlinIosArm64" }
.configureEach { dependsOn(compileMapCoreMetallibIosArm64) }
2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
android.useAndroidX=true
kotlin.mpp.enableCInteropCommonization=true
Binary file added gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
7 changes: 7 additions & 0 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-milestone-1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading
Loading