diff --git a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt index 298c280d52..86675ce6a5 100644 --- a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt +++ b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt @@ -19,7 +19,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import org.jetbrains.compose.resources.readResourceBytes +import components.resources.demo.generated.resources.Res @Composable fun FileRes(paddingValues: PaddingValues) { @@ -28,7 +28,7 @@ fun FileRes(paddingValues: PaddingValues) { ) { Text( modifier = Modifier.padding(16.dp), - text = "File: 'composeRes/drawable/droid_icon.xml'", + text = "File: 'files/icon.xml'", style = MaterialTheme.typography.titleLarge ) OutlinedCard( @@ -38,7 +38,7 @@ fun FileRes(paddingValues: PaddingValues) { ) { var bytes by remember { mutableStateOf(ByteArray(0)) } LaunchedEffect(Unit) { - bytes = readResourceBytes("composeRes/drawable/droid_icon.xml") + bytes = Res.readBytes("files/icon.xml") } Text( modifier = Modifier.padding(8.dp).height(200.dp).verticalScroll(rememberScrollState()), @@ -54,7 +54,7 @@ fun FileRes(paddingValues: PaddingValues) { mutableStateOf(ByteArray(0)) } LaunchedEffect(Unit) { - bytes = readResourceBytes("composeRes/drawable/droid_icon.xml") + bytes = Res.readFileBytes("files/icon.xml") } Text(bytes.decodeToString()) """.trimIndent() diff --git a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt index 3bcf9312ac..4bb1111a1a 100644 --- a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt +++ b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt @@ -33,7 +33,7 @@ fun StringRes(paddingValues: PaddingValues) { ) { Text( modifier = Modifier.padding(16.dp), - text = "composeRes/values/strings.xml", + text = "values/strings.xml", style = MaterialTheme.typography.titleLarge ) OutlinedCard( @@ -43,7 +43,7 @@ fun StringRes(paddingValues: PaddingValues) { ) { var bytes by remember { mutableStateOf(ByteArray(0)) } LaunchedEffect(Unit) { - bytes = readResourceBytes("composeRes/values/strings.xml") + bytes = Res.readBytes("values/strings.xml") } Text( modifier = Modifier.padding(8.dp), diff --git a/components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/compose.png b/components/resources/demo/shared/src/commonMain/resources/composeResources/drawable/compose.png similarity index 100% rename from components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/compose.png rename to components/resources/demo/shared/src/commonMain/resources/composeResources/drawable/compose.png diff --git a/components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/droid_icon.xml b/components/resources/demo/shared/src/commonMain/resources/composeResources/drawable/droid_icon.xml similarity index 100% rename from components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/droid_icon.xml rename to components/resources/demo/shared/src/commonMain/resources/composeResources/drawable/droid_icon.xml diff --git a/components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/insta_icon.xml b/components/resources/demo/shared/src/commonMain/resources/composeResources/drawable/insta_icon.xml similarity index 100% rename from components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/insta_icon.xml rename to components/resources/demo/shared/src/commonMain/resources/composeResources/drawable/insta_icon.xml diff --git a/components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/land.webp b/components/resources/demo/shared/src/commonMain/resources/composeResources/drawable/land.webp similarity index 100% rename from components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/land.webp rename to components/resources/demo/shared/src/commonMain/resources/composeResources/drawable/land.webp diff --git a/components/resources/demo/shared/src/commonMain/resources/composeResources/files/icon.xml b/components/resources/demo/shared/src/commonMain/resources/composeResources/files/icon.xml new file mode 100644 index 0000000000..1f6bb29060 --- /dev/null +++ b/components/resources/demo/shared/src/commonMain/resources/composeResources/files/icon.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/components/resources/demo/shared/src/commonMain/resources/composeRes/font/font_awesome.otf b/components/resources/demo/shared/src/commonMain/resources/composeResources/font/font_awesome.otf similarity index 100% rename from components/resources/demo/shared/src/commonMain/resources/composeRes/font/font_awesome.otf rename to components/resources/demo/shared/src/commonMain/resources/composeResources/font/font_awesome.otf diff --git a/components/resources/demo/shared/src/commonMain/resources/composeRes/values/strings.xml b/components/resources/demo/shared/src/commonMain/resources/composeResources/values/strings.xml similarity index 100% rename from components/resources/demo/shared/src/commonMain/resources/composeRes/values/strings.xml rename to components/resources/demo/shared/src/commonMain/resources/composeResources/values/strings.xml diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.kt index f9fe27a347..38e9e371f3 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.kt @@ -5,6 +5,9 @@ import androidx.compose.runtime.Immutable @RequiresOptIn("This API is experimental and is likely to change in the future.") annotation class ExperimentalResourceApi +@RequiresOptIn("This is internal API of the Compose gradle plugin.") +annotation class InternalResourceApi + /** * Represents a resource with an ID and a set of resource items. * diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt index f437519314..c8d7f9ac1a 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt @@ -11,7 +11,7 @@ class MissingResourceException(path: String) : Exception("Missing resource with * @param path The path of the file to read in the resource's directory. * @return The content of the file as a byte array. */ -@ExperimentalResourceApi +@InternalResourceApi expect suspend fun readResourceBytes(path: String): ByteArray internal interface ResourceReader { @@ -19,7 +19,7 @@ internal interface ResourceReader { } internal val DefaultResourceReader: ResourceReader = object : ResourceReader { - @OptIn(ExperimentalResourceApi::class) + @OptIn(InternalResourceApi::class) override suspend fun read(path: String): ByteArray = readResourceBytes(path) } diff --git a/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.ios.kt b/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.ios.kt index 6865a3bad9..3eabef4cd1 100644 --- a/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.ios.kt +++ b/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.ios.kt @@ -6,7 +6,7 @@ import platform.Foundation.NSBundle import platform.Foundation.NSFileManager import platform.posix.memcpy -@ExperimentalResourceApi +@OptIn(ExperimentalResourceApi::class) actual suspend fun readResourceBytes(path: String): ByteArray { val fileManager = NSFileManager.defaultManager() // todo: support fallback path at bundle root? diff --git a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.js.kt b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.js.kt index 4897a4eb10..5ae6dce1b2 100644 --- a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.js.kt +++ b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.js.kt @@ -8,7 +8,7 @@ import org.khronos.webgl.Int8Array private fun ArrayBuffer.toByteArray(): ByteArray = Int8Array(this, 0, byteLength).unsafeCast() -@ExperimentalResourceApi +@OptIn(ExperimentalResourceApi::class) actual suspend fun readResourceBytes(path: String): ByteArray { val resPath = WebResourcesConfiguration.getResourcePath(path) val response = window.fetch(resPath).await() diff --git a/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.jvmAndAndroid.kt b/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.jvmAndAndroid.kt index c817770744..3360bcaf47 100644 --- a/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.jvmAndAndroid.kt +++ b/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.jvmAndAndroid.kt @@ -2,7 +2,7 @@ package org.jetbrains.compose.resources private object JvmResourceReader -@ExperimentalResourceApi +@OptIn(ExperimentalResourceApi::class) actual suspend fun readResourceBytes(path: String): ByteArray { val classLoader = Thread.currentThread().contextClassLoader ?: JvmResourceReader.javaClass.classLoader val resource = classLoader.getResourceAsStream(path) ?: throw MissingResourceException(path) diff --git a/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.macos.kt b/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.macos.kt index d87d05a161..23f2255aaa 100644 --- a/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.macos.kt +++ b/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.macos.kt @@ -5,7 +5,7 @@ import kotlinx.cinterop.usePinned import platform.Foundation.NSFileManager import platform.posix.memcpy -@ExperimentalResourceApi +@OptIn(ExperimentalResourceApi::class) actual suspend fun readResourceBytes(path: String): ByteArray { val currentDirectoryPath = NSFileManager.defaultManager().currentDirectoryPath val contentsAtPath = NSFileManager.defaultManager().run { diff --git a/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.wasmJs.kt b/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.wasmJs.kt index 72513cc983..2f6577cc41 100644 --- a/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.wasmJs.kt +++ b/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.wasmJs.kt @@ -14,7 +14,7 @@ import kotlin.wasm.unsafe.withScopedMemoryAllocator * @param path The path of the file to read in the resource's directory. * @return The content of the file as a byte array. */ -@ExperimentalResourceApi +@OptIn(ExperimentalResourceApi::class) actual suspend fun readResourceBytes(path: String): ByteArray { val resPath = WebResourcesConfiguration.getResourcePath(path) val response = window.fetch(resPath).await() diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt index 862c87d3e4..7eb8465444 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt @@ -15,7 +15,7 @@ import kotlin.io.path.relativeTo abstract class GenerateResClassTask : DefaultTask() { @get:Input abstract val packageName: Property - + @get:InputDirectory @get:PathSensitive(PathSensitivity.RELATIVE) abstract val resDir: DirectoryProperty @@ -34,14 +34,20 @@ abstract class GenerateResClassTask : DefaultTask() { logger.info("Generate resources for $rootResDir") //get first level dirs - val dirs = rootResDir.listFiles { f -> f.isDirectory }.orEmpty() + val dirs = rootResDir.listFiles().orEmpty() + + dirs.forEach { f -> + if (!f.isDirectory) { + error("${f.name} is not directory! Raw files should be placed in '${rootResDir.name}/files' directory.") + } + } //type -> id -> resource item val resources: Map>> = dirs .flatMap { dir -> - dir.listFiles { f -> !f.isDirectory } + dir.listFiles() .orEmpty() - .mapNotNull { it.fileToResourceItems(rootResDir.parentFile.toPath()) } + .mapNotNull { it.fileToResourceItems(rootResDir.toPath()) } .flatten() } .groupBy { it.type } @@ -61,7 +67,6 @@ abstract class GenerateResClassTask : DefaultTask() { relativeTo: Path ): List? { val file = this - if (file.isDirectory) return null val dirName = file.parentFile.name ?: return null val typeAndQualifiers = dirName.split("-") if (typeAndQualifiers.isEmpty()) return null @@ -70,20 +75,25 @@ abstract class GenerateResClassTask : DefaultTask() { val qualifiers = typeAndQualifiers.takeLast(typeAndQualifiers.size - 1) val path = file.toPath().relativeTo(relativeTo) - return if (typeString == "values" && file.name.equals("strings.xml", true)) { + + if (typeString == "string") { + error("Forbidden directory name '$dirName'! String resources should be declared in 'values/strings.xml'.") + } + + if (typeString == "files") { + if (qualifiers.isNotEmpty()) error("The 'files' directory doesn't support qualifiers: '$dirName'.") + return null + } + + if (typeString == "values" && file.name.equals("strings.xml", true)) { val stringIds = getStringIds(file) - stringIds.map { strId -> + return stringIds.map { strId -> ResourceItem(ResourceType.STRING, qualifiers, strId.asUnderscoredIdentifier(), path) } - } else { - val type = try { - ResourceType.fromString(typeString) - } catch (e: Exception) { - logger.warn("w: Skip file: $path\n${e.message}") - return null - } - listOf(ResourceItem(type, qualifiers, file.nameWithoutExtension.asUnderscoredIdentifier(), path)) } + + val type = ResourceType.fromString(typeString) + return listOf(ResourceItem(type, qualifiers, file.nameWithoutExtension.asUnderscoredIdentifier(), path)) } private val stringTypeNames = listOf("string", "string-array") diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesGenerator.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesGenerator.kt index 5f8a75bc07..5323355095 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesGenerator.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesGenerator.kt @@ -6,72 +6,75 @@ import org.jetbrains.compose.ComposeExtension import org.jetbrains.compose.ComposePlugin import org.jetbrains.compose.ExperimentalComposeLibrary import org.jetbrains.compose.desktop.application.internal.ComposeProperties +import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet import java.io.File -private const val COMPOSE_RESOURCES_DIR = "composeRes" +internal const val COMPOSE_RESOURCES_DIR = "composeResources" private const val RES_GEN_DIR = "generated/compose/resourceGenerator" internal fun Project.configureResourceGenerator() { - val kotlinExtension = project.extensions.getByType(KotlinProjectExtension::class.java) - val commonSourceSet = kotlinExtension.sourceSets.findByName(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME) ?: return - val commonResourcesDir = provider { commonSourceSet.resources.sourceDirectories.first() } + pluginManager.withPlugin(KOTLIN_MPP_PLUGIN_ID) { + val kotlinExtension = project.extensions.getByType(KotlinProjectExtension::class.java) + val commonSourceSet = kotlinExtension.sourceSets.findByName(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME) ?: return@withPlugin + val commonResourcesDir = provider { commonSourceSet.resources.sourceDirectories.first() } - val packageName = provider { - buildString { - val group = project.group.toString().asUnderscoredIdentifier() - append(group) - if (group.isNotEmpty()) append(".") - append("generated.resources") + val packageName = provider { + buildString { + val group = project.group.toString().asUnderscoredIdentifier() + append(group) + if (group.isNotEmpty()) append(".") + append("generated.resources") + } } - } - fun buildDir(path: String) = layout.dir(layout.buildDirectory.map { File(it.asFile, path) }) + fun buildDir(path: String) = layout.dir(layout.buildDirectory.map { File(it.asFile, path) }) - val resDir = layout.dir(commonResourcesDir.map { it.resolve(COMPOSE_RESOURCES_DIR) }) + val resDir = layout.dir(commonResourcesDir.map { it.resolve(COMPOSE_RESOURCES_DIR) }) - //lazy check a dependency on the Resources library - val shouldGenerateResourceAccessors: Provider = provider { - if (ComposeProperties.alwaysGenerateResourceAccessors(providers).get()) { - true - } else { - configurations - .getByName(commonSourceSet.implementationConfigurationName) - .allDependencies.any { dep -> - val depStringNotation = dep.let { "${it.group}:${it.name}:${it.version}" } - depStringNotation == ComposePlugin.CommonComponentsDependencies.resources - } + //lazy check a dependency on the Resources library + val shouldGenerateResourceAccessors: Provider = provider { + if (ComposeProperties.alwaysGenerateResourceAccessors(providers).get()) { + true + } else { + configurations + .getByName(commonSourceSet.implementationConfigurationName) + .allDependencies.any { dep -> + val depStringNotation = dep.let { "${it.group}:${it.name}:${it.version}" } + depStringNotation == ComposePlugin.CommonComponentsDependencies.resources + } + } } - } - val genTask = tasks.register( - "generateComposeResClass", - GenerateResClassTask::class.java - ) { - it.packageName.set(packageName) - it.resDir.set(resDir) - it.codeDir.set(buildDir("$RES_GEN_DIR/kotlin")) - it.onlyIf { shouldGenerateResourceAccessors.get() } - } + val genTask = tasks.register( + "generateComposeResClass", + GenerateResClassTask::class.java + ) { + it.packageName.set(packageName) + it.resDir.set(resDir) + it.codeDir.set(buildDir("$RES_GEN_DIR/kotlin")) + it.onlyIf { shouldGenerateResourceAccessors.get() } + } - //register generated source set - commonSourceSet.kotlin.srcDir(genTask.map { it.codeDir }) + //register generated source set + commonSourceSet.kotlin.srcDir(genTask.map { it.codeDir }) - //setup task execution during IDE import - tasks.configureEach { - if (it.name == "prepareKotlinIdeaImport") { - it.dependsOn(genTask) + //setup task execution during IDE import + tasks.configureEach { + if (it.name == "prepareKotlinIdeaImport") { + it.dependsOn(genTask) + } } - } - val androidExtension = project.extensions.findByName("android") - if (androidExtension != null) { - configureAndroidResources( - commonResourcesDir, - buildDir("$RES_GEN_DIR/androidFonts").map { it.asFile }, - shouldGenerateResourceAccessors - ) + val androidExtension = project.extensions.findByName("android") + if (androidExtension != null) { + configureAndroidResources( + commonResourcesDir, + buildDir("$RES_GEN_DIR/androidFonts").map { it.asFile }, + shouldGenerateResourceAccessors + ) + } } } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt index d168d05484..8d3768ae39 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt @@ -104,6 +104,31 @@ internal fun getResFileSpec( ): FileSpec = FileSpec.builder(packageName, "Res").apply { addType(TypeSpec.objectBuilder("Res").apply { addModifiers(KModifier.INTERNAL) + + //readFileBytes + val readResourceBytes = MemberName("org.jetbrains.compose.resources", "readResourceBytes") + addFunction( + FunSpec.builder("readBytes") + .addAnnotation( + AnnotationSpec.builder(ClassName("kotlin", "OptIn")) + .addMember("org.jetbrains.compose.resources.InternalResourceApi::class") + .build() + ) + .addKdoc(""" + Reads the content of the resource file at the specified path and returns it as a byte array. + + Example: `val bytes = Res.readBytes("files/key.bin")` + + @param path The path of the file to read in the compose resource's directory. + @return The content of the file as a byte array. + """.trimIndent()) + .addParameter("path", String::class) + .addModifiers(KModifier.SUSPEND) + .returns(ByteArray::class) + .addStatement("return %M(\"$COMPOSE_RESOURCES_DIR/\" + path)", readResourceBytes) //todo: add module ID here + .build() + ) + val types = resources.map { (type, idToResources) -> getResourceTypeObject(type, idToResources) }.sortedBy { it.name } @@ -138,7 +163,7 @@ private fun TypeSpec.Builder.addResourceProperty(name: String, items: List