diff --git a/components/gradle/libs.versions.toml b/components/gradle/libs.versions.toml index 6ecd0569a6..bd1a087c30 100644 --- a/components/gradle/libs.versions.toml +++ b/components/gradle/libs.versions.toml @@ -3,7 +3,7 @@ kotlinx-coroutines = "1.8.0" androidx-appcompat = "1.6.1" androidx-activity-compose = "1.8.2" androidx-test = "1.5.0" -androidx-compose = "1.6.0" +androidx-compose = "1.6.1" [libraries] kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } @@ -11,6 +11,7 @@ kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-t androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" } androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" } +androidx-test-monitor = { module = "androidx.test:monitor", version.ref = "androidx-test" } androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test", version.ref = "androidx-compose" } androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidx-compose" } androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-compose" } diff --git a/components/resources/library/build.gradle.kts b/components/resources/library/build.gradle.kts index c53dcba642..088f94323d 100644 --- a/components/resources/library/build.gradle.kts +++ b/components/resources/library/build.gradle.kts @@ -113,6 +113,10 @@ kotlin { } val androidMain by getting { dependsOn(jvmAndAndroidMain) + dependencies { + //it will be called only in android instrumented tests where the library should be available + compileOnly(libs.androidx.test.monitor) + } } val androidInstrumentedTest by getting { dependsOn(jvmAndAndroidTest) diff --git a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/AndroidContextProvider.kt b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/AndroidContextProvider.kt index df9cc46c4c..fafb548b84 100644 --- a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/AndroidContextProvider.kt +++ b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/AndroidContextProvider.kt @@ -10,8 +10,10 @@ import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode +import androidx.test.platform.app.InstrumentationRegistry internal val androidContext get() = AndroidContextProvider.ANDROID_CONTEXT +internal val androidInstrumentedContext get() = InstrumentationRegistry.getInstrumentation().context /** * The function configures the android context diff --git a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.android.kt b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.android.kt index d8e5741966..c58ab133e2 100644 --- a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.android.kt +++ b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.android.kt @@ -2,6 +2,7 @@ package org.jetbrains.compose.resources import android.content.res.AssetManager import android.net.Uri +import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.ProvidableCompositionLocal import java.io.FileNotFoundException @@ -16,6 +17,14 @@ internal actual fun getPlatformResourceReader(): ResourceReader = object : Resou context.assets } + private val instrumentedAssets: AssetManager? + get() = try { + androidInstrumentedContext.assets + } catch (e: NoClassDefFoundError) { + Log.d("ResourceReader", "Android Instrumentation context is not available.") + null + } + override suspend fun read(path: String): ByteArray { val resource = getResourceAsStream(path) return resource.use { input -> input.readBytes() } @@ -52,7 +61,7 @@ internal actual fun getPlatformResourceReader(): ResourceReader = object : Resou } override fun getUri(path: String): String { - val uri = if (assets.hasFile(path)) { + val uri = if (assets.hasFile(path) || instrumentedAssets.hasFile(path)) { Uri.parse("file:///android_asset/$path") } else { val classLoader = getClassLoader() @@ -66,8 +75,12 @@ internal actual fun getPlatformResourceReader(): ResourceReader = object : Resou return try { assets.open(path) } catch (e: FileNotFoundException) { - val classLoader = getClassLoader() - classLoader.getResourceAsStream(path) ?: throw MissingResourceException(path) + try { + instrumentedAssets.open(path) + } catch (e: FileNotFoundException) { + val classLoader = getClassLoader() + classLoader.getResourceAsStream(path) ?: throw MissingResourceException(path) + } } } @@ -75,7 +88,7 @@ internal actual fun getPlatformResourceReader(): ResourceReader = object : Resou return this.javaClass.classLoader ?: error("Cannot find class loader") } - private fun AssetManager.hasFile(path: String): Boolean { + private fun AssetManager?.hasFile(path: String): Boolean { var inputStream: InputStream? = null val result = try { inputStream = open(path) @@ -87,6 +100,9 @@ internal actual fun getPlatformResourceReader(): ResourceReader = object : Resou } return result } + + private fun AssetManager?.open(path: String): InputStream = + this?.open(path) ?: throw FileNotFoundException("Current AssetManager is null.") } internal actual val ProvidableCompositionLocal.currentOrPreview: ResourceReader diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidResources.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidResources.kt index 5b66539d01..432d598b25 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidResources.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidResources.kt @@ -1,7 +1,8 @@ package org.jetbrains.compose.resources import com.android.build.api.variant.AndroidComponentsExtension -import com.android.build.api.variant.Variant +import com.android.build.api.variant.Component +import com.android.build.api.variant.HasAndroidTest import com.android.build.gradle.internal.lint.AndroidLintAnalysisTask import com.android.build.gradle.internal.lint.LintModelWriterTask import org.gradle.api.DefaultTask @@ -10,23 +11,73 @@ import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.FileCollection import org.gradle.api.file.FileSystemOperations import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider import org.gradle.api.tasks.IgnoreEmptyDirectories +import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction import org.jetbrains.compose.internal.utils.registerTask import org.jetbrains.compose.internal.utils.uppercaseFirstChar import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget +import java.io.File import javax.inject.Inject -private fun Project.getAndroidVariantComposeResources( +internal fun Project.configureAndroidComposeResources(moduleResourceDir: Provider? = null) { + //copy all compose resources to android assets + val androidComponents = project.extensions.findByType(AndroidComponentsExtension::class.java) ?: return + androidComponents.onVariants { variant -> + configureGeneratedAndroidComponentAssets(variant, moduleResourceDir) + + if (variant is HasAndroidTest) { + variant.androidTest?.let { androidTest -> + configureGeneratedAndroidComponentAssets(androidTest, moduleResourceDir) + } + } + } +} + +private fun Project.configureGeneratedAndroidComponentAssets( + component: Component, + moduleResourceDir: Provider? +) { + val kotlinExtension = project.extensions.findByType(KotlinMultiplatformExtension::class.java) ?: return + val camelComponentName = component.name.uppercaseFirstChar() + val componentAssets = getAndroidComponentComposeResources(kotlinExtension, component.name) + logger.info("Configure ${component.name} resources for 'android' target") + + val copyComponentAssets = registerTask( + "copy${camelComponentName}ComposeResourcesToAndroidAssets" + ) { + from.set(componentAssets) + moduleResourceDir?.let { relativeResourcePlacement.set(it) } + } + + component.sources.assets?.addGeneratedSourceDirectory( + copyComponentAssets, + CopyResourcesToAndroidAssetsTask::outputDirectory + ) + tasks.configureEach { task -> + //fix agp task dependencies for AndroidStudio preview + if (task.name == "compile${camelComponentName}Sources") { + task.dependsOn(copyComponentAssets) + } + //fix linter task dependencies for `build` task + if (task is AndroidLintAnalysisTask || task is LintModelWriterTask) { + task.mustRunAfter(copyComponentAssets) + } + } +} + +private fun Project.getAndroidComponentComposeResources( kotlinExtension: KotlinMultiplatformExtension, - variant: Variant + componentName: String ): FileCollection = project.files({ kotlinExtension.targets.withType(KotlinAndroidTarget::class.java).flatMap { androidTarget -> androidTarget.compilations.flatMap { compilation -> - if (compilation.androidVariant.name == variant.name) { + if (compilation.androidVariant.name == componentName) { compilation.allKotlinSourceSets.map { kotlinSourceSet -> getPreparedComposeResourcesDir(kotlinSourceSet) } @@ -35,37 +86,6 @@ private fun Project.getAndroidVariantComposeResources( } }) -internal fun Project.configureAndroidComposeResources() { - //copy all compose resources to android assets - val androidComponents = project.extensions.findByType(AndroidComponentsExtension::class.java) ?: return - val kotlinExtension = project.extensions.findByType(KotlinMultiplatformExtension::class.java) ?: return - androidComponents.onVariants { variant -> - val camelVariantName = variant.name.uppercaseFirstChar() - val variantAssets = getAndroidVariantComposeResources(kotlinExtension, variant) - - val copyVariantAssets = registerTask( - "copy${camelVariantName}ComposeResourcesToAndroidAssets" - ) { - from.set(variantAssets) - } - - variant.sources.assets?.addGeneratedSourceDirectory( - copyVariantAssets, - CopyResourcesToAndroidAssetsTask::outputDirectory - ) - tasks.configureEach { task -> - //fix agp task dependencies for AndroidStudio preview - if (task.name == "compile${camelVariantName}Sources") { - task.dependsOn(copyVariantAssets) - } - //fix linter task dependencies for `build` task - if (task is AndroidLintAnalysisTask || task is LintModelWriterTask) { - task.mustRunAfter(copyVariantAssets) - } - } - } -} - //Copy task doesn't work with 'variant.sources?.assets?.addGeneratedSourceDirectory' API internal abstract class CopyResourcesToAndroidAssetsTask : DefaultTask() { @get:Inject @@ -75,6 +95,10 @@ internal abstract class CopyResourcesToAndroidAssetsTask : DefaultTask() { @get:IgnoreEmptyDirectories abstract val from: Property + @get:Input + @get:Optional + abstract val relativeResourcePlacement: Property + @get:OutputDirectory abstract val outputDirectory: DirectoryProperty @@ -83,7 +107,11 @@ internal abstract class CopyResourcesToAndroidAssetsTask : DefaultTask() { fileSystem.copy { it.includeEmptyDirs = false it.from(from) - it.into(outputDirectory) + if (relativeResourcePlacement.isPresent) { + it.into(outputDirectory.dir(relativeResourcePlacement.get().path)) + } else { + it.into(outputDirectory) + } } } } @@ -105,14 +133,3 @@ internal fun Project.fixAndroidLintTaskDependencies() { it.mustRunAfter(tasks.withType(GenerateResourceAccessorsTask::class.java)) } } - -internal fun Project.fixKgpAndroidPreviewTaskDependencies() { - val androidComponents = project.extensions.findByType(AndroidComponentsExtension::class.java) ?: return - androidComponents.onVariants { variant -> - tasks.configureEach { task -> - if (task.name == "compile${variant.name.uppercaseFirstChar()}Sources") { - task.dependsOn("${variant.name}AssetsCopyForAGP") - } - } - } -} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AssembleTargetResourcesTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AssembleTargetResourcesTask.kt new file mode 100644 index 0000000000..556b8e1c9e --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AssembleTargetResourcesTask.kt @@ -0,0 +1,56 @@ +package org.jetbrains.compose.resources + +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.DuplicatesStrategy +import org.gradle.api.file.FileSystemOperations +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault +import java.io.File +import javax.inject.Inject + +@DisableCachingByDefault(because = "There is no logic, just copy files") +internal abstract class AssembleTargetResourcesTask : DefaultTask() { + + @get:Inject + abstract val fileSystem: FileSystemOperations + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val resourceDirectories: ConfigurableFileCollection + + @get:Input + abstract val relativeResourcePlacement: Property + + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + @TaskAction + fun action() { + val outputDirectoryFile = outputDirectory.get().asFile + if (outputDirectoryFile.exists()) { + outputDirectoryFile.deleteRecursively() + } + outputDirectoryFile.mkdirs() + + fileSystem.copy { copy -> + resourceDirectories.files.forEach { dir -> + copy.from(dir) + } + copy.into(outputDirectoryFile.resolve(relativeResourcePlacement.get())) + copy.duplicatesStrategy = DuplicatesStrategy.INCLUDE + } + + if (outputDirectoryFile.listFiles()?.isEmpty() != false) { + // Output an empty directory for the zip task + outputDirectoryFile.resolve(relativeResourcePlacement.get()).mkdirs() + } + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ComposeResources.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ComposeResources.kt index 13255a7e06..e7f410f19a 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ComposeResources.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ComposeResources.kt @@ -2,20 +2,18 @@ package org.jetbrains.compose.resources import org.gradle.api.Project import org.gradle.api.provider.Provider -import org.gradle.api.tasks.SourceSet import org.gradle.util.GradleVersion import org.jetbrains.compose.desktop.application.internal.ComposeProperties import org.jetbrains.compose.internal.KOTLIN_JVM_PLUGIN_ID import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin -import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet import org.jetbrains.kotlin.gradle.plugin.extraProperties internal const val COMPOSE_RESOURCES_DIR = "composeResources" internal const val RES_GEN_DIR = "generated/compose/resourceGenerator" -private const val KMP_RES_EXT = "multiplatformResourcesPublication" +internal const val KMP_RES_EXT = "multiplatformResourcesPublication" private const val MIN_GRADLE_VERSION_FOR_KMP_RESOURCES = "7.6" private val androidPluginIds = listOf( "com.android.application", @@ -38,11 +36,7 @@ private fun Project.onKgpApplied(config: Provider, kgp: Kotl val kmpResourcesAreAvailable = !disableMultimoduleResources && hasKmpResources && currentGradleVersion >= minGradleVersion if (kmpResourcesAreAvailable) { - configureKmpResources(kotlinExtension, extraProperties.get(KMP_RES_EXT)!!, config) - onAgpApplied { - fixKgpAndroidPreviewTaskDependencies() - fixAndroidLintTaskDependencies() - } + configureMultimoduleResources(kotlinExtension, config) } else { if (!disableMultimoduleResources) { if (!hasKmpResources) logger.info( @@ -59,50 +53,21 @@ private fun Project.onKgpApplied(config: Provider, kgp: Kotl ) } - val commonMain = KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME - configureComposeResources(kotlinExtension, commonMain, config) - - onAgpApplied { - configureAndroidComposeResources() - fixAndroidLintTaskDependencies() - } + configureSinglemoduleResources(kotlinExtension, config) } configureSyncIosComposeResources(kotlinExtension) } -private fun Project.onAgpApplied(block: () -> Unit) { +internal fun Project.onKotlinJvmApplied(config: Provider) { + val kotlinExtension = project.extensions.getByType(KotlinJvmProjectExtension::class.java) + configureJvmOnlyResources(kotlinExtension, config) +} + +internal fun Project.onAgpApplied(block: () -> Unit) { androidPluginIds.forEach { pluginId -> plugins.withId(pluginId) { block() } } } - -private fun Project.onKotlinJvmApplied(config: Provider) { - val kotlinExtension = project.extensions.getByType(KotlinProjectExtension::class.java) - val main = SourceSet.MAIN_SOURCE_SET_NAME - configureComposeResources(kotlinExtension, main, config) -} - -private fun Project.configureComposeResources( - kotlinExtension: KotlinProjectExtension, - resClassSourceSetName: String, - config: Provider -) { - logger.info("Configure compose resources") - configureComposeResourcesGeneration(kotlinExtension, resClassSourceSetName, config, false) - - // mark prepared resources as sourceSet.resources - // 1) it automatically packs the resources to JVM jars - // 2) it configures the webpack to use the resources - // 3) for native targets we will use source set resources to pack them into the final app. see IosResources.kt - // 4) for the android it DOESN'T pack resources! we copy resources to assets in AndroidResources.kt - kotlinExtension.sourceSets.all { sourceSet -> - // the HACK is here because KGP copy androidMain java resources to Android target - // if the resources were registered in the androidMain source set before the target declaration - afterEvaluate { - sourceSet.resources.srcDirs(getPreparedComposeResourcesDir(sourceSet)) - } - } -} diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/KmpResources.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/KmpResources.kt deleted file mode 100644 index 5ace0c22a9..0000000000 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/KmpResources.kt +++ /dev/null @@ -1,95 +0,0 @@ -package org.jetbrains.compose.resources - -import org.gradle.api.Project -import org.gradle.api.provider.Provider -import org.jetbrains.kotlin.gradle.ComposeKotlinGradlePluginApi -import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension -import org.jetbrains.kotlin.gradle.plugin.* -import org.jetbrains.kotlin.gradle.plugin.mpp.* -import org.jetbrains.kotlin.gradle.plugin.mpp.resources.KotlinTargetResourcesPublication -import java.io.File - - -@OptIn(ComposeKotlinGradlePluginApi::class) -internal fun Project.configureKmpResources( - kotlinExtension: KotlinProjectExtension, - kmpResources: Any, - config: Provider -) { - kotlinExtension as KotlinMultiplatformExtension - kmpResources as KotlinTargetResourcesPublication - - logger.info("Configure KMP resources") - - val commonMain = KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME - configureComposeResourcesGeneration(kotlinExtension, commonMain, config, true) - - //configure KMP resources publishing for each supported target - kotlinExtension.targets - .matching { target -> kmpResources.canPublishResources(target) } - .all { target -> - logger.info("Configure resources publication for '${target.targetName}' target") - val packedResourceDir = config.getModuleResourcesDir(project) - - if (target !is KotlinAndroidTarget) { - kmpResources.publishResourcesAsKotlinComponent( - target, - { sourceSet -> - KotlinTargetResourcesPublication.ResourceRoot( - getPreparedComposeResourcesDir(sourceSet), - emptyList(), - emptyList() - ) - }, - packedResourceDir - ) - } else { - //for android target publish resources in assets - kmpResources.publishInAndroidAssets( - target, - { sourceSet -> - KotlinTargetResourcesPublication.ResourceRoot( - getPreparedComposeResourcesDir(sourceSet), - emptyList(), - emptyList() - ) - }, - packedResourceDir - ) - } - } - - //add all resolved resources for browser and native compilations - val platformsForSetupCompilation = listOf(KotlinPlatformType.native, KotlinPlatformType.js, KotlinPlatformType.wasm) - kotlinExtension.targets - .matching { target -> target.platformType in platformsForSetupCompilation } - .all { target: KotlinTarget -> - val allResources = kmpResources.resolveResources(target) - target.compilations.all { compilation -> - if (compilation.name == KotlinCompilation.MAIN_COMPILATION_NAME) { - configureResourcesForCompilation(compilation, allResources) - } - } - } -} - -/** - * Add resolved resources to a kotlin compilation to include it into a resulting platform artefact - * It is required for JS and Native targets. - * For JVM and Android it works automatically via jar files - */ -private fun Project.configureResourcesForCompilation( - compilation: KotlinCompilation<*>, - directoryWithAllResourcesForCompilation: Provider -) { - logger.info("Add all resolved resources to '${compilation.target.targetName}' target '${compilation.name}' compilation") - compilation.defaultSourceSet.resources.srcDir(directoryWithAllResourcesForCompilation) - - //JS packaging requires explicit dependency - if (compilation is KotlinJsCompilation) { - tasks.named(compilation.processResourcesTaskName).configure { processResourcesTask -> - processResourcesTask.dependsOn(directoryWithAllResourcesForCompilation) - } - } -} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/MultimoduleResources.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/MultimoduleResources.kt new file mode 100644 index 0000000000..eebd4df8b1 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/MultimoduleResources.kt @@ -0,0 +1,144 @@ +package org.jetbrains.compose.resources + +import org.gradle.api.Project +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.SourceSet +import org.jetbrains.compose.internal.utils.registerTask +import org.jetbrains.compose.internal.utils.uppercaseFirstChar +import org.jetbrains.kotlin.gradle.ComposeKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +import org.jetbrains.kotlin.gradle.plugin.extraProperties +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.resources.KotlinTargetResourcesPublication +import java.io.File + +//configure multi-module resources (with publishing and module isolation) +internal fun Project.configureMultimoduleResources( + kotlinExtension: KotlinMultiplatformExtension, + config: Provider +) { + logger.info("Configure multi-module compose resources") + + val commonMain = KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME + configureComposeResourcesGeneration(kotlinExtension, commonMain, config, true) + + val moduleIsolationDirectory = config.getModuleResourcesDir(project) + + val platformsForSkip = listOf( + KotlinPlatformType.common, KotlinPlatformType.androidJvm + ) + kotlinExtension.targets + .matching { target -> target.platformType !in platformsForSkip } + .all { target -> configureTargetResources(target, moduleIsolationDirectory) } + + //configure ANDROID resources + onAgpApplied { + configureAndroidComposeResources(moduleIsolationDirectory) + fixAndroidLintTaskDependencies() + } +} + +//configure java multi-module resources (with module isolation) +internal fun Project.configureJvmOnlyResources( + kotlinExtension: KotlinJvmProjectExtension, + config: Provider +) { + logger.info("Configure java-only compose resources") + + val main = SourceSet.MAIN_SOURCE_SET_NAME + configureComposeResourcesGeneration(kotlinExtension, main, config, true) + + val moduleIsolationDirectory = config.getModuleResourcesDir(project) + val javaTarget = kotlinExtension.target + + configureTargetResources(javaTarget, moduleIsolationDirectory) +} + +private fun Project.configureTargetResources( + target: KotlinTarget, + moduleIsolationDirectory: Provider +) { + target.compilations.all { compilation -> + logger.info("Configure ${compilation.name} resources for '${target.targetName}' target") + val compilationResources = files({ + compilation.allKotlinSourceSets.map { sourceSet -> getPreparedComposeResourcesDir(sourceSet) } + }) + val assembleResTask = registerTask( + name = "assemble${target.targetName.uppercaseFirstChar()}${compilation.name.uppercaseFirstChar()}Resources" + ) { + resourceDirectories.setFrom(compilationResources) + relativeResourcePlacement.set(moduleIsolationDirectory) + outputDirectory.set( + layout.buildDirectory.dir( + "$RES_GEN_DIR/assembledResources/${target.targetName}${compilation.name.uppercaseFirstChar()}" + ) + ) + } + val allCompilationResources = assembleResTask.flatMap { it.outputDirectory.asFile } + + if ( + target.platformType in platformsForSetupKmpResources + && compilation.name == KotlinCompilation.MAIN_COMPILATION_NAME + ) { + configureKmpResources(compilation, allCompilationResources) + } else { + configureResourcesForCompilation(compilation, allCompilationResources) + } + } +} + +private val platformsForSetupKmpResources = listOf( + KotlinPlatformType.native, KotlinPlatformType.js, KotlinPlatformType.wasm +) + +@OptIn(ComposeKotlinGradlePluginApi::class) +private fun Project.configureKmpResources( + compilation: KotlinCompilation<*>, + allCompilationResources: Provider +) { + require(compilation.platformType in platformsForSetupKmpResources) + val kmpResources = extraProperties.get(KMP_RES_EXT) as KotlinTargetResourcesPublication + + //For Native/Js/Wasm main resources: + // 1) we have to configure new Kotlin component publication + // 2) we have to collect all transitive main resources + + //TODO temporary API misuse. will be changed on the KMP side + //https://youtrack.jetbrains.com/issue/KT-70909 + val target = compilation.target + val kmpResourceRoot = KotlinTargetResourcesPublication.ResourceRoot( + allCompilationResources, + emptyList(), + emptyList() + ) + val kmpEmptyPath = provider { File("") } + logger.info("Configure KMP component publication for '${compilation.target.targetName}'") + kmpResources.publishResourcesAsKotlinComponent( + target, + { kmpResourceRoot }, + kmpEmptyPath + ) + + val allResources = kmpResources.resolveResources(target) + logger.info("Collect resolved ${compilation.name} resources for '${compilation.target.targetName}'") + configureResourcesForCompilation(compilation, allResources) +} + +private fun Project.configureResourcesForCompilation( + compilation: KotlinCompilation<*>, + directoryWithAllResourcesForCompilation: Provider +) { + compilation.defaultSourceSet.resources.srcDir(directoryWithAllResourcesForCompilation) + + //JS packaging requires explicit dependency + if (compilation is KotlinJsCompilation) { + tasks.named(compilation.processResourcesTaskName).configure { processResourcesTask -> + processResourcesTask.dependsOn(directoryWithAllResourcesForCompilation) + } + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/SinglemoduleResources.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/SinglemoduleResources.kt new file mode 100644 index 0000000000..7d4bda4f2f --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/SinglemoduleResources.kt @@ -0,0 +1,34 @@ +package org.jetbrains.compose.resources + +import org.gradle.api.Project +import org.gradle.api.provider.Provider +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet + +//configure single-module resources (no publishing, no module isolation) +internal fun Project.configureSinglemoduleResources( + kotlinExtension: KotlinMultiplatformExtension, + config: Provider +) { + logger.info("Configure single-module compose resources") + val commonMain = KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME + configureComposeResourcesGeneration(kotlinExtension, commonMain, config, false) + + // mark prepared resources as sourceSet.resources + // 1) it automatically packs the resources to JVM jars + // 2) it configures the webpack to use the resources + // 3) for native targets we will use source set resources to pack them into the final app. see IosResources.kt + // 4) for the android it DOESN'T pack resources! we copy resources to assets in AndroidResources.kt + kotlinExtension.sourceSets.all { sourceSet -> + // the HACK is here because KGP copy androidMain java resources to Android target + // if the resources were registered in the androidMain source set before the target declaration + afterEvaluate { + sourceSet.resources.srcDirs(getPreparedComposeResourcesDir(sourceSet)) + } + } + + onAgpApplied { + configureAndroidComposeResources() + fixAndroidLintTaskDependencies() + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt index 95b599bfcb..346daa8756 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt @@ -254,7 +254,7 @@ class ResourcesTest : GradlePluginTestBase() { } gradle(":cmplib:publishAllPublicationsToMavenRepository").checks { - check.logContains("Configure KMP resources") + check.logContains("Configure multi-module compose resources") val resDir = file("cmplib/src/commonMain/composeResources") val resourcesFiles = resDir.walkTopDown() @@ -317,16 +317,12 @@ class ResourcesTest : GradlePluginTestBase() { @Test fun testDisableMultimoduleResourcesWithNewKotlin() { - val environment = defaultTestEnvironment.copy( - kotlinVersion = "2.0.0-RC2" - ) - - with(testProject("misc/kmpResourcePublication", environment)) { + with(testProject("misc/kmpResourcePublication")) { file("gradle.properties").modify { content -> content + "\n" + ComposeProperties.DISABLE_MULTIMODULE_RESOURCES + "=true" } gradle(":cmplib:build").checks { - check.logContains("Configure compose resources") + check.logContains("Configure single-module compose resources") } } } @@ -388,10 +384,10 @@ class ResourcesTest : GradlePluginTestBase() { .getConvertedResources(commonResourcesDir, repackDir) gradle("build").checks { - check.taskSuccessful(":demoDebugAssetsCopyForAGP") - check.taskSuccessful(":demoReleaseAssetsCopyForAGP") - check.taskSuccessful(":fullDebugAssetsCopyForAGP") - check.taskSuccessful(":fullReleaseAssetsCopyForAGP") + check.taskSuccessful(":copyDemoDebugComposeResourcesToAndroidAssets") + check.taskSuccessful(":copyDemoReleaseComposeResourcesToAndroidAssets") + check.taskSuccessful(":copyFullDebugComposeResourcesToAndroidAssets") + check.taskSuccessful(":copyFullReleaseComposeResourcesToAndroidAssets") getAndroidApk("demo", "debug", "Resources-Test").let { apk -> checkResourcesZip(apk, commonResourcesFiles, true) @@ -523,6 +519,7 @@ class ResourcesTest : GradlePluginTestBase() { @Test fun testJvmOnlyProject(): Unit = with(testProject("misc/jvmOnlyResources")) { gradle("jar").checks { + check.logContains("Configure java-only compose resources") assertDirectoriesContentEquals( file("build/generated/compose/resourceGenerator/kotlin"), file("expected") @@ -683,4 +680,22 @@ class ResourcesTest : GradlePluginTestBase() { } } } + + @Test + fun checkTestResources() { + with(testProject("misc/testResources")) { + gradle("check").checks { + check.logContains("Configure main resources for 'desktop' target") + check.logContains("Configure test resources for 'desktop' target") + check.logContains("Configure main resources for 'iosX64' target") + check.logContains("Configure test resources for 'iosX64' target") + check.logContains("Configure main resources for 'iosArm64' target") + 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.taskSuccessful(":desktopTest") + } + } + } } \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/commonResClass/me/app/jvmonlyresources/generated/resources/Res.kt b/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/commonResClass/me/app/jvmonlyresources/generated/resources/Res.kt index b532771763..27b5aed178 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/commonResClass/me/app/jvmonlyresources/generated/resources/Res.kt +++ b/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/commonResClass/me/app/jvmonlyresources/generated/resources/Res.kt @@ -22,7 +22,8 @@ internal object Res { * @return The content of the file as a byte array. */ @ExperimentalResourceApi - public suspend fun readBytes(path: String): ByteArray = readResourceBytes("" + path) + public suspend fun readBytes(path: String): ByteArray = + readResourceBytes("composeResources/me.app.jvmonlyresources.generated.resources/" + path) /** * Returns the URI string of the resource file at the specified path. @@ -33,7 +34,8 @@ internal object Res { * @return The URI string of the file. */ @ExperimentalResourceApi - public fun getUri(path: String): String = getResourceUri("" + path) + public fun getUri(path: String): String = + getResourceUri("composeResources/me.app.jvmonlyresources.generated.resources/" + path) public object drawable diff --git a/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/mainResourceAccessors/me/app/jvmonlyresources/generated/resources/Drawable0.main.kt b/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/mainResourceAccessors/me/app/jvmonlyresources/generated/resources/Drawable0.main.kt index 21c01fd922..2326a5dd63 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/mainResourceAccessors/me/app/jvmonlyresources/generated/resources/Drawable0.main.kt +++ b/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/mainResourceAccessors/me/app/jvmonlyresources/generated/resources/Drawable0.main.kt @@ -24,6 +24,7 @@ internal val Res.drawable.vector: DrawableResource private fun init_vector(): DrawableResource = org.jetbrains.compose.resources.DrawableResource( "drawable:vector", setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/vector.xml", -1, -1), + org.jetbrains.compose.resources.ResourceItem(setOf(), + "composeResources/me.app.jvmonlyresources.generated.resources/drawable/vector.xml", -1, -1), ) ) diff --git a/gradle-plugins/compose/src/test/test-projects/misc/testResources/build.gradle.kts b/gradle-plugins/compose/src/test/test-projects/misc/testResources/build.gradle.kts new file mode 100644 index 0000000000..6ded4832b3 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/testResources/build.gradle.kts @@ -0,0 +1,39 @@ +import org.jetbrains.compose.ExperimentalComposeLibrary + +plugins { + kotlin("multiplatform") + kotlin("plugin.compose") + id("org.jetbrains.compose") +} + +group = "app.group" + +kotlin { + jvm("desktop") + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain { + dependencies { + implementation(compose.runtime) + implementation(compose.material) + implementation(compose.components.resources) + } + } + commonTest { + dependencies { + implementation(kotlin("test")) + @OptIn(ExperimentalComposeLibrary::class) + implementation(compose.uiTest) + } + } + val desktopMain by getting { + dependencies { + implementation(compose.desktop.currentOs) + } + } + } +} diff --git a/gradle-plugins/compose/src/test/test-projects/misc/testResources/gradle.properties b/gradle-plugins/compose/src/test/test-projects/misc/testResources/gradle.properties new file mode 100644 index 0000000000..f4d7109663 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/testResources/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8096M +android.useAndroidX=true +org.jetbrains.compose.experimental.jscanvas.enabled=true \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/testResources/settings.gradle.kts b/gradle-plugins/compose/src/test/test-projects/misc/testResources/settings.gradle.kts new file mode 100644 index 0000000000..292f6220df --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/testResources/settings.gradle.kts @@ -0,0 +1,24 @@ +rootProject.name = "Resources-Test" +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + google() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + } + plugins { + id("com.android.application").version("AGP_VERSION_PLACEHOLDER") + id("org.jetbrains.kotlin.multiplatform").version("KOTLIN_VERSION_PLACEHOLDER") + id("org.jetbrains.kotlin.plugin.compose").version("KOTLIN_VERSION_PLACEHOLDER") + id("org.jetbrains.compose").version("COMPOSE_GRADLE_PLUGIN_VERSION_PLACEHOLDER") + } +} +dependencyResolutionManagement { + repositories { + mavenLocal() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + mavenCentral() + gradlePluginPortal() + google() + } +} diff --git a/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/commonMain/composeResources/files/common.txt b/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/commonMain/composeResources/files/common.txt new file mode 100644 index 0000000000..3986d02b36 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/commonMain/composeResources/files/common.txt @@ -0,0 +1 @@ +common 777 \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/commonMain/composeResources/values/strings.xml b/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/commonMain/composeResources/values/strings.xml new file mode 100644 index 0000000000..cef354128e --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/commonMain/composeResources/values/strings.xml @@ -0,0 +1,3 @@ + + Compose Resources App + diff --git a/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/commonMain/kotlin/App.kt b/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/commonMain/kotlin/App.kt new file mode 100644 index 0000000000..ee1f26fd98 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/commonMain/kotlin/App.kt @@ -0,0 +1,7 @@ +import androidx.compose.material.Text +import androidx.compose.runtime.Composable + +@Composable +fun App() { + Text("app") +} diff --git a/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/commonTest/composeResources/files/data.txt b/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/commonTest/composeResources/files/data.txt new file mode 100644 index 0000000000..6a537b5b36 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/commonTest/composeResources/files/data.txt @@ -0,0 +1 @@ +1234567890 \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/commonTest/composeResources/values/strings.xml b/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/commonTest/composeResources/values/strings.xml new file mode 100644 index 0000000000..b34f38cdbf --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/commonTest/composeResources/values/strings.xml @@ -0,0 +1,3 @@ + + Common test + diff --git a/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/commonTest/kotlin/CommonUiTest.kt b/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/commonTest/kotlin/CommonUiTest.kt new file mode 100644 index 0000000000..45c2cf78c8 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/commonTest/kotlin/CommonUiTest.kt @@ -0,0 +1,34 @@ +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.runComposeUiTest +import app.group.resources_test.generated.resources.Res +import app.group.resources_test.generated.resources.app_name +import app.group.resources_test.generated.resources.test_string +import kotlinx.coroutines.test.runTest +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.stringResource +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +@OptIn(ExperimentalTestApi::class, ExperimentalResourceApi::class) +class CommonUiTest { + + @Test + fun checkTestResources() = runComposeUiTest { + setContent { + val mainStr = stringResource(Res.string.app_name) + val testStr = stringResource(Res.string.test_string) + assertEquals("Compose Resources App", mainStr) + assertEquals("Common test", testStr) + } + } + + @Test + fun checkTestFileResource() = runTest { + val commonFile = Res.readBytes("files/common.txt").decodeToString() + assertEquals("common 777", commonFile) + val testFile = Res.readBytes("files/data.txt").decodeToString() + assertEquals("1234567890", testFile) + } + +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/desktopMain/composeResources/values/desktop_strings.xml b/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/desktopMain/composeResources/values/desktop_strings.xml new file mode 100644 index 0000000000..e3ea685b42 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/desktopMain/composeResources/values/desktop_strings.xml @@ -0,0 +1,3 @@ + + Desktop string + \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/desktopTest/composeResources/values/desktop_strings.xml b/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/desktopTest/composeResources/values/desktop_strings.xml new file mode 100644 index 0000000000..43e8cbf6c5 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/desktopTest/composeResources/values/desktop_strings.xml @@ -0,0 +1,3 @@ + + Desktop test string + diff --git a/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/desktopTest/kotlin/DesktopUiTest.kt b/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/desktopTest/kotlin/DesktopUiTest.kt new file mode 100644 index 0000000000..77a9435ba6 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/testResources/src/desktopTest/kotlin/DesktopUiTest.kt @@ -0,0 +1,29 @@ +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.runComposeUiTest +import app.group.resources_test.generated.resources.Res +import app.group.resources_test.generated.resources.app_name +import app.group.resources_test.generated.resources.desktop_str +import app.group.resources_test.generated.resources.desktop_test_str +import app.group.resources_test.generated.resources.test_string +import org.jetbrains.compose.resources.stringResource +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalTestApi::class) +class DesktopUiTest { + + @Test + fun checkTestResources() = runComposeUiTest { + setContent { + val mainStr = stringResource(Res.string.app_name) + val testStr = stringResource(Res.string.test_string) + val desktopMainStr = stringResource(Res.string.desktop_str) + val desktopTestStr = stringResource(Res.string.desktop_test_str) + assertEquals("Compose Resources App", mainStr) + assertEquals("Common test", testStr) + assertEquals("Desktop string", desktopMainStr) + assertEquals("Desktop test string", desktopTestStr) + } + } + +} \ No newline at end of file