Skip to content

Commit

Permalink
#747 worked solution with static library inside
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex009 committed Dec 7, 2024
1 parent 6cc8055 commit f308f1b
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 13 deletions.
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ kotlin.mpp.stability.nowarn=true
kotlin.mpp.androidGradlePluginCompatibility.nowarn=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.mpp.applyDefaultHierarchyTemplate=false
kotlin.mpp.enableCInteropCommonization=true

org.jetbrains.compose.experimental.jscanvas.enabled=true
org.jetbrains.compose.experimental.uikit.enabled=true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* Copyright 2024 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.konan.target.KonanTarget

plugins {
id("org.jetbrains.kotlin.multiplatform")
}

/*
This code ensures that the Bundle in an iOS application, built with Kotlin Multiplatform (KMP), can be correctly
located at runtime. The issue arises because Kotlin doesn’t allow direct lookup of a Bundle by a class from
Objective-C. To resolve this, a static library written in Objective-C was created and automatically included in the
Kotlin Framework during the build process. This library contains a class used to locate the required Bundle.
Key steps performed by the code:
1. Handling Apple targets in KMP:
The code automatically configures the build for Apple platforms only (iOS, macOS, tvOS, watchOS).
2. Compiling and linking the static library:
- clang is used to compile the source file MRResourcesBundle.m into an object file.
- The object file is linked into a static library (libMRResourcesBundle.a) using the ar utility.
3. Integrating the static library into the Kotlin Framework:
- A C-interop is created, enabling Kotlin to interact with the Objective-C code from the library.
- The C-interop task is configured to depend on the compilation and linking tasks, ensuring the library is ready for
use during the build process.
4. Support for multiple Apple platforms:
- The code adapts the build process for specific Apple SDKs and architectures by using helper functions getAppleSdk
and getClangTarget.
5. Retrieving the SDK path:
The xcrun utility is used to dynamically fetch the SDK path required by clang.
What does this achieve?
As a result, a Kotlin Multiplatform application for iOS, macOS, tvOS, or watchOS can correctly locate the Bundle
containing resources by leveraging standard Apple APIs wrapped in the static library. This process is fully automated
during the project build, requiring no manual intervention from the developer.
Bundle search logic:
resources/src/appleMain/kotlin/dev/icerock/moko/resources/utils/NSBundleExt.kt
*/

kotlin.targets
.withType<KotlinNativeTarget>()
.matching { it.konanTarget.family.isAppleFamily }
.configureEach {
val sdk: String = this.konanTarget.getAppleSdk()
val target: String = this.konanTarget.getClangTarget()

val sdkPath: String = getSdkPath(sdk)

val libsDir = File(buildDir, "moko-resources/cinterop/$name")
libsDir.mkdirs()
val sourceFile = File(projectDir, "src/appleMain/objective-c/MRResourcesBundle.m")
val objectFile = File(libsDir, "MRResourcesBundle.o")
val libFile = File(libsDir, "libMRResourcesBundle.a")
val kotlinTargetPostfix: String = this.name.capitalize()

val compileStaticLibrary = tasks.register("mokoBundleSearcherCompile$kotlinTargetPostfix", Exec::class) {
group = "moko-resources"

commandLine = listOf(
"clang",
"-target",
target,
"-isysroot",
sdkPath,
"-c",
sourceFile.absolutePath,
"-o",
objectFile.absolutePath
)
outputs.file(objectFile.absolutePath)
}
val linkStaticLibrary = tasks.register("mokoBundleSearcherLink$kotlinTargetPostfix", Exec::class) {
group = "moko-resources"

dependsOn(compileStaticLibrary)

commandLine = listOf(
"ar",
"rcs",
libFile.absolutePath,
objectFile.absolutePath
)
outputs.file(libFile.absolutePath)
}

compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME) {
val bundleSearcher by cinterops.creating {
defFile(project.file("src/appleMain/def/bundleSearcher.def"))

includeDirs("$projectDir/src/appleMain/objective-c")
extraOpts("-libraryPath", libsDir.absolutePath)
}

tasks.named(bundleSearcher.interopProcessingTaskName).configure {
dependsOn(linkStaticLibrary)
}
}
}

fun KonanTarget.getAppleSdk(): String {
return when (this) {
KonanTarget.IOS_ARM32,
KonanTarget.IOS_ARM64 -> "iphoneos"

KonanTarget.IOS_SIMULATOR_ARM64,
KonanTarget.IOS_X64 -> "iphonesimulator"

KonanTarget.MACOS_ARM64,
KonanTarget.MACOS_X64 -> "macosx"

KonanTarget.TVOS_ARM64 -> "appletvos"

KonanTarget.TVOS_SIMULATOR_ARM64,
KonanTarget.TVOS_X64 -> "appletvsimulator"

KonanTarget.WATCHOS_ARM32,
KonanTarget.WATCHOS_DEVICE_ARM64 -> "watchos"

KonanTarget.WATCHOS_ARM64,
KonanTarget.WATCHOS_SIMULATOR_ARM64,
KonanTarget.WATCHOS_X64,
KonanTarget.WATCHOS_X86 -> "watchsimulator"

else -> error("Unsupported target for selecting SDK: $this")
}
}

fun KonanTarget.getClangTarget(): String {
return when (this) {
KonanTarget.IOS_ARM32 -> "armv7-apple-ios"
KonanTarget.IOS_ARM64 -> "arm64-apple-ios"
KonanTarget.IOS_SIMULATOR_ARM64 -> "arm64-apple-ios-simulator"
KonanTarget.IOS_X64 -> "x86_64-apple-ios-simulator"

KonanTarget.MACOS_ARM64 -> "arm64-apple-macosx"
KonanTarget.MACOS_X64 -> "x86_64-apple-macosx"

KonanTarget.TVOS_ARM64 -> "arm64-apple-tvos"
KonanTarget.TVOS_SIMULATOR_ARM64 -> "arm64-apple-tvsimulator"
KonanTarget.TVOS_X64 -> "x86_64-apple-tvsimulator"

KonanTarget.WATCHOS_ARM32 -> "armv7k-apple-watchos"
KonanTarget.WATCHOS_ARM64 -> "arm64-apple-watchos"
KonanTarget.WATCHOS_DEVICE_ARM64 -> "arm64_32-apple-watchos"
KonanTarget.WATCHOS_SIMULATOR_ARM64 -> "arm64-apple-watchos"
KonanTarget.WATCHOS_X64 -> "x86_64-apple-watchos"
KonanTarget.WATCHOS_X86 -> "i386-apple-watchos"

else -> error("Unsupported target for selecting clang target: $this")
}
}

fun getSdkPath(sdk: String): String {
val process = ProcessBuilder("xcrun", "--sdk", sdk, "--show-sdk-path")
.redirectErrorStream(true)
.start()
return process.inputStream.bufferedReader().use { it.readText().trim() }
}
1 change: 1 addition & 0 deletions resources/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ plugins {
id("multiplatform-library-extended-convention")
id("multiplatform-android-publish-convention")
id("apple-main-convention")
id("apple-bundle-searcher-convention")
id("detekt-convention")
id("javadoc-stub-convention")
id("publication-convention")
Expand Down
5 changes: 5 additions & 0 deletions resources/src/appleMain/def/bundleSearcher.def
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# https://kotlinlang.org/docs/native-definition-file.html
language = Objective-C
package = dev.icerock.moko.resources.apple.native
staticLibraries = libMRResourcesBundle.a
headers = MRResourcesBundle.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

package dev.icerock.moko.resources.utils

import dev.icerock.moko.resources.apple.native.ResourcesBundleAnchor
import kotlinx.cinterop.ExperimentalForeignApi
import platform.Foundation.NSBundle
import platform.Foundation.NSDirectoryEnumerator
import platform.Foundation.NSFileManager
Expand All @@ -12,18 +14,16 @@ import platform.Foundation.NSURL
import platform.Foundation.pathExtension

fun NSBundle.Companion.loadableBundle(identifier: String): NSBundle {
// at first we try to find required bundle inside Bundle.main, because it's faster way
// https://github.com/icerockdev/moko-resources/issues/708
// but in some cases (for example in SwiftUI Previews) dynamic framework with bundles can be located
// in different location, not inside Bundle.main. So in this case we run less performant way - bundleWithIdentifier
// https://github.com/icerockdev/moko-resources/issues/747
return findBundleInMain(identifier)
?: NSBundle.bundleWithIdentifier(identifier)
?: throw IllegalArgumentException("bundle with identifier $identifier not found")
}

private fun findBundleInMain(identifier: String): NSBundle? {
val bundlePath: String = NSBundle.mainBundle.bundlePath
// we should use search by our class because dynamic framework with resources can be placed in
// external directory, not inside app directory (NSBundle.main). for example in case of
// SwiftUI preview - app directory empty, but dynamic framework with resources will be in
// different directory (DerivedData)
// more details inside resources-build-logic/src/main/kotlin/apple-bundle-searcher-convention.gradle.kts
@OptIn(ExperimentalForeignApi::class)
val rootBundle: NSBundle = requireNotNull(ResourcesBundleAnchor.getResourcesBundle()) {
"root NSBundle can't be found"
}
val bundlePath: String = rootBundle.bundlePath

val enumerator: NSDirectoryEnumerator = requireNotNull(
NSFileManager.defaultManager.enumeratorAtPath(bundlePath)
Expand All @@ -46,7 +46,7 @@ private fun findBundleInMain(identifier: String): NSBundle? {
}
}

return null
throw IllegalArgumentException("bundle with identifier $identifier not found")
}

var isBundleSearchLogEnabled = false
8 changes: 8 additions & 0 deletions resources/src/appleMain/objective-c/MRResourcesBundle.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#import <stdarg.h>
#import <Foundation/NSBundle.h>

@interface ResourcesBundleAnchor : NSObject

+ (NSBundle*) getResourcesBundle;

@end
15 changes: 15 additions & 0 deletions resources/src/appleMain/objective-c/MRResourcesBundle.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// clang -target arm64-apple-ios -isysroot $(xcrun --sdk iphoneos --show-sdk-path) -c MRResourcesBundle.m -o source.o
// ar rcs libMRResourcesBundle.a source.o
// lipo -info libMRResourcesBundle.a

#import <stdarg.h>
#import <Foundation/NSBundle.h>
#import "MRResourcesBundle.h"

@implementation ResourcesBundleAnchor

+ (NSBundle*) getResourcesBundle {
return [NSBundle bundleForClass:[ResourcesBundleAnchor class]];
}

@end

0 comments on commit f308f1b

Please sign in to comment.