From 5d9dfde149fe6a8c7e57a8b41bfa25ecbf208114 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Wed, 3 Apr 2024 16:07:39 +0200 Subject: [PATCH] XML resource optimizations (#4559) Users noticed if an app has big a `string.xml` file it affects the app startup time: https://github.com/JetBrains/compose-multiplatform/issues/4537 The problem is slow XML parsing. Possible ways for optimization: 1) inject text resources direct to the source code 2) convert XMLs to an optimized format to read it faster We selected the second way because texts injected to source code have several problems: - strict limitations on text size - increase compilation and analysation time - affects a class loader and GC > Note: android resources do the same and converts xml values to own `resources.arsc` file Things was done in the PR: 1) added support any XML files in the `values` directory 2) **[BREAKING CHANGE]** added `Res.array` accessor for string-array resources 3) in a final app there won't be original `values*/*.xml` files. There will be converted `values*/*.cvr` files. 4) generated code points on string resources as file -> offset+size 5) string resource cache is by item now (it was by the full xml file before) 6) implemented random access to read CVR files 7) tasks for syncing ios resources to a final app were seriously refactored to support generated resources (CVR files) 8) restriction for 3-party resources plugin were deleted 9) Gradle property `compose.resources.always.generate.accessors` was deleted. It was for internal needs only. Fixes https://github.com/JetBrains/compose-multiplatform/issues/4537 --- .../resources/demo/shared/build.gradle.kts | 5 + .../resources/demo/shared/gradle.properties | 1 - .../resources/demo/shared/StringRes.kt | 25 +- components/resources/library/build.gradle.kts | 1 + .../resources/FontResources.android.kt | 2 +- .../resources/ResourceEnvironment.android.kt | 1 - .../resources/ResourceReader.android.kt | 39 +- .../compose/resources/FontResources.kt | 4 +- .../compose/resources/ImageResources.kt | 10 +- .../resources/PluralStringResources.kt | 121 ++++++ .../jetbrains/compose/resources/Resource.kt | 6 +- .../compose/resources/ResourceEnvironment.kt | 13 +- .../compose/resources/ResourceReader.kt | 10 +- .../compose/resources/StringArrayResources.kt | 63 +++ .../compose/resources/StringResources.kt | 273 +------------ .../compose/resources/StringResourcesUtils.kt | 76 ++++ .../compose/resources/ComposeResourceTest.kt | 60 +-- .../compose/resources/ResourceTest.kt | 46 +-- .../resources/TestComposeEnvironment.kt | 1 - .../compose/resources/TestResourceReader.kt | 5 + .../jetbrains/compose/resources/TestUtils.kt | 28 +- .../src/commonTest/resources/strings.cvr | 9 + .../src/commonTest/resources/strings.xml | 23 -- .../resources/ResourceEnvironment.desktop.kt | 1 - .../resources/ResourceReader.desktop.kt | 31 +- .../resources/ResourceEnvironment.ios.kt | 1 - .../compose/resources/ResourceReader.ios.kt | 41 +- .../resources/ResourceEnvironment.js.kt | 1 - .../compose/resources/ResourceReader.js.kt | 35 +- .../resources/ResourceEnvironment.macos.kt | 1 - .../compose/resources/ResourceReader.macos.kt | 51 ++- .../compose/resources/FontResources.skiko.kt | 2 +- .../resources/ResourceEnvironment.wasmJs.kt | 1 - .../resources/ResourceReader.wasmJs.kt | 72 ++-- .../org/jetbrains/compose/ComposePlugin.kt | 14 +- .../internal/ComposeProjectProperties.kt | 7 +- .../internal/utils/cocoapodsDslHelpers.kt | 26 -- .../internal/utils/kotlinNativeTargetUtils.kt | 22 - .../uikit/tasks/AbstractComposeIosTask.kt | 53 --- .../compose/resources/AndroidResources.kt | 100 +++++ .../compose/resources/ComposeResources.kt | 110 +++++ .../compose/resources/GenerateResClassTask.kt | 61 +-- ...ourcesSpec.kt => GeneratedResClassSpec.kt} | 38 +- .../compose/resources/IosResources.kt | 132 ++++++ .../compose/resources/IosResourcesTasks.kt | 148 +++++++ .../compose/resources/KmpResources.kt | 123 ++++++ .../resources/PrepareComposeResources.kt | 235 +++++++++++ .../compose/resources/ResClassGeneration.kt | 65 +++ ...{ResourcesExtension.kt => ResourcesDSL.kt} | 18 +- .../compose/resources/ResourcesGenerator.kt | 381 ------------------ .../resources/ios/IosTargetResources.kt | 56 --- .../ios/SyncComposeResourcesForIosTask.kt | 108 ----- .../ios/configureSyncIosResources.kt | 265 ------------ .../ios/determineIosKonanTargetsFromEnv.kt | 38 -- .../tests/integration/GradlePluginTest.kt | 22 - .../test/tests/integration/ResourcesTest.kt | 32 +- .../tests/unit/TestEscapedResourceSymbols.kt | 16 + .../expected-open-res/Drawable0.kt | 63 +-- .../expected-open-res/Font0.kt | 10 +- .../expected-open-res/Plurals0.kt | 2 +- .../commonResources/expected-open-res/Res.kt | 2 + .../expected-open-res/String0.kt | 87 ++-- .../commonResources/expected/Drawable0.kt | 63 +-- .../misc/commonResources/expected/Font0.kt | 4 +- .../misc/commonResources/expected/Plurals0.kt | 2 +- .../misc/commonResources/expected/Res.kt | 2 + .../misc/commonResources/expected/String0.kt | 87 ++-- .../composeResources/values/strings.xml | 5 - .../misc/emptyResources/expected/Res.kt | 2 + .../misc/iosMokoResources/build.gradle | 53 --- .../misc/iosMokoResources/gradle.properties | 1 - .../misc/iosMokoResources/settings.gradle | 27 -- .../src/commonMain/kotlin/App.kt | 10 - .../jvmOnlyResources/expected/Drawable0.kt | 4 +- .../misc/jvmOnlyResources/expected/Res.kt | 2 + 75 files changed, 1747 insertions(+), 1808 deletions(-) delete mode 100644 components/resources/demo/shared/gradle.properties create mode 100644 components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PluralStringResources.kt create mode 100644 components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringArrayResources.kt create mode 100644 components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResourcesUtils.kt create mode 100644 components/resources/library/src/commonTest/resources/strings.cvr delete mode 100644 components/resources/library/src/commonTest/resources/strings.xml delete mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/cocoapodsDslHelpers.kt delete mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/kotlinNativeTargetUtils.kt delete mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/tasks/AbstractComposeIosTask.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidResources.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ComposeResources.kt rename gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/{ResourcesSpec.kt => GeneratedResClassSpec.kt} (89%) create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/IosResources.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/IosResourcesTasks.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/KmpResources.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/PrepareComposeResources.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResClassGeneration.kt rename gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/{ResourcesExtension.kt => ResourcesDSL.kt} (61%) delete mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesGenerator.kt delete mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/IosTargetResources.kt delete mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/SyncComposeResourcesForIosTask.kt delete mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/configureSyncIosResources.kt delete mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/determineIosKonanTargetsFromEnv.kt create mode 100644 gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/unit/TestEscapedResourceSymbols.kt delete mode 100644 gradle-plugins/compose/src/test/test-projects/misc/iosMokoResources/build.gradle delete mode 100644 gradle-plugins/compose/src/test/test-projects/misc/iosMokoResources/gradle.properties delete mode 100644 gradle-plugins/compose/src/test/test-projects/misc/iosMokoResources/settings.gradle delete mode 100644 gradle-plugins/compose/src/test/test-projects/misc/iosMokoResources/src/commonMain/kotlin/App.kt diff --git a/components/resources/demo/shared/build.gradle.kts b/components/resources/demo/shared/build.gradle.kts index fd6510bd45..dfa29aca24 100644 --- a/components/resources/demo/shared/build.gradle.kts +++ b/components/resources/demo/shared/build.gradle.kts @@ -83,3 +83,8 @@ android { compose.experimental { web.application {} } + +//because the dependency on the compose library is a project dependency +compose.resources { + generateResClass = always +} diff --git a/components/resources/demo/shared/gradle.properties b/components/resources/demo/shared/gradle.properties deleted file mode 100644 index d6e0f63843..0000000000 --- a/components/resources/demo/shared/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -compose.resources.always.generate.accessors=true \ No newline at end of file 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 5ae0091647..03001ad7a6 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 @@ -18,27 +18,6 @@ fun StringRes(paddingValues: PaddingValues) { Column( modifier = Modifier.padding(paddingValues).verticalScroll(rememberScrollState()) ) { - Text( - modifier = Modifier.padding(16.dp), - text = "values/strings.xml", - style = MaterialTheme.typography.titleLarge - ) - OutlinedCard( - modifier = Modifier.padding(horizontal = 16.dp).fillMaxWidth(), - shape = RoundedCornerShape(4.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) - ) { - var bytes by remember { mutableStateOf(ByteArray(0)) } - LaunchedEffect(Unit) { - bytes = Res.readBytes("values/strings.xml") - } - Text( - modifier = Modifier.padding(8.dp), - text = bytes.decodeToString(), - color = MaterialTheme.colorScheme.onPrimaryContainer, - softWrap = false - ) - } OutlinedTextField( modifier = Modifier.padding(16.dp).fillMaxWidth(), value = stringResource(Res.string.app_name), @@ -89,9 +68,9 @@ fun StringRes(paddingValues: PaddingValues) { ) OutlinedTextField( modifier = Modifier.padding(16.dp).fillMaxWidth(), - value = stringArrayResource(Res.string.str_arr).toString(), + value = stringArrayResource(Res.array.str_arr).toString(), onValueChange = {}, - label = { Text("Text(stringArrayResource(Res.string.str_arr).toString())") }, + label = { Text("Text(stringArrayResource(Res.array.str_arr).toString())") }, enabled = false, colors = TextFieldDefaults.colors( disabledTextColor = MaterialTheme.colorScheme.onSurface, diff --git a/components/resources/library/build.gradle.kts b/components/resources/library/build.gradle.kts index 1bfe97d25d..6cb66172d9 100644 --- a/components/resources/library/build.gradle.kts +++ b/components/resources/library/build.gradle.kts @@ -52,6 +52,7 @@ kotlin { optIn("kotlin.RequiresOptIn") optIn("kotlinx.cinterop.ExperimentalForeignApi") optIn("kotlin.experimental.ExperimentalNativeApi") + optIn("org.jetbrains.compose.resources.InternalResourceApi") } } diff --git a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt index 46212bb3dc..854b66580b 100644 --- a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt +++ b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt @@ -9,6 +9,6 @@ import androidx.compose.ui.text.font.* @Composable actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): Font { val environment = LocalComposeEnvironment.current.rememberEnvironment() - val path = remember(environment) { resource.getPathByEnvironment(environment) } + val path = remember(environment) { resource.getResourceItemByEnvironment(environment).path } return Font(path, LocalContext.current.assets, weight, style) } \ No newline at end of file diff --git a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.android.kt b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.android.kt index 205de0e47c..cc4c816ab3 100644 --- a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.android.kt +++ b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.android.kt @@ -4,7 +4,6 @@ import android.content.res.Configuration import android.content.res.Resources import java.util.* -@OptIn(InternalResourceApi::class) internal actual fun getSystemEnvironment(): ResourceEnvironment { val locale = Locale.getDefault() val configuration = Resources.getSystem().configuration 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 b742de7b9f..e457804f18 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 @@ -1,18 +1,33 @@ package org.jetbrains.compose.resources import java.io.File +import java.io.InputStream -private object AndroidResourceReader +internal actual fun getPlatformResourceReader(): ResourceReader = object : ResourceReader { + override suspend fun read(path: String): ByteArray { + val resource = getResourceAsStream(path) + return resource.readBytes() + } -@OptIn(ExperimentalResourceApi::class) -@InternalResourceApi -actual suspend fun readResourceBytes(path: String): ByteArray { - val classLoader = Thread.currentThread().contextClassLoader ?: AndroidResourceReader.javaClass.classLoader - val resource = classLoader.getResourceAsStream(path) ?: run { - //try to find a font in the android assets - if (File(path).parentFile?.name.orEmpty().startsWith("font")) { - classLoader.getResourceAsStream("assets/$path") - } else null - } ?: throw MissingResourceException(path) - return resource.readBytes() + override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray { + val resource = getResourceAsStream(path) + val result = ByteArray(size.toInt()) + resource.use { input -> + input.skip(offset) + input.read(result, 0, size.toInt()) + } + return result + } + + @OptIn(ExperimentalResourceApi::class) + private fun getResourceAsStream(path: String): InputStream { + val classLoader = Thread.currentThread().contextClassLoader ?: this.javaClass.classLoader + val resource = classLoader.getResourceAsStream(path) ?: run { + //try to find a font in the android assets + if (File(path).parentFile?.name.orEmpty().startsWith("font")) { + classLoader.getResourceAsStream("assets/$path") + } else null + } ?: throw MissingResourceException(path) + return resource + } } \ No newline at end of file diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt index 9aee02b875..0054858dcf 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.text.font.* * * @see Resource */ -@OptIn(InternalResourceApi::class) @ExperimentalResourceApi @Immutable class FontResource @@ -24,11 +23,10 @@ class FontResource * @param path The path to the font resource file. * @return A new [FontResource] object. */ -@OptIn(InternalResourceApi::class) @ExperimentalResourceApi fun FontResource(path: String): FontResource = FontResource( id = "FontResource:$path", - items = setOf(ResourceItem(emptySet(), path)) + items = setOf(ResourceItem(emptySet(), path, -1, -1)) ) /** diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt index 7916ab5238..9f2db79983 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt @@ -20,7 +20,6 @@ import org.jetbrains.compose.resources.vector.xmldom.Element * @param id The unique identifier of the drawable resource. * @param items The set of resource items associated with the image resource. */ -@OptIn(InternalResourceApi::class) @ExperimentalResourceApi @Immutable class DrawableResource @@ -32,11 +31,10 @@ class DrawableResource * @param path The path of the drawable resource. * @return An [DrawableResource] object. */ -@OptIn(InternalResourceApi::class) @ExperimentalResourceApi fun DrawableResource(path: String): DrawableResource = DrawableResource( id = "DrawableResource:$path", - items = setOf(ResourceItem(emptySet(), path)) + items = setOf(ResourceItem(emptySet(), path, -1, -1)) ) /** @@ -50,7 +48,7 @@ fun DrawableResource(path: String): DrawableResource = DrawableResource( @Composable fun painterResource(resource: DrawableResource): Painter { val environment = LocalComposeEnvironment.current.rememberEnvironment() - val filePath = remember(resource, environment) { resource.getPathByEnvironment(environment) } + val filePath = remember(resource, environment) { resource.getResourceItemByEnvironment(environment).path } val isXml = filePath.endsWith(".xml", true) if (isXml) { return rememberVectorPainter(vectorResource(resource)) @@ -72,7 +70,7 @@ private val emptyImageBitmap: ImageBitmap by lazy { ImageBitmap(1, 1) } fun imageResource(resource: DrawableResource): ImageBitmap { val resourceReader = LocalResourceReader.current val imageBitmap by rememberResourceState(resource, { emptyImageBitmap }) { env -> - val path = resource.getPathByEnvironment(env) + val path = resource.getResourceItemByEnvironment(env).path val cached = loadImage(path, resourceReader) { ImageCache.Bitmap(it.toImageBitmap()) } as ImageCache.Bitmap @@ -97,7 +95,7 @@ fun vectorResource(resource: DrawableResource): ImageVector { val resourceReader = LocalResourceReader.current val density = LocalDensity.current val imageVector by rememberResourceState(resource, { emptyImageVector }) { env -> - val path = resource.getPathByEnvironment(env) + val path = resource.getResourceItemByEnvironment(env).path val cached = loadImage(path, resourceReader) { ImageCache.Vector(it.toXmlElement().toImageVector(density)) } as ImageCache.Vector diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PluralStringResources.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PluralStringResources.kt new file mode 100644 index 0000000000..477e1ba3ba --- /dev/null +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PluralStringResources.kt @@ -0,0 +1,121 @@ +package org.jetbrains.compose.resources + +import androidx.compose.runtime.* +import org.jetbrains.compose.resources.plural.PluralCategory +import org.jetbrains.compose.resources.plural.PluralRuleList + +/** + * Represents a quantity string resource in the application. + * + * @param id The unique identifier of the resource. + * @param key The key used to retrieve the string resource. + * @param items The set of resource items associated with the string resource. + */ +@ExperimentalResourceApi +@Immutable +class PluralStringResource +@InternalResourceApi constructor(id: String, val key: String, items: Set) : Resource(id, items) + +/** + * Retrieves the string for the pluralization for the given quantity using the specified quantity string resource. + * + * @param resource The quantity string resource to be used. + * @param quantity The quantity of the pluralization to use. + * @return The retrieved string resource. + * + * @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file. + */ +@ExperimentalResourceApi +@Composable +fun pluralStringResource(resource: PluralStringResource, quantity: Int): String { + val resourceReader = LocalResourceReader.current + val pluralStr by rememberResourceState(resource, quantity, { "" }) { env -> + loadPluralString(resource, quantity, resourceReader, env) + } + return pluralStr +} + +/** + * Loads a string using the specified string resource. + * + * @param resource The string resource to be used. + * @param quantity The quantity of the pluralization to use. + * @return The loaded string resource. + * + * @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file. + */ +@ExperimentalResourceApi +suspend fun getPluralString(resource: PluralStringResource, quantity: Int): String = + loadPluralString(resource, quantity, DefaultResourceReader, getResourceEnvironment()) + +@OptIn(InternalResourceApi::class, ExperimentalResourceApi::class) +private suspend fun loadPluralString( + resource: PluralStringResource, + quantity: Int, + resourceReader: ResourceReader, + environment: ResourceEnvironment +): String { + val resourceItem = resource.getResourceItemByEnvironment(environment) + val item = getStringItem(resourceItem, resourceReader) as StringItem.Plurals + val pluralRuleList = PluralRuleList.getInstance( + environment.language, + environment.region, + ) + val pluralCategory = pluralRuleList.getCategory(quantity) + val str = item.items[pluralCategory] + ?: item.items[PluralCategory.OTHER] + ?: error("Quantity string ID=`${resource.key}` does not have the pluralization $pluralCategory for quantity $quantity!") + return str +} + +/** + * Retrieves the string for the pluralization for the given quantity using the specified quantity string resource. + * + * @param resource The quantity string resource to be used. + * @param quantity The quantity of the pluralization to use. + * @param formatArgs The arguments to be inserted into the formatted string. + * @return The retrieved string resource. + * + * @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file. + */ +@ExperimentalResourceApi +@Composable +fun pluralStringResource(resource: PluralStringResource, quantity: Int, vararg formatArgs: Any): String { + val resourceReader = LocalResourceReader.current + val args = formatArgs.map { it.toString() } + val pluralStr by rememberResourceState(resource, quantity, args, { "" }) { env -> + loadPluralString(resource, quantity, args, resourceReader, env) + } + return pluralStr +} + +/** + * Loads a string using the specified string resource. + * + * @param resource The string resource to be used. + * @param quantity The quantity of the pluralization to use. + * @param formatArgs The arguments to be inserted into the formatted string. + * @return The loaded string resource. + * + * @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file. + */ +@ExperimentalResourceApi +suspend fun getPluralString(resource: PluralStringResource, quantity: Int, vararg formatArgs: Any): String = + loadPluralString( + resource, quantity, + formatArgs.map { it.toString() }, + DefaultResourceReader, + getResourceEnvironment(), + ) + +@OptIn(ExperimentalResourceApi::class) +private suspend fun loadPluralString( + resource: PluralStringResource, + quantity: Int, + args: List, + resourceReader: ResourceReader, + environment: ResourceEnvironment +): String { + val str = loadPluralString(resource, quantity, resourceReader, environment) + return str.replaceWithArgs(args) +} 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 c9074f65fb..e752cb8422 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 @@ -40,10 +40,14 @@ sealed class Resource * * @property qualifiers The qualifiers of the resource item. * @property path The path of the resource item. + * @property offset The offset in bytes of the resource in the file. '-1' means the resource is whole file + * @property size The size in bytes of the resource in the file. '-1' means the resource is whole file */ @InternalResourceApi @Immutable data class ResourceItem( internal val qualifiers: Set, - internal val path: String + internal val path: String, + internal val offset: Long, + internal val size: Long, ) diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.kt index 561abf3745..b39ce64153 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.intl.Locale -@OptIn(InternalResourceApi::class) internal data class ResourceEnvironment( val language: LanguageQualifier, val region: RegionQualifier, @@ -18,7 +17,6 @@ internal interface ComposeEnvironment { fun rememberEnvironment(): ResourceEnvironment } -@OptIn(InternalResourceApi::class) internal val DefaultComposeEnvironment = object : ComposeEnvironment { @Composable override fun rememberEnvironment(): ResourceEnvironment { @@ -51,17 +49,17 @@ internal expect fun getSystemEnvironment(): ResourceEnvironment internal var getResourceEnvironment = ::getSystemEnvironment @OptIn(InternalResourceApi::class, ExperimentalResourceApi::class) -internal fun Resource.getPathByEnvironment(environment: ResourceEnvironment): String { +internal fun Resource.getResourceItemByEnvironment(environment: ResourceEnvironment): ResourceItem { //Priority of environments: https://developer.android.com/guide/topics/resources/providing-resources#table2 items.toList() .filterBy(environment.language) - .also { if (it.size == 1) return it.first().path } + .also { if (it.size == 1) return it.first() } .filterBy(environment.region) - .also { if (it.size == 1) return it.first().path } + .also { if (it.size == 1) return it.first() } .filterBy(environment.theme) - .also { if (it.size == 1) return it.first().path } + .also { if (it.size == 1) return it.first() } .filterBy(environment.density) - .also { if (it.size == 1) return it.first().path } + .also { if (it.size == 1) return it.first() } .let { items -> if (items.isEmpty()) { error("Resource with ID='$id' not found") @@ -71,7 +69,6 @@ internal fun Resource.getPathByEnvironment(environment: ResourceEnvironment): St } } -@OptIn(InternalResourceApi::class) private fun List.filterBy(qualifier: Qualifier): List { //Android has a slightly different algorithm, //but it provides the same result: https://developer.android.com/guide/topics/resources/providing-resources#BestMatch 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 c8d7f9ac1a..adacab3229 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 @@ -12,16 +12,16 @@ class MissingResourceException(path: String) : Exception("Missing resource with * @return The content of the file as a byte array. */ @InternalResourceApi -expect suspend fun readResourceBytes(path: String): ByteArray +suspend fun readResourceBytes(path: String): ByteArray = DefaultResourceReader.read(path) internal interface ResourceReader { suspend fun read(path: String): ByteArray + suspend fun readPart(path: String, offset: Long, size: Long): ByteArray } -internal val DefaultResourceReader: ResourceReader = object : ResourceReader { - @OptIn(InternalResourceApi::class) - override suspend fun read(path: String): ByteArray = readResourceBytes(path) -} +internal expect fun getPlatformResourceReader(): ResourceReader + +internal val DefaultResourceReader = getPlatformResourceReader() //ResourceReader provider will be overridden for tests internal val LocalResourceReader = staticCompositionLocalOf { DefaultResourceReader } diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringArrayResources.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringArrayResources.kt new file mode 100644 index 0000000000..2ddfe5a897 --- /dev/null +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringArrayResources.kt @@ -0,0 +1,63 @@ +package org.jetbrains.compose.resources + +import androidx.compose.runtime.* +import kotlinx.coroutines.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.jetbrains.compose.resources.plural.PluralCategory +import org.jetbrains.compose.resources.plural.PluralRuleList +import org.jetbrains.compose.resources.vector.xmldom.Element +import org.jetbrains.compose.resources.vector.xmldom.NodeList + +/** + * Represents a string array resource in the application. + * + * @param id The unique identifier of the resource. + * @param key The key used to retrieve the string array resource. + * @param items The set of resource items associated with the string array resource. + */ +@ExperimentalResourceApi +@Immutable +class StringArrayResource +@InternalResourceApi constructor(id: String, val key: String, items: Set) : Resource(id, items) + +/** + * Retrieves a list of strings using the specified string array resource. + * + * @param resource The string array resource to be used. + * @return A list of strings representing the items in the string array. + * + * @throws IllegalStateException if the string array with the given ID is not found. + */ +@ExperimentalResourceApi +@Composable +fun stringArrayResource(resource: StringArrayResource): List { + val resourceReader = LocalResourceReader.current + val array by rememberResourceState(resource, { emptyList() }) { env -> + loadStringArray(resource, resourceReader, env) + } + return array +} + +/** + * Loads a list of strings using the specified string array resource. + * + * @param resource The string array resource to be used. + * @return A list of strings representing the items in the string array. + * + * @throws IllegalStateException if the string array with the given ID is not found. + */ +@ExperimentalResourceApi +suspend fun getStringArray(resource: StringArrayResource): List = + loadStringArray(resource, DefaultResourceReader, getResourceEnvironment()) + +@OptIn(ExperimentalResourceApi::class, InternalResourceApi::class) +private suspend fun loadStringArray( + resource: StringArrayResource, + resourceReader: ResourceReader, + environment: ResourceEnvironment +): List { + val resourceItem = resource.getResourceItemByEnvironment(environment) + val item = getStringItem(resourceItem, resourceReader) as StringItem.Array + return item.items +} diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt index 00055d584f..dab3241d2f 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt @@ -1,19 +1,6 @@ package org.jetbrains.compose.resources import androidx.compose.runtime.* -import kotlinx.coroutines.* -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import org.jetbrains.compose.resources.plural.PluralCategory -import org.jetbrains.compose.resources.plural.PluralRuleList -import org.jetbrains.compose.resources.vector.xmldom.Element -import org.jetbrains.compose.resources.vector.xmldom.NodeList - -private val SimpleStringFormatRegex = Regex("""%(\d)\$[ds]""") - -private fun String.replaceWithArgs(args: List) = SimpleStringFormatRegex.replace(this) { matchResult -> - args[matchResult.groupValues[1].toInt() - 1] -} /** * Represents a string resource in the application. @@ -22,79 +9,11 @@ private fun String.replaceWithArgs(args: List) = SimpleStringFormatRegex * @param key The key used to retrieve the string resource. * @param items The set of resource items associated with the string resource. */ -@OptIn(InternalResourceApi::class) @ExperimentalResourceApi @Immutable class StringResource @InternalResourceApi constructor(id: String, val key: String, items: Set) : Resource(id, items) -/** - * Represents a quantity string resource in the application. - * - * @param id The unique identifier of the resource. - * @param key The key used to retrieve the string resource. - * @param items The set of resource items associated with the string resource. - */ -@OptIn(InternalResourceApi::class) -@ExperimentalResourceApi -@Immutable -class PluralStringResource -@InternalResourceApi constructor(id: String, val key: String, items: Set) : Resource(id, items) - -private sealed interface StringItem { - data class Value(val text: String) : StringItem - data class Plurals(val items: Map) : StringItem - data class Array(val items: List) : StringItem -} - -private val stringsCacheMutex = Mutex() -private val parsedStringsCache = mutableMapOf>>() - -//@TestOnly -internal fun dropStringsCache() { - parsedStringsCache.clear() -} - -private suspend fun getParsedStrings( - path: String, - resourceReader: ResourceReader -): Map = coroutineScope { - val deferred = stringsCacheMutex.withLock { - parsedStringsCache.getOrPut(path) { - //LAZY - to free the mutex lock as fast as possible - async(start = CoroutineStart.LAZY) { - parseStringXml(path, resourceReader) - } - } - } - deferred.await() -} - -private suspend fun parseStringXml(path: String, resourceReader: ResourceReader): Map { - val nodes = resourceReader.read(path).toXmlElement().childNodes - val strings = nodes.getElementsWithName("string").associate { element -> - val rawString = element.textContent.orEmpty() - element.getAttribute("name") to StringItem.Value(handleSpecialCharacters(rawString)) - } - val plurals = nodes.getElementsWithName("plurals").associate { pluralElement -> - val items = pluralElement.childNodes.getElementsWithName("item").mapNotNull { element -> - val pluralCategory = PluralCategory.fromString( - element.getAttribute("quantity"), - ) ?: return@mapNotNull null - pluralCategory to handleSpecialCharacters(element.textContent.orEmpty()) - } - pluralElement.getAttribute("name") to StringItem.Plurals(items.toMap()) - } - val arrays = nodes.getElementsWithName("string-array").associate { arrayElement -> - val items = arrayElement.childNodes.getElementsWithName("item").map { element -> - val rawString = element.textContent.orEmpty() - handleSpecialCharacters(rawString) - } - arrayElement.getAttribute("name") to StringItem.Array(items) - } - return strings + plurals + arrays -} - /** * Retrieves a string using the specified string resource. * @@ -125,16 +44,14 @@ fun stringResource(resource: StringResource): String { suspend fun getString(resource: StringResource): String = loadString(resource, DefaultResourceReader, getResourceEnvironment()) -@OptIn(ExperimentalResourceApi::class) +@OptIn(ExperimentalResourceApi::class, InternalResourceApi::class) private suspend fun loadString( resource: StringResource, resourceReader: ResourceReader, environment: ResourceEnvironment ): String { - val path = resource.getPathByEnvironment(environment) - val keyToValue = getParsedStrings(path, resourceReader) - val item = keyToValue[resource.key] as? StringItem.Value - ?: error("String ID=`${resource.key}` is not found!") + val resourceItem = resource.getResourceItemByEnvironment(environment) + val item = getStringItem(resourceItem, resourceReader) as StringItem.Value return item.text } @@ -185,187 +102,3 @@ private suspend fun loadString( val str = loadString(resource, resourceReader, environment) return str.replaceWithArgs(args) } - -/** - * Retrieves the string for the pluralization for the given quantity using the specified quantity string resource. - * - * @param resource The quantity string resource to be used. - * @param quantity The quantity of the pluralization to use. - * @return The retrieved string resource. - * - * @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file. - */ -@ExperimentalResourceApi -@Composable -fun pluralStringResource(resource: PluralStringResource, quantity: Int): String { - val resourceReader = LocalResourceReader.current - val pluralStr by rememberResourceState(resource, quantity, { "" }) { env -> - loadPluralString(resource, quantity, resourceReader, env) - } - return pluralStr -} - -/** - * Loads a string using the specified string resource. - * - * @param resource The string resource to be used. - * @param quantity The quantity of the pluralization to use. - * @return The loaded string resource. - * - * @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file. - */ -@ExperimentalResourceApi -suspend fun getPluralString(resource: PluralStringResource, quantity: Int): String = - loadPluralString(resource, quantity, DefaultResourceReader, getResourceEnvironment()) - -@OptIn(InternalResourceApi::class, ExperimentalResourceApi::class) -private suspend fun loadPluralString( - resource: PluralStringResource, - quantity: Int, - resourceReader: ResourceReader, - environment: ResourceEnvironment -): String { - val path = resource.getPathByEnvironment(environment) - val keyToValue = getParsedStrings(path, resourceReader) - val item = keyToValue[resource.key] as? StringItem.Plurals - ?: error("Quantity string ID=`${resource.key}` is not found!") - val pluralRuleList = PluralRuleList.getInstance( - environment.language, - environment.region, - ) - val pluralCategory = pluralRuleList.getCategory(quantity) - val str = item.items[pluralCategory] - ?: item.items[PluralCategory.OTHER] - ?: error("Quantity string ID=`${resource.key}` does not have the pluralization $pluralCategory for quantity $quantity!") - return str -} - -/** - * Retrieves the string for the pluralization for the given quantity using the specified quantity string resource. - * - * @param resource The quantity string resource to be used. - * @param quantity The quantity of the pluralization to use. - * @param formatArgs The arguments to be inserted into the formatted string. - * @return The retrieved string resource. - * - * @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file. - */ -@ExperimentalResourceApi -@Composable -fun pluralStringResource(resource: PluralStringResource, quantity: Int, vararg formatArgs: Any): String { - val resourceReader = LocalResourceReader.current - val args = formatArgs.map { it.toString() } - val pluralStr by rememberResourceState(resource, quantity, args, { "" }) { env -> - loadPluralString(resource, quantity, args, resourceReader, env) - } - return pluralStr -} - -/** - * Loads a string using the specified string resource. - * - * @param resource The string resource to be used. - * @param quantity The quantity of the pluralization to use. - * @param formatArgs The arguments to be inserted into the formatted string. - * @return The loaded string resource. - * - * @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file. - */ -@ExperimentalResourceApi -suspend fun getPluralString(resource: PluralStringResource, quantity: Int, vararg formatArgs: Any): String = - loadPluralString( - resource, quantity, - formatArgs.map { it.toString() }, - DefaultResourceReader, - getResourceEnvironment(), - ) - -@OptIn(ExperimentalResourceApi::class) -private suspend fun loadPluralString( - resource: PluralStringResource, - quantity: Int, - args: List, - resourceReader: ResourceReader, - environment: ResourceEnvironment -): String { - val str = loadPluralString(resource, quantity, resourceReader, environment) - return str.replaceWithArgs(args) -} - -/** - * Retrieves a list of strings using the specified string array resource. - * - * @param resource The string resource to be used. - * @return A list of strings representing the items in the string array. - * - * @throws IllegalStateException if the string array with the given ID is not found. - */ -@ExperimentalResourceApi -@Composable -fun stringArrayResource(resource: StringResource): List { - val resourceReader = LocalResourceReader.current - val array by rememberResourceState(resource, { emptyList() }) { env -> - loadStringArray(resource, resourceReader, env) - } - return array -} - -/** - * Loads a list of strings using the specified string array resource. - * - * @param resource The string resource to be used. - * @return A list of strings representing the items in the string array. - * - * @throws IllegalStateException if the string array with the given ID is not found. - */ -@ExperimentalResourceApi -suspend fun getStringArray(resource: StringResource): List = - loadStringArray(resource, DefaultResourceReader, getResourceEnvironment()) - -@OptIn(ExperimentalResourceApi::class) -private suspend fun loadStringArray( - resource: StringResource, - resourceReader: ResourceReader, - environment: ResourceEnvironment -): List { - val path = resource.getPathByEnvironment(environment) - val keyToValue = getParsedStrings(path, resourceReader) - val item = keyToValue[resource.key] as? StringItem.Array - ?: error("String array ID=`${resource.key}` is not found!") - return item.items -} - -private fun NodeList.getElementsWithName(name: String): List = - List(length) { item(it) } - .filterIsInstance() - .filter { it.localName == name } - -//https://developer.android.com/guide/topics/resources/string-resource#escaping_quotes -/** - * Replaces - * - * '\n' -> new line - * - * '\t' -> tab - * - * '\uXXXX' -> unicode symbol - * - * '\\' -> '\' - * - * @param string The input string to handle. - * @return The string with special characters replaced according to the logic. - */ -internal fun handleSpecialCharacters(string: String): String { - val unicodeNewLineTabRegex = Regex("""\\u[a-fA-F\d]{4}|\\n|\\t""") - val doubleSlashRegex = Regex("""\\\\""") - val doubleSlashIndexes = doubleSlashRegex.findAll(string).map { it.range.first } - val handledString = unicodeNewLineTabRegex.replace(string) { matchResult -> - if (doubleSlashIndexes.contains(matchResult.range.first - 1)) matchResult.value - else when (matchResult.value) { - "\\n" -> "\n" - "\\t" -> "\t" - else -> matchResult.value.substring(2).toInt(16).toChar().toString() - } - }.replace("""\\""", """\""") - return handledString -} diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResourcesUtils.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResourcesUtils.kt new file mode 100644 index 0000000000..34b9aa7578 --- /dev/null +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResourcesUtils.kt @@ -0,0 +1,76 @@ +package org.jetbrains.compose.resources + +import kotlinx.coroutines.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.jetbrains.compose.resources.plural.PluralCategory +import org.jetbrains.compose.resources.vector.xmldom.Element +import org.jetbrains.compose.resources.vector.xmldom.NodeList +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +private val SimpleStringFormatRegex = Regex("""%(\d)\$[ds]""") +internal fun String.replaceWithArgs(args: List) = SimpleStringFormatRegex.replace(this) { matchResult -> + args[matchResult.groupValues[1].toInt() - 1] +} + +internal sealed interface StringItem { + data class Value(val text: String) : StringItem + data class Plurals(val items: Map) : StringItem + data class Array(val items: List) : StringItem +} + +private val stringsCacheMutex = Mutex() +private val stringItemsCache = mutableMapOf>() +//@TestOnly +internal fun dropStringItemsCache() { + stringItemsCache.clear() +} + +internal suspend fun getStringItem( + resourceItem: ResourceItem, + resourceReader: ResourceReader +): StringItem = coroutineScope { + val deferred = stringsCacheMutex.withLock { + stringItemsCache.getOrPut("${resourceItem.path}/${resourceItem.offset}-${resourceItem.size}") { + //LAZY - to free the mutex lock as fast as possible + async(start = CoroutineStart.LAZY) { + val record = resourceReader.readPart( + resourceItem.path, + resourceItem.offset, + resourceItem.size + ).decodeToString() + val recordItems = record.split('|') + val recordType = recordItems.first() + val recordData = recordItems.last() + when (recordType) { + "plurals" -> recordData.decodeAsPlural() + "string-array" -> recordData.decodeAsArray() + else -> recordData.decodeAsString() + } + } + } + } + deferred.await() +} + +@OptIn(ExperimentalEncodingApi::class) +private fun String.decodeAsString(): StringItem.Value = StringItem.Value( + Base64.decode(this).decodeToString() +) + +@OptIn(ExperimentalEncodingApi::class) +private fun String.decodeAsArray(): StringItem.Array = StringItem.Array( + split(",").map { item -> + Base64.decode(item).decodeToString() + } +) + +@OptIn(ExperimentalEncodingApi::class) +private fun String.decodeAsPlural(): StringItem.Plurals = StringItem.Plurals( + split(",").associate { item -> + val category = item.substringBefore(':') + val valueBase64 = item.substringAfter(':') + PluralCategory.fromString(category)!! to Base64.decode(valueBase64).decodeToString() + } +) diff --git a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt index 212d9d81e9..81b98b343a 100644 --- a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt +++ b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt @@ -12,7 +12,7 @@ import kotlin.test.* class ComposeResourceTest { init { - dropStringsCache() + dropStringItemsCache() dropImageCache() getResourceEnvironment = ::getTestEnvironment } @@ -71,7 +71,7 @@ class ComposeResourceTest { ) { str = stringResource(res) Text(str) - Text(stringArrayResource(TestStringResource("str_arr")).joinToString()) + Text(stringArrayResource(TestStringArrayResource("str_arr")).joinToString()) } } waitForIdle() @@ -82,9 +82,25 @@ class ComposeResourceTest { res = TestStringResource("app_name") waitForIdle() assertEquals(str, "Compose Resources App") + res = TestStringResource("hello") + waitForIdle() + assertEquals(str, "\uD83D\uDE0A Hello world!") + res = TestStringResource("app_name") + waitForIdle() + assertEquals(str, "Compose Resources App") + res = TestStringResource("hello") + waitForIdle() + assertEquals(str, "\uD83D\uDE0A Hello world!") + res = TestStringResource("app_name") + waitForIdle() + assertEquals(str, "Compose Resources App") assertEquals( - expected = listOf("strings.xml"), //just one string.xml read + expected = listOf( + "strings.cvr/314-44", + "strings.cvr/211-47", + "strings.cvr/359-37" + ), //only three different actual = testResourceReader.readPaths ) } @@ -100,7 +116,7 @@ class ComposeResourceTest { app_name = stringResource(TestStringResource("app_name")) accentuated_characters = stringResource(TestStringResource("accentuated_characters")) str_template = stringResource(TestStringResource("str_template"), "test-name", 42) - str_arr = stringArrayResource(TestStringResource("str_arr")) + str_arr = stringArrayResource(TestStringArrayResource("str_arr")) } } waitForIdle() @@ -141,7 +157,7 @@ class ComposeResourceTest { "Hello, test-name! You have 42 new messages.", getString(TestStringResource("str_template"), "test-name", 42) ) - assertEquals(listOf("item 1", "item 2", "item 3"), getStringArray(TestStringResource("str_arr"))) + assertEquals(listOf("item 1", "item 2", "item 3"), getStringArray(TestStringArrayResource("str_arr"))) } @Test @@ -253,32 +269,18 @@ class ComposeResourceTest { @Test fun testReadFileResource() = runTest { - val bytes = readResourceBytes("strings.xml") + val bytes = readResourceBytes("strings.cvr") assertEquals( """ - - Compose Resources App - 😊 Hello world! - Créer une table - Hello, %1${'$'}s! You have %2${'$'}d new messages. - - item 1 - item 2 - item 3 - - - one - other - - - another one - another other - - - %1${'$'}d message for %2${'$'}s - %1${'$'}d messages for %2${'$'}s - - + version:0 + plurals|another_plurals|ONE:YW5vdGhlciBvbmU=,OTHER:YW5vdGhlciBvdGhlcg== + plurals|messages|ONE:JTEkZCBtZXNzYWdlIGZvciAlMiRz,OTHER:JTEkZCBtZXNzYWdlcyBmb3IgJTIkcw== + plurals|plurals|ONE:b25l,OTHER:b3RoZXI= + string-array|str_arr|aXRlbSAx,aXRlbSAy,aXRlbSAz + string|accentuated_characters|Q3LDqWVyIHVuZSB0YWJsZQ== + string|app_name|Q29tcG9zZSBSZXNvdXJjZXMgQXBw + string|hello|8J+YiiBIZWxsbyB3b3JsZCE= + string|str_template|SGVsbG8sICUxJHMhIFlvdSBoYXZlICUyJGQgbmV3IG1lc3NhZ2VzLg== """.trimIndent(), bytes.decodeToString() diff --git a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ResourceTest.kt b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ResourceTest.kt index 0ea746a0b2..8f1f8b36de 100644 --- a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ResourceTest.kt +++ b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ResourceTest.kt @@ -27,11 +27,11 @@ class ResourceTest { val resource = DrawableResource( id = "ImageResource:test", items = setOf( - ResourceItem(setOf(), "default"), - ResourceItem(setOf(LanguageQualifier("en")), "en"), - ResourceItem(setOf(LanguageQualifier("en"), RegionQualifier("US"), XHDPI), "en-rUS-xhdpi"), - ResourceItem(setOf(LanguageQualifier("fr"), LIGHT), "fr-light"), - ResourceItem(setOf(DARK), "dark"), + ResourceItem(setOf(), "default", -1, -1), + ResourceItem(setOf(LanguageQualifier("en")), "en", -1, -1), + ResourceItem(setOf(LanguageQualifier("en"), RegionQualifier("US"), XHDPI), "en-rUS-xhdpi", -1, -1), + ResourceItem(setOf(LanguageQualifier("fr"), LIGHT), "fr-light", -1, -1), + ResourceItem(setOf(DARK), "dark", -1, -1), ) ) fun env(lang: String, reg: String, theme: ThemeQualifier, density: DensityQualifier) = ResourceEnvironment( @@ -42,46 +42,46 @@ class ResourceTest { ) assertEquals( "en-rUS-xhdpi", - resource.getPathByEnvironment(env("en", "US", DARK, XXHDPI)) + resource.getResourceItemByEnvironment(env("en", "US", DARK, XXHDPI)).path ) assertEquals( "en", - resource.getPathByEnvironment(env("en", "IN", LIGHT, LDPI)) + resource.getResourceItemByEnvironment(env("en", "IN", LIGHT, LDPI)).path ) assertEquals( "default", - resource.getPathByEnvironment(env("ch", "", LIGHT, MDPI)) + resource.getResourceItemByEnvironment(env("ch", "", LIGHT, MDPI)).path ) assertEquals( "dark", - resource.getPathByEnvironment(env("ch", "", DARK, MDPI)) + resource.getResourceItemByEnvironment(env("ch", "", DARK, MDPI)).path ) assertEquals( "fr-light", - resource.getPathByEnvironment(env("fr", "", DARK, MDPI)) + resource.getResourceItemByEnvironment(env("fr", "", DARK, MDPI)).path ) assertEquals( "fr-light", - resource.getPathByEnvironment(env("fr", "IN", LIGHT, MDPI)) + resource.getResourceItemByEnvironment(env("fr", "IN", LIGHT, MDPI)).path ) assertEquals( "default", - resource.getPathByEnvironment(env("ru", "US", LIGHT, XHDPI)) + resource.getResourceItemByEnvironment(env("ru", "US", LIGHT, XHDPI)).path ) assertEquals( "dark", - resource.getPathByEnvironment(env("ru", "US", DARK, XHDPI)) + resource.getResourceItemByEnvironment(env("ru", "US", DARK, XHDPI)).path ) val resourceWithNoDefault = DrawableResource( id = "ImageResource:test2", items = setOf( - ResourceItem(setOf(LanguageQualifier("en")), "en"), - ResourceItem(setOf(LanguageQualifier("fr"), LIGHT), "fr-light") + ResourceItem(setOf(LanguageQualifier("en")), "en", -1, -1), + ResourceItem(setOf(LanguageQualifier("fr"), LIGHT), "fr-light", -1, -1) ) ) assertFailsWith { - resourceWithNoDefault.getPathByEnvironment(env("ru", "US", DARK, XHDPI)) + resourceWithNoDefault.getResourceItemByEnvironment(env("ru", "US", DARK, XHDPI)) }.message.let { msg -> assertEquals("Resource with ID='ImageResource:test2' not found", msg) } @@ -89,22 +89,14 @@ class ResourceTest { val resourceWithFewFiles = DrawableResource( id = "ImageResource:test3", items = setOf( - ResourceItem(setOf(LanguageQualifier("en")), "en1"), - ResourceItem(setOf(LanguageQualifier("en")), "en2") + ResourceItem(setOf(LanguageQualifier("en")), "en1", -1, -1), + ResourceItem(setOf(LanguageQualifier("en")), "en2", -1, -1) ) ) assertFailsWith { - resourceWithFewFiles.getPathByEnvironment(env("en", "US", DARK, XHDPI)) + resourceWithFewFiles.getResourceItemByEnvironment(env("en", "US", DARK, XHDPI)) }.message.let { msg -> assertEquals("Resource with ID='ImageResource:test3' has more than one file: en1, en2", msg) } } - - @Test - fun testEscapedSymbols() { - assertEquals( - "abc \n \\n \t \\t \u1234 \ua45f \\u1234 \\ \\u355g", - handleSpecialCharacters("""abc \n \\n \t \\t \u1234 \ua45f \\u1234 \\ \u355g""") - ) - } } diff --git a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestComposeEnvironment.kt b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestComposeEnvironment.kt index c2491b5e88..ad19013d8b 100644 --- a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestComposeEnvironment.kt +++ b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestComposeEnvironment.kt @@ -2,7 +2,6 @@ package org.jetbrains.compose.resources import androidx.compose.runtime.Composable -@OptIn(InternalResourceApi::class) internal fun getTestEnvironment() = ResourceEnvironment( language = LanguageQualifier("en"), region = RegionQualifier("US"), diff --git a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestResourceReader.kt b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestResourceReader.kt index 3255ccf857..8b8366a2d4 100644 --- a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestResourceReader.kt +++ b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestResourceReader.kt @@ -8,4 +8,9 @@ internal class TestResourceReader : ResourceReader { readPathsList.add(path) return DefaultResourceReader.read(path) } + + override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray { + readPathsList.add("$path/$offset-$size") + return DefaultResourceReader.readPart(path, offset, size) + } } \ No newline at end of file diff --git a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt index 9ab4d9462c..d36ac89781 100644 --- a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt +++ b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt @@ -4,18 +4,38 @@ import org.jetbrains.compose.resources.plural.PluralCategory import org.jetbrains.compose.resources.plural.PluralRule import org.jetbrains.compose.resources.plural.PluralRuleList -@OptIn(InternalResourceApi::class, ExperimentalResourceApi::class) +private val cvrMap: Map = mapOf( + "accentuated_characters" to ResourceItem(setOf(), "strings.cvr", 259, 54), + "app_name" to ResourceItem(setOf(), "strings.cvr", 314, 44), + "hello" to ResourceItem(setOf(), "strings.cvr", 359, 37), + "str_template" to ResourceItem(setOf(), "strings.cvr", 397, 76), + + "another_plurals" to ResourceItem(setOf(), "strings.cvr", 10, 71), + "messages" to ResourceItem(setOf(), "strings.cvr", 82, 88), + "plurals" to ResourceItem(setOf(), "strings.cvr", 171, 39), + + "str_arr" to ResourceItem(setOf(), "strings.cvr", 211, 47), +) + +@OptIn(ExperimentalResourceApi::class) internal fun TestStringResource(key: String) = StringResource( "STRING:$key", key, - setOf(ResourceItem(emptySet(), "strings.xml")) + setOf(cvrMap[key] ?: error("String ID=`$key` is not found!")) +) + +@OptIn(ExperimentalResourceApi::class) +internal fun TestStringArrayResource(key: String) = StringArrayResource( + "STRING:$key", + key, + setOf(cvrMap[key] ?: error("String ID=`$key` is not found!")) ) -@OptIn(InternalResourceApi::class, ExperimentalResourceApi::class) +@OptIn(ExperimentalResourceApi::class) internal fun TestPluralStringResource(key: String) = PluralStringResource( "PLURALS:$key", key, - setOf(ResourceItem(emptySet(), "strings.xml")) + setOf(cvrMap[key] ?: error("String ID=`$key` is not found!")) ) internal fun parsePluralSamples(samples: String): List { diff --git a/components/resources/library/src/commonTest/resources/strings.cvr b/components/resources/library/src/commonTest/resources/strings.cvr new file mode 100644 index 0000000000..8da74b6bb6 --- /dev/null +++ b/components/resources/library/src/commonTest/resources/strings.cvr @@ -0,0 +1,9 @@ +version:0 +plurals|another_plurals|ONE:YW5vdGhlciBvbmU=,OTHER:YW5vdGhlciBvdGhlcg== +plurals|messages|ONE:JTEkZCBtZXNzYWdlIGZvciAlMiRz,OTHER:JTEkZCBtZXNzYWdlcyBmb3IgJTIkcw== +plurals|plurals|ONE:b25l,OTHER:b3RoZXI= +string-array|str_arr|aXRlbSAx,aXRlbSAy,aXRlbSAz +string|accentuated_characters|Q3LDqWVyIHVuZSB0YWJsZQ== +string|app_name|Q29tcG9zZSBSZXNvdXJjZXMgQXBw +string|hello|8J+YiiBIZWxsbyB3b3JsZCE= +string|str_template|SGVsbG8sICUxJHMhIFlvdSBoYXZlICUyJGQgbmV3IG1lc3NhZ2VzLg== diff --git a/components/resources/library/src/commonTest/resources/strings.xml b/components/resources/library/src/commonTest/resources/strings.xml deleted file mode 100644 index a1fe3a6c29..0000000000 --- a/components/resources/library/src/commonTest/resources/strings.xml +++ /dev/null @@ -1,23 +0,0 @@ - - Compose Resources App - 😊 Hello world! - Créer une table - Hello, %1$s! You have %2$d new messages. - - item 1 - item 2 - item 3 - - - one - other - - - another one - another other - - - %1$d message for %2$s - %1$d messages for %2$s - - diff --git a/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.desktop.kt b/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.desktop.kt index 5b14bd60d0..8c45734ebe 100644 --- a/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.desktop.kt +++ b/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.desktop.kt @@ -5,7 +5,6 @@ import org.jetbrains.skiko.currentSystemTheme import java.awt.Toolkit import java.util.* -@OptIn(InternalResourceApi::class) internal actual fun getSystemEnvironment(): ResourceEnvironment { val locale = Locale.getDefault() //FIXME: don't use skiko internals diff --git a/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceReader.desktop.kt b/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceReader.desktop.kt index 9d393b859a..419682c7b3 100644 --- a/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceReader.desktop.kt +++ b/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceReader.desktop.kt @@ -1,11 +1,26 @@ package org.jetbrains.compose.resources -private object JvmResourceReader - -@OptIn(ExperimentalResourceApi::class) -@InternalResourceApi -actual suspend fun readResourceBytes(path: String): ByteArray { - val classLoader = Thread.currentThread().contextClassLoader ?: JvmResourceReader.javaClass.classLoader - val resource = classLoader.getResourceAsStream(path) ?: throw MissingResourceException(path) - return resource.readBytes() +import java.io.InputStream + +internal actual fun getPlatformResourceReader(): ResourceReader = object : ResourceReader { + override suspend fun read(path: String): ByteArray { + val resource = getResourceAsStream(path) + return resource.readBytes() + } + + override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray { + val resource = getResourceAsStream(path) + val result = ByteArray(size.toInt()) + resource.use { input -> + input.skip(offset) + input.read(result, 0, size.toInt()) + } + return result + } + + @OptIn(ExperimentalResourceApi::class) + private fun getResourceAsStream(path: String): InputStream { + val classLoader = Thread.currentThread().contextClassLoader ?: this.javaClass.classLoader + return classLoader.getResourceAsStream(path) ?: throw MissingResourceException(path) + } } \ No newline at end of file diff --git a/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.ios.kt b/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.ios.kt index 0b5a36655f..82e89e64bc 100644 --- a/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.ios.kt +++ b/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.ios.kt @@ -4,7 +4,6 @@ import platform.Foundation.* import platform.UIKit.UIScreen import platform.UIKit.UIUserInterfaceStyle -@OptIn(InternalResourceApi::class) internal actual fun getSystemEnvironment(): ResourceEnvironment { val locale = NSLocale.preferredLanguages.firstOrNull() ?.let { NSLocale(it as String) } 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 0a81769a56..3de80b7d9c 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 @@ -2,20 +2,39 @@ package org.jetbrains.compose.resources import kotlinx.cinterop.addressOf import kotlinx.cinterop.usePinned -import platform.Foundation.NSBundle -import platform.Foundation.NSFileManager +import platform.Foundation.* import platform.posix.memcpy @OptIn(ExperimentalResourceApi::class) -@InternalResourceApi -actual suspend fun readResourceBytes(path: String): ByteArray { - val fileManager = NSFileManager.defaultManager() - // todo: support fallback path at bundle root? - val composeResourcesPath = NSBundle.mainBundle.resourcePath + "/compose-resources/" + path - val contentsAtPath = fileManager.contentsAtPath(composeResourcesPath) ?: throw MissingResourceException(path) - return ByteArray(contentsAtPath.length.toInt()).apply { - usePinned { - memcpy(it.addressOf(0), contentsAtPath.bytes, contentsAtPath.length) +internal actual fun getPlatformResourceReader(): ResourceReader = object : ResourceReader { + override suspend fun read(path: String): ByteArray { + val data = readData(getPathInBundle(path)) + return ByteArray(data.length.toInt()).apply { + usePinned { memcpy(it.addressOf(0), data.bytes, data.length) } } } + + override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray { + val data = readData(getPathInBundle(path), offset, size) + return ByteArray(data.length.toInt()).apply { + usePinned { memcpy(it.addressOf(0), data.bytes, data.length) } + } + } + + private fun readData(path: String): NSData { + return NSFileManager.defaultManager().contentsAtPath(path) ?: throw MissingResourceException(path) + } + + private fun readData(path: String, offset: Long, size: Long): NSData { + val fileHandle = NSFileHandle.fileHandleForReadingAtPath(path) ?: throw MissingResourceException(path) + fileHandle.seekToOffset(offset.toULong(), null) + val result = fileHandle.readDataOfLength(size.toULong()) + fileHandle.closeFile() + return result + } + + private fun getPathInBundle(path: String): String { + // todo: support fallback path at bundle root? + return NSBundle.mainBundle.resourcePath + "/compose-resources/" + path + } } \ No newline at end of file diff --git a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.js.kt b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.js.kt index 589e0b8587..4039c79438 100644 --- a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.js.kt +++ b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.js.kt @@ -9,7 +9,6 @@ private external class Intl { } } -@OptIn(InternalResourceApi::class) internal actual fun getSystemEnvironment(): ResourceEnvironment { val locale = Intl.Locale(window.navigator.language) val isDarkTheme = window.matchMedia("(prefers-color-scheme: dark)").matches 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 8cfb63b243..851cf50114 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 @@ -4,17 +4,32 @@ import kotlinx.browser.window import kotlinx.coroutines.await import org.khronos.webgl.ArrayBuffer import org.khronos.webgl.Int8Array - -private fun ArrayBuffer.toByteArray(): ByteArray = - Int8Array(this, 0, byteLength).unsafeCast() +import org.w3c.files.Blob +import kotlin.js.Promise @OptIn(ExperimentalResourceApi::class) -@InternalResourceApi -actual suspend fun readResourceBytes(path: String): ByteArray { - val resPath = WebResourcesConfiguration.getResourcePath(path) - val response = window.fetch(resPath).await() - if (!response.ok) { - throw MissingResourceException(resPath) +internal actual fun getPlatformResourceReader(): ResourceReader = object : ResourceReader { + override suspend fun read(path: String): ByteArray { + return readAsBlob(path).asByteArray() + } + + override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray { + val part = readAsBlob(path).slice(offset.toInt(), (offset + size).toInt()) + return part.asByteArray() + } + + private suspend fun readAsBlob(path: String): Blob { + val resPath = WebResourcesConfiguration.getResourcePath(path) + val response = window.fetch(resPath).await() + if (!response.ok) { + throw MissingResourceException(resPath) + } + return response.blob().await() + } + + private suspend fun Blob.asByteArray(): ByteArray { + //https://developer.mozilla.org/en-US/docs/Web/API/Blob/arrayBuffer + val buffer = asDynamic().arrayBuffer() as Promise + return Int8Array(buffer.await()).unsafeCast() } - return response.arrayBuffer().await().toByteArray() } \ No newline at end of file diff --git a/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.macos.kt b/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.macos.kt index ca2ba2507e..83404652ee 100644 --- a/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.macos.kt +++ b/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.macos.kt @@ -6,7 +6,6 @@ import platform.CoreGraphics.CGDisplayPixelsWide import platform.CoreGraphics.CGDisplayScreenSize import platform.Foundation.* -@OptIn(InternalResourceApi::class) internal actual fun getSystemEnvironment(): ResourceEnvironment { val locale = NSLocale.currentLocale() val isDarkTheme = NSUserDefaults.standardUserDefaults.stringForKey("AppleInterfaceStyle") == "Dark" 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 906df6ae6c..5f8ba0cc51 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 @@ -2,23 +2,46 @@ package org.jetbrains.compose.resources import kotlinx.cinterop.addressOf import kotlinx.cinterop.usePinned -import platform.Foundation.NSFileManager +import platform.Foundation.* import platform.posix.memcpy @OptIn(ExperimentalResourceApi::class) -@InternalResourceApi -actual suspend fun readResourceBytes(path: String): ByteArray { - val currentDirectoryPath = NSFileManager.defaultManager().currentDirectoryPath - val contentsAtPath = NSFileManager.defaultManager().run { - //todo in future bundle resources with app and use all sourceSets (skikoMain, nativeMain) - contentsAtPath("$currentDirectoryPath/src/macosMain/composeResources/$path") - ?: contentsAtPath("$currentDirectoryPath/src/macosTest/composeResources/$path") - ?: contentsAtPath("$currentDirectoryPath/src/commonMain/composeResources/$path") - ?: contentsAtPath("$currentDirectoryPath/src/commonTest/composeResources/$path") - } ?: throw MissingResourceException(path) - return ByteArray(contentsAtPath.length.toInt()).apply { - usePinned { - memcpy(it.addressOf(0), contentsAtPath.bytes, contentsAtPath.length) +internal actual fun getPlatformResourceReader(): ResourceReader = object : ResourceReader { + override suspend fun read(path: String): ByteArray { + val data = readData(getPathOnDisk(path)) + return ByteArray(data.length.toInt()).apply { + usePinned { memcpy(it.addressOf(0), data.bytes, data.length) } } } + + override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray { + val data = readData(getPathOnDisk(path), offset, size) + return ByteArray(data.length.toInt()).apply { + usePinned { memcpy(it.addressOf(0), data.bytes, data.length) } + } + } + + private fun readData(path: String): NSData { + return NSFileManager.defaultManager().contentsAtPath(path) ?: throw MissingResourceException(path) + } + + private fun readData(path: String, offset: Long, size: Long): NSData { + val fileHandle = NSFileHandle.fileHandleForReadingAtPath(path) ?: throw MissingResourceException(path) + fileHandle.seekToOffset(offset.toULong(), null) + val result = fileHandle.readDataOfLength(size.toULong()) + fileHandle.closeFile() + return result + } + + private fun getPathOnDisk(path: String): String { + val fm = NSFileManager.defaultManager() + val currentDirectoryPath = fm.currentDirectoryPath + return listOf( + //todo in future bundle resources with app and use all sourceSets (skikoMain, nativeMain) + "$currentDirectoryPath/src/macosMain/composeResources/$path", + "$currentDirectoryPath/src/macosTest/composeResources/$path", + "$currentDirectoryPath/src/commonMain/composeResources/$path", + "$currentDirectoryPath/src/commonTest/composeResources/$path" + ).firstOrNull { p -> fm.fileExistsAtPath(p) } ?: throw MissingResourceException(path) + } } \ No newline at end of file diff --git a/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt b/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt index c254829c81..b15d7c0dfc 100644 --- a/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt +++ b/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt @@ -34,7 +34,7 @@ private val defaultEmptyFont by lazy { Font("org.jetbrains.compose.emptyFont", B actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): Font { val resourceReader = LocalResourceReader.current val fontFile by rememberResourceState(resource, weight, style, { defaultEmptyFont }) { env -> - val path = resource.getPathByEnvironment(env) + val path = resource.getResourceItemByEnvironment(env).path val fontBytes = resourceReader.read(path) Font(path, fontBytes, weight, style) } diff --git a/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.wasmJs.kt b/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.wasmJs.kt index 7a0bfcfb27..48f3acd851 100644 --- a/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.wasmJs.kt +++ b/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.wasmJs.kt @@ -9,7 +9,6 @@ private external class Intl { } } -@OptIn(InternalResourceApi::class) internal actual fun getSystemEnvironment(): ResourceEnvironment { val locale = Intl.Locale(window.navigator.language) val isDarkTheme = window.matchMedia("(prefers-color-scheme: dark)").matches 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 efb80613b3..02c330f435 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 @@ -5,31 +5,11 @@ import kotlinx.coroutines.await import org.khronos.webgl.ArrayBuffer import org.khronos.webgl.Int8Array import org.w3c.fetch.Response +import org.w3c.files.Blob +import kotlin.js.Promise import kotlin.wasm.unsafe.UnsafeWasmMemoryApi import kotlin.wasm.unsafe.withScopedMemoryAllocator -/** - * Reads the content of the resource file at the specified path and returns it as a byte array. - * - * @param path The path of the file to read in the resource's directory. - * @return The content of the file as a byte array. - */ -@OptIn(ExperimentalResourceApi::class) -@InternalResourceApi -actual suspend fun readResourceBytes(path: String): ByteArray { - val resPath = WebResourcesConfiguration.getResourcePath(path) - val response = window.fetch(resPath).await() - if (!response.ok) { - throw MissingResourceException(resPath) - } - return response.arrayBuffer().await().toByteArray() -} - -private fun ArrayBuffer.toByteArray(): ByteArray { - val source = Int8Array(this, 0, byteLength) - return jsInt8ArrayToKotlinByteArray(source) -} - @JsFun( """ (src, size, dstAddr) => { const mem8 = new Int8Array(wasmExports.memory.buffer, dstAddr, size); @@ -37,16 +17,46 @@ private fun ArrayBuffer.toByteArray(): ByteArray { } """ ) -internal external fun jsExportInt8ArrayToWasm(src: Int8Array, size: Int, dstAddr: Int) +private external fun jsExportInt8ArrayToWasm(src: Int8Array, size: Int, dstAddr: Int) + +@JsFun("(blob) => blob.arrayBuffer()") +private external fun jsExportBlobAsArrayBuffer(blob: Blob): Promise + +@OptIn(ExperimentalResourceApi::class) +internal actual fun getPlatformResourceReader(): ResourceReader = object : ResourceReader { + override suspend fun read(path: String): ByteArray { + return readAsBlob(path).asByteArray() + } + + override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray { + val part = readAsBlob(path).slice(offset.toInt(), (offset + size).toInt()) + return part.asByteArray() + } + + private suspend fun readAsBlob(path: String): Blob { + val resPath = WebResourcesConfiguration.getResourcePath(path) + val response = window.fetch(resPath).await() + if (!response.ok) { + throw MissingResourceException(resPath) + } + return response.blob().await() + } + + private suspend fun Blob.asByteArray(): ByteArray { + val buffer: ArrayBuffer = jsExportBlobAsArrayBuffer(this).await() + return Int8Array(buffer).asByteArray() + } -internal fun jsInt8ArrayToKotlinByteArray(x: Int8Array): ByteArray { - val size = x.length + private fun Int8Array.asByteArray(): ByteArray { + val array = this + val size = array.length - @OptIn(UnsafeWasmMemoryApi::class) - return withScopedMemoryAllocator { allocator -> - val memBuffer = allocator.allocate(size) - val dstAddress = memBuffer.address.toInt() - jsExportInt8ArrayToWasm(x, size, dstAddress) - ByteArray(size) { i -> (memBuffer + i).loadByte() } + @OptIn(UnsafeWasmMemoryApi::class) + return withScopedMemoryAllocator { allocator -> + val memBuffer = allocator.allocate(size) + val dstAddress = memBuffer.address.toInt() + jsExportInt8ArrayToWasm(array, size, dstAddress) + ByteArray(size) { i -> (memBuffer + i).loadByte() } + } } } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt index f8c35990f0..4457bdd56b 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt @@ -19,24 +19,17 @@ import org.jetbrains.compose.desktop.DesktopExtension import org.jetbrains.compose.desktop.application.internal.configureDesktop import org.jetbrains.compose.desktop.preview.internal.initializePreview import org.jetbrains.compose.experimental.dsl.ExperimentalExtension -import org.jetbrains.compose.experimental.internal.configureExperimental -import org.jetbrains.compose.experimental.internal.configureExperimentalTargetsFlagsCheck -import org.jetbrains.compose.experimental.internal.configureNativeCompilerCaching -import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID -import org.jetbrains.compose.internal.mppExt -import org.jetbrains.compose.internal.mppExtOrNull +import org.jetbrains.compose.experimental.internal.* +import org.jetbrains.compose.internal.* import org.jetbrains.compose.internal.service.ConfigurationProblemReporterService import org.jetbrains.compose.internal.service.GradlePropertySnapshotService import org.jetbrains.compose.internal.utils.currentTarget import org.jetbrains.compose.resources.ResourcesExtension import org.jetbrains.compose.resources.configureComposeResources -import org.jetbrains.compose.resources.ios.configureSyncTask import org.jetbrains.compose.web.WebExtension import org.jetbrains.kotlin.gradle.dsl.KotlinCompile import org.jetbrains.kotlin.gradle.dsl.KotlinJsCompile -import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler -import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType -import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion +import org.jetbrains.kotlin.gradle.plugin.* internal val composeVersion get() = ComposeBuildConfig.composeVersion @@ -75,7 +68,6 @@ abstract class ComposePlugin : Plugin { project.plugins.withId(KOTLIN_MPP_PLUGIN_ID) { val mppExt = project.mppExt project.configureExperimentalTargetsFlagsCheck(mppExt) - project.configureSyncTask(mppExt) } project.tasks.withType(KotlinCompile::class.java).configureEach { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt index f9268adf0e..065d258221 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt @@ -56,10 +56,7 @@ internal object ComposeProperties { providers.valueOrNull(CHECK_JDK_VENDOR).toBooleanProvider(true) //providers.valueOrNull works only with root gradle.properties - fun alwaysGenerateResourceAccessors(project: Project): Provider = project.provider { - project.findProperty(ALWAYS_GENERATE_RESOURCE_ACCESSORS)?.toString().equals("true", true) + fun dontSyncResources(project: Project): Provider = project.provider { + project.findProperty(SYNC_RESOURCES_PROPERTY)?.toString().equals("false", true) } - - fun syncResources(providers: ProviderFactory): Provider = - providers.valueOrNull(SYNC_RESOURCES_PROPERTY).toBooleanProvider(true) } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/cocoapodsDslHelpers.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/cocoapodsDslHelpers.kt deleted file mode 100644 index 34815b8fae..0000000000 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/cocoapodsDslHelpers.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2020-2023 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.experimental.uikit.internal.utils - -import org.gradle.api.Project -import org.gradle.api.plugins.ExtensionAware -import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension - -private const val COCOAPODS_PLUGIN_ID = "org.jetbrains.kotlin.native.cocoapods" -internal fun Project.withCocoapodsPlugin(fn: () -> Unit) { - project.plugins.withId(COCOAPODS_PLUGIN_ID) { - fn() - } -} - -internal val KotlinMultiplatformExtension.cocoapodsExt: CocoapodsExtension - get() { - val extensionAware = (this as? ExtensionAware) ?: error("KotlinMultiplatformExtension is not ExtensionAware") - val extName = "cocoapods" - val ext = extensionAware.extensions.findByName(extName) ?: error("KotlinMultiplatformExtension does not contain '$extName' extension") - return ext as CocoapodsExtension - } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/kotlinNativeTargetUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/kotlinNativeTargetUtils.kt deleted file mode 100644 index a6811a749c..0000000000 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/kotlinNativeTargetUtils.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2020-2023 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.experimental.uikit.internal.utils - -import org.jetbrains.kotlin.gradle.plugin.KotlinTarget -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget -import org.jetbrains.kotlin.konan.target.KonanTarget - -internal fun KotlinNativeTarget.isIosSimulatorTarget(): Boolean = - konanTarget === KonanTarget.IOS_X64 || konanTarget === KonanTarget.IOS_SIMULATOR_ARM64 - -internal fun KotlinNativeTarget.isIosDeviceTarget(): Boolean = - konanTarget === KonanTarget.IOS_ARM64 || konanTarget === KonanTarget.IOS_ARM32 - -internal fun KotlinNativeTarget.isIosTarget(): Boolean = - isIosSimulatorTarget() || isIosDeviceTarget() - -internal fun KotlinTarget.asIosNativeTargetOrNull(): KotlinNativeTarget? = - (this as? KotlinNativeTarget)?.takeIf { it.isIosTarget() } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/tasks/AbstractComposeIosTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/tasks/AbstractComposeIosTask.kt deleted file mode 100644 index ea6c6b827f..0000000000 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/tasks/AbstractComposeIosTask.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.experimental.uikit.tasks - -import org.gradle.api.DefaultTask -import org.gradle.api.file.Directory -import org.gradle.api.file.FileSystemOperations -import org.gradle.api.file.ProjectLayout -import org.gradle.api.model.ObjectFactory -import org.gradle.api.provider.Property -import org.gradle.api.provider.Provider -import org.gradle.api.provider.ProviderFactory -import org.gradle.api.tasks.Internal -import org.gradle.api.tasks.LocalState -import org.gradle.process.ExecOperations -import org.jetbrains.compose.desktop.application.internal.ComposeProperties -import org.jetbrains.compose.desktop.application.internal.ExternalToolRunner -import org.jetbrains.compose.internal.utils.notNullProperty -import javax.inject.Inject - -abstract class AbstractComposeIosTask : DefaultTask() { - @get:Inject - protected abstract val objects: ObjectFactory - - @get:Inject - protected abstract val providers: ProviderFactory - - @get:Inject - protected abstract val execOperations: ExecOperations - - @get:Inject - protected abstract val fileOperations: FileSystemOperations - - @get:Inject - protected abstract val layout: ProjectLayout - - @get:LocalState - protected val logsDir: Provider = project.layout.buildDirectory.dir("compose/logs/$name") - - @get:Internal - val verbose: Property = objects.notNullProperty().apply { - set(providers.provider { - logger.isDebugEnabled || ComposeProperties.isVerbose(providers).get() - }) - } - - @get:Internal - internal val runExternalTool: ExternalToolRunner - get() = ExternalToolRunner(verbose, logsDir, execOperations) -} 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 new file mode 100644 index 0000000000..7f6f0b4f61 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidResources.kt @@ -0,0 +1,100 @@ +package org.jetbrains.compose.resources + +import com.android.build.api.variant.AndroidComponentsExtension +import com.android.build.gradle.BaseExtension +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileSystemOperations +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.* +import org.jetbrains.compose.internal.utils.registerTask +import org.jetbrains.compose.internal.utils.uppercaseFirstChar +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation +import org.jetbrains.kotlin.gradle.plugin.sources.android.androidSourceSetInfoOrNull +import org.jetbrains.kotlin.gradle.utils.ObservableSet +import java.io.File +import javax.inject.Inject + +@OptIn(ExperimentalKotlinGradlePluginApi::class) +internal fun Project.configureAndroidComposeResources( + kotlinExtension: KotlinMultiplatformExtension, + androidExtension: BaseExtension, + preparedCommonResources: Provider +) { + val commonMain = KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME + val commonResourcesDir = projectDir.resolve("src/$commonMain/$COMPOSE_RESOURCES_DIR") + + // 1) get the Kotlin Android Target Compilation -> [A] + // 2) get default source set name for the 'A' + // 3) find the associated Android SourceSet in the AndroidExtension -> [B] + // 4) get all source sets in the 'A' and add its resources to the 'B' + kotlinExtension.targets.withType(KotlinAndroidTarget::class.java).all { androidTarget -> + androidTarget.compilations.all { compilation: KotlinJvmAndroidCompilation -> + + //fix for AGP < 8.0 + //usually 'androidSourceSet.resources.srcDir(preparedCommonResources)' should be enough + compilation.androidVariant.processJavaResourcesProvider.configure { it.dependsOn(preparedCommonResources) } + + compilation.defaultSourceSet.androidSourceSetInfoOrNull?.let { kotlinAndroidSourceSet -> + androidExtension.sourceSets + .matching { it.name == kotlinAndroidSourceSet.androidSourceSetName } + .all { androidSourceSet -> + (compilation.allKotlinSourceSets as? ObservableSet)?.forAll { kotlinSourceSet -> + if (kotlinSourceSet.name == commonMain) { + androidSourceSet.resources.srcDir(preparedCommonResources) + } else { + androidSourceSet.resources.srcDir( + projectDir.resolve("src/${kotlinSourceSet.name}/$COMPOSE_RESOURCES_DIR") + ) + } + } + } + } + } + } + + //copy fonts from the compose resources dir to android assets + val androidComponents = project.extensions.findByType(AndroidComponentsExtension::class.java) ?: return + androidComponents.onVariants { variant -> + val copyFonts = registerTask( + "copy${variant.name.uppercaseFirstChar()}FontsToAndroidAssets" + ) { + from.set(commonResourcesDir) + } + variant.sources?.assets?.addGeneratedSourceDirectory( + taskProvider = copyFonts, + wiredWith = CopyAndroidFontsToAssetsTask::outputDirectory + ) + //exclude a duplication of fonts in apks + variant.packaging.resources.excludes.add("**/font*/*") + } +} + +//Copy task doesn't work with 'variant.sources?.assets?.addGeneratedSourceDirectory' API +internal abstract class CopyAndroidFontsToAssetsTask : DefaultTask() { + @get:Inject + abstract val fileSystem: FileSystemOperations + + @get:InputFiles + @get:IgnoreEmptyDirectories + abstract val from: Property + + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + @TaskAction + fun action() { + fileSystem.copy { + it.includeEmptyDirs = false + it.from(from) + it.include("**/font*/*") + it.into(outputDirectory) + } + } +} \ 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 new file mode 100644 index 0000000000..e9a1a71718 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ComposeResources.kt @@ -0,0 +1,110 @@ +package org.jetbrains.compose.resources + +import com.android.build.gradle.BaseExtension +import org.gradle.api.Project +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.TaskProvider +import org.gradle.util.GradleVersion +import org.jetbrains.compose.internal.KOTLIN_JVM_PLUGIN_ID +import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.extraProperties +import java.io.File + + +internal const val COMPOSE_RESOURCES_DIR = "composeResources" +internal const val RES_GEN_DIR = "generated/compose/resourceGenerator" +private const val KMP_RES_EXT = "multiplatformResourcesPublication" +private const val MIN_GRADLE_VERSION_FOR_KMP_RESOURCES = "7.6" +private val androidPluginIds = listOf( + "com.android.application", + "com.android.library" +) + +internal fun Project.configureComposeResources(extension: ResourcesExtension) { + val config = provider { extension } + plugins.withId(KOTLIN_MPP_PLUGIN_ID) { onKgpApplied(config) } + plugins.withId(KOTLIN_JVM_PLUGIN_ID) { onKotlinJvmApplied(config) } +} + +private fun Project.onKgpApplied(config: Provider) { + val kotlinExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + + //common resources must be converted (XML -> CVR) + val commonMain = KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME + val preparedCommonResources = prepareCommonResources(commonMain) + + val hasKmpResources = extraProperties.has(KMP_RES_EXT) + val currentGradleVersion = GradleVersion.current() + val minGradleVersion = GradleVersion.version(MIN_GRADLE_VERSION_FOR_KMP_RESOURCES) + val kmpResourcesAreAvailable = hasKmpResources && currentGradleVersion >= minGradleVersion + + if (kmpResourcesAreAvailable) { + configureKmpResources(kotlinExtension, extraProperties.get(KMP_RES_EXT)!!, preparedCommonResources, config) + } else { + if (!hasKmpResources) logger.info( + """ + Compose resources publication requires Kotlin Gradle Plugin >= 2.0 + Current Kotlin Gradle Plugin is ${KotlinVersion.CURRENT} + """.trimIndent() + ) + if (currentGradleVersion < minGradleVersion) logger.info( + """ + Compose resources publication requires Gradle >= $MIN_GRADLE_VERSION_FOR_KMP_RESOURCES + Current Gradle is ${currentGradleVersion.version} + """.trimIndent() + ) + + configureComposeResources(kotlinExtension, commonMain, preparedCommonResources, config) + + //when applied AGP then configure android resources + androidPluginIds.forEach { pluginId -> + plugins.withId(pluginId) { + val androidExtension = project.extensions.getByType(BaseExtension::class.java) + configureAndroidComposeResources(kotlinExtension, androidExtension, preparedCommonResources) + } + } + } + + configureSyncIosComposeResources(kotlinExtension) +} + +private fun Project.onKotlinJvmApplied(config: Provider) { + val kotlinExtension = project.extensions.getByType(KotlinProjectExtension::class.java) + val main = SourceSet.MAIN_SOURCE_SET_NAME + val preparedCommonResources = prepareCommonResources(main) + configureComposeResources(kotlinExtension, main, preparedCommonResources, config) +} + +//common resources must be converted (XML -> CVR) +private fun Project.prepareCommonResources(commonSourceSetName: String): Provider { + val preparedResourcesTask = registerPrepareComposeResourcesTask( + project.projectDir.resolve("src/$commonSourceSetName/$COMPOSE_RESOURCES_DIR"), + layout.buildDirectory.dir("$RES_GEN_DIR/preparedResources/$commonSourceSetName/$COMPOSE_RESOURCES_DIR") + ) + return preparedResourcesTask.flatMap { it.outputDir } +} + +// sourceSet.resources.srcDirs doesn't work for Android targets. +// Android resources should be configured separately +private fun Project.configureComposeResources( + kotlinExtension: KotlinProjectExtension, + commonSourceSetName: String, + preparedCommonResources: Provider, + config: Provider +) { + logger.info("Configure compose resources") + kotlinExtension.sourceSets.all { sourceSet -> + val sourceSetName = sourceSet.name + val resourcesDir = project.projectDir.resolve("src/$sourceSetName/$COMPOSE_RESOURCES_DIR") + if (sourceSetName == commonSourceSetName) { + sourceSet.resources.srcDirs(preparedCommonResources) + configureGenerationComposeResClass(preparedCommonResources, sourceSet, config, false) + } else { + sourceSet.resources.srcDirs(resourcesDir) + } + } +} 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 5e7a234cd3..3c7811d838 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 @@ -1,12 +1,11 @@ package org.jetbrains.compose.resources import org.gradle.api.DefaultTask -import org.gradle.api.file.DirectoryProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.* import java.io.File +import java.io.RandomAccessFile import java.nio.file.Path -import javax.xml.parsers.DocumentBuilderFactory import kotlin.io.path.relativeTo /** @@ -31,12 +30,12 @@ internal abstract class GenerateResClassTask : DefaultTask() { abstract val resDir: Property @get:OutputDirectory - abstract val codeDir: DirectoryProperty + abstract val codeDir: Property @TaskAction fun generate() { try { - val kotlinDir = codeDir.get().asFile + val kotlinDir = codeDir.get() logger.info("Clean directory $kotlinDir") kotlinDir.deleteRecursively() kotlinDir.mkdirs() @@ -100,35 +99,49 @@ internal abstract class GenerateResClassTask : DefaultTask() { return null } - if (typeString == "values" && file.name.equals("strings.xml", true)) { - return getStringResources(file).mapNotNull { (typeName, strId) -> - val type = when(typeName) { - "string", "string-array" -> ResourceType.STRING - "plurals" -> ResourceType.PLURAL_STRING - else -> return@mapNotNull null - } - ResourceItem(type, qualifiers, strId.asUnderscoredIdentifier(), path) - } + if (typeString == "values" && file.extension.equals(XmlValuesConverterTask.CONVERTED_RESOURCE_EXT, true)) { + return getValueResourceItems(file, qualifiers, path) } - val type = ResourceType.fromString(typeString) + val type = ResourceType.fromString(typeString) ?: error("Unknown resource type: '$typeString'.") return listOf(ResourceItem(type, qualifiers, file.nameWithoutExtension.asUnderscoredIdentifier(), path)) } - //type -> id - private val stringTypeNames = listOf("string", "string-array", "plurals") - private fun getStringResources(stringsXml: File): List> { - val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(stringsXml) - val items = doc.getElementsByTagName("resources").item(0).childNodes - return List(items.length) { items.item(it) } - .filter { it.nodeName in stringTypeNames } - .map { it.nodeName to it.attributes.getNamedItem("name").nodeValue } + private fun getValueResourceItems(dataFile: File, qualifiers: List, path: Path) : List { + val result = mutableListOf() + dataFile.bufferedReader().use { f -> + var offset = 0L + var line: String? = f.readLine() + while (line != null) { + val size = line.encodeToByteArray().size + + //first line is meta info + if (offset > 0) { + result.add(getValueResourceItem(line, offset, size.toLong(), qualifiers, path)) + } + + offset += size + 1 // "+1" for newline character + line = f.readLine() + } + } + return result } - private fun File.listNotHiddenFiles(): List = - listFiles()?.filter { !it.isHidden }.orEmpty() + private fun getValueResourceItem( + recordString: String, + offset: Long, + size: Long, + qualifiers: List, + path: Path + ) : ResourceItem { + val record = ValueResourceRecord.createFromString(recordString) + return ResourceItem(record.type, qualifiers, record.key.asUnderscoredIdentifier(), path, offset, size) + } } +internal fun File.listNotHiddenFiles(): List = + listFiles()?.filter { !it.isHidden }.orEmpty() + internal fun String.asUnderscoredIdentifier(): String = replace('-', '_') .let { if (it.isNotEmpty() && it.first().isDigit()) "_$it" else it } \ No newline at end of file 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/GeneratedResClassSpec.kt similarity index 89% rename from gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt rename to gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GeneratedResClassSpec.kt index 87e2d38dbd..23b541ef32 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/GeneratedResClassSpec.kt @@ -6,19 +6,18 @@ import java.nio.file.Path import java.util.* import kotlin.io.path.invariantSeparatorsPathString -internal enum class ResourceType(val typeName: String) { - DRAWABLE("drawable"), - STRING("string"), - PLURAL_STRING("plurals"), - FONT("font"); +internal enum class ResourceType(val typeName: String, val accessorName: String) { + DRAWABLE("drawable", "drawable"), + STRING("string", "string"), + STRING_ARRAY("string-array", "array"), + PLURAL_STRING("plurals", "plurals"), + FONT("font", "font"); override fun toString(): String = typeName companion object { - fun fromString(str: String): ResourceType = - ResourceType.values() - .firstOrNull { it.typeName.equals(str, true) } - ?: error("Unknown resource type: '$str'.") + fun fromString(str: String): ResourceType? = + ResourceType.values().firstOrNull { it.typeName.equals(str, true) } } } @@ -26,16 +25,22 @@ internal data class ResourceItem( val type: ResourceType, val qualifiers: List, val name: String, - val path: Path + val path: Path, + val offset: Long = -1, + val size: Long = -1, ) private fun ResourceType.getClassName(): ClassName = when (this) { ResourceType.DRAWABLE -> ClassName("org.jetbrains.compose.resources", "DrawableResource") + ResourceType.FONT -> ClassName("org.jetbrains.compose.resources", "FontResource") ResourceType.STRING -> ClassName("org.jetbrains.compose.resources", "StringResource") + ResourceType.STRING_ARRAY -> ClassName("org.jetbrains.compose.resources", "StringArrayResource") ResourceType.PLURAL_STRING -> ClassName("org.jetbrains.compose.resources", "PluralStringResource") - ResourceType.FONT -> ClassName("org.jetbrains.compose.resources", "FontResource") } +private fun ResourceType.requiresKeyName() = + this in setOf(ResourceType.STRING, ResourceType.STRING_ARRAY, ResourceType.PLURAL_STRING) + private val resourceItemClass = ClassName("org.jetbrains.compose.resources", "ResourceItem") private val experimentalAnnotation = AnnotationSpec.builder( ClassName("org.jetbrains.compose.resources", "ExperimentalResourceApi") @@ -156,7 +161,7 @@ internal fun getResFileSpecs( .build() ) ResourceType.values().forEach { type -> - resObject.addType(TypeSpec.objectBuilder(type.typeName).build()) + resObject.addType(TypeSpec.objectBuilder(type.accessorName).build()) } }.build()) }.build() @@ -191,7 +196,7 @@ private fun getChunkFileSpec( resModifier: KModifier, idToResources: Map> ): FileSpec { - val chunkClassName = type.typeName.uppercaseFirstChar() + index + val chunkClassName = type.accessorName.uppercaseFirstChar() + index return FileSpec.builder(packageName, chunkClassName).also { chunkFile -> chunkFile.addAnnotation( AnnotationSpec.builder(ClassName("kotlin", "OptIn")) @@ -213,7 +218,7 @@ private fun getChunkFileSpec( idToResources.forEach { (resName, items) -> val accessor = PropertySpec.builder(resName, type.getClassName(), resModifier) - .receiver(ClassName(packageName, "Res", type.typeName)) + .receiver(ClassName(packageName, "Res", type.accessorName)) .addAnnotation(experimentalAnnotation) .getter(FunSpec.getterBuilder().addStatement("return $chunkClassName.$resName").build()) .build() @@ -227,14 +232,15 @@ private fun getChunkFileSpec( CodeBlock.builder() .add("return %T(\n", type.getClassName()).withIndent { add("\"${type}:${resName}\",") - if (type == ResourceType.STRING || type == ResourceType.PLURAL_STRING) add(" \"$resName\",") + if (type.requiresKeyName()) add(" \"$resName\",") withIndent { add("\nsetOf(\n").withIndent { items.forEach { item -> add("%T(", resourceItemClass) add("setOf(").addQualifiers(item).add("), ") //file separator should be '/' on all platforms - add("\"$moduleDir${item.path.invariantSeparatorsPathString}\"") + add("\"$moduleDir${item.path.invariantSeparatorsPathString}\", ") + add("${item.offset}, ${item.size}") add("),\n") } } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/IosResources.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/IosResources.kt new file mode 100644 index 0000000000..0534a752f7 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/IosResources.kt @@ -0,0 +1,132 @@ +package org.jetbrains.compose.resources + +import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Copy +import org.jetbrains.compose.desktop.application.internal.ComposeProperties +import org.jetbrains.compose.internal.utils.* +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension +import org.jetbrains.kotlin.gradle.plugin.mpp.* +import org.jetbrains.kotlin.konan.target.KonanTarget +import java.io.File + +private const val COCOAPODS_PLUGIN_ID = "org.jetbrains.kotlin.native.cocoapods" +private const val IOS_COMPOSE_RESOURCES_ROOT_DIR = "compose-resources" + +internal fun Project.configureSyncIosComposeResources( + kotlinExtension: KotlinMultiplatformExtension +) { + if (ComposeProperties.dontSyncResources(project).get()) { + logger.info( + "Compose Multiplatform resource management for iOS is disabled: " + + "'${ComposeProperties.SYNC_RESOURCES_PROPERTY}' value is 'false'" + ) + return + } + + kotlinExtension.targets.withType(KotlinNativeTarget::class.java).all { nativeTarget -> + if (nativeTarget.isIosTarget()) { + nativeTarget.binaries.withType(Framework::class.java).all { iosFramework -> + val frameworkClassifier = iosFramework.getClassifier() + val checkNoSandboxTask = tasks.registerOrConfigure( + "checkCanSync${frameworkClassifier}ComposeResourcesForIos" + ) {} + + val frameworkResources = files() + iosFramework.compilation.allKotlinSourceSets.forAll { ss -> + frameworkResources.from(ss.resources.sourceDirectories) + } + val syncComposeResourcesTask = tasks.registerOrConfigure( + iosFramework.getSyncResourcesTaskName() + ) { + dependsOn(checkNoSandboxTask) + dependsOn(frameworkResources) //!!! explicit dependency because targetResources is not an input + + outputDir.set(iosFramework.getFinalResourcesDir()) + targetResources.put(iosFramework.target.konanTarget.name, frameworkResources) + } + + val externalTaskName = if (iosFramework.isCocoapodsFramework()) { + "syncFramework" + } else { + "embedAndSign${frameworkClassifier}AppleFrameworkForXcode" + } + + project.tasks.named(externalTaskName).dependsOn(syncComposeResourcesTask) + } + + nativeTarget.binaries.withType(TestExecutable::class.java).all { testExec -> + val copyTestResourcesTask = tasks.registerOrConfigure( + "copyTestComposeResourcesFor${testExec.target.targetName.uppercaseFirstChar()}" + ) { + from({ + (testExec.compilation.associatedCompilations + testExec.compilation).flatMap { compilation -> + compilation.allKotlinSourceSets.map { it.resources } + } + }) + into(testExec.outputDirectory.resolve(IOS_COMPOSE_RESOURCES_ROOT_DIR)) + } + testExec.linkTask.dependsOn(copyTestResourcesTask) + } + } + } + + plugins.withId(COCOAPODS_PLUGIN_ID) { + project.extensions.getByType(CocoapodsExtension::class.java).apply { + framework { podFramework -> + val syncDir = podFramework.getFinalResourcesDir().get().asFile.relativeTo(projectDir) + val specAttr = "['${syncDir.path}']" + extraSpecAttributes["resources"] = specAttr + project.tasks.named("podInstall").configure { + it.doFirst { + if (extraSpecAttributes["resources"] != specAttr) { + error(""" + |Kotlin.cocoapods.extraSpecAttributes["resources"] is not compatible with Compose Multiplatform's resources management for iOS. + | * Recommended action: remove extraSpecAttributes["resources"] from '${project.buildFile}' and run '${project.path}:podInstall' once; + | * Alternative action: turn off Compose Multiplatform's resources management for iOS by adding '${ComposeProperties.SYNC_RESOURCES_PROPERTY}=false' to your gradle.properties; + """.trimMargin()) + } + syncDir.mkdirs() + } + } + } + } + } +} + +private fun Framework.getClassifier(): String { + val suffix = joinLowerCamelCase(buildType.getName(), outputKind.taskNameClassifier) + return if (name == suffix) "" + else name.substringBeforeLast(suffix.uppercaseFirstChar()).uppercaseFirstChar() +} + +internal fun Framework.getSyncResourcesTaskName() = "sync${getClassifier()}ComposeResourcesForIos" +private fun Framework.isCocoapodsFramework() = name.startsWith("pod") + +private fun Framework.getFinalResourcesDir(): Provider { + val providers = project.providers + return if (isCocoapodsFramework()) { + project.layout.buildDirectory.dir("compose/ios/$baseName/$IOS_COMPOSE_RESOURCES_ROOT_DIR/") + } else { + providers.environmentVariable("BUILT_PRODUCTS_DIR") + .zip( + providers.environmentVariable("CONTENTS_FOLDER_PATH") + ) { builtProductsDir, contentsFolderPath -> + File("$builtProductsDir/$contentsFolderPath/$IOS_COMPOSE_RESOURCES_ROOT_DIR").canonicalPath + } + .flatMap { + project.objects.directoryProperty().apply { set(File(it)) } + } + } +} + +private fun KotlinNativeTarget.isIosSimulatorTarget(): Boolean = + konanTarget === KonanTarget.IOS_X64 || konanTarget === KonanTarget.IOS_SIMULATOR_ARM64 + +private fun KotlinNativeTarget.isIosDeviceTarget(): Boolean = + konanTarget === KonanTarget.IOS_ARM64 || konanTarget === KonanTarget.IOS_ARM32 + +private fun KotlinNativeTarget.isIosTarget(): Boolean = + isIosSimulatorTarget() || isIosDeviceTarget() \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/IosResourcesTasks.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/IosResourcesTasks.kt new file mode 100644 index 0000000000..90681d3166 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/IosResourcesTasks.kt @@ -0,0 +1,148 @@ +package org.jetbrains.compose.resources + +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileCollection +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.* +import org.gradle.api.tasks.* +import org.jetbrains.kotlin.konan.target.KonanTarget +import javax.inject.Inject + +internal abstract class SyncComposeResourcesForIosTask : DefaultTask() { + + private fun Provider.orElseThrowMissingAttributeError(attribute: String): Provider { + val noProvidedValue = "__NO_PROVIDED_VALUE__" + return this.orElse(noProvidedValue).map { + if (it == noProvidedValue) { + error( + "Could not infer iOS target $attribute. Make sure to build " + + "via XCode (directly or via Kotlin Multiplatform Mobile plugin for Android Studio)" + ) + } + it + } + } + + @get:Inject + protected abstract val providers: ProviderFactory + + @get:Inject + protected abstract val objects: ObjectFactory + + @get:Input + val xcodeTargetPlatform: Provider = + providers.gradleProperty("compose.ios.resources.platform") + .orElse(providers.environmentVariable("PLATFORM_NAME")) + .orElseThrowMissingAttributeError("platform") + + + @get:Input + val xcodeTargetArchs: Provider> = + providers.gradleProperty("compose.ios.resources.archs") + .orElse(providers.environmentVariable("ARCHS")) + .orElseThrowMissingAttributeError("architectures") + .map { str -> str.split(",", " ").filter { it.isNotBlank() } } + + @get:Internal + internal abstract val targetResources: MapProperty + + @get:PathSensitive(PathSensitivity.ABSOLUTE) + @get:InputFiles + val resourceFiles: Provider = + xcodeTargetPlatform.zip(xcodeTargetArchs, ::Pair).map { (xcodeTargetPlatform, xcodeTargetArchs) -> + val allResources = getRequestedKonanTargetsByXcode(xcodeTargetPlatform, xcodeTargetArchs) + .mapNotNull { konanTarget -> targetResources.getting(konanTarget.name).get() } + objects.fileCollection().from(*allResources.toTypedArray()) + } + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @TaskAction + fun run() { + val outputDir = outputDir.get().asFile + outputDir.deleteRecursively() + outputDir.mkdirs() + logger.info("Clean ${outputDir.path}") + + resourceFiles.get().forEach { dir -> + if (dir.exists() && dir.isDirectory) { + logger.info("Copy '${dir.path}' to '${outputDir.path}'") + dir.walkTopDown().filter { !it.isDirectory && !it.isHidden }.forEach { file -> + val targetFile = outputDir.resolve(file.relativeTo(dir)) + if (targetFile.exists()) { + logger.info("Skip [already exists] '${file.path}'") + } else { + logger.info(" -> '${file.path}'") + file.copyTo(targetFile) + } + } + } else { + logger.info("File '${dir.path}' is not a dir or doesn't exist") + } + } + } +} + +// based on AppleSdk.kt from Kotlin Gradle Plugin +// See https://github.com/JetBrains/kotlin/blob/142421da5b966049b4eab44ce6856eb172cf122a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/AppleSdk.kt +private fun getRequestedKonanTargetsByXcode(platform: String, archs: List): List { + val targets: MutableSet = mutableSetOf() + + when { + platform.startsWith("iphoneos") -> { + targets.addAll(archs.map { arch -> + when (arch) { + "arm64", "arm64e" -> KonanTarget.IOS_ARM64 + "armv7", "armv7s" -> KonanTarget.IOS_ARM32 + else -> error("Unknown iOS device arch: '$arch'") + } + }) + } + + platform.startsWith("iphonesimulator") -> { + targets.addAll(archs.map { arch -> + when (arch) { + "arm64", "arm64e" -> KonanTarget.IOS_SIMULATOR_ARM64 + "x86_64" -> KonanTarget.IOS_X64 + else -> error("Unknown iOS simulator arch: '$arch'") + } + }) + } + + else -> error("Unknown iOS platform: '$platform'") + } + + return targets.toList() +} + +/** + * Since Xcode 15, there is a new default setting: `ENABLE_USER_SCRIPT_SANDBOXING = YES` + * It's set in project.pbxproj + * + * SyncComposeResourcesForIosTask fails to work with it right now. + * + * Gradle attempts to create an output folder for SyncComposeResourcesForIosTask on our behalf, + * so we can't handle an exception when it occurs. Therefore, we make SyncComposeResourcesForIosTask + * depend on CheckCanAccessComposeResourcesDirectory, where we check ENABLE_USER_SCRIPT_SANDBOXING. + */ +internal abstract class CheckCanAccessComposeResourcesDirectory : DefaultTask() { + @get:Input + val enabled = project.providers.environmentVariable("ENABLE_USER_SCRIPT_SANDBOXING") + .orElse("NOT_DEFINED") + .map { it == "YES" } + + @TaskAction + fun run() { + if (enabled.get()) { + logger.error(""" + Failed to sync compose resources! + Please make sure ENABLE_USER_SCRIPT_SANDBOXING is set to 'NO' in 'project.pbxproj' + """.trimIndent()) + throw IllegalStateException( + "Sandbox environment detected (ENABLE_USER_SCRIPT_SANDBOXING = YES). It's not supported so far." + ) + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..a2df7e50b1 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/KmpResources.kt @@ -0,0 +1,123 @@ +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, + preparedCommonResources: Provider, + config: Provider +) { + kotlinExtension as KotlinMultiplatformExtension + kmpResources as KotlinTargetResourcesPublication + + logger.info("Configure KMP resources") + + //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) + + kmpResources.publishResourcesAsKotlinComponent( + target, + { sourceSet -> + val sourceSetName = sourceSet.name + val composeResDir: Provider + if (sourceSetName == KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME) { + composeResDir = preparedCommonResources + } else { + composeResDir = provider { project.file("src/$sourceSetName/$COMPOSE_RESOURCES_DIR") } + } + + KotlinTargetResourcesPublication.ResourceRoot( + composeResDir, + emptyList(), + //for android target exclude fonts + if (target is KotlinAndroidTarget) listOf("**/font*/*") else emptyList() + ) + }, + packedResourceDir + ) + + if (target is KotlinAndroidTarget) { + //for android target publish fonts in assets + logger.info("Configure fonts relocation for '${target.targetName}' target") + kmpResources.publishInAndroidAssets( + target, + { sourceSet -> + val sourceSetName = sourceSet.name + val composeResDir: Provider + if (sourceSetName == KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME) { + composeResDir = preparedCommonResources + } else { + composeResDir = provider { project.file("src/$sourceSetName/$COMPOSE_RESOURCES_DIR") } + } + KotlinTargetResourcesPublication.ResourceRoot( + composeResDir, + listOf("**/font*/*"), + emptyList() + ) + }, + packedResourceDir + ) + } + } + + //generate accessors for common resources + kotlinExtension.sourceSets.all { sourceSet -> + val sourceSetName = sourceSet.name + if (sourceSetName == KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME) { + configureGenerationComposeResClass( + preparedCommonResources, + sourceSet, + config, + true + ) + } + } + + //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/PrepareComposeResources.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/PrepareComposeResources.kt new file mode 100644 index 0000000000..b50bbdb22e --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/PrepareComposeResources.kt @@ -0,0 +1,235 @@ +package org.jetbrains.compose.resources + +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.* +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.* +import org.w3c.dom.Node +import java.io.File +import java.util.* +import javax.inject.Inject +import javax.xml.parsers.DocumentBuilderFactory + +internal fun Project.registerPrepareComposeResourcesTask( + userComposeResourcesDir: File, + preparedComposeResourcesDir: Provider +): TaskProvider { + val convertXmlValueResources = tasks.register( + "convertXmlValueResources", + XmlValuesConverterTask::class.java + ) { task -> + task.originalResourcesDir.set(userComposeResourcesDir) + task.outputDir.set(preparedComposeResourcesDir) + } + + val copyNonXmlValueResources = tasks.register( + "copyNonXmlValueResources", + CopyNonXmlValueResourcesTask::class.java + ) { task -> + task.originalResourcesDir.set(userComposeResourcesDir) + task.outputDir.set(preparedComposeResourcesDir) + } + + val prepareComposeResourcesTask = tasks.register( + "prepareComposeResourcesTask", + PrepareComposeResourcesTask::class.java + ) { task -> + task.convertedXmls.set(convertXmlValueResources.map { it.realOutputFiles.get() }) + task.copiedNonXmls.set(copyNonXmlValueResources.map { it.realOutputFiles.get() }) + task.outputDir.set(preparedComposeResourcesDir.map { it.asFile }) + } + + return prepareComposeResourcesTask +} + +internal abstract class CopyNonXmlValueResourcesTask : DefaultTask() { + @get:Inject + abstract val fileSystem: FileSystemOperations + + @get:Internal + abstract val originalResourcesDir: DirectoryProperty + + @get:InputFiles + val realInputFiles = originalResourcesDir.map { dir -> + dir.asFileTree.matching { it.exclude("values*/*.xml") } + } + + @get:Internal + abstract val outputDir: DirectoryProperty + + @get:OutputFiles + val realOutputFiles = outputDir.map { dir -> + dir.asFileTree.matching { it.exclude("values*/*.${XmlValuesConverterTask.CONVERTED_RESOURCE_EXT}") } + } + + @TaskAction + fun run() { + realOutputFiles.get().forEach { f -> f.delete() } + fileSystem.copy { copy -> + copy.includeEmptyDirs = false + copy.from(originalResourcesDir) { + it.exclude("values*/*.xml") + } + copy.into(outputDir) + } + } +} + +internal abstract class PrepareComposeResourcesTask : DefaultTask() { + @get:InputFiles + abstract val convertedXmls: Property + + @get:InputFiles + abstract val copiedNonXmls: Property + + @get:OutputDirectory + abstract val outputDir: Property + + @TaskAction + fun run() {} +} + +internal data class ValueResourceRecord( + val type: ResourceType, + val key: String, + val content: String +) { + fun getAsString(): String { + return listOf(type.typeName, key, content).joinToString(SEPARATOR) + } + + companion object { + private const val SEPARATOR = "|" + fun createFromString(string: String): ValueResourceRecord { + val parts = string.split(SEPARATOR) + return ValueResourceRecord( + ResourceType.fromString(parts[0])!!, + parts[1], + parts[2] + ) + } + } +} + +internal abstract class XmlValuesConverterTask : DefaultTask() { + companion object { + const val CONVERTED_RESOURCE_EXT = "cvr" //Compose Value Resource + private const val FORMAT_VERSION = 0 + } + + @get:Internal + abstract val originalResourcesDir: DirectoryProperty + + @get:InputFiles + val realInputFiles = originalResourcesDir.map { dir -> + dir.asFileTree.matching { it.include("values*/*.xml") } + } + + @get:Internal + abstract val outputDir: DirectoryProperty + + @get:OutputFiles + val realOutputFiles = outputDir.map { dir -> + dir.asFileTree.matching { it.include("values*/*.$CONVERTED_RESOURCE_EXT") } + } + + @TaskAction + fun run() { + val outDir = outputDir.get().asFile + realOutputFiles.get().forEach { f -> f.delete() } + originalResourcesDir.get().asFile.listNotHiddenFiles().forEach { valuesDir -> + if (valuesDir.isDirectory && valuesDir.name.startsWith("values")) { + valuesDir.listNotHiddenFiles().forEach { f -> + if (f.extension.equals("xml", true)) { + val output = outDir + .resolve(f.parentFile.name) + .resolve(f.nameWithoutExtension + ".$CONVERTED_RESOURCE_EXT") + output.parentFile.mkdirs() + convert(f, output) + } + } + } + } + } + + private fun convert(original: File, converted: File) { + val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(original) + val items = doc.getElementsByTagName("resources").item(0).childNodes + val records = List(items.length) { items.item(it) }.mapNotNull { getItemRecord(it)?.getAsString() } + val fileContent = buildString { + appendLine("version:$FORMAT_VERSION") + records.sorted().forEach { appendLine(it) } + } + converted.writeText(fileContent) + } + + private fun getItemRecord(node: Node): ValueResourceRecord? { + val type = ResourceType.fromString(node.nodeName) ?: return null + val key = node.attributes.getNamedItem("name").nodeValue + val value: String + when (type) { + ResourceType.STRING -> { + val content = handleSpecialCharacters(node.textContent) + value = content.asBase64() + } + + ResourceType.STRING_ARRAY -> { + val children = node.childNodes + value = List(children.length) { children.item(it) } + .filter { it.nodeName == "item" } + .joinToString(",") { child -> + val content = handleSpecialCharacters(child.textContent) + content.asBase64() + } + } + + ResourceType.PLURAL_STRING -> { + val children = node.childNodes + value = List(children.length) { children.item(it) } + .filter { it.nodeName == "item" } + .joinToString(",") { child -> + val content = handleSpecialCharacters(child.textContent) + val quantity = child.attributes.getNamedItem("quantity").nodeValue + quantity.uppercase() + ":" + content.asBase64() + } + } + else -> error("Unknown string resource type: '$type'") + } + return ValueResourceRecord(type, key, value) + } + + private fun String.asBase64() = + Base64.getEncoder().encode(this.encodeToByteArray()).decodeToString() +} + +//https://developer.android.com/guide/topics/resources/string-resource#escaping_quotes +/** + * Replaces + * + * '\n' -> new line + * + * '\t' -> tab + * + * '\uXXXX' -> unicode symbol + * + * '\\' -> '\' + * + * @param string The input string to handle. + * @return The string with special characters replaced according to the logic. + */ +internal fun handleSpecialCharacters(string: String): String { + val unicodeNewLineTabRegex = Regex("""\\u[a-fA-F\d]{4}|\\n|\\t""") + val doubleSlashRegex = Regex("""\\\\""") + val doubleSlashIndexes = doubleSlashRegex.findAll(string).map { it.range.first } + val handledString = unicodeNewLineTabRegex.replace(string) { matchResult -> + if (doubleSlashIndexes.contains(matchResult.range.first - 1)) matchResult.value + else when (matchResult.value) { + "\\n" -> "\n" + "\\t" -> "\t" + else -> matchResult.value.substring(2).toInt(16).toChar().toString() + } + }.replace("""\\""", """\""") + return handledString +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResClassGeneration.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResClassGeneration.kt new file mode 100644 index 0000000000..c8f213f58d --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResClassGeneration.kt @@ -0,0 +1,65 @@ +package org.jetbrains.compose.resources + +import org.gradle.api.Project +import org.gradle.api.provider.Provider +import org.jetbrains.compose.ComposePlugin +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import java.io.File + +internal fun Project.configureGenerationComposeResClass( + commonComposeResourcesDir: Provider, + commonSourceSet: KotlinSourceSet, + config: Provider, + generateModulePath: Boolean +) { + logger.info("Configure accessors for '${commonSourceSet.name}'") + + //lazy check a dependency on the Resources library + val shouldGenerateResClass = config.map { + when (it.generateResClass) { + ResourcesExtension.ResourceClassGeneration.Auto -> { + configurations.run { + //because the implementation configuration doesn't extend the api in the KGP ¯\_(ツ)_/¯ + getByName(commonSourceSet.implementationConfigurationName).allDependencies + + getByName(commonSourceSet.apiConfigurationName).allDependencies + }.any { dep -> + val depStringNotation = dep.let { "${it.group}:${it.name}:${it.version}" } + depStringNotation == ComposePlugin.CommonComponentsDependencies.resources + } + } + + ResourcesExtension.ResourceClassGeneration.Always -> { + true + } + + ResourcesExtension.ResourceClassGeneration.Never -> { + false + } + } + } + + val genTask = tasks.register( + "generateComposeResClass", + GenerateResClassTask::class.java + ) { task -> + task.packageName.set(config.getResourcePackage(project)) + task.shouldGenerateResClass.set(shouldGenerateResClass) + task.makeResClassPublic.set(config.map { it.publicResClass }) + task.resDir.set(commonComposeResourcesDir) + task.codeDir.set(layout.buildDirectory.dir("$RES_GEN_DIR/kotlin").map { it.asFile }) + + if (generateModulePath) { + task.moduleDir.set(config.getModuleResourcesDir(project)) + } + } + + //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) + } + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesExtension.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesDSL.kt similarity index 61% rename from gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesExtension.kt rename to gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesDSL.kt index f50dae4624..17abaf96ff 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesExtension.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesDSL.kt @@ -1,5 +1,9 @@ package org.jetbrains.compose.resources +import org.gradle.api.Project +import org.gradle.api.provider.Provider +import java.io.File + abstract class ResourcesExtension { /** * Whether the generated resources accessors class should be public or not. @@ -32,4 +36,16 @@ abstract class ResourcesExtension { * - `never`: Never generate the Res class. */ var generateResClass: ResourceClassGeneration = auto -} \ No newline at end of file +} + +internal fun Provider.getResourcePackage(project: Project) = map { config -> + config.packageOfResClass.takeIf { it.isNotEmpty() } ?: run { + val groupName = project.group.toString().lowercase().asUnderscoredIdentifier() + val moduleName = project.name.lowercase().asUnderscoredIdentifier() + val id = if (groupName.isNotEmpty()) "$groupName.$moduleName" else moduleName + "$id.generated.resources" + } +} +//the dir where resources must be placed in the final artefact +internal fun Provider.getModuleResourcesDir(project: Project) = + getResourcePackage(project).map { packageName -> File("$COMPOSE_RESOURCES_DIR/$packageName") } 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 deleted file mode 100644 index ff90790463..0000000000 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesGenerator.kt +++ /dev/null @@ -1,381 +0,0 @@ -package org.jetbrains.compose.resources - -import com.android.build.api.variant.AndroidComponentsExtension -import com.android.build.gradle.BaseExtension -import org.gradle.api.DefaultTask -import org.gradle.api.Project -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.file.FileSystemOperations -import org.gradle.api.provider.Property -import org.gradle.api.provider.Provider -import org.gradle.api.tasks.* -import org.gradle.util.GradleVersion -import org.jetbrains.compose.ComposePlugin -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.compose.internal.utils.registerTask -import org.jetbrains.compose.internal.utils.uppercaseFirstChar -import org.jetbrains.compose.resources.ios.getSyncResourcesTaskName -import org.jetbrains.kotlin.gradle.ComposeKotlinGradlePluginApi -import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi -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 org.jetbrains.kotlin.gradle.plugin.sources.android.androidSourceSetInfoOrNull -import org.jetbrains.kotlin.gradle.utils.ObservableSet -import java.io.File -import javax.inject.Inject - -private const val COMPOSE_RESOURCES_DIR = "composeResources" -private const val RES_GEN_DIR = "generated/compose/resourceGenerator" -private const val KMP_RES_EXT = "multiplatformResourcesPublication" -private const val MIN_GRADLE_VERSION_FOR_KMP_RESOURCES = "7.6" -private val androidPluginIds = listOf( - "com.android.application", - "com.android.library" -) - -internal fun Project.configureComposeResources(config: ResourcesExtension) { - val resourcePackage = provider { - config.packageOfResClass.takeIf { it.isNotEmpty() } ?: run { - val groupName = project.group.toString().lowercase().asUnderscoredIdentifier() - val moduleName = project.name.lowercase().asUnderscoredIdentifier() - val id = if (groupName.isNotEmpty()) "$groupName.$moduleName" else moduleName - "$id.generated.resources" - } - } - - val publicResClass = provider { config.publicResClass } - - val generateResClassMode = provider { config.generateResClass } - - plugins.withId(KOTLIN_MPP_PLUGIN_ID) { - val kotlinExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) - - val hasKmpResources = extraProperties.has(KMP_RES_EXT) - val currentGradleVersion = GradleVersion.current() - val minGradleVersion = GradleVersion.version(MIN_GRADLE_VERSION_FOR_KMP_RESOURCES) - if (hasKmpResources && currentGradleVersion >= minGradleVersion) { - configureKmpResources( - kotlinExtension, - extraProperties.get(KMP_RES_EXT)!!, - resourcePackage, - publicResClass, - generateResClassMode - ) - } else { - if (!hasKmpResources) { - logger.info( - """ - Compose resources publication requires Kotlin Gradle Plugin >= 2.0 - Current Kotlin Gradle Plugin is ${KotlinVersion.CURRENT} - """.trimIndent() - ) - } - if (currentGradleVersion < minGradleVersion) { - logger.info( - """ - Compose resources publication requires Gradle >= $MIN_GRADLE_VERSION_FOR_KMP_RESOURCES - Current Gradle is ${currentGradleVersion.version} - """.trimIndent() - ) - } - - //current KGP doesn't have KPM resources - configureComposeResources( - kotlinExtension, - KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME, - resourcePackage, - publicResClass, - generateResClassMode - ) - - //when applied AGP then configure android resources - androidPluginIds.forEach { pluginId -> - plugins.withId(pluginId) { - val androidExtension = project.extensions.getByType(BaseExtension::class.java) - configureAndroidComposeResources(kotlinExtension, androidExtension) - } - } - } - } - plugins.withId(KOTLIN_JVM_PLUGIN_ID) { - val kotlinExtension = project.extensions.getByType(KotlinProjectExtension::class.java) - configureComposeResources( - kotlinExtension, - SourceSet.MAIN_SOURCE_SET_NAME, - resourcePackage, - publicResClass, - generateResClassMode - ) - } -} - -private fun Project.configureComposeResources( - kotlinExtension: KotlinProjectExtension, - commonSourceSetName: String, - resourcePackage: Provider, - publicResClass: Provider, - generateResClassMode: Provider -) { - logger.info("Configure compose resources") - kotlinExtension.sourceSets.all { sourceSet -> - val sourceSetName = sourceSet.name - val composeResourcesPath = project.projectDir.resolve("src/$sourceSetName/$COMPOSE_RESOURCES_DIR") - - //To compose resources will be packed to a final artefact we need to mark them as resources - //sourceSet.resources works for all targets except ANDROID! - sourceSet.resources.srcDirs(composeResourcesPath) - - if (sourceSetName == commonSourceSetName) { - configureResourceGenerator( - composeResourcesPath, - sourceSet, - resourcePackage, - publicResClass, - generateResClassMode, - false - ) - } - } -} - -@OptIn(ComposeKotlinGradlePluginApi::class) -private fun Project.configureKmpResources( - kotlinExtension: KotlinProjectExtension, - kmpResources: Any, - resourcePackage: Provider, - publicResClass: Provider, - generateResClassMode: Provider -) { - kotlinExtension as KotlinMultiplatformExtension - kmpResources as KotlinTargetResourcesPublication - - logger.info("Configure KMP resources") - - //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") - kmpResources.publishResourcesAsKotlinComponent( - target, - { sourceSet -> - KotlinTargetResourcesPublication.ResourceRoot( - project.provider { project.file("src/${sourceSet.name}/$COMPOSE_RESOURCES_DIR") }, - emptyList(), - //for android target exclude fonts - if (target is KotlinAndroidTarget) listOf("**/font*/*") else emptyList() - ) - }, - resourcePackage.asModuleDir() - ) - - if (target is KotlinAndroidTarget) { - //for android target publish fonts in assets - logger.info("Configure fonts relocation for '${target.targetName}' target") - kmpResources.publishInAndroidAssets( - target, - { sourceSet -> - KotlinTargetResourcesPublication.ResourceRoot( - project.provider { project.file("src/${sourceSet.name}/$COMPOSE_RESOURCES_DIR") }, - listOf("**/font*/*"), - emptyList() - ) - }, - resourcePackage.asModuleDir() - ) - } - } - - //generate accessors for common resources - kotlinExtension.sourceSets.all { sourceSet -> - val sourceSetName = sourceSet.name - if (sourceSetName == KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME) { - val composeResourcesPath = project.projectDir.resolve("src/$sourceSetName/$COMPOSE_RESOURCES_DIR") - configureResourceGenerator( - composeResourcesPath, - sourceSet, - resourcePackage, - publicResClass, - generateResClassMode, - true - ) - } - } - - //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) - if (compilation is KotlinJsCompilation) { - tasks.named(compilation.processResourcesTaskName).configure { processResourcesTask -> - processResourcesTask.dependsOn(directoryWithAllResourcesForCompilation) - } - } - if (compilation is KotlinNativeCompilation) { - compilation.target.binaries.withType(Framework::class.java).all { framework -> - tasks.configureEach { task -> - if (task.name == framework.getSyncResourcesTaskName()) { - task.dependsOn(directoryWithAllResourcesForCompilation) - } - } - } - } -} - -@OptIn(ExperimentalKotlinGradlePluginApi::class) -private fun Project.configureAndroidComposeResources( - kotlinExtension: KotlinMultiplatformExtension, - androidExtension: BaseExtension -) { - //mark all composeResources as Android resources - kotlinExtension.targets.withType(KotlinAndroidTarget::class.java).all { androidTarget -> - androidTarget.compilations.all { compilation: KotlinJvmAndroidCompilation -> - compilation.defaultSourceSet.androidSourceSetInfoOrNull?.let { kotlinAndroidSourceSet -> - androidExtension.sourceSets - .matching { it.name == kotlinAndroidSourceSet.androidSourceSetName } - .all { androidSourceSet -> - (compilation.allKotlinSourceSets as? ObservableSet)?.forAll { kotlinSourceSet -> - androidSourceSet.resources.srcDir( - projectDir.resolve("src/${kotlinSourceSet.name}/$COMPOSE_RESOURCES_DIR") - ) - } - } - } - } - } - - //copy fonts from the compose resources dir to android assets - val commonResourcesDir = projectDir.resolve( - "src/${KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME}/$COMPOSE_RESOURCES_DIR" - ) - val androidComponents = project.extensions.findByType(AndroidComponentsExtension::class.java) ?: return - androidComponents.onVariants { variant -> - val copyFonts = registerTask( - "copy${variant.name.uppercaseFirstChar()}FontsToAndroidAssets" - ) { - from.set(commonResourcesDir) - } - variant.sources?.assets?.addGeneratedSourceDirectory( - taskProvider = copyFonts, - wiredWith = CopyAndroidFontsToAssetsTask::outputDirectory - ) - //exclude a duplication of fonts in apks - variant.packaging.resources.excludes.add("**/font*/*") - } -} - -private fun Project.configureResourceGenerator( - commonComposeResourcesDir: File, - commonSourceSet: KotlinSourceSet, - resourcePackage: Provider, - publicResClass: Provider, - generateResClassMode: Provider, - generateModulePath: Boolean -) { - logger.info("Configure accessors for '${commonSourceSet.name}'") - - fun buildDir(path: String) = layout.dir(layout.buildDirectory.map { File(it.asFile, path) }) - - //lazy check a dependency on the Resources library - val shouldGenerateResClass = generateResClassMode.map { mode -> - when (mode) { - ResourcesExtension.ResourceClassGeneration.Auto -> { - //todo remove the gradle property when the gradle plugin will be published - if (ComposeProperties.alwaysGenerateResourceAccessors(project).get()) { - true - } else { - configurations.run { - //because the implementation configuration doesn't extend the api in the KGP ¯\_(ツ)_/¯ - getByName(commonSourceSet.implementationConfigurationName).allDependencies + - getByName(commonSourceSet.apiConfigurationName).allDependencies - }.any { dep -> - val depStringNotation = dep.let { "${it.group}:${it.name}:${it.version}" } - depStringNotation == ComposePlugin.CommonComponentsDependencies.resources - } - } - } - ResourcesExtension.ResourceClassGeneration.Always -> { - true - } - ResourcesExtension.ResourceClassGeneration.Never -> { - false - } - } - } - - val genTask = tasks.register( - "generateComposeResClass", - GenerateResClassTask::class.java - ) { task -> - task.packageName.set(resourcePackage) - task.shouldGenerateResClass.set(shouldGenerateResClass) - task.makeResClassPublic.set(publicResClass) - task.resDir.set(commonComposeResourcesDir) - task.codeDir.set(buildDir("$RES_GEN_DIR/kotlin")) - - if (generateModulePath) { - task.moduleDir.set(resourcePackage.asModuleDir()) - } - } - - //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) - } - } -} - -private fun Provider.asModuleDir() = map { File("$COMPOSE_RESOURCES_DIR/$it") } - -//Copy task doesn't work with 'variant.sources?.assets?.addGeneratedSourceDirectory' API -internal abstract class CopyAndroidFontsToAssetsTask : DefaultTask() { - @get:Inject - abstract val fileSystem: FileSystemOperations - - @get:InputFiles - @get:IgnoreEmptyDirectories - abstract val from: Property - - @get:OutputDirectory - abstract val outputDirectory: DirectoryProperty - - @TaskAction - fun action() { - fileSystem.copy { - it.includeEmptyDirs = false - it.from(from) - it.include("**/font*/*") - it.into(outputDirectory) - } - } -} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/IosTargetResources.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/IosTargetResources.kt deleted file mode 100644 index 4edad4d9db..0000000000 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/IosTargetResources.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2020-2023 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.resources.ios - -import org.gradle.api.provider.Property -import org.gradle.api.provider.SetProperty -import org.gradle.api.tasks.Input -import java.io.ObjectInputStream -import java.io.ObjectOutputStream -import java.io.Serializable - -internal abstract class IosTargetResources : Serializable { - @get:Input - abstract val name: Property - - @get:Input - abstract val konanTarget: Property - - @get:Input - abstract val dirs: SetProperty - - @Suppress("unused") // used by Gradle Configuration Cache - fun readObject(input: ObjectInputStream) { - name.set(input.readUTF()) - konanTarget.set(input.readUTF()) - dirs.set(input.readUTFStrings()) - } - - @Suppress("unused") // used by Gradle Configuration Cache - fun writeObject(output: ObjectOutputStream) { - output.writeUTF(name.get()) - output.writeUTF(konanTarget.get()) - output.writeUTFStrings(dirs.get()) - } - - private fun ObjectOutputStream.writeUTFStrings(collection: Collection) { - writeInt(collection.size) - collection.forEach { writeUTF(it) } - } - - private fun ObjectInputStream.readUTFStrings(): Set { - val size = readInt() - return LinkedHashSet(size).apply { - repeat(size) { - add(readUTF()) - } - } - } - - companion object { - private const val serialVersionUID: Long = 0 - } -} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/SyncComposeResourcesForIosTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/SyncComposeResourcesForIosTask.kt deleted file mode 100644 index 6df77666cf..0000000000 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/SyncComposeResourcesForIosTask.kt +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2020-2023 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.resources.ios - -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.file.FileCollection -import org.gradle.api.provider.Provider -import org.gradle.api.provider.SetProperty -import org.gradle.api.tasks.* -import org.jetbrains.compose.experimental.uikit.tasks.AbstractComposeIosTask -import org.jetbrains.compose.internal.utils.clearDirs -import java.io.File -import kotlin.io.path.Path -import kotlin.io.path.pathString -import kotlin.io.path.relativeTo - -abstract class SyncComposeResourcesForIosTask : AbstractComposeIosTask() { - - private fun Provider.orElseThrowMissingAttributeError(attribute: String): Provider { - val noProvidedValue = "__NO_PROVIDED_VALUE__" - return this.orElse(noProvidedValue).map { - if (it == noProvidedValue) { - error( - "Could not infer iOS target $attribute. Make sure to build " + - "via XCode (directly or via Kotlin Multiplatform Mobile plugin for Android Studio)") - } - it - } - } - - @get:Input - val xcodeTargetPlatform: Provider = - providers.gradleProperty("compose.ios.resources.platform") - .orElse(providers.environmentVariable("PLATFORM_NAME")) - .orElseThrowMissingAttributeError("platform") - - - @get:Input - val xcodeTargetArchs: Provider> = - providers.gradleProperty("compose.ios.resources.archs") - .orElse(providers.environmentVariable("ARCHS")) - .orElseThrowMissingAttributeError("architectures") - .map { - it.split(",", " ").filter { it.isNotBlank() } - } - - @get:Input - internal val iosTargets: SetProperty = objects.setProperty(IosTargetResources::class.java) - - @get:PathSensitive(PathSensitivity.ABSOLUTE) - @get:InputFiles - val resourceFiles: Provider = xcodeTargetPlatform.zip(xcodeTargetArchs, ::Pair) - .map { (xcodeTargetPlatform, xcodeTargetArchs) -> - val allResources = objects.fileCollection() - val activeKonanTargets = determineIosKonanTargetsFromEnv(xcodeTargetPlatform, xcodeTargetArchs) - .mapTo(HashSet()) { it.name } - val dirsToInclude = iosTargets.get() - .filter { it.konanTarget.get() in activeKonanTargets } - .flatMapTo(HashSet()) { it.dirs.get() } - for (dirPath in dirsToInclude) { - val fileTree = objects.fileTree().apply { - setDir(layout.projectDirectory.dir(dirPath)) - include("**/*") - } - allResources.from(fileTree) - } - allResources - } - - @get:OutputDirectory - val outputDir: DirectoryProperty = objects.directoryProperty() - - @TaskAction - fun run() { - val outputDir = outputDir.get().asFile - fileOperations.clearDirs(outputDir) - val allResourceDirs = iosTargets.get().flatMapTo(HashSet()) { it.dirs.get().map { Path(it).toAbsolutePath() } } - - fun copyFileToOutputDir(file: File) { - for (dir in allResourceDirs) { - val path = file.toPath().toAbsolutePath() - if (path.startsWith(dir)) { - val targetFile = outputDir.resolve(path.relativeTo(dir).pathString) - file.copyTo(targetFile, overwrite = true) - return - } - } - - error( - buildString { - appendLine("Resource file '$file' does not belong to a known resource directory:") - allResourceDirs.forEach { - appendLine("* $it") - } - } - ) - } - - val resourceFiles = resourceFiles.get().files - for (file in resourceFiles) { - copyFileToOutputDir(file) - } - logger.info("Synced Compose resource files. Copied ${resourceFiles.size} files to $outputDir") - } -} diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/configureSyncIosResources.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/configureSyncIosResources.kt deleted file mode 100644 index 3bdae41086..0000000000 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/configureSyncIosResources.kt +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Copyright 2020-2023 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.resources.ios - -import org.gradle.api.Project -import org.gradle.api.file.Directory -import org.gradle.api.provider.Provider -import org.gradle.api.tasks.Copy -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.TaskContainer -import org.gradle.api.tasks.TaskAction -import org.jetbrains.compose.desktop.application.internal.ComposeProperties -import org.jetbrains.compose.experimental.uikit.internal.utils.asIosNativeTargetOrNull -import org.jetbrains.compose.experimental.uikit.internal.utils.cocoapodsExt -import org.jetbrains.compose.experimental.uikit.internal.utils.withCocoapodsPlugin -import org.jetbrains.compose.experimental.uikit.tasks.AbstractComposeIosTask -import org.jetbrains.compose.internal.utils.joinLowerCamelCase -import org.jetbrains.compose.internal.utils.new -import org.jetbrains.compose.internal.utils.registerOrConfigure -import org.jetbrains.compose.internal.utils.uppercaseFirstChar -import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -import org.jetbrains.kotlin.gradle.plugin.mpp.* -import java.io.File - -private val incompatiblePlugins = listOf( - "dev.icerock.mobile.multiplatform-resources", - "io.github.skeptick.libres", -) - -private const val IOS_COMPOSE_RESOURCES_ROOT_DIR = "compose-resources" - -internal fun Project.configureSyncTask(mppExt: KotlinMultiplatformExtension) { - fun reportSyncIsDisabled(reason: String) { - logger.info("Compose Multiplatform resource management for iOS is disabled: $reason") - } - - if (!ComposeProperties.syncResources(providers).get()) { - reportSyncIsDisabled("'${ComposeProperties.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() - configureCocoapodsResourcesAttribute() - } -} - -private class SyncIosResourcesContext( - val project: Project, - val mppExt: KotlinMultiplatformExtension -) { - fun syncDirFor(framework: Framework): Provider { - val providers = framework.project.providers - return if (framework.isCocoapodsFramework) { - project.layout.buildDirectory.dir("compose/ios/${framework.baseName}/$IOS_COMPOSE_RESOURCES_ROOT_DIR/") - } else { - providers.environmentVariable("BUILT_PRODUCTS_DIR") - .zip(providers.environmentVariable("CONTENTS_FOLDER_PATH")) { builtProductsDir, contentsFolderPath -> - File(builtProductsDir) - .resolve(contentsFolderPath) - .resolve(IOS_COMPOSE_RESOURCES_ROOT_DIR) - .canonicalPath - }.flatMap { - framework.project.objects.directoryProperty().apply { set(File(it)) } - } - } - } - - fun configureEachIosTestExecutable(fn: (TestExecutable) -> Unit) { - mppExt.targets.all { target -> - target.asIosNativeTargetOrNull()?.let { iosTarget -> - iosTarget.binaries.withType(TestExecutable::class.java).configureEach { bin -> - fn(bin) - } - } - } - } - - fun configureEachIosFramework(fn: (Framework) -> Unit) { - mppExt.targets.all { target -> - target.asIosNativeTargetOrNull()?.let { iosTarget -> - iosTarget.binaries.withType(Framework::class.java).configureEach { framework -> - fn(framework) - } - } - } - } -} - -private const val RESOURCES_SPEC_ATTR = "resources" -private fun SyncIosResourcesContext.configureCocoapodsResourcesAttribute() { - project.withCocoapodsPlugin { - project.gradle.taskGraph.whenReady { - val cocoapodsExt = mppExt.cocoapodsExt - val specAttributes = cocoapodsExt.extraSpecAttributes - val resourcesSpec = specAttributes[RESOURCES_SPEC_ATTR] - if (!resourcesSpec.isNullOrBlank()) { - error(""" - |Kotlin.cocoapods.extraSpecAttributes["resources"] is not compatible with Compose Multiplatform's resources management for iOS. - | * Recommended action: remove extraSpecAttributes["resources"] from '${project.buildFile}' and run '${project.path}:podInstall' once; - | * Alternative action: turn off Compose Multiplatform's resources management for iOS by adding '${ComposeProperties.SYNC_RESOURCES_PROPERTY}=false' to your gradle.properties; - """.trimMargin()) - } - cocoapodsExt.framework { - val syncDir = syncDirFor(this).get().asFile - specAttributes[RESOURCES_SPEC_ATTR] = "['${syncDir.relativeTo(project.projectDir).path}']" - project.tasks.named("podInstall").configure { - it.doFirst { - syncDir.mkdirs() - } - } - } - } - } -} - -/** - * Since Xcode 15, there is a new default setting: `ENABLE_USER_SCRIPT_SANDBOXING = YES` - * It's set in project.pbxproj - * - * SyncComposeResourcesForIosTask fails to work with it right now. - * - * Gradle attempts to create an output folder for SyncComposeResourcesForIosTask on our behalf, - * so we can't handle an exception when it occurs. Therefore, we make SyncComposeResourcesForIosTask - * depend on CheckCanAccessComposeResourcesDirectory, where we check ENABLE_USER_SCRIPT_SANDBOXING. - */ -internal abstract class CheckCanAccessComposeResourcesDirectory : AbstractComposeIosTask() { - - @get:Input - val enabled = providers.environmentVariable("ENABLE_USER_SCRIPT_SANDBOXING") - .orElse("NOT_DEFINED") - .map { it == "YES" } - - private val errorMessage = """ - |Failed to sync compose resources! - |Please make sure ENABLE_USER_SCRIPT_SANDBOXING is set to 'NO' in 'project.pbxproj' - |""".trimMargin() - - @TaskAction - fun run() { - if (enabled.get()) { - logger.error(errorMessage) - throw IllegalStateException( - "Sandbox environment detected (ENABLE_USER_SCRIPT_SANDBOXING = YES). It's not supported so far." - ) - } - } -} - -private fun SyncIosResourcesContext.configureSyncResourcesTasks() { - val lazyTasksDependencies = LazyTasksDependencyConfigurator(project.tasks) - configureEachIosFramework { framework -> - val frameworkClassifier = framework.namePrefix.uppercaseFirstChar() - val syncResourcesTaskName = framework.getSyncResourcesTaskName() - val checkSyncResourcesTaskName = "checkCanSync${frameworkClassifier}ComposeResourcesForIos" - val checkNoSandboxTask = framework.project.tasks.registerOrConfigure(checkSyncResourcesTaskName) {} - val syncTask = framework.project.tasks.registerOrConfigure(syncResourcesTaskName) { - dependsOn(checkNoSandboxTask) - outputDir.set(syncDirFor(framework)) - iosTargets.add(iosTargetResourcesProvider(framework)) - } - with (lazyTasksDependencies) { - if (framework.isCocoapodsFramework) { - "syncFramework".lazyDependsOn(syncTask.name) - } else { - "embedAndSign${frameworkClassifier}AppleFrameworkForXcode".lazyDependsOn(syncTask.name) - } - } - } - configureEachIosTestExecutable { bin -> - val copyTestResourcesTask = "copyTestComposeResourcesFor${bin.target.targetName.uppercaseFirstChar()}" - val task = project.tasks.registerOrConfigure(copyTestResourcesTask) { - from({ - (bin.compilation.associateWith + bin.compilation).flatMap { compilation -> - compilation.allKotlinSourceSets.map { it.resources } - } - }) - into(bin.outputDirectory.resolve(IOS_COMPOSE_RESOURCES_ROOT_DIR)) - } - bin.linkTask.dependsOn(task) - } -} - -internal fun Framework.getSyncResourcesTaskName() = - "sync${namePrefix.uppercaseFirstChar()}ComposeResourcesForIos" - -private val Framework.isCocoapodsFramework: Boolean - get() = name.startsWith("pod") - -private val Framework.namePrefix: String - get() = extractPrefixFromBinaryName( - name, - buildType, - outputKind.taskNameClassifier - ) - -private fun extractPrefixFromBinaryName(name: String, buildType: NativeBuildType, outputKindClassifier: String): String { - val suffix = joinLowerCamelCase(buildType.getName(), outputKindClassifier) - return if (name == suffix) - "" - else - name.substringBeforeLast(suffix.uppercaseFirstChar()) -} - -private fun iosTargetResourcesProvider(bin: NativeBinary): Provider { - val kotlinTarget = bin.target - val project = bin.project - return project.provider { - val resourceDirs = bin.compilation.allKotlinSourceSets - .flatMap { sourceSet -> - sourceSet.resources.srcDirs.map { it.canonicalPath } - } - project.objects.new().apply { - name.set(kotlinTarget.name) - konanTarget.set(kotlinTarget.konanTarget.name) - dirs.set(resourceDirs) - } - } -} - -/** - * Ensures, that a dependency between tasks is set up, - * when a dependent task (fromTask) is created, while avoiding eager configuration. - */ -private class LazyTasksDependencyConfigurator(private val tasks: TaskContainer) { - private val existingDependencies = HashSet>() - private val requestedDependencies = HashMap>() - - init { - tasks.configureEach { fromTask -> - val onTasks = requestedDependencies.remove(fromTask.name) ?: return@configureEach - for (onTaskName in onTasks) { - val dependency = fromTask.name to onTaskName - if (existingDependencies.add(dependency)) { - fromTask.dependsOn(onTaskName) - } - } - } - } - - fun String.lazyDependsOn(dependencyTask: String) { - val dependingTask = this - val dependency = dependingTask to dependencyTask - if (dependency in existingDependencies) return - - if (dependingTask in tasks.names) { - tasks.named(dependingTask).configure { it.dependsOn(dependencyTask) } - existingDependencies.add(dependency) - } else { - requestedDependencies - .getOrPut(dependingTask) { HashSet() } - .add(dependencyTask) - } - } -} diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/determineIosKonanTargetsFromEnv.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/determineIosKonanTargetsFromEnv.kt deleted file mode 100644 index 64fd097239..0000000000 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/determineIosKonanTargetsFromEnv.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2020-2023 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.resources.ios - -import org.jetbrains.kotlin.konan.target.KonanTarget - -// based on AppleSdk.kt from Kotlin Gradle Plugin -// See https://github.com/JetBrains/kotlin/blob/142421da5b966049b4eab44ce6856eb172cf122a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/AppleSdk.kt -internal fun determineIosKonanTargetsFromEnv(platform: String, archs: List): List { - val targets: MutableSet = mutableSetOf() - - when { - platform.startsWith("iphoneos") -> { - targets.addAll(archs.map { arch -> - when (arch) { - "arm64", "arm64e" -> KonanTarget.IOS_ARM64 - "armv7", "armv7s" -> KonanTarget.IOS_ARM32 - else -> error("Unknown iOS device arch: '$arch'") - } - }) - } - platform.startsWith("iphonesimulator") -> { - targets.addAll(archs.map { arch -> - when (arch) { - "arm64", "arm64e" -> KonanTarget.IOS_SIMULATOR_ARM64 - "x86_64" -> KonanTarget.IOS_X64 - else -> error("Unknown iOS simulator arch: '$arch'") - } - }) - } - else -> error("Unknown iOS platform: '$platform'") - } - - return targets.toList() -} \ No newline at end of file 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 3f0e6afff0..3d81cf7047 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 @@ -103,28 +103,6 @@ class GradlePluginTest : GradlePluginTestBase() { } } - @Test - fun iosMokoResources() { - Assumptions.assumeTrue(currentOS == OS.MacOS) - val iosTestEnv = iosTestEnv() - val testEnv = defaultTestEnvironment.copy( - 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 nativeCacheKind() { Assumptions.assumeTrue(currentOS == OS.MacOS) 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 7f46f68c7c..68a5ebbfb6 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 @@ -2,6 +2,7 @@ package org.jetbrains.compose.test.tests.integration import org.gradle.util.GradleVersion import org.jetbrains.compose.internal.utils.* +import org.jetbrains.compose.resources.XmlValuesConverterTask import org.jetbrains.compose.test.utils.* import org.junit.jupiter.api.Test import java.io.File @@ -163,7 +164,7 @@ class ResourcesTest : GradlePluginTestBase() { val resDir = file("cmplib/src/commonMain/composeResources") val resourcesFiles = resDir.walkTopDown() .filter { !it.isDirectory && !it.isHidden } - .map { it.relativeTo(resDir).invariantSeparatorsPath } + .getConvertedResources(resDir) val subdir = "me.sample.library.resources" fun libpath(target: String, ext: String) = @@ -293,7 +294,7 @@ class ResourcesTest : GradlePluginTestBase() { val commonResourcesDir = file("src/commonMain/composeResources") val commonResourcesFiles = commonResourcesDir.walkTopDown() .filter { !it.isDirectory && !it.isHidden } - .map { it.relativeTo(commonResourcesDir).invariantSeparatorsPath } + .getConvertedResources(commonResourcesDir) gradle("build").checks { check.taskSuccessful(":copyDemoDebugFontsToAndroidAssets") @@ -351,7 +352,7 @@ class ResourcesTest : GradlePluginTestBase() { val commonResourcesDir = file("src/commonMain/composeResources") val commonResourcesFiles = commonResourcesDir.walkTopDown() .filter { !it.isDirectory && !it.isHidden } - .map { it.relativeTo(commonResourcesDir).invariantSeparatorsPath } + .getConvertedResources(commonResourcesDir) gradle("assembleDebug").checks { check.taskSuccessful(":copyDebugFontsToAndroidAssets") @@ -366,7 +367,7 @@ class ResourcesTest : GradlePluginTestBase() { ) val newCommonResourcesFiles = commonResourcesDir.walkTopDown() .filter { !it.isDirectory && !it.isHidden } - .map { it.relativeTo(commonResourcesDir).invariantSeparatorsPath } + .getConvertedResources(commonResourcesDir) gradle("assembleDebug").checks { check.taskSuccessful(":copyDebugFontsToAndroidAssets") @@ -376,6 +377,19 @@ class ResourcesTest : GradlePluginTestBase() { } } + private fun Sequence.getConvertedResources(baseDir: File) = map { file -> + val newFile = if ( + file.parentFile.name.startsWith("value") && + file.extension.equals("xml", true) + ) { + file.parentFile.resolve(file.nameWithoutExtension + "." + XmlValuesConverterTask.CONVERTED_RESOURCE_EXT) + } else { + file + } + newFile.relativeTo(baseDir).invariantSeparatorsPath + } + + private fun File.writeNewFile(text: String) { parentFile.mkdirs() createNewFile() @@ -434,22 +448,12 @@ class ResourcesTest : GradlePluginTestBase() { assertFalse(file("build/generated/compose/resourceGenerator/kotlin/app/group/resources_test/generated/resources/Res.kt").exists()) } - gradle("prepareKotlinIdeaImport", "-Pcompose.resources.always.generate.accessors=true").checks { - check.taskSuccessful(":generateComposeResClass") - assertTrue(file("build/generated/compose/resourceGenerator/kotlin/app/group/resources_test/generated/resources/Res.kt").exists()) - } - modifyText("build.gradle.kts") { str -> str.replace( "//api(compose.components.resources)", "api(compose.components.resources)" ) } - gradle("prepareKotlinIdeaImport").checks { - check.taskUpToDate(":generateComposeResClass") - assertTrue(file("build/generated/compose/resourceGenerator/kotlin/app/group/resources_test/generated/resources/Res.kt").exists()) - } - modifyText("build.gradle.kts") { str -> str.replace( "group = \"app.group\"", diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/unit/TestEscapedResourceSymbols.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/unit/TestEscapedResourceSymbols.kt new file mode 100644 index 0000000000..60da0b7d6a --- /dev/null +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/unit/TestEscapedResourceSymbols.kt @@ -0,0 +1,16 @@ +package org.jetbrains.compose.test.tests.unit + +import org.jetbrains.compose.resources.handleSpecialCharacters +import kotlin.test.Test +import kotlin.test.assertEquals + +class TestEscapedResourceSymbols { + + @Test + fun testEscapedSymbols() { + assertEquals( + "abc \n \\n \t \\t \u1234 \ua45f \\u1234 \\ \\u355g", + handleSpecialCharacters("""abc \n \\n \t \\t \u1234 \ua45f \\u1234 \\ \u355g""") + ) + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Drawable0.kt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Drawable0.kt index ff718ae836..6bf2031934 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Drawable0.kt +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Drawable0.kt @@ -8,17 +8,17 @@ import org.jetbrains.compose.resources.ExperimentalResourceApi @ExperimentalResourceApi private object Drawable0 { - public val _3_strange_name: DrawableResource by - lazy { init__3_strange_name() } + public val _3_strange_name: DrawableResource by + lazy { init__3_strange_name() } - public val camelCaseName: DrawableResource by - lazy { init_camelCaseName() } + public val camelCaseName: DrawableResource by + lazy { init_camelCaseName() } - public val vector: DrawableResource by - lazy { init_vector() } + public val vector: DrawableResource by + lazy { init_vector() } - public val vector_2: DrawableResource by - lazy { init_vector_2() } + public val vector_2: DrawableResource by + lazy { init_vector_2() } } @ExperimentalResourceApi @@ -27,12 +27,12 @@ public val Res.drawable._3_strange_name: DrawableResource @ExperimentalResourceApi private fun init__3_strange_name(): DrawableResource = - org.jetbrains.compose.resources.DrawableResource( - "drawable:_3_strange_name", + org.jetbrains.compose.resources.DrawableResource( + "drawable:_3_strange_name", setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/3-strange-name.xml"), + org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/3-strange-name.xml", -1, -1), ) -) + ) @ExperimentalResourceApi public val Res.drawable.camelCaseName: DrawableResource @@ -40,12 +40,12 @@ public val Res.drawable.camelCaseName: DrawableResource @ExperimentalResourceApi private fun init_camelCaseName(): DrawableResource = - org.jetbrains.compose.resources.DrawableResource( - "drawable:camelCaseName", + org.jetbrains.compose.resources.DrawableResource( + "drawable:camelCaseName", setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/camelCaseName.xml"), + org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/camelCaseName.xml", -1, -1), ) -) + ) @ExperimentalResourceApi public val Res.drawable.vector: DrawableResource @@ -54,21 +54,22 @@ public val Res.drawable.vector: DrawableResource @ExperimentalResourceApi private fun init_vector(): DrawableResource = org.jetbrains.compose.resources.DrawableResource( "drawable:vector", - setOf( - + setOf( + org.jetbrains.compose.resources.ResourceItem(setOf(org.jetbrains.compose.resources.LanguageQualifier("ast"), - ), "drawable-ast/vector.xml"), - + ), "drawable-ast/vector.xml", -1, -1), + org.jetbrains.compose.resources.ResourceItem(setOf(org.jetbrains.compose.resources.LanguageQualifier("au"), - org.jetbrains.compose.resources.RegionQualifier("US"), ), "drawable-au-rUS/vector.xml"), - + org.jetbrains.compose.resources.RegionQualifier("US"), ), "drawable-au-rUS/vector.xml", -1, -1), + org.jetbrains.compose.resources.ResourceItem(setOf(org.jetbrains.compose.resources.ThemeQualifier.DARK, - org.jetbrains.compose.resources.LanguageQualifier("ge"), ), "drawable-dark-ge/vector.xml"), - + org.jetbrains.compose.resources.LanguageQualifier("ge"), ), + "drawable-dark-ge/vector.xml", -1, -1), + org.jetbrains.compose.resources.ResourceItem(setOf(org.jetbrains.compose.resources.LanguageQualifier("en"), - ), "drawable-en/vector.xml"), - org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/vector.xml"), - ) + ), "drawable-en/vector.xml", -1, -1), + org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/vector.xml", -1, -1), + ) ) @ExperimentalResourceApi @@ -78,7 +79,7 @@ public val Res.drawable.vector_2: DrawableResource @ExperimentalResourceApi private fun init_vector_2(): DrawableResource = org.jetbrains.compose.resources.DrawableResource( "drawable:vector_2", - setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/vector_2.xml"), - ) -) + setOf( + org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/vector_2.xml", -1, -1), + ) +) \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Font0.kt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Font0.kt index d0e70a049c..7bd9c50a6d 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Font0.kt +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Font0.kt @@ -8,8 +8,8 @@ import org.jetbrains.compose.resources.FontResource @ExperimentalResourceApi private object Font0 { - public val emptyFont: FontResource by - lazy { init_emptyFont() } + public val emptyFont: FontResource by + lazy { init_emptyFont() } } @ExperimentalResourceApi @@ -22,7 +22,7 @@ private fun init_emptyFont(): FontResource = org.jetbrains.compose.resources.Fon setOf( org.jetbrains.compose.resources.ResourceItem(setOf(org.jetbrains.compose.resources.LanguageQualifier("en"), - ), "font-en/emptyFont.otf"), - org.jetbrains.compose.resources.ResourceItem(setOf(), "font/emptyFont.otf"), + ), "font-en/emptyFont.otf", -1, -1), + org.jetbrains.compose.resources.ResourceItem(setOf(), "font/emptyFont.otf", -1, -1), ) -) +) \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Plurals0.kt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Plurals0.kt index 31cac7ea1f..11358c0905 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Plurals0.kt +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Plurals0.kt @@ -21,6 +21,6 @@ private fun init_numberOfSongsAvailable(): PluralStringResource = org.jetbrains.compose.resources.PluralStringResource( "plurals:numberOfSongsAvailable", "numberOfSongsAvailable", setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.cvr", 10, 124), ) ) \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Res.kt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Res.kt index 4644d746fe..16edee76a2 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Res.kt +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Res.kt @@ -27,6 +27,8 @@ public object Res { public object string + public object array + public object plurals public object font diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/String0.kt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/String0.kt index e3f5324347..f2164b60fd 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/String0.kt +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/String0.kt @@ -8,29 +8,26 @@ import org.jetbrains.compose.resources.StringResource @ExperimentalResourceApi private object String0 { - public val PascalCase: StringResource by - lazy { init_PascalCase() } + public val PascalCase: StringResource by + lazy { init_PascalCase() } - public val _1_kebab_case: StringResource by - lazy { init__1_kebab_case() } + public val _1_kebab_case: StringResource by + lazy { init__1_kebab_case() } - public val app_name: StringResource by - lazy { init_app_name() } + public val app_name: StringResource by + lazy { init_app_name() } - public val camelCase: StringResource by - lazy { init_camelCase() } + public val camelCase: StringResource by + lazy { init_camelCase() } - public val hello: StringResource by - lazy { init_hello() } + public val hello: StringResource by + lazy { init_hello() } - public val multi_line: StringResource by - lazy { init_multi_line() } + public val multi_line: StringResource by + lazy { init_multi_line() } - public val str_arr: StringResource by - lazy { init_str_arr() } - - public val str_template: StringResource by - lazy { init_str_template() } + public val str_template: StringResource by + lazy { init_str_template() } } @ExperimentalResourceApi @@ -40,9 +37,9 @@ public val Res.string.PascalCase: StringResource @ExperimentalResourceApi private fun init_PascalCase(): StringResource = org.jetbrains.compose.resources.StringResource( "string:PascalCase", "PascalCase", - setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), - ) + setOf( + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.cvr", 172, 34), + ) ) @ExperimentalResourceApi @@ -52,9 +49,9 @@ public val Res.string._1_kebab_case: StringResource @ExperimentalResourceApi private fun init__1_kebab_case(): StringResource = org.jetbrains.compose.resources.StringResource( "string:_1_kebab_case", "_1_kebab_case", - setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), - ) + setOf( + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.cvr", 135, 36), + ) ) @ExperimentalResourceApi @@ -64,9 +61,9 @@ public val Res.string.app_name: StringResource @ExperimentalResourceApi private fun init_app_name(): StringResource = org.jetbrains.compose.resources.StringResource( "string:app_name", "app_name", - setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), - ) + setOf( + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.cvr", 207, 44), + ) ) @ExperimentalResourceApi @@ -76,9 +73,9 @@ public val Res.string.camelCase: StringResource @ExperimentalResourceApi private fun init_camelCase(): StringResource = org.jetbrains.compose.resources.StringResource( "string:camelCase", "camelCase", - setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), - ) + setOf( + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.cvr", 252, 29), + ) ) @ExperimentalResourceApi @@ -88,9 +85,9 @@ public val Res.string.hello: StringResource @ExperimentalResourceApi private fun init_hello(): StringResource = org.jetbrains.compose.resources.StringResource( "string:hello", "hello", - setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), - ) + setOf( + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.cvr", 282, 37), + ) ) @ExperimentalResourceApi @@ -100,21 +97,9 @@ public val Res.string.multi_line: StringResource @ExperimentalResourceApi private fun init_multi_line(): StringResource = org.jetbrains.compose.resources.StringResource( "string:multi_line", "multi_line", - setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), - ) -) - -@ExperimentalResourceApi -public val Res.string.str_arr: StringResource - get() = String0.str_arr - -@ExperimentalResourceApi -private fun init_str_arr(): StringResource = org.jetbrains.compose.resources.StringResource( - "string:str_arr", "str_arr", - setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), - ) + setOf( + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.cvr", 320, 178), + ) ) @ExperimentalResourceApi @@ -124,7 +109,7 @@ public val Res.string.str_template: StringResource @ExperimentalResourceApi private fun init_str_template(): StringResource = org.jetbrains.compose.resources.StringResource( "string:str_template", "str_template", - setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), - ) -) + setOf( + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.cvr", 499, 76), + ) +) \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Drawable0.kt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Drawable0.kt index c4cc7c209f..e424678b9c 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Drawable0.kt +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Drawable0.kt @@ -8,17 +8,17 @@ import org.jetbrains.compose.resources.ExperimentalResourceApi @ExperimentalResourceApi private object Drawable0 { - public val _3_strange_name: DrawableResource by - lazy { init__3_strange_name() } + public val _3_strange_name: DrawableResource by + lazy { init__3_strange_name() } - public val camelCaseName: DrawableResource by - lazy { init_camelCaseName() } + public val camelCaseName: DrawableResource by + lazy { init_camelCaseName() } - public val vector: DrawableResource by - lazy { init_vector() } + public val vector: DrawableResource by + lazy { init_vector() } - public val vector_2: DrawableResource by - lazy { init_vector_2() } + public val vector_2: DrawableResource by + lazy { init_vector_2() } } @ExperimentalResourceApi @@ -27,12 +27,12 @@ internal val Res.drawable._3_strange_name: DrawableResource @ExperimentalResourceApi private fun init__3_strange_name(): DrawableResource = - org.jetbrains.compose.resources.DrawableResource( - "drawable:_3_strange_name", + org.jetbrains.compose.resources.DrawableResource( + "drawable:_3_strange_name", setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/3-strange-name.xml"), + org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/3-strange-name.xml", -1, -1), ) -) + ) @ExperimentalResourceApi internal val Res.drawable.camelCaseName: DrawableResource @@ -40,12 +40,12 @@ internal val Res.drawable.camelCaseName: DrawableResource @ExperimentalResourceApi private fun init_camelCaseName(): DrawableResource = - org.jetbrains.compose.resources.DrawableResource( - "drawable:camelCaseName", + org.jetbrains.compose.resources.DrawableResource( + "drawable:camelCaseName", setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/camelCaseName.xml"), + org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/camelCaseName.xml", -1, -1), ) -) + ) @ExperimentalResourceApi internal val Res.drawable.vector: DrawableResource @@ -54,21 +54,22 @@ internal val Res.drawable.vector: DrawableResource @ExperimentalResourceApi private fun init_vector(): DrawableResource = org.jetbrains.compose.resources.DrawableResource( "drawable:vector", - setOf( - + setOf( + org.jetbrains.compose.resources.ResourceItem(setOf(org.jetbrains.compose.resources.LanguageQualifier("ast"), - ), "drawable-ast/vector.xml"), - + ), "drawable-ast/vector.xml", -1, -1), + org.jetbrains.compose.resources.ResourceItem(setOf(org.jetbrains.compose.resources.LanguageQualifier("au"), - org.jetbrains.compose.resources.RegionQualifier("US"), ), "drawable-au-rUS/vector.xml"), - + org.jetbrains.compose.resources.RegionQualifier("US"), ), "drawable-au-rUS/vector.xml", -1, -1), + org.jetbrains.compose.resources.ResourceItem(setOf(org.jetbrains.compose.resources.ThemeQualifier.DARK, - org.jetbrains.compose.resources.LanguageQualifier("ge"), ), "drawable-dark-ge/vector.xml"), - + org.jetbrains.compose.resources.LanguageQualifier("ge"), ), + "drawable-dark-ge/vector.xml", -1, -1), + org.jetbrains.compose.resources.ResourceItem(setOf(org.jetbrains.compose.resources.LanguageQualifier("en"), - ), "drawable-en/vector.xml"), - org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/vector.xml"), - ) + ), "drawable-en/vector.xml", -1, -1), + org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/vector.xml", -1, -1), + ) ) @ExperimentalResourceApi @@ -78,7 +79,7 @@ internal val Res.drawable.vector_2: DrawableResource @ExperimentalResourceApi private fun init_vector_2(): DrawableResource = org.jetbrains.compose.resources.DrawableResource( "drawable:vector_2", - setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/vector_2.xml"), - ) -) + setOf( + org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/vector_2.xml", -1, -1), + ) +) \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Font0.kt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Font0.kt index 2530f99e84..c3ec37ed30 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Font0.kt +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Font0.kt @@ -22,7 +22,7 @@ private fun init_emptyFont(): FontResource = org.jetbrains.compose.resources.Fon setOf( org.jetbrains.compose.resources.ResourceItem(setOf(org.jetbrains.compose.resources.LanguageQualifier("en"), - ), "font-en/emptyFont.otf"), - org.jetbrains.compose.resources.ResourceItem(setOf(), "font/emptyFont.otf"), + ), "font-en/emptyFont.otf", -1, -1), + org.jetbrains.compose.resources.ResourceItem(setOf(), "font/emptyFont.otf", -1, -1), ) ) \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Plurals0.kt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Plurals0.kt index 81148dce1d..2bb9863f7d 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Plurals0.kt +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Plurals0.kt @@ -21,6 +21,6 @@ private fun init_numberOfSongsAvailable(): PluralStringResource = org.jetbrains.compose.resources.PluralStringResource( "plurals:numberOfSongsAvailable", "numberOfSongsAvailable", setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.cvr", 10, 124), ) ) \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt index 416db64499..0471dc8135 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt @@ -27,6 +27,8 @@ internal object Res { public object string + public object array + public object plurals public object font diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/String0.kt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/String0.kt index ce0ec62bac..0cb840a271 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/String0.kt +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/String0.kt @@ -8,123 +8,108 @@ import org.jetbrains.compose.resources.StringResource @ExperimentalResourceApi private object String0 { - public val PascalCase: StringResource by - lazy { init_PascalCase() } + public val PascalCase: StringResource by + lazy { init_PascalCase() } - public val _1_kebab_case: StringResource by - lazy { init__1_kebab_case() } + public val _1_kebab_case: StringResource by + lazy { init__1_kebab_case() } - public val app_name: StringResource by - lazy { init_app_name() } + public val app_name: StringResource by + lazy { init_app_name() } - public val camelCase: StringResource by - lazy { init_camelCase() } + public val camelCase: StringResource by + lazy { init_camelCase() } - public val hello: StringResource by - lazy { init_hello() } + public val hello: StringResource by + lazy { init_hello() } - public val multi_line: StringResource by - lazy { init_multi_line() } + public val multi_line: StringResource by + lazy { init_multi_line() } - public val str_arr: StringResource by - lazy { init_str_arr() } - - public val str_template: StringResource by - lazy { init_str_template() } + public val str_template: StringResource by + lazy { init_str_template() } } @ExperimentalResourceApi internal val Res.string.PascalCase: StringResource - get() = String0.PascalCase + get() = String0.PascalCase @ExperimentalResourceApi private fun init_PascalCase(): StringResource = org.jetbrains.compose.resources.StringResource( - "string:PascalCase", "PascalCase", + "string:PascalCase", "PascalCase", setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.cvr", 172, 34), ) ) @ExperimentalResourceApi internal val Res.string._1_kebab_case: StringResource - get() = String0._1_kebab_case + get() = String0._1_kebab_case @ExperimentalResourceApi private fun init__1_kebab_case(): StringResource = org.jetbrains.compose.resources.StringResource( - "string:_1_kebab_case", "_1_kebab_case", + "string:_1_kebab_case", "_1_kebab_case", setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.cvr", 135, 36), ) ) @ExperimentalResourceApi internal val Res.string.app_name: StringResource - get() = String0.app_name + get() = String0.app_name @ExperimentalResourceApi private fun init_app_name(): StringResource = org.jetbrains.compose.resources.StringResource( - "string:app_name", "app_name", + "string:app_name", "app_name", setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.cvr", 207, 44), ) ) @ExperimentalResourceApi internal val Res.string.camelCase: StringResource - get() = String0.camelCase + get() = String0.camelCase @ExperimentalResourceApi private fun init_camelCase(): StringResource = org.jetbrains.compose.resources.StringResource( - "string:camelCase", "camelCase", + "string:camelCase", "camelCase", setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.cvr", 252, 29), ) ) @ExperimentalResourceApi internal val Res.string.hello: StringResource - get() = String0.hello + get() = String0.hello @ExperimentalResourceApi private fun init_hello(): StringResource = org.jetbrains.compose.resources.StringResource( - "string:hello", "hello", + "string:hello", "hello", setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.cvr", 282, 37), ) ) @ExperimentalResourceApi internal val Res.string.multi_line: StringResource - get() = String0.multi_line + get() = String0.multi_line @ExperimentalResourceApi private fun init_multi_line(): StringResource = org.jetbrains.compose.resources.StringResource( - "string:multi_line", "multi_line", + "string:multi_line", "multi_line", setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), - ) -) - -@ExperimentalResourceApi -internal val Res.string.str_arr: StringResource - get() = String0.str_arr - -@ExperimentalResourceApi -private fun init_str_arr(): StringResource = org.jetbrains.compose.resources.StringResource( - "string:str_arr", "str_arr", - setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.cvr", 320, 178), ) ) @ExperimentalResourceApi internal val Res.string.str_template: StringResource - get() = String0.str_template + get() = String0.str_template @ExperimentalResourceApi private fun init_str_template(): StringResource = org.jetbrains.compose.resources.StringResource( - "string:str_template", "str_template", + "string:str_template", "str_template", setOf( - org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.cvr", 499, 76), ) -) +) \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/composeResources/values/strings.xml b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/composeResources/values/strings.xml index 9e537278ba..2afea688cf 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/composeResources/values/strings.xml +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/composeResources/values/strings.xml @@ -5,11 +5,6 @@ consectetur adipiscing elit. Donec eget turpis ac sem ultricies consequat. Hello, %1$s! You have %2$d new messages. - - item 1 - item 2 - item 3 - PascalCase 1-kebab-case camelCase diff --git a/gradle-plugins/compose/src/test/test-projects/misc/emptyResources/expected/Res.kt b/gradle-plugins/compose/src/test/test-projects/misc/emptyResources/expected/Res.kt index 160bec3ba9..1d087e14d8 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/emptyResources/expected/Res.kt +++ b/gradle-plugins/compose/src/test/test-projects/misc/emptyResources/expected/Res.kt @@ -27,6 +27,8 @@ internal object Res { public object string + public object array + public object plurals public object font 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 deleted file mode 100644 index f04770477f..0000000000 --- a/gradle-plugins/compose/src/test/test-projects/misc/iosMokoResources/build.gradle +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index 689880ee3f..0000000000 --- a/gradle-plugins/compose/src/test/test-projects/misc/iosMokoResources/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 791e06efa4..0000000000 --- a/gradle-plugins/compose/src/test/test-projects/misc/iosMokoResources/settings.gradle +++ /dev/null @@ -1,27 +0,0 @@ -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() - mavenCentral() - google() - maven { - url 'https://maven.pkg.jetbrains.space/public/p/compose/dev' - } - } -} -dependencyResolutionManagement { - repositories { - mavenLocal() - mavenCentral() - google() - maven { - url 'https://maven.pkg.jetbrains.space/public/p/compose/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 deleted file mode 100644 index 052b410fb1..0000000000 --- a/gradle-plugins/compose/src/test/test-projects/misc/iosMokoResources/src/commonMain/kotlin/App.kt +++ /dev/null @@ -1,10 +0,0 @@ -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/jvmOnlyResources/expected/Drawable0.kt b/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Drawable0.kt index da64d44b89..cdc1831274 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Drawable0.kt +++ b/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Drawable0.kt @@ -9,7 +9,7 @@ import org.jetbrains.compose.resources.ExperimentalResourceApi @ExperimentalResourceApi private object Drawable0 { public val vector: DrawableResource by - lazy { init_vector() } + lazy { init_vector() } } @ExperimentalResourceApi @@ -20,6 +20,6 @@ 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"), + org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/vector.xml", -1, -1), ) ) \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Res.kt b/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Res.kt index 21484d23c2..74e01c770b 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Res.kt +++ b/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Res.kt @@ -27,6 +27,8 @@ internal object Res { public object string + public object array + public object plurals public object font