Skip to content

Commit

Permalink
Merge pull request #790 from icerockdev/develop
Browse files Browse the repository at this point in the history
Release 0.24.4
  • Loading branch information
Alex009 authored Dec 8, 2024
2 parents 2c48480 + 0b16b7a commit 7d894f0
Show file tree
Hide file tree
Showing 18 changed files with 543 additions and 516 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ buildscript {
}
dependencies {
classpath "dev.icerock.moko:resources-generator:0.24.3"
classpath "dev.icerock.moko:resources-generator:0.24.4"
}
}
Expand All @@ -82,10 +82,10 @@ project build.gradle
apply plugin: "dev.icerock.mobile.multiplatform-resources"
dependencies {
commonMainApi("dev.icerock.moko:resources:0.24.3")
commonMainApi("dev.icerock.moko:resources-compose:0.24.3") // for compose multiplatform
commonMainApi("dev.icerock.moko:resources:0.24.4")
commonMainApi("dev.icerock.moko:resources-compose:0.24.4") // for compose multiplatform
commonTestImplementation("dev.icerock.moko:resources-test:0.24.3")
commonTestImplementation("dev.icerock.moko:resources-test:0.24.4")
}
multiplatformResources {
Expand Down Expand Up @@ -132,7 +132,7 @@ should [add `export` declarations](https://kotlinlang.org/docs/multiplatform-bui

```
framework {
export("dev.icerock.moko:resources:0.24.3")
export("dev.icerock.moko:resources:0.24.4")
export("dev.icerock.moko:graphics:0.9.0") // toUIColor here
}
```
Expand Down
2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ kotlin.code.style=official
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
2 changes: 1 addition & 1 deletion gradle/moko.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
resourcesVersion = "0.24.3"
resourcesVersion = "0.24.4"

[libraries]
resources = { module = "dev.icerock.moko:resources", version.ref = "resourcesVersion" }
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 -> "aarch64-apple-ios"
KonanTarget.IOS_SIMULATOR_ARM64 -> "aarch64-apple-ios-simulator"
KonanTarget.IOS_X64 -> "x86_64-apple-ios-simulator"

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

KonanTarget.TVOS_ARM64 -> "aarch64-apple-tvos"
KonanTarget.TVOS_SIMULATOR_ARM64 -> "aarch64-apple-tvos-simulator"
KonanTarget.TVOS_X64 -> "x86_64-apple-tvos-simulator"

KonanTarget.WATCHOS_ARM32 -> "armv7k-apple-watchos"
KonanTarget.WATCHOS_ARM64 -> "arm64_32-apple-watchos"
KonanTarget.WATCHOS_DEVICE_ARM64 -> "aarch64-apple-watchos"
KonanTarget.WATCHOS_SIMULATOR_ARM64 -> "aarch64-apple-watchos-simulator"
KonanTarget.WATCHOS_X64 -> "x86_64-apple-watchos-simulator"
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() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package dev.icerock.gradle.actions.apple
import dev.icerock.gradle.utils.klibs
import org.gradle.api.Action
import org.gradle.api.Task
import org.gradle.api.logging.Logger
import org.jetbrains.kotlin.gradle.tasks.KotlinNativeLink
import org.jetbrains.kotlin.library.KotlinLibraryLayout
import org.jetbrains.kotlin.library.impl.KotlinLibraryLayoutImpl
Expand All @@ -18,25 +19,50 @@ internal abstract class CopyResourcesFromKLibsAction : Action<Task> {
linkTask: KotlinNativeLink,
outputDir: File
) {
linkTask.klibs
val packedKlibs: List<File> = linkTask.klibs
.filter { it.exists() }
.filter { it.extension == "klib" }
.map { it }
val unpackedKlibs: List<File> = linkTask.klibs
.filter { it.exists() }
// we need only unpacked klibs
.filter { it.name == "manifest" && it.parentFile.name == "default" }
// manifest stored in klib inside directory default
.map { it.parentFile.parentFile }

(packedKlibs + unpackedKlibs)
.forEach { inputFile ->
linkTask.logger.info("copy resources from $inputFile into $outputDir")
val klibKonan = org.jetbrains.kotlin.konan.file.File(inputFile.path)
val klib = KotlinLibraryLayoutImpl(klib = klibKonan, component = "default")
val layout: KotlinLibraryLayout = klib.extractingToTemp

try {
File(layout.resourcesDir.path).copyRecursively(
target = outputDir,
overwrite = true
)
} catch (@Suppress("SwallowedException") exc: NoSuchFileException) {
linkTask.logger.info("resources in $inputFile not found")
} catch (@Suppress("SwallowedException") exc: java.nio.file.NoSuchFileException) {
linkTask.logger.info("resources in $inputFile not found (empty lib)")
}
linkTask.logger.info("found dependency $inputFile, try to copy resources")

val layout: KotlinLibraryLayout = getKotlinLibraryLayout(inputFile)

copyResourcesFromKlib(
logger = linkTask.logger,
layout = layout,
outputDir = outputDir,
)
}
}

private fun copyResourcesFromKlib(logger: Logger, layout: KotlinLibraryLayout, outputDir: File) {
logger.info("copy resources from $layout into $outputDir")

try {
File(layout.resourcesDir.path).copyRecursively(
target = outputDir,
overwrite = true
)
} catch (@Suppress("SwallowedException") exc: NoSuchFileException) {
logger.info("resources in $layout not found")
} catch (@Suppress("SwallowedException") exc: java.nio.file.NoSuchFileException) {
logger.info("resources in $layout not found (empty lib)")
}
}

private fun getKotlinLibraryLayout(file: File): KotlinLibraryLayout {
val klibKonan = org.jetbrains.kotlin.konan.file.File(file.path)
val klib = KotlinLibraryLayoutImpl(klib = klibKonan, component = "default")

return if (klib.isZipped) klib.extractingToTemp else klib
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile
import org.jetbrains.kotlin.konan.file.zipDirAs
import java.io.File
import java.util.Properties
import org.jetbrains.kotlin.konan.file.File as KonanFile

internal class PackAppleResourcesToKLibAction(
private val assetsDirectory: Provider<File>,
Expand Down Expand Up @@ -43,11 +44,48 @@ internal class PackAppleResourcesToKLibAction(

val klibFile: File = task.outputFile.get()
val repackDir = File(klibFile.parent, klibFile.nameWithoutExtension)
val defaultDir = File(repackDir, "default")
val resRepackDir = File(defaultDir, "resources")

task.logger.info("Adding resources to klib file `{}`", klibFile)
unzipTo(zipFile = klibFile, outputDirectory = repackDir)
if (klibFile.isDirectory) {
task.logger.info("Adding resources to unpacked klib directory `{}`", klibFile)

addResourcesToUnpackedKlib(
klibDir = klibFile,
resourcesGenerationDir = resourcesGenerationDir,
assetsDirectory = assetsDirectory,
task = task
)
} else {
task.logger.info("Adding resources to packed klib directory `{}`", klibFile)

unzipTo(zipFile = klibFile, outputDirectory = repackDir)

addResourcesToUnpackedKlib(
klibDir = repackDir,
resourcesGenerationDir = resourcesGenerationDir,
assetsDirectory = assetsDirectory,
task = task
)

val repackKonan = KonanFile(repackDir.path)
val klibKonan = KonanFile(klibFile.path)

klibFile.delete()
repackKonan.zipDirAs(klibKonan)

repackDir.deleteRecursively()
}
}

private fun addResourcesToUnpackedKlib(
klibDir: File,
resourcesGenerationDir: File,
assetsDirectory: File,
task: KotlinNativeCompile
) {
assert(klibDir.isDirectory) { "should be used directory as KLib" }

val defaultDir = File(klibDir, "default")
val resRepackDir = File(defaultDir, "resources")

val manifestFile = File(defaultDir, "manifest")
val manifest = Properties()
Expand Down Expand Up @@ -83,14 +121,6 @@ internal class PackAppleResourcesToKLibAction(
} else {
task.logger.info("assets not found, compilation not required")
}

val repackKonan = org.jetbrains.kotlin.konan.file.File(repackDir.path)
val klibKonan = org.jetbrains.kotlin.konan.file.File(klibFile.path)

klibFile.delete()
repackKonan.zipDirAs(klibKonan)

repackDir.deleteRecursively()
}

private fun compileAppleAssets(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,16 @@ internal fun String.removeAndroidMirroringFormat(): String {
.replace("""\@""", "@")
}

private val androidLinkingCharacters = setOf('@', '?')

internal fun String.convertXmlStringToAndroidLocalization(): String {
// Android resources should comply with requirements:
// https://developer.android.com/guide/topics/resources/string-resource#escaping_quotes
return StringEscapeUtils
.unescapeXml(this)
.replace("\n", "\\n")
.let { StringEscapeUtils.escapeXml11(it) }
.let {
if (it.getOrNull(0) == '@') {
replaceFirst("@", """\@""")
} else {
it
}
}
.replaceFirstChar { if (it in androidLinkingCharacters) "\\$it" else "$it" }
.replace("&quot;", "\\&quot;")
.replace("&apos;", "\\&apos;")
}
Expand Down
Loading

0 comments on commit 7d894f0

Please sign in to comment.