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