Skip to content

Commit

Permalink
[gradle] Add DSL to configure compose resources (#4482)
Browse files Browse the repository at this point in the history
Example:
```kotlin
compose.resources {
    publicResClass = true
    packageOfResClass = "me.sample.library.resources"
    generateResClass = auto
}
```
  • Loading branch information
terrakok committed Mar 19, 2024
1 parent c43b64d commit 0d0e133
Show file tree
Hide file tree
Showing 13 changed files with 431 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import org.jetbrains.compose.internal.mppExtOrNull
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
Expand All @@ -52,6 +53,7 @@ abstract class ComposePlugin : Plugin<Project> {
val desktopExtension = composeExtension.extensions.create("desktop", DesktopExtension::class.java)
val androidExtension = composeExtension.extensions.create("android", AndroidExtension::class.java)
val experimentalExtension = composeExtension.extensions.create("experimental", ExperimentalExtension::class.java)
val resourcesExtension = composeExtension.extensions.create("resources", ResourcesExtension::class.java)

project.dependencies.extensions.add("compose", Dependencies(project))

Expand All @@ -65,7 +67,7 @@ abstract class ComposePlugin : Plugin<Project> {
project.plugins.apply(ComposeCompilerKotlinSupportPlugin::class.java)
project.configureNativeCompilerCaching()

project.configureComposeResources()
project.configureComposeResources(resourcesExtension)

project.afterEvaluate {
configureDesktop(project, desktopExtension)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ internal abstract class GenerateResClassTask : DefaultTask() {
@get:Input
abstract val shouldGenerateResClass: Property<Boolean>

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

@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val resDir: Property<File>
Expand Down Expand Up @@ -63,7 +66,8 @@ internal abstract class GenerateResClassTask : DefaultTask() {
getResFileSpecs(
resources,
packageName.get(),
moduleDir.getOrNull()?.let { it.invariantSeparatorsPath + "/" } ?: ""
moduleDir.getOrNull()?.let { it.invariantSeparatorsPath + "/" } ?: "",
makeResClassPublic.get()
).forEach { it.writeTo(kotlinDir) }
} else {
logger.info("Generation Res class is disabled")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.jetbrains.compose.resources

abstract class ResourcesExtension {
/**
* Whether the generated resources accessors class should be public or not.
*
* Default is false.
*/
var publicResClass: Boolean = false

/**
* The unique identifier of the resources in the current project.
* Uses as package for the generated Res class and for isolation resources in a final artefact.
*
* If it is empty then `{group name}.{module name}.generated.resources` will be used.
*
*/
var packageOfResClass: String = ""

enum class ResourceClassGeneration { Auto, Always }

//to support groovy DSL
val auto = ResourceClassGeneration.Auto
val always = ResourceClassGeneration.Always

/**
* The mode of resource class generation.
*
* - `auto`: The Res class will be generated if the current project has an explicit "implementation" or "api" dependency on the resource's library.
* - `always`: Unconditionally generate the Res class. This may be useful when the resources library is available transitively.
*/
var generateResClass: ResourceClassGeneration = auto
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,34 @@ private val androidPluginIds = listOf(
"com.android.library"
)

internal fun Project.configureComposeResources() {
val projectId = provider {
val groupName = project.group.toString().lowercase().asUnderscoredIdentifier()
val moduleName = project.name.lowercase().asUnderscoredIdentifier()
if (groupName.isNotEmpty()) "$groupName.$moduleName"
else moduleName
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)!!, projectId)
configureKmpResources(
kotlinExtension,
extraProperties.get(KMP_RES_EXT)!!,
resourcePackage,
publicResClass,
generateResClassMode
)
} else {
if (!hasKmpResources) {
logger.info(
Expand All @@ -73,7 +85,13 @@ internal fun Project.configureComposeResources() {
}

//current KGP doesn't have KPM resources
configureComposeResources(kotlinExtension, KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME, projectId)
configureComposeResources(
kotlinExtension,
KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME,
resourcePackage,
publicResClass,
generateResClassMode
)

//when applied AGP then configure android resources
androidPluginIds.forEach { pluginId ->
Expand All @@ -86,14 +104,22 @@ internal fun Project.configureComposeResources() {
}
plugins.withId(KOTLIN_JVM_PLUGIN_ID) {
val kotlinExtension = project.extensions.getByType(KotlinProjectExtension::class.java)
configureComposeResources(kotlinExtension, SourceSet.MAIN_SOURCE_SET_NAME, projectId)
configureComposeResources(
kotlinExtension,
SourceSet.MAIN_SOURCE_SET_NAME,
resourcePackage,
publicResClass,
generateResClassMode
)
}
}

private fun Project.configureComposeResources(
kotlinExtension: KotlinProjectExtension,
commonSourceSetName: String,
projectId: Provider<String>
resourcePackage: Provider<String>,
publicResClass: Provider<Boolean>,
generateResClassMode: Provider<ResourcesExtension.ResourceClassGeneration>
) {
logger.info("Configure compose resources")
kotlinExtension.sourceSets.all { sourceSet ->
Expand All @@ -105,7 +131,14 @@ private fun Project.configureComposeResources(
sourceSet.resources.srcDirs(composeResourcesPath)

if (sourceSetName == commonSourceSetName) {
configureResourceGenerator(composeResourcesPath, sourceSet, projectId, false)
configureResourceGenerator(
composeResourcesPath,
sourceSet,
resourcePackage,
publicResClass,
generateResClassMode,
false
)
}
}
}
Expand All @@ -114,7 +147,9 @@ private fun Project.configureComposeResources(
private fun Project.configureKmpResources(
kotlinExtension: KotlinProjectExtension,
kmpResources: Any,
projectId: Provider<String>
resourcePackage: Provider<String>,
publicResClass: Provider<Boolean>,
generateResClassMode: Provider<ResourcesExtension.ResourceClassGeneration>
) {
kotlinExtension as KotlinMultiplatformExtension
kmpResources as KotlinTargetResourcesPublication
Expand All @@ -136,7 +171,7 @@ private fun Project.configureKmpResources(
if (target is KotlinAndroidTarget) listOf("**/font*/*") else emptyList()
)
},
projectId.asModuleDir()
resourcePackage.asModuleDir()
)

if (target is KotlinAndroidTarget) {
Expand All @@ -151,7 +186,7 @@ private fun Project.configureKmpResources(
emptyList()
)
},
projectId.asModuleDir()
resourcePackage.asModuleDir()
)
}
}
Expand All @@ -161,7 +196,14 @@ private fun Project.configureKmpResources(
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, projectId, true)
configureResourceGenerator(
composeResourcesPath,
sourceSet,
resourcePackage,
publicResClass,
generateResClassMode,
true
)
}
}

Expand Down Expand Up @@ -251,27 +293,35 @@ private fun Project.configureAndroidComposeResources(
private fun Project.configureResourceGenerator(
commonComposeResourcesDir: File,
commonSourceSet: KotlinSourceSet,
projectId: Provider<String>,
resourcePackage: Provider<String>,
publicResClass: Provider<Boolean>,
generateResClassMode: Provider<ResourcesExtension.ResourceClassGeneration>,
generateModulePath: Boolean
) {
val packageName = projectId.map { "$it.generated.resources" }

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: Provider<Boolean> = provider {
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
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
}
}
}
Expand All @@ -280,13 +330,14 @@ private fun Project.configureResourceGenerator(
"generateComposeResClass",
GenerateResClassTask::class.java
) { task ->
task.packageName.set(packageName)
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(projectId.asModuleDir())
task.moduleDir.set(resourcePackage.asModuleDir())
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,10 @@ internal fun getResFileSpecs(
//type -> id -> items
resources: Map<ResourceType, Map<String, List<ResourceItem>>>,
packageName: String,
moduleDir: String
moduleDir: String,
isPublic: Boolean
): List<FileSpec> {
val resModifier = if (isPublic) KModifier.PUBLIC else KModifier.INTERNAL
val files = mutableListOf<FileSpec>()
val resClass = FileSpec.builder(packageName, "Res").also { file ->
file.addAnnotation(
Expand All @@ -128,7 +130,7 @@ internal fun getResFileSpecs(
.build()
)
file.addType(TypeSpec.objectBuilder("Res").also { resObject ->
resObject.addModifiers(KModifier.INTERNAL)
resObject.addModifiers(resModifier)
resObject.addAnnotation(experimentalAnnotation)

//readFileBytes
Expand Down Expand Up @@ -169,6 +171,7 @@ internal fun getResFileSpecs(
index,
packageName,
moduleDir,
resModifier,
idToResources.subMap(ids.first(), true, ids.last(), true)
)
)
Expand All @@ -183,6 +186,7 @@ private fun getChunkFileSpec(
index: Int,
packageName: String,
moduleDir: String,
resModifier: KModifier,
idToResources: Map<String, List<ResourceItem>>
): FileSpec {
val chunkClassName = type.typeName.uppercaseFirstChar() + index
Expand All @@ -206,7 +210,7 @@ private fun getChunkFileSpec(
chunkFile.addType(objectSpec)

idToResources.forEach { (resName, items) ->
val accessor = PropertySpec.builder(resName, type.getClassName(), KModifier.INTERNAL)
val accessor = PropertySpec.builder(resName, type.getClassName(), resModifier)
.receiver(ClassName(packageName, "Res", type.typeName))
.addAnnotation(experimentalAnnotation)
.getter(FunSpec.getterBuilder().addStatement("return $chunkClassName.$resName").build())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,20 @@ class ResourcesTest : GradlePluginTestBase() {
file("src/commonMain/composeResources/drawable/vector_3.xml").renameTo(
file("src/commonMain/composeResources/drawable/vector_2.xml")
)

file("build.gradle.kts").modify { txt ->
txt + """
compose.resources {
publicResClass = true
packageOfResClass = "my.lib.res"
}
""".trimIndent()
}

gradle("generateComposeResClass").checks {
assertDirectoriesContentEquals(
file("build/generated/compose/resourceGenerator/kotlin/app/group/resources_test/generated/resources"),
file("expected")
file("build/generated/compose/resourceGenerator/kotlin/my/lib/res"),
file("expected-open-res")
)
}
}
Expand All @@ -155,7 +165,7 @@ class ResourcesTest : GradlePluginTestBase() {
val resourcesFiles = resDir.walkTopDown()
.filter { !it.isDirectory && !it.isHidden }
.map { it.relativeTo(resDir).invariantSeparatorsPath }
val subdir = "me.sample.library.cmplib"
val subdir = "me.sample.library.resources"

fun libpath(target: String, ext: String) =
"my-mvn/me/sample/library/cmplib-$target/1.0/cmplib-$target-1.0$ext"
Expand Down

0 comments on commit 0d0e133

Please sign in to comment.