Skip to content

Commit 7eae205

Browse files
authored
feat: Support Compose Resources in Native macOS Executables (#5431)
Support for Compose Resources in native macOS executables. - Added a new integration test `macosExecutableResources` to verify resource copying for macOS executables. - Updated `configureNativeApplication.kt` to pass Compose resource directories from Kotlin Native binaries to the packaging task. - Resources are now copied into the `.app/Contents/Resources` directory for packaged macOS applications. - Packaging tasks for macOS now depend on the corresponding resource copying task. ## Testing Executing tasks: [:compose:test-Gradle(9.0.0)-Agp(8.9.0), --tests, org.jetbrains.compose.test.tests.integration.ResourcesTest.macosExectuableResources] ## Release Notes ### Fixes - Resources - Fixed an issue where resources were not copied when packaging the macOS native target, causing the application to crash when it attempted to read those resources.
1 parent 68f2a95 commit 7eae205

File tree

10 files changed

+299
-0
lines changed

10 files changed

+299
-0
lines changed

gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureNativeApplication.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ private fun configureNativeApplication(
7070
copyright.set(project.provider {
7171
app.distributions.copyright ?: "Copyright (C) ${Calendar.getInstance().get(Calendar.YEAR)}"
7272
})
73+
if (binary.outputKind == NativeOutputKind.EXECUTABLE) {
74+
val binaryResources = (binary.compilation.associatedCompilations + binary.compilation).flatMap { compilation ->
75+
compilation.allKotlinSourceSets.map { it.resources }
76+
}
77+
composeResourcesDirs.setFrom(binaryResources)
78+
}
7379
}
7480

7581
if (TargetFormat.Dmg in app.distributions.targetFormats) {

gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNativeMacApplicationPackageAppDirTask.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package org.jetbrains.compose.desktop.application.tasks
77

8+
import org.gradle.api.file.ConfigurableFileCollection
89
import org.gradle.api.file.RegularFileProperty
910
import org.gradle.api.provider.Property
1011
import org.gradle.api.tasks.*
@@ -43,6 +44,11 @@ abstract class AbstractNativeMacApplicationPackageAppDirTask : AbstractNativeMac
4344
@get:Optional
4445
val minimumSystemVersion: Property<String?> = objects.nullableProperty()
4546

47+
@get:InputFiles
48+
@get:Optional
49+
@get:PathSensitive(PathSensitivity.ABSOLUTE)
50+
val composeResourcesDirs: ConfigurableFileCollection = objects.fileCollection()
51+
4652
override fun createPackage(destinationDir: File, workingDir: File) {
4753
val packageName = packageName.get()
4854
val appDir = destinationDir.resolve("$packageName.app").apply { mkdirs() }
@@ -61,6 +67,13 @@ abstract class AbstractNativeMacApplicationPackageAppDirTask : AbstractNativeMac
6167
setupInfoPlist(executableName = appExecutableFile.name)
6268
writeToFile(contentsDir.resolve("Info.plist"))
6369
}
70+
71+
if (!composeResourcesDirs.isEmpty) {
72+
fileOperations.copy { copySpec ->
73+
copySpec.from(composeResourcesDirs)
74+
copySpec.into(appResourcesDir.resolve("compose-resources").apply { mkdirs() })
75+
}
76+
}
6477
}
6578

6679
private fun InfoPlistBuilder.setupInfoPlist(executableName: String) {

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

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import org.jetbrains.compose.test.utils.TestProject
1313
import org.jetbrains.compose.test.utils.assertEqualTextFiles
1414
import org.jetbrains.compose.test.utils.assertNotEqualTextFiles
1515
import org.jetbrains.compose.test.utils.checkExists
16+
import org.jetbrains.compose.test.utils.checkNotExists
1617
import org.jetbrains.compose.test.utils.checks
1718
import org.jetbrains.compose.test.utils.modify
1819
import org.junit.jupiter.api.Assumptions
@@ -1026,6 +1027,88 @@ class ResourcesTest : GradlePluginTestBase() {
10261027
}
10271028
}
10281029

1030+
@Test
1031+
fun macosExecutableResources() {
1032+
Assumptions.assumeTrue(currentOS == OS.MacOS)
1033+
with(testProject("misc/macosNativeResources")) {
1034+
val appName = "Test Resources"
1035+
gradle(":createDistributableNativeDebugMacosX64").checks {
1036+
val targetResourcesDir = "build/compose/binaries/main/native-macosX64-debug-app-image/${appName}.app/Contents/Resources"
1037+
file("$targetResourcesDir/compose-resources/composeResources/appleresources.generated.resources/drawable/compose-multiplatform.xml").checkExists()
1038+
file("$targetResourcesDir/compose-resources/composeResources/appleresources.generated.resources/drawable/icon.xml").checkExists()
1039+
}
1040+
}
1041+
}
1042+
1043+
@Test
1044+
fun macosExecutableResourcesWithResourceChanged() {
1045+
Assumptions.assumeTrue(currentOS == OS.MacOS)
1046+
with(testProject("misc/macosNativeResources")) {
1047+
val appName = "Test Resources"
1048+
val taskName = ":createDistributableNativeDebugMacosX64"
1049+
val comment = "<!-- Test resources changed -->"
1050+
val fileNames = listOf(
1051+
"compose-multiplatform.xml",
1052+
"icon.xml"
1053+
)
1054+
val targetResourcesDir = "build/compose/binaries/main/native-macosX64-debug-app-image/${appName}.app/Contents/Resources/compose-resources/composeResources/appleresources.generated.resources/drawable/"
1055+
gradle(taskName).checks {
1056+
fileNames.forEach { name ->
1057+
check(!file(targetResourcesDir + name).readText().startsWith(comment)) {
1058+
"The resources file contains the test content before change"
1059+
}
1060+
}
1061+
}
1062+
1063+
listOf(
1064+
"src/commonMain/composeResources/drawable/compose-multiplatform.xml",
1065+
"src/macosMain/composeResources/drawable/icon.xml"
1066+
).forEach { path ->
1067+
file(path).modify {
1068+
comment + it
1069+
}
1070+
}
1071+
gradle(taskName).checks {
1072+
check.taskSuccessful(taskName)
1073+
fileNames.forEach { name ->
1074+
check(file(targetResourcesDir + name).readText().startsWith(comment)) {
1075+
"The resources file does not contain the test content after changed"
1076+
}
1077+
}
1078+
}
1079+
}
1080+
}
1081+
1082+
@Test
1083+
fun macosExecutableResourcesWithResourceDeleted() {
1084+
Assumptions.assumeTrue(currentOS == OS.MacOS)
1085+
with(testProject("misc/macosNativeResources")) {
1086+
val appName = "Test Resources"
1087+
val taskName = ":createDistributableNativeDebugMacosX64"
1088+
1089+
val targetResource = "src/commonMain/composeResources/drawable/compose-multiplatform2.xml"
1090+
file(targetResource).apply {
1091+
check(createNewFile())
1092+
writeText(file(targetResource.replace("compose-multiplatform2", "compose-multiplatform")).readText())
1093+
}
1094+
1095+
gradle(taskName).checks {
1096+
val targetResourcesDir = "build/compose/binaries/main/native-macosX64-debug-app-image/${appName}.app/Contents/Resources"
1097+
file("$targetResourcesDir/compose-resources/composeResources/appleresources.generated.resources/drawable/compose-multiplatform.xml").checkExists()
1098+
file("$targetResourcesDir/compose-resources/composeResources/appleresources.generated.resources/drawable/compose-multiplatform2.xml").checkExists()
1099+
file("$targetResourcesDir/compose-resources/composeResources/appleresources.generated.resources/drawable/icon.xml").checkExists()
1100+
}
1101+
check(file(targetResource).delete())
1102+
gradle(taskName).checks {
1103+
check.taskSuccessful(taskName)
1104+
val targetResourcesDir = "build/compose/binaries/main/native-macosX64-debug-app-image/${appName}.app/Contents/Resources"
1105+
file("$targetResourcesDir/compose-resources/composeResources/appleresources.generated.resources/drawable/compose-multiplatform.xml").checkExists()
1106+
file("$targetResourcesDir/compose-resources/composeResources/appleresources.generated.resources/drawable/compose-multiplatform2.xml").checkNotExists()
1107+
file("$targetResourcesDir/compose-resources/composeResources/appleresources.generated.resources/drawable/icon.xml").checkExists()
1108+
}
1109+
}
1110+
}
1111+
10291112
@Test
10301113
fun iosTestResources() {
10311114
Assumptions.assumeTrue(currentOS == OS.MacOS)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
2+
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
3+
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
4+
5+
plugins {
6+
kotlin("multiplatform")
7+
kotlin("plugin.compose")
8+
id("org.jetbrains.compose")
9+
}
10+
11+
kotlin {
12+
13+
listOf(
14+
macosX64(),
15+
macosArm64(),
16+
).forEach {
17+
it.binaries {
18+
executable {
19+
entryPoint = "main"
20+
freeCompilerArgs += listOf(
21+
"-linker-options",
22+
"-framework",
23+
"-linker-option",
24+
"Metal",
25+
"-Xdisable-phases=VerifyBitcode"
26+
)
27+
}
28+
}
29+
}
30+
31+
sourceSets {
32+
commonMain {
33+
dependencies {
34+
implementation(compose.runtime)
35+
implementation(compose.material)
36+
implementation(compose.components.resources)
37+
}
38+
}
39+
}
40+
}
41+
42+
compose.desktop {
43+
nativeApplication {
44+
targets(
45+
targets = kotlin.targets.filter {
46+
it.platformType == KotlinPlatformType.native &&
47+
it.name.contains("macos")
48+
}.toTypedArray()
49+
)
50+
distributions {
51+
macOS {
52+
targetFormats(TargetFormat.Dmg)
53+
packageName = "Test Resources"
54+
packageVersion = "1.0.0"
55+
}
56+
}
57+
}
58+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
org.gradle.jvmargs=-Xmx8096M
2+
org.jetbrains.compose.experimental.macos.enabled=true
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
rootProject.name = "appleResources"
2+
pluginManagement {
3+
repositories {
4+
mavenLocal()
5+
gradlePluginPortal()
6+
google()
7+
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
8+
maven("https://packages.jetbrains.team/maven/p/kt/dev")
9+
}
10+
plugins {
11+
id("org.jetbrains.kotlin.multiplatform").version("KOTLIN_VERSION_PLACEHOLDER")
12+
id("org.jetbrains.kotlin.plugin.compose").version("KOTLIN_VERSION_PLACEHOLDER")
13+
id("org.jetbrains.kotlin.native.cocoapods").version("KOTLIN_VERSION_PLACEHOLDER")
14+
id("org.jetbrains.compose").version("COMPOSE_GRADLE_PLUGIN_VERSION_PLACEHOLDER")
15+
}
16+
}
17+
dependencyResolutionManagement {
18+
repositories {
19+
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
20+
maven("https://packages.jetbrains.team/maven/p/kt/dev")
21+
mavenCentral()
22+
gradlePluginPortal()
23+
google()
24+
mavenLocal()
25+
}
26+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="600dp"
3+
android:height="600dp"
4+
android:viewportWidth="600"
5+
android:viewportHeight="600">
6+
<path
7+
android:pathData="M301.21,418.53C300.97,418.54 300.73,418.56 300.49,418.56C297.09,418.59 293.74,417.72 290.79,416.05L222.6,377.54C220.63,376.43 219,374.82 217.85,372.88C216.7,370.94 216.09,368.73 216.07,366.47L216.07,288.16C216.06,287.32 216.09,286.49 216.17,285.67C216.38,283.54 216.91,281.5 217.71,279.6L199.29,268.27L177.74,256.19C175.72,260.43 174.73,265.23 174.78,270.22L174.79,387.05C174.85,393.89 178.57,400.2 184.53,403.56L286.26,461.02C290.67,463.51 295.66,464.8 300.73,464.76C300.91,464.76 301.09,464.74 301.27,464.74C301.24,449.84 301.22,439.23 301.22,439.23L301.21,418.53Z"
8+
android:fillColor="#041619"
9+
android:fillType="nonZero"/>
10+
<path
11+
android:pathData="M409.45,242.91L312.64,188.23C303.64,183.15 292.58,183.26 283.68,188.51L187.92,245C183.31,247.73 179.93,251.62 177.75,256.17L177.74,256.19L199.29,268.27L217.71,279.6C217.83,279.32 217.92,279.02 218.05,278.74C218.24,278.36 218.43,277.98 218.64,277.62C219.06,276.88 219.52,276.18 220.04,275.51C221.37,273.8 223.01,272.35 224.87,271.25L289.06,233.39C290.42,232.59 291.87,231.96 293.39,231.51C295.53,230.87 297.77,230.6 300,230.72C302.98,230.88 305.88,231.73 308.47,233.2L373.37,269.85C375.54,271.08 377.49,272.68 379.13,274.57C379.68,275.19 380.18,275.85 380.65,276.53C380.86,276.84 381.05,277.15 381.24,277.47L397.79,266.39L420.34,252.93L420.31,252.88C417.55,248.8 413.77,245.35 409.45,242.91Z"
12+
android:fillColor="#37BF6E"
13+
android:fillType="nonZero"/>
14+
<path
15+
android:pathData="M381.24,277.47C381.51,277.92 381.77,278.38 382.01,278.84C382.21,279.24 382.39,279.65 382.57,280.06C382.91,280.88 383.19,281.73 383.41,282.59C383.74,283.88 383.92,285.21 383.93,286.57L383.93,361.1C383.96,363.95 383.35,366.77 382.16,369.36C381.93,369.86 381.69,370.35 381.42,370.83C379.75,373.79 377.32,376.27 374.39,378L310.2,415.87C307.47,417.48 304.38,418.39 301.21,418.53L301.22,439.23C301.22,439.23 301.24,449.84 301.27,464.74C306.1,464.61 310.91,463.3 315.21,460.75L410.98,404.25C419.88,399 425.31,389.37 425.22,379.03L425.22,267.85C425.17,262.48 423.34,257.34 420.34,252.93L397.79,266.39L381.24,277.47Z"
16+
android:fillColor="#3870B2"
17+
android:fillType="nonZero"/>
18+
<path
19+
android:pathData="M177.75,256.17C179.93,251.62 183.31,247.73 187.92,245L283.68,188.51C292.58,183.26 303.64,183.15 312.64,188.23L409.45,242.91C413.77,245.35 417.55,248.8 420.31,252.88L420.34,252.93L498.59,206.19C494.03,199.46 487.79,193.78 480.67,189.75L320.86,99.49C306.01,91.1 287.75,91.27 273.07,99.95L114.99,193.2C107.39,197.69 101.81,204.11 98.21,211.63L177.74,256.19L177.75,256.17ZM301.27,464.74C301.09,464.74 300.91,464.76 300.73,464.76C295.66,464.8 290.67,463.51 286.26,461.02L184.53,403.56C178.57,400.2 174.85,393.89 174.79,387.05L174.78,270.22C174.73,265.23 175.72,260.43 177.74,256.19L98.21,211.63C94.86,218.63 93.23,226.58 93.31,234.82L93.31,427.67C93.42,438.97 99.54,449.37 109.4,454.92L277.31,549.77C284.6,553.88 292.84,556.01 301.2,555.94L301.2,555.8C301.39,543.78 301.33,495.26 301.27,464.74Z"
20+
android:strokeWidth="10"
21+
android:fillColor="#00000000"
22+
android:strokeColor="#083042"
23+
android:fillType="nonZero"/>
24+
<path
25+
android:pathData="M498.59,206.19L420.34,252.93C423.34,257.34 425.17,262.48 425.22,267.85L425.22,379.03C425.31,389.37 419.88,399 410.98,404.25L315.21,460.75C310.91,463.3 306.1,464.61 301.27,464.74C301.33,495.26 301.39,543.78 301.2,555.8L301.2,555.94C309.48,555.87 317.74,553.68 325.11,549.32L483.18,456.06C497.87,447.39 506.85,431.49 506.69,414.43L506.69,230.91C506.6,222.02 503.57,213.5 498.59,206.19Z"
26+
android:strokeWidth="10"
27+
android:fillColor="#00000000"
28+
android:strokeColor="#083042"
29+
android:fillType="nonZero"/>
30+
<path
31+
android:pathData="M301.2,555.94C292.84,556.01 284.6,553.88 277.31,549.76L109.4,454.92C99.54,449.37 93.42,438.97 93.31,427.67L93.31,234.82C93.23,226.58 94.86,218.63 98.21,211.63C101.81,204.11 107.39,197.69 114.99,193.2L273.07,99.95C287.75,91.27 306.01,91.1 320.86,99.49L480.67,189.75C487.79,193.78 494.03,199.46 498.59,206.19C503.57,213.5 506.6,222.02 506.69,230.91L506.69,414.43C506.85,431.49 497.87,447.39 483.18,456.06L325.11,549.32C317.74,553.68 309.48,555.87 301.2,555.94Z"
32+
android:strokeWidth="10"
33+
android:fillColor="#00000000"
34+
android:strokeColor="#083042"
35+
android:fillType="nonZero"/>
36+
</vector>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import androidx.compose.animation.AnimatedVisibility
2+
import androidx.compose.foundation.Image
3+
import androidx.compose.foundation.layout.Column
4+
import androidx.compose.foundation.layout.fillMaxWidth
5+
import androidx.compose.material.Button
6+
import androidx.compose.material.MaterialTheme
7+
import androidx.compose.material.Text
8+
import androidx.compose.runtime.Composable
9+
import androidx.compose.runtime.getValue
10+
import androidx.compose.runtime.mutableStateOf
11+
import androidx.compose.runtime.remember
12+
import androidx.compose.runtime.setValue
13+
import androidx.compose.ui.Alignment
14+
import androidx.compose.ui.Modifier
15+
import org.jetbrains.compose.resources.painterResource
16+
import appleresources.generated.resources.*
17+
18+
@Composable
19+
fun App() {
20+
MaterialTheme {
21+
var greetingText by remember { mutableStateOf("Hello, World!") }
22+
var showImage by remember { mutableStateOf(false) }
23+
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
24+
Button(onClick = {
25+
showImage = !showImage
26+
}) {
27+
Text(greetingText)
28+
}
29+
AnimatedVisibility(showImage) {
30+
Image(
31+
painterResource(Res.drawable.compose_multiplatform),
32+
null
33+
)
34+
}
35+
}
36+
}
37+
}

0 commit comments

Comments
 (0)