Skip to content

Commit 76d9411

Browse files
authored
Resources improvements for native macOS app (#5219)
See old PR for details: #5169 ~~Requires skiko update (JetBrains/compose-multiplatform-core#1804
1 parent e3d06cd commit 76d9411

File tree

19 files changed

+417
-11
lines changed

19 files changed

+417
-11
lines changed

components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.macos.kt

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,32 @@ internal actual fun getPlatformResourceReader(): ResourceReader = object : Resou
3939
private fun getPathOnDisk(path: String): String {
4040
val fm = NSFileManager.defaultManager()
4141
val currentDirectoryPath = fm.currentDirectoryPath
42+
val pathFix = getPathWithoutPackage(path)
4243
return listOf(
44+
// Framework binary
45+
// todo: support fallback path at bundle root?
46+
NSBundle.mainBundle.resourcePath + "/compose-resources/" + path,
47+
// Executable binary
4348
//todo in future bundle resources with app and use all sourceSets (skikoMain, nativeMain)
44-
"$currentDirectoryPath/src/macosMain/composeResources/$path",
45-
"$currentDirectoryPath/src/macosTest/composeResources/$path",
46-
"$currentDirectoryPath/src/commonMain/composeResources/$path",
47-
"$currentDirectoryPath/src/commonTest/composeResources/$path"
49+
"$currentDirectoryPath/src/macosMain/composeResources/$pathFix",
50+
"$currentDirectoryPath/src/macosTest/composeResources/$pathFix",
51+
"$currentDirectoryPath/src/commonMain/composeResources/$pathFix",
52+
"$currentDirectoryPath/src/commonTest/composeResources/$pathFix"
4853
).firstOrNull { p -> fm.fileExistsAtPath(p) } ?: throw MissingResourceException(path)
4954
}
55+
56+
private fun getPathWithoutPackage(path: String): String {
57+
// At the moment resources are not bundled when running a macOS executable binary.
58+
// As a workaround, load the resources from the actual path on disk. So the
59+
// "composeResources/PACKAGE/" prefix must be removed. For example:
60+
// "composeResources/chat_mpp.shared.generated.resources/drawable/background.jpg"
61+
// Will be transformed into:
62+
// "drawable/background.jpg"
63+
// In the future when resources are bundled when running macOS executable binary this
64+
// workaround is no longer needed.
65+
require(path.startsWith("composeResources/")) { "Invalid path: $path" }
66+
return path
67+
.substringAfter("composeResources/") // remove "composeResources/" part
68+
.substringAfter("/") // remove PACKAGE path
69+
}
5070
}

gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/IosResources.kt

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ internal fun Project.configureSyncIosComposeResources(
3333
}
3434

3535
kotlinExtension.targets.withType(KotlinNativeTarget::class.java).all { nativeTarget ->
36-
if (nativeTarget.isIosTarget()) {
36+
if (nativeTarget.isIosOrMacTarget()) {
3737
nativeTarget.binaries.withType(Framework::class.java).all { iosFramework ->
3838
val frameworkClassifier = iosFramework.getClassifier()
3939
val checkNoSandboxTask = tasks.registerOrConfigure<CheckCanAccessComposeResourcesDirectory>(
@@ -116,6 +116,7 @@ private fun Framework.getClassifier(): String {
116116
}
117117

118118
internal fun Framework.getSyncResourcesTaskName() = "sync${getClassifier()}ComposeResourcesForIos"
119+
119120
private fun Framework.isCocoapodsFramework() = name.startsWith("pod")
120121

121122
private fun Framework.getFinalResourcesDir(): Provider<Directory> {
@@ -125,9 +126,9 @@ private fun Framework.getFinalResourcesDir(): Provider<Directory> {
125126
} else {
126127
providers.environmentVariable("BUILT_PRODUCTS_DIR")
127128
.zip(
128-
providers.environmentVariable("CONTENTS_FOLDER_PATH")
129-
) { builtProductsDir, contentsFolderPath ->
130-
File("$builtProductsDir/$contentsFolderPath/$IOS_COMPOSE_RESOURCES_ROOT_DIR").canonicalPath
129+
providers.environmentVariable("UNLOCALIZED_RESOURCES_FOLDER_PATH")
130+
) { builtProductsDir, unlocalizedResourcesFolderPath ->
131+
File("$builtProductsDir/$unlocalizedResourcesFolderPath/$IOS_COMPOSE_RESOURCES_ROOT_DIR").canonicalPath
131132
}
132133
.flatMap {
133134
project.objects.directoryProperty().apply { set(File(it)) }
@@ -142,4 +143,10 @@ private fun KotlinNativeTarget.isIosDeviceTarget(): Boolean =
142143
konanTarget === KonanTarget.IOS_ARM64
143144

144145
private fun KotlinNativeTarget.isIosTarget(): Boolean =
145-
isIosSimulatorTarget() || isIosDeviceTarget()
146+
isIosSimulatorTarget() || isIosDeviceTarget()
147+
148+
private fun KotlinNativeTarget.isMacTarget(): Boolean =
149+
konanTarget === KonanTarget.MACOS_X64 || konanTarget === KonanTarget.MACOS_ARM64
150+
151+
private fun KotlinNativeTarget.isIosOrMacTarget(): Boolean =
152+
isIosTarget() || isMacTarget()

gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/IosResourcesTasks.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,17 @@ private fun getRequestedKonanTargetsByXcode(platform: String, archs: List<String
118118
})
119119
}
120120

121-
else -> error("Unknown iOS platform: '$platform'")
121+
platform.startsWith("macosx") -> {
122+
targets.addAll(archs.map { arch ->
123+
when (arch) {
124+
"arm64" -> KonanTarget.MACOS_ARM64
125+
"x86_64" -> KonanTarget.MACOS_X64
126+
else -> error("Unknown macOS arch: '$arch'")
127+
}
128+
})
129+
}
130+
131+
else -> error("Unknown Apple platform: '$platform'")
122132
}
123133

124134
return targets.toList()

gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,13 @@ class ResourcesTest : GradlePluginTestBase() {
280280
libpath("iossimulatorarm64", "-kotlin_resources.kotlin_resources.zip")
281281
)
282282
checkResourcesZip(iossimulatorarm64ResZip, resourcesFiles, false)
283+
284+
val macosx64ResZip =
285+
file(libpath("macosx64", "-kotlin_resources.kotlin_resources.zip"))
286+
checkResourcesZip(macosx64ResZip, resourcesFiles, false)
287+
val macosarm64ResZip =
288+
file(libpath("macosarm64", "-kotlin_resources.kotlin_resources.zip"))
289+
checkResourcesZip(macosarm64ResZip, resourcesFiles, false)
283290
}
284291
val jsResZip = file(libpath("js", "-kotlin_resources.kotlin_resources.zip"))
285292
checkResourcesZip(jsResZip, resourcesFiles, false)
@@ -300,6 +307,13 @@ class ResourcesTest : GradlePluginTestBase() {
300307
":appModule:iosSimulatorArm64Test"
301308
}
302309
gradle(iosTask)
310+
311+
val macosTask = if (currentArch == Arch.X64) {
312+
":appModule:macosX64Test"
313+
} else {
314+
":appModule:macosArm64Test"
315+
}
316+
gradle(macosTask)
303317
}
304318

305319
file("featureModule/src/commonMain/kotlin/me/sample/app/Feature.kt").modify { content ->
@@ -687,6 +701,95 @@ class ResourcesTest : GradlePluginTestBase() {
687701
}
688702
}
689703

704+
@Test
705+
fun macosResources() {
706+
Assumptions.assumeTrue(currentOS == OS.MacOS)
707+
val macosEnv = mapOf(
708+
"PLATFORM_NAME" to "macosx",
709+
"ARCHS" to "arm64",
710+
"CONFIGURATION" to "Debug",
711+
)
712+
val testEnv = defaultTestEnvironment.copy(
713+
additionalEnvVars = macosEnv
714+
)
715+
716+
with(TestProject("misc/macosResources", testEnv)) {
717+
gradle(":podspec", "-Pkotlin.native.cocoapods.generate.wrapper=true").checks {
718+
assertEqualTextFiles(
719+
file("macosResources.podspec"),
720+
file("expected/macosResources.podspec")
721+
)
722+
file("build/compose/cocoapods/compose-resources").checkExists()
723+
}
724+
725+
gradle(
726+
":syncFramework",
727+
"-Pkotlin.native.cocoapods.platform=${macosEnv["PLATFORM_NAME"]}",
728+
"-Pkotlin.native.cocoapods.archs=${macosEnv["ARCHS"]}",
729+
"-Pkotlin.native.cocoapods.configuration=${macosEnv["CONFIGURATION"]}",
730+
"--dry-run"
731+
).checks {
732+
check.taskSkipped(":generateComposeResClass")
733+
734+
check.taskSkipped(":convertXmlValueResourcesForCommonMain")
735+
check.taskSkipped(":copyNonXmlValueResourcesForCommonMain")
736+
check.taskSkipped(":prepareComposeResourcesTaskForCommonMain")
737+
check.taskSkipped(":generateResourceAccessorsForCommonMain")
738+
739+
check.taskSkipped(":convertXmlValueResourcesForNativeMain")
740+
check.taskSkipped(":copyNonXmlValueResourcesForNativeMain")
741+
check.taskSkipped(":prepareComposeResourcesTaskForNativeMain")
742+
check.taskSkipped(":generateResourceAccessorsForNativeMain")
743+
744+
check.taskSkipped(":convertXmlValueResourcesForAppleMain")
745+
check.taskSkipped(":copyNonXmlValueResourcesForAppleMain")
746+
check.taskSkipped(":prepareComposeResourcesTaskForAppleMain")
747+
check.taskSkipped(":generateResourceAccessorsForAppleMain")
748+
749+
check.taskSkipped(":convertXmlValueResourcesForMacosMain")
750+
check.taskSkipped(":copyNonXmlValueResourcesForMacosMain")
751+
check.taskSkipped(":prepareComposeResourcesTaskForMacosMain")
752+
check.taskSkipped(":generateResourceAccessorsForMacosMain")
753+
754+
check.taskSkipped(":convertXmlValueResourcesForMacosX64Main")
755+
check.taskSkipped(":copyNonXmlValueResourcesForMacosX64Main")
756+
check.taskSkipped(":prepareComposeResourcesTaskForMacosX64Main")
757+
check.taskSkipped(":generateResourceAccessorsForMacosX64Main")
758+
759+
check.taskSkipped(":syncPodComposeResourcesForIos")
760+
}
761+
gradle(":syncPodComposeResourcesForIos").checks {
762+
check.taskNoSource(":convertXmlValueResourcesForCommonMain")
763+
check.taskSuccessful(":copyNonXmlValueResourcesForCommonMain")
764+
check.taskSuccessful(":prepareComposeResourcesTaskForCommonMain")
765+
check.taskSkipped(":generateResourceAccessorsForCommonMain")
766+
767+
check.taskNoSource(":convertXmlValueResourcesForNativeMain")
768+
check.taskNoSource(":copyNonXmlValueResourcesForNativeMain")
769+
check.taskNoSource(":prepareComposeResourcesTaskForNativeMain")
770+
check.taskSkipped(":generateResourceAccessorsForNativeMain")
771+
772+
check.taskNoSource(":convertXmlValueResourcesForAppleMain")
773+
check.taskNoSource(":copyNonXmlValueResourcesForAppleMain")
774+
check.taskNoSource(":prepareComposeResourcesTaskForAppleMain")
775+
check.taskSkipped(":generateResourceAccessorsForAppleMain")
776+
777+
check.taskNoSource(":convertXmlValueResourcesForMacosMain")
778+
check.taskSuccessful(":copyNonXmlValueResourcesForMacosMain")
779+
check.taskSuccessful(":prepareComposeResourcesTaskForMacosMain")
780+
check.taskSkipped(":generateResourceAccessorsForMacosMain")
781+
782+
check.taskNoSource(":convertXmlValueResourcesForMacosX64Main")
783+
check.taskNoSource(":copyNonXmlValueResourcesForMacosX64Main")
784+
check.taskNoSource(":prepareComposeResourcesTaskForMacosX64Main")
785+
check.taskSkipped(":generateResourceAccessorsForMacosX64Main")
786+
787+
file("build/compose/cocoapods/compose-resources/composeResources/macosresources.generated.resources/drawable/compose-multiplatform.xml").checkExists()
788+
file("build/compose/cocoapods/compose-resources/composeResources/macosresources.generated.resources/drawable/icon.xml").checkExists()
789+
}
790+
}
791+
}
792+
690793
@Test
691794
fun iosTestResources() {
692795
Assumptions.assumeTrue(currentOS == OS.MacOS)
@@ -702,6 +805,21 @@ class ResourcesTest : GradlePluginTestBase() {
702805
}
703806
}
704807

808+
@Test
809+
fun macosTestResources() {
810+
Assumptions.assumeTrue(currentOS == OS.MacOS)
811+
with(testProject("misc/macosResources")) {
812+
gradle(":linkDebugTestMacosX64", "--dry-run").checks {
813+
check.taskSkipped(":copyTestComposeResourcesForMacosX64")
814+
check.taskSkipped(":linkDebugTestMacosX64")
815+
}
816+
gradle(":copyTestComposeResourcesForMacosX64").checks {
817+
file("build/bin/macosX64/debugTest/compose-resources/composeResources/macosresources.generated.resources/drawable/compose-multiplatform.xml").checkExists()
818+
file("build/bin/macosX64/debugTest/compose-resources/composeResources/macosresources.generated.resources/drawable/icon.xml").checkExists()
819+
}
820+
}
821+
}
822+
705823
@Test
706824
fun checkTestResources() {
707825
with(testProject("misc/testResources")) {
@@ -714,6 +832,10 @@ class ResourcesTest : GradlePluginTestBase() {
714832
check.logContains("Configure test resources for 'iosArm64' target")
715833
check.logContains("Configure main resources for 'iosSimulatorArm64' target")
716834
check.logContains("Configure test resources for 'iosSimulatorArm64' target")
835+
check.logContains("Configure main resources for 'macosX64' target")
836+
check.logContains("Configure test resources for 'macosX64' target")
837+
check.logContains("Configure main resources for 'macosArm64' target")
838+
check.logContains("Configure test resources for 'macosArm64' target")
717839

718840
check.taskSuccessful(":desktopTest")
719841
}

gradle-plugins/compose/src/test/test-projects/misc/kmpResourcePublication/appModule/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ kotlin {
1313
iosX64()
1414
iosArm64()
1515
iosSimulatorArm64()
16+
macosX64()
17+
macosArm64()
1618
js { browser() }
1719
wasmJs { browser() }
1820

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<resources>
2+
<string name="macOS_str">macOS string</string>
3+
</resources>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package me.sample.app
2+
3+
import androidx.compose.runtime.Composable
4+
import kmpresourcepublication.appmodule.generated.resources.Res
5+
import kmpresourcepublication.appmodule.generated.resources.macOS_str
6+
import org.jetbrains.compose.resources.stringResource
7+
8+
@Composable
9+
actual fun getPlatformSpecificString(): String =
10+
stringResource(Res.string.macOS_str)

gradle-plugins/compose/src/test/test-projects/misc/kmpResourcePublication/cmplib/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ kotlin {
2222
iosX64()
2323
iosArm64()
2424
iosSimulatorArm64()
25+
macosX64()
26+
macosArm64()
2527
js { browser() }
2628
wasmJs { browser() }
2729

gradle-plugins/compose/src/test/test-projects/misc/kmpResourcePublication/featureModule/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ kotlin {
1111
iosX64()
1212
iosArm64()
1313
iosSimulatorArm64()
14+
macosX64()
15+
macosArm64()
1416
js { browser() }
1517
wasmJs { browser() }
1618

gradle-plugins/compose/src/test/test-projects/misc/kmpResourcePublication/gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ android.useAndroidX=true
55
org.jetbrains.compose.experimental.uikit.enabled=true
66
org.jetbrains.compose.experimental.jscanvas.enabled=true
77
org.jetbrains.compose.experimental.wasm.enabled=true
8+
org.jetbrains.compose.experimental.macos.enabled=true

0 commit comments

Comments
 (0)