Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[gradle] Add DSL to configure compose resources #4482

Merged
merged 3 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading
Loading