Skip to content

Commit

Permalink
Generate functions to find resources by a string ID. (#5068)
Browse files Browse the repository at this point in the history
The PR adds a generation special properties with maps a string ID to the
resource for each type of resources:
```kotlin
val Res.allDrawableResources: Map<String, DrawableResource>
val Res.allStringResources: Map<String, StringResource>
val Res.allStringArrayResources: Map<String, StringArrayResource>
val Res.allPluralStringResources: Map<String, PluralStringResource>
val Res.allFontResources: Map<String, FontResource>
```

<!-- Optional -->
Fixes #4880
Fixes https://youtrack.jetbrains.com/issue/CMP-1607

## Testing
I checked it in the sample project but this should be tested by QA (KMP
and JVM only projects)

## Release Notes
### Features - Resources
- Now the gradle plugin generates resources map to find a resource by a
string ID
  • Loading branch information
terrakok committed Jul 10, 2024
1 parent a98b37d commit e4c7703
Show file tree
Hide file tree
Showing 31 changed files with 1,218 additions and 352 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ import org.jetbrains.compose.ComposePlugin
import org.jetbrains.compose.internal.IDEA_IMPORT_TASK_NAME
import org.jetbrains.compose.internal.IdeaImportTask
import org.jetbrains.compose.internal.utils.uppercaseFirstChar
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMetadataTarget
import org.jetbrains.kotlin.tooling.core.withClosure
import java.io.File

internal fun Project.configureComposeResourcesGeneration(
Expand Down Expand Up @@ -67,6 +73,8 @@ internal fun Project.configureComposeResourcesGeneration(
)
}

configureResourceCollectorsGeneration(kotlinExtension, shouldGenerateCode, packageName, makeAccessorsPublic)

//setup task execution during IDE import
tasks.configureEach { importTask ->
if (importTask.name == IDEA_IMPORT_TASK_NAME) {
Expand Down Expand Up @@ -115,7 +123,7 @@ private fun Project.configureResourceAccessorsGeneration(
logger.info("Configure resource accessors generation for ${sourceSet.name}")

val genTask = tasks.register(
"generateResourceAccessorsFor${sourceSet.name.uppercaseFirstChar()}",
sourceSet.getResourceAccessorsGenerationTaskName(),
GenerateResourceAccessorsTask::class.java
) { task ->
task.packageName.set(packageName)
Expand All @@ -130,6 +138,132 @@ private fun Project.configureResourceAccessorsGeneration(
}
}

//register generated source set
sourceSet.kotlin.srcDir(genTask.map { it.codeDir })
}

private fun KotlinSourceSet.getResourceAccessorsGenerationTaskName(): String {
return "generateResourceAccessorsFor${this.name.uppercaseFirstChar()}"
}

//we have to generate actual resource collector functions for each leaf source set
private fun Project.configureResourceCollectorsGeneration(
kotlinExtension: KotlinProjectExtension,
shouldGenerateCode: Provider<Boolean>,
packageName: Provider<String>,
makeAccessorsPublic: Provider<Boolean>
) {
if (kotlinExtension is KotlinMultiplatformExtension) {
kotlinExtension.sourceSets
.matching { it.name == KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME }
.all { commonMainSourceSet ->
configureExpectResourceCollectorsGeneration(
commonMainSourceSet,
shouldGenerateCode,
packageName,
makeAccessorsPublic
)
}

kotlinExtension.targets.all { target ->
if (target is KotlinAndroidTarget) {
kotlinExtension.sourceSets.matching { it.name == "androidMain" }.all { androidMain ->
configureActualResourceCollectorsGeneration(
androidMain,
shouldGenerateCode,
packageName,
makeAccessorsPublic,
true
)
}
} else if (target !is KotlinMetadataTarget) {
target.compilations.matching { it.name == KotlinCompilation.MAIN_COMPILATION_NAME }.all { compilation ->
configureActualResourceCollectorsGeneration(
compilation.defaultSourceSet,
shouldGenerateCode,
packageName,
makeAccessorsPublic,
true
)
}
}
}
} else if (kotlinExtension is KotlinSingleTargetExtension<*>) {
//JVM only projects
kotlinExtension.target.compilations
.findByName(KotlinCompilation.MAIN_COMPILATION_NAME)
?.let { compilation ->
configureActualResourceCollectorsGeneration(
compilation.defaultSourceSet,
shouldGenerateCode,
packageName,
makeAccessorsPublic,
false
)
}
}

}

private fun Project.configureExpectResourceCollectorsGeneration(
sourceSet: KotlinSourceSet,
shouldGenerateCode: Provider<Boolean>,
packageName: Provider<String>,
makeAccessorsPublic: Provider<Boolean>
) {
logger.info("Configure expect resource collectors generation for ${sourceSet.name}")


val genTask = tasks.register(
"generateExpectResourceCollectorsFor${sourceSet.name.uppercaseFirstChar()}",
GenerateExpectResourceCollectorsTask::class.java
) { task ->
task.packageName.set(packageName)
task.shouldGenerateCode.set(shouldGenerateCode)
task.makeAccessorsPublic.set(makeAccessorsPublic)
task.codeDir.set(layout.buildDirectory.dir("$RES_GEN_DIR/kotlin/${sourceSet.name}ResourceCollectors"))
}

//register generated source set
sourceSet.kotlin.srcDir(genTask.map { it.codeDir })
}

private fun Project.configureActualResourceCollectorsGeneration(
sourceSet: KotlinSourceSet,
shouldGenerateCode: Provider<Boolean>,
packageName: Provider<String>,
makeAccessorsPublic: Provider<Boolean>,
useActualModifier: Boolean
) {
val taskName = "generateActualResourceCollectorsFor${sourceSet.name.uppercaseFirstChar()}"
if (tasks.names.contains(taskName)) {
logger.info("Actual resource collectors generation for ${sourceSet.name} is already configured")
return
}
logger.info("Configure actual resource collectors generation for ${sourceSet.name}")

val accessorDirs = project.files({
val allSourceSets = sourceSet.withClosure { it.dependsOn }
allSourceSets.mapNotNull { item ->
val accessorsTaskName = item.getResourceAccessorsGenerationTaskName()
if (tasks.names.contains(accessorsTaskName)) {
tasks.named(accessorsTaskName, GenerateResourceAccessorsTask::class.java).map { it.codeDir }
} else null
}
})

val genTask = tasks.register(
taskName,
GenerateActualResourceCollectorsTask::class.java
) { task ->
task.packageName.set(packageName)
task.shouldGenerateCode.set(shouldGenerateCode)
task.makeAccessorsPublic.set(makeAccessorsPublic)
task.useActualModifier.set(useActualModifier)
task.resourceAccessorDirs.from(accessorDirs)
task.codeDir.set(layout.buildDirectory.dir("$RES_GEN_DIR/kotlin/${sourceSet.name}ResourceCollectors"))
}

//register generated source set
sourceSet.kotlin.srcDir(genTask.map { it.codeDir })
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package org.jetbrains.compose.resources

import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.jetbrains.compose.internal.IdeaImportTask
import org.jetbrains.compose.internal.utils.uppercaseFirstChar

internal abstract class GenerateExpectResourceCollectorsTask : IdeaImportTask() {
@get:Input
abstract val packageName: Property<String>

@get:Input
abstract val shouldGenerateCode: Property<Boolean>

@get:Input
abstract val makeAccessorsPublic: Property<Boolean>

@get:OutputDirectory
abstract val codeDir: DirectoryProperty

override fun safeAction() {
val kotlinDir = codeDir.get().asFile

logger.info("Clean directory $kotlinDir")
kotlinDir.deleteRecursively()
kotlinDir.mkdirs()

if (shouldGenerateCode.get()) {
logger.info("Generate expect ResourceCollectors for $kotlinDir")

val pkgName = packageName.get()
val isPublic = makeAccessorsPublic.get()
val spec = getExpectResourceCollectorsFileSpec(pkgName, "ExpectResourceCollectors", isPublic)
spec.writeTo(kotlinDir)
}
}
}

internal abstract class GenerateActualResourceCollectorsTask : IdeaImportTask() {
@get:Input
abstract val packageName: Property<String>

@get:Input
abstract val shouldGenerateCode: Property<Boolean>

@get:Input
abstract val makeAccessorsPublic: Property<Boolean>

@get:Input
abstract val useActualModifier: Property<Boolean>

@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val resourceAccessorDirs: ConfigurableFileCollection

@get:OutputDirectory
abstract val codeDir: DirectoryProperty

override fun safeAction() {
val kotlinDir = codeDir.get().asFile
val inputDirs = resourceAccessorDirs.files

logger.info("Clean directory $kotlinDir")
kotlinDir.deleteRecursively()
kotlinDir.mkdirs()

val inputFiles = inputDirs.flatMap { dir ->
dir.walkTopDown().filter { !it.isHidden && it.isFile && it.extension == "kt" }.toList()
}

if (shouldGenerateCode.get()) {
logger.info("Generate actual ResourceCollectors for $kotlinDir")
val funNames = inputFiles.mapNotNull { inputFile ->
if (inputFile.nameWithoutExtension.contains('.')) {
val (fileName, suffix) = inputFile.nameWithoutExtension.split('.')
val type = ResourceType.values().firstOrNull { fileName.startsWith(it.accessorName, true) }
val name = "_collect${suffix.uppercaseFirstChar()}${fileName}Resources"

if (type == null) {
logger.warn("Unknown resources type: `$inputFile`")
null
} else if (!inputFile.readText().contains(name)) {
logger.warn("A function '$name' is not found in the `$inputFile` file!")
null
} else {
logger.info("Found collector function: `$name`")
type to name
}
} else {
logger.warn("Unknown file name: `$inputFile`")
null
}
}.groupBy({ it.first }, { it.second })

val pkgName = packageName.get()
val isPublic = makeAccessorsPublic.get()
val useActual = useActualModifier.get()
val spec = getActualResourceCollectorsFileSpec(
pkgName,
"ActualResourceCollectors",
isPublic,
useActual,
funNames
)
spec.writeTo(kotlinDir)
} else {
logger.info("Generation ResourceCollectors for $kotlinDir is disabled")
}
}
}
Loading

0 comments on commit e4c7703

Please sign in to comment.