diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/configureSyncIosResources.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/configureSyncIosResources.kt index 7e86148dd5..e640f8e9ba 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/configureSyncIosResources.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/configureSyncIosResources.kt @@ -23,8 +23,27 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.Framework import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType import java.io.File +private val incompatiblePlugins = listOf( + "dev.icerock.mobile.multiplatform-resources", + "io.github.skeptick.libres", +) + internal fun Project.configureSyncTask(mppExt: KotlinMultiplatformExtension) { - if (!IosGradleProperties.syncResources(providers).get()) return + fun reportSyncIsDisabled(reason: String) { + logger.info("Compose Multiplatform resource management for iOS is disabled: $reason") + } + + if (!IosGradleProperties.syncResources(providers).get()) { + reportSyncIsDisabled("'${IosGradleProperties.SYNC_RESOURCES_PROPERTY}' value is 'false'") + return + } + + for (incompatiblePluginId in incompatiblePlugins) { + if (project.plugins.hasPlugin(incompatiblePluginId)) { + reportSyncIsDisabled("resource management is not compatible with '$incompatiblePluginId'") + return + } + } with (SyncIosResourcesContext(project, mppExt)) { configureSyncResourcesTasks() diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/GradlePluginTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/GradlePluginTest.kt index ab4c765ef6..f32fde4f74 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/GradlePluginTest.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/GradlePluginTest.kt @@ -8,7 +8,10 @@ package org.jetbrains.compose.test.tests.integration import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.PreviewLogger import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.RemoteConnection import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.receiveConfigFromGradle +import org.jetbrains.compose.internal.utils.OS +import org.jetbrains.compose.internal.utils.currentOS import org.jetbrains.compose.test.utils.* +import org.junit.jupiter.api.Assumptions import java.net.ServerSocket import java.net.Socket @@ -17,8 +20,94 @@ import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger import kotlin.concurrent.thread import org.junit.jupiter.api.Test +import java.io.File class GradlePluginTest : GradlePluginTestBase() { + private data class IosTestEnv( + val targetBuildDir: File, + val appDir: File, + val envVars: Map + ) + + enum class IosPlatform(val id: String) { + SIMULATOR("iphonesimulator"), IOS("iphoneos") + } + enum class IosArch(val id: String) { + X64("x86_64"), ARM64("arm64") + } + + enum class IosBuildConfiguration(val id: String) { + DEBUG("Debug"), RELEASE("Release") + } + + private fun iosTestEnv( + platform: IosPlatform = IosPlatform.SIMULATOR, + arch: IosArch = IosArch.X64, + configuration: IosBuildConfiguration = IosBuildConfiguration.DEBUG + ): IosTestEnv { + val targetBuildDir = testWorkDir.resolve("build/ios/${configuration.id}-${platform.id}").apply { mkdirs() } + val appDir = targetBuildDir.resolve("App.app").apply { mkdirs() } + val envVars = mapOf( + "PLATFORM_NAME" to platform.id, + "ARCHS" to arch.id, + "BUILT_PRODUCTS_DIR" to targetBuildDir.canonicalPath, + "CONTENTS_FOLDER_PATH" to appDir.name, + ) + return IosTestEnv( + targetBuildDir = targetBuildDir, + appDir = appDir, + envVars = envVars + ) + } + + @Test + fun iosResources() { + Assumptions.assumeTrue(currentOS == OS.MacOS) + val iosTestEnv = iosTestEnv() + val testEnv = defaultTestEnvironment.copy( + // for some reason configuration cache + test kit + custom vars does not work + useGradleConfigurationCache = false, + additionalEnvVars = iosTestEnv.envVars + ) + + with(testProject(TestProjects.iosResources, testEnv)) { + gradle(":embedAndSignAppleFrameworkForXcode", "--dry-run").checks { + // This test is not intended to actually run embedAndSignAppleFrameworkForXcode. + // Instead, it should check that embedAndSign depends on syncComposeResources using dry run + check.taskSkipped(":syncComposeResourcesForIos") + check.taskSkipped(":embedAndSignAppleFrameworkForXcode") + } + gradle(":syncComposeResourcesForIos").checks { + check.taskSuccessful(":syncComposeResourcesForIos") + iosTestEnv.appDir.resolve("compose-resources/compose-multiplatform.xml").checkExists() + } + } + } + + @Test + fun iosMokoResources() { + Assumptions.assumeTrue(currentOS == OS.MacOS) + val iosTestEnv = iosTestEnv() + val testEnv = defaultTestEnvironment.copy( + // for some reason configuration cache + test kit + custom vars does not work + useGradleConfigurationCache = false, + additionalEnvVars = iosTestEnv.envVars + ) + with(testProject(TestProjects.iosMokoResources, testEnv)) { + gradle( + ":embedAndSignAppleFrameworkForXcode", + ":copyFrameworkResourcesToApp", + "--dry-run", + "--info" + ).checks { + // This test is not intended to actually run embedAndSignAppleFrameworkForXcode. + // Instead, it should check that the sync disables itself. + check.logContains("Compose Multiplatform resource management for iOS is disabled") + check.logDoesntContain(":syncComposeResourcesForIos") + } + } + } + @Test fun skikoWasm() = with( testProject( diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProject.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProject.kt index 3b4be8f071..ac5b024917 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProject.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProject.kt @@ -16,16 +16,19 @@ data class TestEnvironment( val workingDir: File, val kotlinVersion: String = TestKotlinVersions.Default, val composeGradlePluginVersion: String = TestProperties.composeGradlePluginVersion, + val mokoResourcesPluginVersion: String = "0.23.0", val composeCompilerPlugin: String? = null, val composeCompilerArgs: String? = null, val composeVerbose: Boolean = true, val useGradleConfigurationCache: Boolean = TestProperties.gradleConfigurationCache, + val additionalEnvVars: Map = mapOf() ) { private val placeholders = linkedMapOf( "COMPOSE_GRADLE_PLUGIN_VERSION_PLACEHOLDER" to composeGradlePluginVersion, "KOTLIN_VERSION_PLACEHOLDER" to kotlinVersion, "COMPOSE_COMPILER_PLUGIN_PLACEHOLDER" to composeCompilerPlugin, "COMPOSE_COMPILER_PLUGIN_ARGS_PLACEHOLDER" to composeCompilerArgs, + "MOKO_RESOURCES_PLUGIN_VERSION_PLACEHOLDER" to mokoResourcesPluginVersion, ) fun replacePlaceholdersInFile(file: File) { @@ -121,6 +124,10 @@ class TestProject( withGradleVersion(TestProperties.gradleVersionForTests) withProjectDir(testEnvironment.workingDir) withArguments(allArgs) + if (testEnvironment.additionalEnvVars.isNotEmpty()) { + val newEnv = HashMap(System.getenv() + testEnvironment.additionalEnvVars) + withEnvironment(newEnv) + } forwardOutput() } } diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProjects.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProjects.kt index 27475fbb85..8ca826726a 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProjects.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProjects.kt @@ -24,4 +24,6 @@ object TestProjects { const val jsMpp = "misc/jsMpp" const val skikoWasm = "misc/skikoWasm" const val jvmPreview = "misc/jvmPreview" + const val iosResources = "misc/iosResources" + const val iosMokoResources = "misc/iosMokoResources" } \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/assertUtils.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/assertUtils.kt index 56fbb3fea2..955616674c 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/assertUtils.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/assertUtils.kt @@ -53,6 +53,13 @@ internal class BuildResultChecks(private val result: BuildResult) { taskOutcome(task, TaskOutcome.FROM_CACHE) } + fun taskSkipped(task: String) { + // task outcome for skipped task is null in Gradle 7.x + if (result.task(task)?.outcome != null) { + taskOutcome(task, TaskOutcome.SKIPPED) + } + } + private fun taskOutcome(task: String, expectedOutcome: TaskOutcome) { val actualOutcome = result.task(task)?.outcome if (actualOutcome != expectedOutcome) { diff --git a/gradle-plugins/compose/src/test/test-projects/misc/iosMokoResources/build.gradle b/gradle-plugins/compose/src/test/test-projects/misc/iosMokoResources/build.gradle new file mode 100644 index 0000000000..f04770477f --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/iosMokoResources/build.gradle @@ -0,0 +1,53 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.compose" + id "dev.icerock.mobile.multiplatform-resources" +} + +kotlin { + iosX64 { + binaries.framework { + baseName = "shared" + isStatic = true + } + } + iosArm64 { + binaries.framework { + baseName = "shared" + isStatic = true + } + } + iosSimulatorArm64 { + binaries.framework { + baseName = "shared" + isStatic = true + } + } + + sourceSets { + def commonMain = named("commonMain") { + dependencies { + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material) + implementation("dev.icerock.moko:resources-compose:MOKO_RESOURCES_PLUGIN_VERSION_PLACEHOLDER") // for compose multiplatform + } + } + def iosMain = create("iosMain") { + dependsOn(commonMain.get()) + } + named("iosX64Main") { + dependsOn(iosMain) + } + named("iosArm64Main") { + dependsOn(iosMain) + } + named("iosSimulatorArm64Main") { + dependsOn(iosMain) + } + } +} + +multiplatformResources { + multiplatformResourcesPackage = "org.example" +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/iosMokoResources/gradle.properties b/gradle-plugins/compose/src/test/test-projects/misc/iosMokoResources/gradle.properties new file mode 100644 index 0000000000..689880ee3f --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/iosMokoResources/gradle.properties @@ -0,0 +1 @@ +org.jetbrains.compose.experimental.uikit.enabled=true \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/iosMokoResources/settings.gradle b/gradle-plugins/compose/src/test/test-projects/misc/iosMokoResources/settings.gradle new file mode 100644 index 0000000000..7ce2258db9 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/iosMokoResources/settings.gradle @@ -0,0 +1,13 @@ +pluginManagement { + plugins { + id 'org.jetbrains.kotlin.multiplatform' version 'KOTLIN_VERSION_PLACEHOLDER' + id 'org.jetbrains.compose' version 'COMPOSE_GRADLE_PLUGIN_VERSION_PLACEHOLDER' + id 'dev.icerock.mobile.multiplatform-resources' version 'MOKO_RESOURCES_PLUGIN_VERSION_PLACEHOLDER' + } + repositories { + mavenLocal() + gradlePluginPortal() + maven { url "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" } + } +} +rootProject.name = "iosResources" \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/iosMokoResources/src/commonMain/kotlin/App.kt b/gradle-plugins/compose/src/test/test-projects/misc/iosMokoResources/src/commonMain/kotlin/App.kt new file mode 100644 index 0000000000..052b410fb1 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/iosMokoResources/src/commonMain/kotlin/App.kt @@ -0,0 +1,10 @@ +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable + +@Composable +fun App() { + MaterialTheme { + Text("Hello, World!") + } +} diff --git a/gradle-plugins/compose/src/test/test-projects/misc/iosResources/build.gradle b/gradle-plugins/compose/src/test/test-projects/misc/iosResources/build.gradle new file mode 100644 index 0000000000..df4dca6b00 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/iosResources/build.gradle @@ -0,0 +1,48 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.compose" +} + +kotlin { + iosX64 { + binaries.framework { + baseName = "shared" + isStatic = true + } + } + iosArm64 { + binaries.framework { + baseName = "shared" + isStatic = true + } + } + iosSimulatorArm64 { + binaries.framework { + baseName = "shared" + isStatic = true + } + } + + sourceSets { + def commonMain = named("commonMain") { + dependencies { + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.components.resources) + } + } + def iosMain = create("iosMain") { + dependsOn(commonMain.get()) + } + named("iosX64Main") { + dependsOn(iosMain) + } + named("iosArm64Main") { + dependsOn(iosMain) + } + named("iosSimulatorArm64Main") { + dependsOn(iosMain) + } + } +} diff --git a/gradle-plugins/compose/src/test/test-projects/misc/iosResources/gradle.properties b/gradle-plugins/compose/src/test/test-projects/misc/iosResources/gradle.properties new file mode 100644 index 0000000000..689880ee3f --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/iosResources/gradle.properties @@ -0,0 +1 @@ +org.jetbrains.compose.experimental.uikit.enabled=true \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/iosResources/settings.gradle b/gradle-plugins/compose/src/test/test-projects/misc/iosResources/settings.gradle new file mode 100644 index 0000000000..bcfce8c1d4 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/iosResources/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + plugins { + id 'org.jetbrains.kotlin.multiplatform' version 'KOTLIN_VERSION_PLACEHOLDER' + id 'org.jetbrains.compose' version 'COMPOSE_GRADLE_PLUGIN_VERSION_PLACEHOLDER' + } + repositories { + mavenLocal() + gradlePluginPortal() + maven { url "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" } + } +} +rootProject.name = "iosResources" \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/iosResources/src/commonMain/kotlin/App.kt b/gradle-plugins/compose/src/test/test-projects/misc/iosResources/src/commonMain/kotlin/App.kt new file mode 100644 index 0000000000..4b856546d9 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/iosResources/src/commonMain/kotlin/App.kt @@ -0,0 +1,38 @@ +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.painterResource + +@OptIn(ExperimentalResourceApi::class) +@Composable +fun App() { + MaterialTheme { + var greetingText by remember { mutableStateOf("Hello, World!") } + var showImage by remember { mutableStateOf(false) } + Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + Button(onClick = { + showImage = !showImage + }) { + Text(greetingText) + } + AnimatedVisibility(showImage) { + Image( + painterResource("compose-multiplatform.xml"), + null + ) + } + } + } +} diff --git a/gradle-plugins/compose/src/test/test-projects/misc/iosResources/src/commonMain/resources/compose-multiplatform.xml b/gradle-plugins/compose/src/test/test-projects/misc/iosResources/src/commonMain/resources/compose-multiplatform.xml new file mode 100644 index 0000000000..d7bf7955f4 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/iosResources/src/commonMain/resources/compose-multiplatform.xml @@ -0,0 +1,36 @@ + + + + + + + +