Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resources improvements for native macOS app #5169

Merged
merged 5 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,32 @@ internal actual fun getPlatformResourceReader(): ResourceReader = object : Resou
private fun getPathOnDisk(path: String): String {
val fm = NSFileManager.defaultManager()
val currentDirectoryPath = fm.currentDirectoryPath
val pathFix = getPathWithoutPackage(path)
return listOf(
// Framework binary
// todo: support fallback path at bundle root?
NSBundle.mainBundle.resourcePath + "/compose-resources/" + path,
// Executable binary
//todo in future bundle resources with app and use all sourceSets (skikoMain, nativeMain)
"$currentDirectoryPath/src/macosMain/composeResources/$path",
"$currentDirectoryPath/src/macosTest/composeResources/$path",
"$currentDirectoryPath/src/commonMain/composeResources/$path",
"$currentDirectoryPath/src/commonTest/composeResources/$path"
"$currentDirectoryPath/src/macosMain/composeResources/$pathFix",
"$currentDirectoryPath/src/macosTest/composeResources/$pathFix",
"$currentDirectoryPath/src/commonMain/composeResources/$pathFix",
"$currentDirectoryPath/src/commonTest/composeResources/$pathFix"
).firstOrNull { p -> fm.fileExistsAtPath(p) } ?: throw MissingResourceException(path)
}

private fun getPathWithoutPackage(path: String): String {
// At the moment resources are not bundled when running a macOS executable binary.
// As a workaround, load the resources from the actual path on disk. So the
// "composeResources/PACKAGE/" prefix must be removed. For example:
// "composeResources/chat_mpp.shared.generated.resources/drawable/background.jpg"
// Will be transformed into:
// "drawable/background.jpg"
// In the future when resources are bundled when running macOS executable binary this
// workaround is no longer needed.
require(path.startsWith("composeResources/")) { "Invalid path: $path" }
return path
.substringAfter("composeResources/") // remove "composeResources/" part
.substringAfter("/") // remove PACKAGE path
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ internal fun Project.configureSyncIosComposeResources(
}

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

internal fun Framework.getSyncResourcesTaskName() = "sync${getClassifier()}ComposeResourcesForIos"

private fun Framework.isCocoapodsFramework() = name.startsWith("pod")

private fun Framework.getFinalResourcesDir(): Provider<Directory> {
Expand All @@ -125,9 +126,9 @@ private fun Framework.getFinalResourcesDir(): Provider<Directory> {
} else {
providers.environmentVariable("BUILT_PRODUCTS_DIR")
.zip(
providers.environmentVariable("CONTENTS_FOLDER_PATH")
) { builtProductsDir, contentsFolderPath ->
File("$builtProductsDir/$contentsFolderPath/$IOS_COMPOSE_RESOURCES_ROOT_DIR").canonicalPath
providers.environmentVariable("UNLOCALIZED_RESOURCES_FOLDER_PATH")
) { builtProductsDir, unlocalizedResourcesFolderPath ->
File("$builtProductsDir/$unlocalizedResourcesFolderPath/$IOS_COMPOSE_RESOURCES_ROOT_DIR").canonicalPath
}
.flatMap {
project.objects.directoryProperty().apply { set(File(it)) }
Expand All @@ -142,4 +143,10 @@ private fun KotlinNativeTarget.isIosDeviceTarget(): Boolean =
konanTarget === KonanTarget.IOS_ARM64

private fun KotlinNativeTarget.isIosTarget(): Boolean =
isIosSimulatorTarget() || isIosDeviceTarget()
isIosSimulatorTarget() || isIosDeviceTarget()

private fun KotlinNativeTarget.isMacTarget(): Boolean =
konanTarget === KonanTarget.MACOS_X64 || konanTarget === KonanTarget.MACOS_ARM64

private fun KotlinNativeTarget.isIosOrMacTarget(): Boolean =
isIosTarget() || isMacTarget()
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,17 @@ private fun getRequestedKonanTargetsByXcode(platform: String, archs: List<String
})
}

else -> error("Unknown iOS platform: '$platform'")
platform.startsWith("macosx") -> {
targets.addAll(archs.map { arch ->
when (arch) {
"arm64" -> KonanTarget.MACOS_ARM64
"x86_64" -> KonanTarget.MACOS_X64
else -> error("Unknown macOS arch: '$arch'")
}
})
}

else -> error("Unknown Apple platform: '$platform'")
}

return targets.toList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,13 @@ class ResourcesTest : GradlePluginTestBase() {
libpath("iossimulatorarm64", "-kotlin_resources.kotlin_resources.zip")
)
checkResourcesZip(iossimulatorarm64ResZip, resourcesFiles, false)

val macosx64ResZip =
file(libpath("macosx64", "-kotlin_resources.kotlin_resources.zip"))
checkResourcesZip(macosx64ResZip, resourcesFiles, false)
val macosarm64ResZip =
file(libpath("macosarm64", "-kotlin_resources.kotlin_resources.zip"))
checkResourcesZip(macosarm64ResZip, resourcesFiles, false)
}
val jsResZip = file(libpath("js", "-kotlin_resources.kotlin_resources.zip"))
checkResourcesZip(jsResZip, resourcesFiles, false)
Expand All @@ -300,6 +307,13 @@ class ResourcesTest : GradlePluginTestBase() {
":appModule:iosSimulatorArm64Test"
}
gradle(iosTask)

val macosTask = if (currentArch == Arch.X64) {
":appModule:macosX64Test"
} else {
":appModule:macosArm64Test"
}
gradle(macosTask)
}

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

@Test
fun macosResources() {
Assumptions.assumeTrue(currentOS == OS.MacOS)
val macosEnv = mapOf(
"PLATFORM_NAME" to "macosx",
"ARCHS" to "arm64",
"CONFIGURATION" to "Debug",
)
val testEnv = defaultTestEnvironment.copy(
additionalEnvVars = macosEnv
)

with(TestProject("misc/macosResources", testEnv)) {
gradle(":podspec", "-Pkotlin.native.cocoapods.generate.wrapper=true").checks {
assertEqualTextFiles(
file("macosResources.podspec"),
file("expected/macosResources.podspec")
)
file("build/compose/cocoapods/compose-resources").checkExists()
}

gradle(
":syncFramework",
"-Pkotlin.native.cocoapods.platform=${macosEnv["PLATFORM_NAME"]}",
"-Pkotlin.native.cocoapods.archs=${macosEnv["ARCHS"]}",
"-Pkotlin.native.cocoapods.configuration=${macosEnv["CONFIGURATION"]}",
"--dry-run"
).checks {
check.taskSkipped(":generateComposeResClass")

check.taskSkipped(":convertXmlValueResourcesForCommonMain")
check.taskSkipped(":copyNonXmlValueResourcesForCommonMain")
check.taskSkipped(":prepareComposeResourcesTaskForCommonMain")
check.taskSkipped(":generateResourceAccessorsForCommonMain")

check.taskSkipped(":convertXmlValueResourcesForNativeMain")
check.taskSkipped(":copyNonXmlValueResourcesForNativeMain")
check.taskSkipped(":prepareComposeResourcesTaskForNativeMain")
check.taskSkipped(":generateResourceAccessorsForNativeMain")

check.taskSkipped(":convertXmlValueResourcesForAppleMain")
check.taskSkipped(":copyNonXmlValueResourcesForAppleMain")
check.taskSkipped(":prepareComposeResourcesTaskForAppleMain")
check.taskSkipped(":generateResourceAccessorsForAppleMain")

check.taskSkipped(":convertXmlValueResourcesForMacosMain")
check.taskSkipped(":copyNonXmlValueResourcesForMacosMain")
check.taskSkipped(":prepareComposeResourcesTaskForMacosMain")
check.taskSkipped(":generateResourceAccessorsForMacosMain")

check.taskSkipped(":convertXmlValueResourcesForMacosX64Main")
check.taskSkipped(":copyNonXmlValueResourcesForMacosX64Main")
check.taskSkipped(":prepareComposeResourcesTaskForMacosX64Main")
check.taskSkipped(":generateResourceAccessorsForMacosX64Main")

check.taskSkipped(":syncPodComposeResourcesForIos")
}
gradle(":syncPodComposeResourcesForIos").checks {
check.taskNoSource(":convertXmlValueResourcesForCommonMain")
check.taskSuccessful(":copyNonXmlValueResourcesForCommonMain")
check.taskSuccessful(":prepareComposeResourcesTaskForCommonMain")
check.taskSkipped(":generateResourceAccessorsForCommonMain")

check.taskNoSource(":convertXmlValueResourcesForNativeMain")
check.taskNoSource(":copyNonXmlValueResourcesForNativeMain")
check.taskNoSource(":prepareComposeResourcesTaskForNativeMain")
check.taskSkipped(":generateResourceAccessorsForNativeMain")

check.taskNoSource(":convertXmlValueResourcesForAppleMain")
check.taskNoSource(":copyNonXmlValueResourcesForAppleMain")
check.taskNoSource(":prepareComposeResourcesTaskForAppleMain")
check.taskSkipped(":generateResourceAccessorsForAppleMain")

check.taskNoSource(":convertXmlValueResourcesForMacosMain")
check.taskSuccessful(":copyNonXmlValueResourcesForMacosMain")
check.taskSuccessful(":prepareComposeResourcesTaskForMacosMain")
check.taskSkipped(":generateResourceAccessorsForMacosMain")

check.taskNoSource(":convertXmlValueResourcesForMacosX64Main")
check.taskNoSource(":copyNonXmlValueResourcesForMacosX64Main")
check.taskNoSource(":prepareComposeResourcesTaskForMacosX64Main")
check.taskSkipped(":generateResourceAccessorsForMacosX64Main")

file("build/compose/cocoapods/compose-resources/composeResources/macosresources.generated.resources/drawable/compose-multiplatform.xml").checkExists()
file("build/compose/cocoapods/compose-resources/composeResources/macosresources.generated.resources/drawable/icon.xml").checkExists()
}
}
}

@Test
fun iosTestResources() {
Assumptions.assumeTrue(currentOS == OS.MacOS)
Expand All @@ -702,6 +805,21 @@ class ResourcesTest : GradlePluginTestBase() {
}
}

@Test
fun macosTestResources() {
Assumptions.assumeTrue(currentOS == OS.MacOS)
with(testProject("misc/macosResources")) {
gradle(":linkDebugTestMacosX64", "--dry-run").checks {
check.taskSkipped(":copyTestComposeResourcesForMacosX64")
check.taskSkipped(":linkDebugTestMacosX64")
}
gradle(":copyTestComposeResourcesForMacosX64").checks {
file("build/bin/macosX64/debugTest/compose-resources/composeResources/macosresources.generated.resources/drawable/compose-multiplatform.xml").checkExists()
file("build/bin/macosX64/debugTest/compose-resources/composeResources/macosresources.generated.resources/drawable/icon.xml").checkExists()
}
}
}

@Test
fun checkTestResources() {
with(testProject("misc/testResources")) {
Expand All @@ -714,6 +832,10 @@ class ResourcesTest : GradlePluginTestBase() {
check.logContains("Configure test resources for 'iosArm64' target")
check.logContains("Configure main resources for 'iosSimulatorArm64' target")
check.logContains("Configure test resources for 'iosSimulatorArm64' target")
check.logContains("Configure main resources for 'macosX64' target")
check.logContains("Configure test resources for 'macosX64' target")
check.logContains("Configure main resources for 'macosArm64' target")
check.logContains("Configure test resources for 'macosArm64' target")

check.taskSuccessful(":desktopTest")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ kotlin {
iosX64()
iosArm64()
iosSimulatorArm64()
macosX64()
macosArm64()
js { browser() }
wasmJs { browser() }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<resources>
<string name="macOS_str">macOS string</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package me.sample.app

import androidx.compose.runtime.Composable
import kmpresourcepublication.appmodule.generated.resources.Res
import kmpresourcepublication.appmodule.generated.resources.macOS_str
import org.jetbrains.compose.resources.stringResource

@Composable
actual fun getPlatformSpecificString(): String =
stringResource(Res.string.macOS_str)
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ kotlin {
iosX64()
iosArm64()
iosSimulatorArm64()
macosX64()
macosArm64()
js { browser() }
wasmJs { browser() }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ kotlin {
iosX64()
iosArm64()
iosSimulatorArm64()
macosX64()
macosArm64()
js { browser() }
wasmJs { browser() }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ android.useAndroidX=true
org.jetbrains.compose.experimental.uikit.enabled=true
org.jetbrains.compose.experimental.jscanvas.enabled=true
org.jetbrains.compose.experimental.wasm.enabled=true
org.jetbrains.compose.experimental.macos.enabled=true
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
plugins {
kotlin("multiplatform")
kotlin("plugin.compose")
kotlin("native.cocoapods")
id("org.jetbrains.compose")
}

kotlin {
cocoapods {
version = "1.0"
summary = "Some description for a Kotlin/Native module"
homepage = "Link to a Kotlin/Native module homepage"
pod("Base64", "1.1.2")
framework {
baseName = "shared"
isStatic = true
}
}

macosX64()
macosArm64()

sourceSets {
commonMain {
dependencies {
implementation(compose.runtime)
implementation(compose.material)
implementation(compose.components.resources)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
Pod::Spec.new do |spec|
spec.name = 'macosResources'
spec.version = '1.0'
spec.homepage = 'Link to a Kotlin/Native module homepage'
spec.source = { :http=> ''}
spec.authors = ''
spec.license = ''
spec.summary = 'Some description for a Kotlin/Native module'
spec.vendored_frameworks = 'build/cocoapods/framework/shared.framework'
spec.libraries = 'c++'

spec.dependency 'Base64', '1.1.2'

if !Dir.exist?('build/cocoapods/framework/shared.framework') || Dir.empty?('build/cocoapods/framework/shared.framework')
raise "

Kotlin framework 'shared' doesn't exist yet, so a proper Xcode project can't be generated.
'pod install' should be executed after running ':generateDummyFramework' Gradle task:

./gradlew :generateDummyFramework

Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)"
end

spec.xcconfig = {
'ENABLE_USER_SCRIPT_SANDBOXING' => 'NO',
}

spec.pod_target_xcconfig = {
'KOTLIN_PROJECT_PATH' => '',
'PRODUCT_MODULE_NAME' => 'shared',
}

spec.script_phases = [
{
:name => 'Build macosResources',
:execution_position => :before_compile,
:shell_path => '/bin/sh',
:script => <<-SCRIPT
if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
exit 0
fi
set -ev
REPO_ROOT="$PODS_TARGET_SRCROOT"
"$REPO_ROOT/gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \
-Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \
-Pkotlin.native.cocoapods.archs="$ARCHS" \
-Pkotlin.native.cocoapods.configuration="$CONFIGURATION"
SCRIPT
}
]
spec.resources = ['build/compose/cocoapods/compose-resources']
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8096M
org.jetbrains.compose.experimental.macos.enabled=true
Loading
Loading