Skip to content

Commit 7d894f0

Browse files
authored
Merge pull request #790 from icerockdev/develop
Release 0.24.4
2 parents 2c48480 + 0b16b7a commit 7d894f0

18 files changed

Lines changed: 543 additions & 516 deletions

File tree

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ buildscript {
6464
}
6565
6666
dependencies {
67-
classpath "dev.icerock.moko:resources-generator:0.24.3"
67+
classpath "dev.icerock.moko:resources-generator:0.24.4"
6868
}
6969
}
7070
@@ -82,10 +82,10 @@ project build.gradle
8282
apply plugin: "dev.icerock.mobile.multiplatform-resources"
8383
8484
dependencies {
85-
commonMainApi("dev.icerock.moko:resources:0.24.3")
86-
commonMainApi("dev.icerock.moko:resources-compose:0.24.3") // for compose multiplatform
85+
commonMainApi("dev.icerock.moko:resources:0.24.4")
86+
commonMainApi("dev.icerock.moko:resources-compose:0.24.4") // for compose multiplatform
8787
88-
commonTestImplementation("dev.icerock.moko:resources-test:0.24.3")
88+
commonTestImplementation("dev.icerock.moko:resources-test:0.24.4")
8989
}
9090
9191
multiplatformResources {
@@ -132,7 +132,7 @@ should [add `export` declarations](https://kotlinlang.org/docs/multiplatform-bui
132132

133133
```
134134
framework {
135-
export("dev.icerock.moko:resources:0.24.3")
135+
export("dev.icerock.moko:resources:0.24.4")
136136
export("dev.icerock.moko:graphics:0.9.0") // toUIColor here
137137
}
138138
```

gradle.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ kotlin.code.style=official
77
kotlin.mpp.stability.nowarn=true
88
kotlin.mpp.androidGradlePluginCompatibility.nowarn=true
99
kotlin.mpp.androidSourceSetLayoutVersion=2
10+
kotlin.mpp.applyDefaultHierarchyTemplate=false
11+
kotlin.mpp.enableCInteropCommonization=true
1012

1113
org.jetbrains.compose.experimental.jscanvas.enabled=true
1214
org.jetbrains.compose.experimental.uikit.enabled=true

gradle/moko.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[versions]
2-
resourcesVersion = "0.24.3"
2+
resourcesVersion = "0.24.4"
33

44
[libraries]
55
resources = { module = "dev.icerock.moko:resources", version.ref = "resourcesVersion" }
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Copyright 2024 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
6+
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
7+
import org.jetbrains.kotlin.konan.target.KonanTarget
8+
9+
plugins {
10+
id("org.jetbrains.kotlin.multiplatform")
11+
}
12+
13+
/*
14+
This code ensures that the Bundle in an iOS application, built with Kotlin Multiplatform (KMP), can be correctly
15+
located at runtime. The issue arises because Kotlin doesn’t allow direct lookup of a Bundle by a class from
16+
Objective-C. To resolve this, a static library written in Objective-C was created and automatically included in the
17+
Kotlin Framework during the build process. This library contains a class used to locate the required Bundle.
18+
19+
Key steps performed by the code:
20+
21+
1. Handling Apple targets in KMP:
22+
The code automatically configures the build for Apple platforms only (iOS, macOS, tvOS, watchOS).
23+
2. Compiling and linking the static library:
24+
- clang is used to compile the source file MRResourcesBundle.m into an object file.
25+
- The object file is linked into a static library (libMRResourcesBundle.a) using the ar utility.
26+
3. Integrating the static library into the Kotlin Framework:
27+
- A C-interop is created, enabling Kotlin to interact with the Objective-C code from the library.
28+
- The C-interop task is configured to depend on the compilation and linking tasks, ensuring the library is ready for
29+
use during the build process.
30+
4. Support for multiple Apple platforms:
31+
- The code adapts the build process for specific Apple SDKs and architectures by using helper functions getAppleSdk
32+
and getClangTarget.
33+
5. Retrieving the SDK path:
34+
The xcrun utility is used to dynamically fetch the SDK path required by clang.
35+
36+
What does this achieve?
37+
38+
As a result, a Kotlin Multiplatform application for iOS, macOS, tvOS, or watchOS can correctly locate the Bundle
39+
containing resources by leveraging standard Apple APIs wrapped in the static library. This process is fully automated
40+
during the project build, requiring no manual intervention from the developer.
41+
42+
Bundle search logic:
43+
resources/src/appleMain/kotlin/dev/icerock/moko/resources/utils/NSBundleExt.kt
44+
*/
45+
46+
kotlin.targets
47+
.withType<KotlinNativeTarget>()
48+
.matching { it.konanTarget.family.isAppleFamily }
49+
.configureEach {
50+
val sdk: String = this.konanTarget.getAppleSdk()
51+
val target: String = this.konanTarget.getClangTarget()
52+
53+
val sdkPath: String = getSdkPath(sdk)
54+
55+
val libsDir = File(buildDir, "moko-resources/cinterop/$name")
56+
libsDir.mkdirs()
57+
val sourceFile = File(projectDir, "src/appleMain/objective-c/MRResourcesBundle.m")
58+
val objectFile = File(libsDir, "MRResourcesBundle.o")
59+
val libFile = File(libsDir, "libMRResourcesBundle.a")
60+
val kotlinTargetPostfix: String = this.name.capitalize()
61+
62+
val compileStaticLibrary = tasks.register("mokoBundleSearcherCompile$kotlinTargetPostfix", Exec::class) {
63+
group = "moko-resources"
64+
65+
commandLine = listOf(
66+
"clang",
67+
"-target",
68+
target,
69+
"-isysroot",
70+
sdkPath,
71+
"-c",
72+
sourceFile.absolutePath,
73+
"-o",
74+
objectFile.absolutePath
75+
)
76+
outputs.file(objectFile.absolutePath)
77+
}
78+
val linkStaticLibrary = tasks.register("mokoBundleSearcherLink$kotlinTargetPostfix", Exec::class) {
79+
group = "moko-resources"
80+
81+
dependsOn(compileStaticLibrary)
82+
83+
commandLine = listOf(
84+
"ar",
85+
"rcs",
86+
libFile.absolutePath,
87+
objectFile.absolutePath
88+
)
89+
outputs.file(libFile.absolutePath)
90+
}
91+
92+
compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME) {
93+
val bundleSearcher by cinterops.creating {
94+
defFile(project.file("src/appleMain/def/bundleSearcher.def"))
95+
96+
includeDirs("$projectDir/src/appleMain/objective-c")
97+
extraOpts("-libraryPath", libsDir.absolutePath)
98+
}
99+
100+
tasks.named(bundleSearcher.interopProcessingTaskName).configure {
101+
dependsOn(linkStaticLibrary)
102+
}
103+
}
104+
}
105+
106+
fun KonanTarget.getAppleSdk(): String {
107+
return when (this) {
108+
KonanTarget.IOS_ARM32,
109+
KonanTarget.IOS_ARM64 -> "iphoneos"
110+
111+
KonanTarget.IOS_SIMULATOR_ARM64,
112+
KonanTarget.IOS_X64 -> "iphonesimulator"
113+
114+
KonanTarget.MACOS_ARM64,
115+
KonanTarget.MACOS_X64 -> "macosx"
116+
117+
KonanTarget.TVOS_ARM64 -> "appletvos"
118+
119+
KonanTarget.TVOS_SIMULATOR_ARM64,
120+
KonanTarget.TVOS_X64 -> "appletvsimulator"
121+
122+
KonanTarget.WATCHOS_ARM32,
123+
KonanTarget.WATCHOS_DEVICE_ARM64 -> "watchos"
124+
125+
KonanTarget.WATCHOS_ARM64,
126+
KonanTarget.WATCHOS_SIMULATOR_ARM64,
127+
KonanTarget.WATCHOS_X64,
128+
KonanTarget.WATCHOS_X86 -> "watchsimulator"
129+
130+
else -> error("Unsupported target for selecting SDK: $this")
131+
}
132+
}
133+
134+
fun KonanTarget.getClangTarget(): String {
135+
return when (this) {
136+
KonanTarget.IOS_ARM32 -> "armv7-apple-ios"
137+
KonanTarget.IOS_ARM64 -> "aarch64-apple-ios"
138+
KonanTarget.IOS_SIMULATOR_ARM64 -> "aarch64-apple-ios-simulator"
139+
KonanTarget.IOS_X64 -> "x86_64-apple-ios-simulator"
140+
141+
KonanTarget.MACOS_ARM64 -> "aarch64-apple-macosx"
142+
KonanTarget.MACOS_X64 -> "x86_64-apple-macosx"
143+
144+
KonanTarget.TVOS_ARM64 -> "aarch64-apple-tvos"
145+
KonanTarget.TVOS_SIMULATOR_ARM64 -> "aarch64-apple-tvos-simulator"
146+
KonanTarget.TVOS_X64 -> "x86_64-apple-tvos-simulator"
147+
148+
KonanTarget.WATCHOS_ARM32 -> "armv7k-apple-watchos"
149+
KonanTarget.WATCHOS_ARM64 -> "arm64_32-apple-watchos"
150+
KonanTarget.WATCHOS_DEVICE_ARM64 -> "aarch64-apple-watchos"
151+
KonanTarget.WATCHOS_SIMULATOR_ARM64 -> "aarch64-apple-watchos-simulator"
152+
KonanTarget.WATCHOS_X64 -> "x86_64-apple-watchos-simulator"
153+
KonanTarget.WATCHOS_X86 -> "i386-apple-watchos"
154+
155+
else -> error("Unsupported target for selecting clang target: $this")
156+
}
157+
}
158+
159+
fun getSdkPath(sdk: String): String {
160+
val process = ProcessBuilder("xcrun", "--sdk", sdk, "--show-sdk-path")
161+
.redirectErrorStream(true)
162+
.start()
163+
return process.inputStream.bufferedReader().use { it.readText().trim() }
164+
}

resources-generator/src/main/kotlin/dev/icerock/gradle/actions/apple/CopyResourcesFromKLibsAction.kt

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package dev.icerock.gradle.actions.apple
77
import dev.icerock.gradle.utils.klibs
88
import org.gradle.api.Action
99
import org.gradle.api.Task
10+
import org.gradle.api.logging.Logger
1011
import org.jetbrains.kotlin.gradle.tasks.KotlinNativeLink
1112
import org.jetbrains.kotlin.library.KotlinLibraryLayout
1213
import org.jetbrains.kotlin.library.impl.KotlinLibraryLayoutImpl
@@ -18,25 +19,50 @@ internal abstract class CopyResourcesFromKLibsAction : Action<Task> {
1819
linkTask: KotlinNativeLink,
1920
outputDir: File
2021
) {
21-
linkTask.klibs
22+
val packedKlibs: List<File> = linkTask.klibs
23+
.filter { it.exists() }
2224
.filter { it.extension == "klib" }
25+
.map { it }
26+
val unpackedKlibs: List<File> = linkTask.klibs
2327
.filter { it.exists() }
28+
// we need only unpacked klibs
29+
.filter { it.name == "manifest" && it.parentFile.name == "default" }
30+
// manifest stored in klib inside directory default
31+
.map { it.parentFile.parentFile }
32+
33+
(packedKlibs + unpackedKlibs)
2434
.forEach { inputFile ->
25-
linkTask.logger.info("copy resources from $inputFile into $outputDir")
26-
val klibKonan = org.jetbrains.kotlin.konan.file.File(inputFile.path)
27-
val klib = KotlinLibraryLayoutImpl(klib = klibKonan, component = "default")
28-
val layout: KotlinLibraryLayout = klib.extractingToTemp
29-
30-
try {
31-
File(layout.resourcesDir.path).copyRecursively(
32-
target = outputDir,
33-
overwrite = true
34-
)
35-
} catch (@Suppress("SwallowedException") exc: NoSuchFileException) {
36-
linkTask.logger.info("resources in $inputFile not found")
37-
} catch (@Suppress("SwallowedException") exc: java.nio.file.NoSuchFileException) {
38-
linkTask.logger.info("resources in $inputFile not found (empty lib)")
39-
}
35+
linkTask.logger.info("found dependency $inputFile, try to copy resources")
36+
37+
val layout: KotlinLibraryLayout = getKotlinLibraryLayout(inputFile)
38+
39+
copyResourcesFromKlib(
40+
logger = linkTask.logger,
41+
layout = layout,
42+
outputDir = outputDir,
43+
)
4044
}
4145
}
46+
47+
private fun copyResourcesFromKlib(logger: Logger, layout: KotlinLibraryLayout, outputDir: File) {
48+
logger.info("copy resources from $layout into $outputDir")
49+
50+
try {
51+
File(layout.resourcesDir.path).copyRecursively(
52+
target = outputDir,
53+
overwrite = true
54+
)
55+
} catch (@Suppress("SwallowedException") exc: NoSuchFileException) {
56+
logger.info("resources in $layout not found")
57+
} catch (@Suppress("SwallowedException") exc: java.nio.file.NoSuchFileException) {
58+
logger.info("resources in $layout not found (empty lib)")
59+
}
60+
}
61+
62+
private fun getKotlinLibraryLayout(file: File): KotlinLibraryLayout {
63+
val klibKonan = org.jetbrains.kotlin.konan.file.File(file.path)
64+
val klib = KotlinLibraryLayoutImpl(klib = klibKonan, component = "default")
65+
66+
return if (klib.isZipped) klib.extractingToTemp else klib
67+
}
4268
}

resources-generator/src/main/kotlin/dev/icerock/gradle/actions/apple/PackAppleResourcesToKLibAction.kt

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile
1515
import org.jetbrains.kotlin.konan.file.zipDirAs
1616
import java.io.File
1717
import java.util.Properties
18+
import org.jetbrains.kotlin.konan.file.File as KonanFile
1819

1920
internal class PackAppleResourcesToKLibAction(
2021
private val assetsDirectory: Provider<File>,
@@ -43,11 +44,48 @@ internal class PackAppleResourcesToKLibAction(
4344

4445
val klibFile: File = task.outputFile.get()
4546
val repackDir = File(klibFile.parent, klibFile.nameWithoutExtension)
46-
val defaultDir = File(repackDir, "default")
47-
val resRepackDir = File(defaultDir, "resources")
4847

49-
task.logger.info("Adding resources to klib file `{}`", klibFile)
50-
unzipTo(zipFile = klibFile, outputDirectory = repackDir)
48+
if (klibFile.isDirectory) {
49+
task.logger.info("Adding resources to unpacked klib directory `{}`", klibFile)
50+
51+
addResourcesToUnpackedKlib(
52+
klibDir = klibFile,
53+
resourcesGenerationDir = resourcesGenerationDir,
54+
assetsDirectory = assetsDirectory,
55+
task = task
56+
)
57+
} else {
58+
task.logger.info("Adding resources to packed klib directory `{}`", klibFile)
59+
60+
unzipTo(zipFile = klibFile, outputDirectory = repackDir)
61+
62+
addResourcesToUnpackedKlib(
63+
klibDir = repackDir,
64+
resourcesGenerationDir = resourcesGenerationDir,
65+
assetsDirectory = assetsDirectory,
66+
task = task
67+
)
68+
69+
val repackKonan = KonanFile(repackDir.path)
70+
val klibKonan = KonanFile(klibFile.path)
71+
72+
klibFile.delete()
73+
repackKonan.zipDirAs(klibKonan)
74+
75+
repackDir.deleteRecursively()
76+
}
77+
}
78+
79+
private fun addResourcesToUnpackedKlib(
80+
klibDir: File,
81+
resourcesGenerationDir: File,
82+
assetsDirectory: File,
83+
task: KotlinNativeCompile
84+
) {
85+
assert(klibDir.isDirectory) { "should be used directory as KLib" }
86+
87+
val defaultDir = File(klibDir, "default")
88+
val resRepackDir = File(defaultDir, "resources")
5189

5290
val manifestFile = File(defaultDir, "manifest")
5391
val manifest = Properties()
@@ -83,14 +121,6 @@ internal class PackAppleResourcesToKLibAction(
83121
} else {
84122
task.logger.info("assets not found, compilation not required")
85123
}
86-
87-
val repackKonan = org.jetbrains.kotlin.konan.file.File(repackDir.path)
88-
val klibKonan = org.jetbrains.kotlin.konan.file.File(klibFile.path)
89-
90-
klibFile.delete()
91-
repackKonan.zipDirAs(klibKonan)
92-
93-
repackDir.deleteRecursively()
94124
}
95125

96126
private fun compileAppleAssets(

resources-generator/src/main/kotlin/dev/icerock/gradle/utils/StringExt.kt

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,20 +57,16 @@ internal fun String.removeAndroidMirroringFormat(): String {
5757
.replace("""\@""", "@")
5858
}
5959

60+
private val androidLinkingCharacters = setOf('@', '?')
61+
6062
internal fun String.convertXmlStringToAndroidLocalization(): String {
6163
// Android resources should comply with requirements:
6264
// https://developer.android.com/guide/topics/resources/string-resource#escaping_quotes
6365
return StringEscapeUtils
6466
.unescapeXml(this)
6567
.replace("\n", "\\n")
6668
.let { StringEscapeUtils.escapeXml11(it) }
67-
.let {
68-
if (it.getOrNull(0) == '@') {
69-
replaceFirst("@", """\@""")
70-
} else {
71-
it
72-
}
73-
}
69+
.replaceFirstChar { if (it in androidLinkingCharacters) "\\$it" else "$it" }
7470
.replace("&quot;", "\\&quot;")
7571
.replace("&apos;", "\\&apos;")
7672
}

0 commit comments

Comments
 (0)