Browse Source
1) The PR adds a support test resources in Compose multiplatform projects. 2) The PR adds a support multi-module resources in JVM-only projects. Fixes https://youtrack.jetbrains.com/issue/CMP-1470 Fixes https://youtrack.jetbrains.com/issue/CMP-5963 ## Release Notes ### Features - Resources - Added support of test resources in Compose Multiplatform projects - Added support of multi-module resources in JVM-only projectsrelease/1.7.0-beta01
Konstantin
3 months ago
committed by
GitHub
25 changed files with 518 additions and 206 deletions
@ -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<File> |
||||
|
||||
@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() |
||||
} |
||||
} |
||||
} |
@ -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<ResourcesExtension> |
||||
) { |
||||
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<File> |
||||
) { |
||||
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) |
||||
} |
||||
} |
||||
} |
@ -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<ResourcesExtension> |
||||
) { |
||||
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<ResourcesExtension> |
||||
) { |
||||
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<File> |
||||
) { |
||||
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<AssembleTargetResourcesTask>( |
||||
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<File> |
||||
) { |
||||
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<File> |
||||
) { |
||||
compilation.defaultSourceSet.resources.srcDir(directoryWithAllResourcesForCompilation) |
||||
|
||||
//JS packaging requires explicit dependency |
||||
if (compilation is KotlinJsCompilation) { |
||||
tasks.named(compilation.processResourcesTaskName).configure { processResourcesTask -> |
||||
processResourcesTask.dependsOn(directoryWithAllResourcesForCompilation) |
||||
} |
||||
} |
||||
} |
@ -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<ResourcesExtension> |
||||
) { |
||||
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() |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx8096M |
||||
android.useAndroidX=true |
||||
org.jetbrains.compose.experimental.jscanvas.enabled=true |
@ -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() |
||||
} |
||||
} |
@ -0,0 +1 @@
|
||||
common 777 |
@ -0,0 +1,3 @@
|
||||
<resources> |
||||
<string name="app_name">Compose Resources App</string> |
||||
</resources> |
@ -0,0 +1,7 @@
|
||||
import androidx.compose.material.Text |
||||
import androidx.compose.runtime.Composable |
||||
|
||||
@Composable |
||||
fun App() { |
||||
Text("app") |
||||
} |
@ -0,0 +1 @@
|
||||
1234567890 |
@ -0,0 +1,3 @@
|
||||
<resources> |
||||
<string name="test_string">Common test</string> |
||||
</resources> |
@ -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) |
||||
} |
||||
|
||||
} |
@ -0,0 +1,3 @@
|
||||
<resources> |
||||
<string name="desktop_str">Desktop string</string> |
||||
</resources> |
@ -0,0 +1,3 @@
|
||||
<resources> |
||||
<string name="desktop_test_str">Desktop test string</string> |
||||
</resources> |
@ -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) |
||||
} |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue