Skip to content

Commit

Permalink
[gradle] Add DSL to configure compose resources
Browse files Browse the repository at this point in the history
Example:
compose.resources {
    publicResClass = true
    resourceProjectId = "me.sample.library.resources"
    generateResClass = auto
}
  • Loading branch information
terrakok committed Mar 14, 2024
1 parent 629cd05 commit 2dfc72d
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 33 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 resourceProjectId: 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() {
internal fun Project.configureComposeResources(config: ResourcesExtension) {
val projectId = provider {
val groupName = project.group.toString().lowercase().asUnderscoredIdentifier()
val moduleName = project.name.lowercase().asUnderscoredIdentifier()
if (groupName.isNotEmpty()) "$groupName.$moduleName"
else moduleName
config.resourceProjectId.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)!!,
projectId,
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,
projectId,
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,
projectId,
publicResClass,
generateResClassMode
)
}
}

private fun Project.configureComposeResources(
kotlinExtension: KotlinProjectExtension,
commonSourceSetName: String,
projectId: Provider<String>
projectId: 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,
projectId,
publicResClass,
generateResClassMode,
false
)
}
}
}
Expand All @@ -114,7 +147,9 @@ private fun Project.configureComposeResources(
private fun Project.configureKmpResources(
kotlinExtension: KotlinProjectExtension,
kmpResources: Any,
projectId: Provider<String>
projectId: Provider<String>,
publicResClass: Provider<Boolean>,
generateResClassMode: Provider<ResourcesExtension.ResourceClassGeneration>
) {
kotlinExtension as KotlinMultiplatformExtension
kmpResources as KotlinTargetResourcesPublication
Expand Down Expand Up @@ -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,
projectId,
publicResClass,
generateResClassMode,
true
)
}
}

Expand Down Expand Up @@ -252,26 +294,34 @@ private fun Project.configureResourceGenerator(
commonComposeResourcesDir: File,
commonSourceSet: KotlinSourceSet,
projectId: 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,8 +330,9 @@ private fun Project.configureResourceGenerator(
"generateComposeResClass",
GenerateResClassTask::class.java
) { task ->
task.packageName.set(packageName)
task.packageName.set(projectId)
task.shouldGenerateResClass.set(shouldGenerateResClass)
task.makeResClassPublic.set(publicResClass)
task.resDir.set(commonComposeResourcesDir)
task.codeDir.set(buildDir("$RES_GEN_DIR/kotlin"))

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 @@ -155,7 +155,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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import me.sample.app.MyFeatureText
import me.sample.library.MyLibraryText
import org.jetbrains.compose.resources.stringResource
import kmpresourcepublication.appmodule.generated.resources.*
import me.sample.library.resources.Res as LibRes
import me.sample.library.resources.*
import kotlin.test.Test

@OptIn(ExperimentalTestApi::class)
Expand All @@ -28,11 +30,18 @@ class ComposeAppTest {
)
MyFeatureText(Modifier.testTag("feature-text"), txt)
MyLibraryText(Modifier.testTag("library-text"), txt)

//direct read a resource from library
Text(
modifier = Modifier.testTag("library-resource-text"),
text = stringResource(LibRes.string.str_1)
)
}
}

onNodeWithTag("app-text").assertTextEquals("test text: App text str_1")
onNodeWithTag("feature-text").assertTextEquals("test text: Feature text str_1")
onNodeWithTag("library-text").assertTextEquals("test text: Library text str_1")
onNodeWithTag("library-resource-text").assertTextEquals("Library text str_1")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,9 @@ android {
compose {
kotlinCompilerPlugin.set(dependencies.compiler.forKotlin("COMPOSE_COMPILER_PLUGIN_PLACEHOLDER"))
kotlinCompilerPluginArgs.add("suppressKotlinVersionCompatibilityCheck=KOTLIN_VERSION_PLACEHOLDER")
}

compose.resources {
publicResClass = true
resourceProjectId = "me.sample.library.resources"
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import me.sample.library.cmplib.generated.resources.*
import me.sample.library.resources.*
import org.jetbrains.compose.resources.Font
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
Expand Down

0 comments on commit 2dfc72d

Please sign in to comment.