Skip to content

Commit 0d0e133

Browse files
authored
[gradle] Add DSL to configure compose resources (JetBrains#4482)
Example: ```kotlin compose.resources { publicResClass = true packageOfResClass = "me.sample.library.resources" generateResClass = auto } ```
1 parent c43b64d commit 0d0e133

File tree

13 files changed

+431
-40
lines changed

13 files changed

+431
-40
lines changed

gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import org.jetbrains.compose.internal.mppExtOrNull
2828
import org.jetbrains.compose.internal.service.ConfigurationProblemReporterService
2929
import org.jetbrains.compose.internal.service.GradlePropertySnapshotService
3030
import org.jetbrains.compose.internal.utils.currentTarget
31+
import org.jetbrains.compose.resources.ResourcesExtension
3132
import org.jetbrains.compose.resources.configureComposeResources
3233
import org.jetbrains.compose.resources.ios.configureSyncTask
3334
import org.jetbrains.compose.web.WebExtension
@@ -52,6 +53,7 @@ abstract class ComposePlugin : Plugin<Project> {
5253
val desktopExtension = composeExtension.extensions.create("desktop", DesktopExtension::class.java)
5354
val androidExtension = composeExtension.extensions.create("android", AndroidExtension::class.java)
5455
val experimentalExtension = composeExtension.extensions.create("experimental", ExperimentalExtension::class.java)
56+
val resourcesExtension = composeExtension.extensions.create("resources", ResourcesExtension::class.java)
5557

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

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

68-
project.configureComposeResources()
70+
project.configureComposeResources(resourcesExtension)
6971

7072
project.afterEvaluate {
7173
configureDesktop(project, desktopExtension)

gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ internal abstract class GenerateResClassTask : DefaultTask() {
2323
@get:Input
2424
abstract val shouldGenerateResClass: Property<Boolean>
2525

26+
@get:Input
27+
abstract val makeResClassPublic: Property<Boolean>
28+
2629
@get:InputFiles
2730
@get:PathSensitive(PathSensitivity.RELATIVE)
2831
abstract val resDir: Property<File>
@@ -63,7 +66,8 @@ internal abstract class GenerateResClassTask : DefaultTask() {
6366
getResFileSpecs(
6467
resources,
6568
packageName.get(),
66-
moduleDir.getOrNull()?.let { it.invariantSeparatorsPath + "/" } ?: ""
69+
moduleDir.getOrNull()?.let { it.invariantSeparatorsPath + "/" } ?: "",
70+
makeResClassPublic.get()
6771
).forEach { it.writeTo(kotlinDir) }
6872
} else {
6973
logger.info("Generation Res class is disabled")
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.jetbrains.compose.resources
2+
3+
abstract class ResourcesExtension {
4+
/**
5+
* Whether the generated resources accessors class should be public or not.
6+
*
7+
* Default is false.
8+
*/
9+
var publicResClass: Boolean = false
10+
11+
/**
12+
* The unique identifier of the resources in the current project.
13+
* Uses as package for the generated Res class and for isolation resources in a final artefact.
14+
*
15+
* If it is empty then `{group name}.{module name}.generated.resources` will be used.
16+
*
17+
*/
18+
var packageOfResClass: String = ""
19+
20+
enum class ResourceClassGeneration { Auto, Always }
21+
22+
//to support groovy DSL
23+
val auto = ResourceClassGeneration.Auto
24+
val always = ResourceClassGeneration.Always
25+
26+
/**
27+
* The mode of resource class generation.
28+
*
29+
* - `auto`: The Res class will be generated if the current project has an explicit "implementation" or "api" dependency on the resource's library.
30+
* - `always`: Unconditionally generate the Res class. This may be useful when the resources library is available transitively.
31+
*/
32+
var generateResClass: ResourceClassGeneration = auto
33+
}

gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesGenerator.kt

Lines changed: 82 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,34 @@ private val androidPluginIds = listOf(
3838
"com.android.library"
3939
)
4040

41-
internal fun Project.configureComposeResources() {
42-
val projectId = provider {
43-
val groupName = project.group.toString().lowercase().asUnderscoredIdentifier()
44-
val moduleName = project.name.lowercase().asUnderscoredIdentifier()
45-
if (groupName.isNotEmpty()) "$groupName.$moduleName"
46-
else moduleName
41+
internal fun Project.configureComposeResources(config: ResourcesExtension) {
42+
val resourcePackage = provider {
43+
config.packageOfResClass.takeIf { it.isNotEmpty() } ?: run {
44+
val groupName = project.group.toString().lowercase().asUnderscoredIdentifier()
45+
val moduleName = project.name.lowercase().asUnderscoredIdentifier()
46+
val id = if (groupName.isNotEmpty()) "$groupName.$moduleName" else moduleName
47+
"$id.generated.resources"
48+
}
4749
}
4850

51+
val publicResClass = provider { config.publicResClass }
52+
53+
val generateResClassMode = provider { config.generateResClass }
54+
4955
plugins.withId(KOTLIN_MPP_PLUGIN_ID) {
5056
val kotlinExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java)
5157

5258
val hasKmpResources = extraProperties.has(KMP_RES_EXT)
5359
val currentGradleVersion = GradleVersion.current()
5460
val minGradleVersion = GradleVersion.version(MIN_GRADLE_VERSION_FOR_KMP_RESOURCES)
5561
if (hasKmpResources && currentGradleVersion >= minGradleVersion) {
56-
configureKmpResources(kotlinExtension, extraProperties.get(KMP_RES_EXT)!!, projectId)
62+
configureKmpResources(
63+
kotlinExtension,
64+
extraProperties.get(KMP_RES_EXT)!!,
65+
resourcePackage,
66+
publicResClass,
67+
generateResClassMode
68+
)
5769
} else {
5870
if (!hasKmpResources) {
5971
logger.info(
@@ -73,7 +85,13 @@ internal fun Project.configureComposeResources() {
7385
}
7486

7587
//current KGP doesn't have KPM resources
76-
configureComposeResources(kotlinExtension, KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME, projectId)
88+
configureComposeResources(
89+
kotlinExtension,
90+
KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME,
91+
resourcePackage,
92+
publicResClass,
93+
generateResClassMode
94+
)
7795

7896
//when applied AGP then configure android resources
7997
androidPluginIds.forEach { pluginId ->
@@ -86,14 +104,22 @@ internal fun Project.configureComposeResources() {
86104
}
87105
plugins.withId(KOTLIN_JVM_PLUGIN_ID) {
88106
val kotlinExtension = project.extensions.getByType(KotlinProjectExtension::class.java)
89-
configureComposeResources(kotlinExtension, SourceSet.MAIN_SOURCE_SET_NAME, projectId)
107+
configureComposeResources(
108+
kotlinExtension,
109+
SourceSet.MAIN_SOURCE_SET_NAME,
110+
resourcePackage,
111+
publicResClass,
112+
generateResClassMode
113+
)
90114
}
91115
}
92116

93117
private fun Project.configureComposeResources(
94118
kotlinExtension: KotlinProjectExtension,
95119
commonSourceSetName: String,
96-
projectId: Provider<String>
120+
resourcePackage: Provider<String>,
121+
publicResClass: Provider<Boolean>,
122+
generateResClassMode: Provider<ResourcesExtension.ResourceClassGeneration>
97123
) {
98124
logger.info("Configure compose resources")
99125
kotlinExtension.sourceSets.all { sourceSet ->
@@ -105,7 +131,14 @@ private fun Project.configureComposeResources(
105131
sourceSet.resources.srcDirs(composeResourcesPath)
106132

107133
if (sourceSetName == commonSourceSetName) {
108-
configureResourceGenerator(composeResourcesPath, sourceSet, projectId, false)
134+
configureResourceGenerator(
135+
composeResourcesPath,
136+
sourceSet,
137+
resourcePackage,
138+
publicResClass,
139+
generateResClassMode,
140+
false
141+
)
109142
}
110143
}
111144
}
@@ -114,7 +147,9 @@ private fun Project.configureComposeResources(
114147
private fun Project.configureKmpResources(
115148
kotlinExtension: KotlinProjectExtension,
116149
kmpResources: Any,
117-
projectId: Provider<String>
150+
resourcePackage: Provider<String>,
151+
publicResClass: Provider<Boolean>,
152+
generateResClassMode: Provider<ResourcesExtension.ResourceClassGeneration>
118153
) {
119154
kotlinExtension as KotlinMultiplatformExtension
120155
kmpResources as KotlinTargetResourcesPublication
@@ -136,7 +171,7 @@ private fun Project.configureKmpResources(
136171
if (target is KotlinAndroidTarget) listOf("**/font*/*") else emptyList()
137172
)
138173
},
139-
projectId.asModuleDir()
174+
resourcePackage.asModuleDir()
140175
)
141176

142177
if (target is KotlinAndroidTarget) {
@@ -151,7 +186,7 @@ private fun Project.configureKmpResources(
151186
emptyList()
152187
)
153188
},
154-
projectId.asModuleDir()
189+
resourcePackage.asModuleDir()
155190
)
156191
}
157192
}
@@ -161,7 +196,14 @@ private fun Project.configureKmpResources(
161196
val sourceSetName = sourceSet.name
162197
if (sourceSetName == KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME) {
163198
val composeResourcesPath = project.projectDir.resolve("src/$sourceSetName/$COMPOSE_RESOURCES_DIR")
164-
configureResourceGenerator(composeResourcesPath, sourceSet, projectId, true)
199+
configureResourceGenerator(
200+
composeResourcesPath,
201+
sourceSet,
202+
resourcePackage,
203+
publicResClass,
204+
generateResClassMode,
205+
true
206+
)
165207
}
166208
}
167209

@@ -251,27 +293,35 @@ private fun Project.configureAndroidComposeResources(
251293
private fun Project.configureResourceGenerator(
252294
commonComposeResourcesDir: File,
253295
commonSourceSet: KotlinSourceSet,
254-
projectId: Provider<String>,
296+
resourcePackage: Provider<String>,
297+
publicResClass: Provider<Boolean>,
298+
generateResClassMode: Provider<ResourcesExtension.ResourceClassGeneration>,
255299
generateModulePath: Boolean
256300
) {
257-
val packageName = projectId.map { "$it.generated.resources" }
258-
259301
logger.info("Configure accessors for '${commonSourceSet.name}'")
260302

261303
fun buildDir(path: String) = layout.dir(layout.buildDirectory.map { File(it.asFile, path) })
262304

263305
//lazy check a dependency on the Resources library
264-
val shouldGenerateResClass: Provider<Boolean> = provider {
265-
if (ComposeProperties.alwaysGenerateResourceAccessors(project).get()) {
266-
true
267-
} else {
268-
configurations.run {
269-
//because the implementation configuration doesn't extend the api in the KGP ¯\_(ツ)_/¯
270-
getByName(commonSourceSet.implementationConfigurationName).allDependencies +
271-
getByName(commonSourceSet.apiConfigurationName).allDependencies
272-
}.any { dep ->
273-
val depStringNotation = dep.let { "${it.group}:${it.name}:${it.version}" }
274-
depStringNotation == ComposePlugin.CommonComponentsDependencies.resources
306+
val shouldGenerateResClass = generateResClassMode.map { mode ->
307+
when (mode) {
308+
ResourcesExtension.ResourceClassGeneration.Auto -> {
309+
//todo remove the gradle property when the gradle plugin will be published
310+
if (ComposeProperties.alwaysGenerateResourceAccessors(project).get()) {
311+
true
312+
} else {
313+
configurations.run {
314+
//because the implementation configuration doesn't extend the api in the KGP ¯\_(ツ)_/¯
315+
getByName(commonSourceSet.implementationConfigurationName).allDependencies +
316+
getByName(commonSourceSet.apiConfigurationName).allDependencies
317+
}.any { dep ->
318+
val depStringNotation = dep.let { "${it.group}:${it.name}:${it.version}" }
319+
depStringNotation == ComposePlugin.CommonComponentsDependencies.resources
320+
}
321+
}
322+
}
323+
ResourcesExtension.ResourceClassGeneration.Always -> {
324+
true
275325
}
276326
}
277327
}
@@ -280,13 +330,14 @@ private fun Project.configureResourceGenerator(
280330
"generateComposeResClass",
281331
GenerateResClassTask::class.java
282332
) { task ->
283-
task.packageName.set(packageName)
333+
task.packageName.set(resourcePackage)
284334
task.shouldGenerateResClass.set(shouldGenerateResClass)
335+
task.makeResClassPublic.set(publicResClass)
285336
task.resDir.set(commonComposeResourcesDir)
286337
task.codeDir.set(buildDir("$RES_GEN_DIR/kotlin"))
287338

288339
if (generateModulePath) {
289-
task.moduleDir.set(projectId.asModuleDir())
340+
task.moduleDir.set(resourcePackage.asModuleDir())
290341
}
291342
}
292343

gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,10 @@ internal fun getResFileSpecs(
117117
//type -> id -> items
118118
resources: Map<ResourceType, Map<String, List<ResourceItem>>>,
119119
packageName: String,
120-
moduleDir: String
120+
moduleDir: String,
121+
isPublic: Boolean
121122
): List<FileSpec> {
123+
val resModifier = if (isPublic) KModifier.PUBLIC else KModifier.INTERNAL
122124
val files = mutableListOf<FileSpec>()
123125
val resClass = FileSpec.builder(packageName, "Res").also { file ->
124126
file.addAnnotation(
@@ -128,7 +130,7 @@ internal fun getResFileSpecs(
128130
.build()
129131
)
130132
file.addType(TypeSpec.objectBuilder("Res").also { resObject ->
131-
resObject.addModifiers(KModifier.INTERNAL)
133+
resObject.addModifiers(resModifier)
132134
resObject.addAnnotation(experimentalAnnotation)
133135

134136
//readFileBytes
@@ -169,6 +171,7 @@ internal fun getResFileSpecs(
169171
index,
170172
packageName,
171173
moduleDir,
174+
resModifier,
172175
idToResources.subMap(ids.first(), true, ids.last(), true)
173176
)
174177
)
@@ -183,6 +186,7 @@ private fun getChunkFileSpec(
183186
index: Int,
184187
packageName: String,
185188
moduleDir: String,
189+
resModifier: KModifier,
186190
idToResources: Map<String, List<ResourceItem>>
187191
): FileSpec {
188192
val chunkClassName = type.typeName.uppercaseFirstChar() + index
@@ -206,7 +210,7 @@ private fun getChunkFileSpec(
206210
chunkFile.addType(objectSpec)
207211

208212
idToResources.forEach { (resName, items) ->
209-
val accessor = PropertySpec.builder(resName, type.getClassName(), KModifier.INTERNAL)
213+
val accessor = PropertySpec.builder(resName, type.getClassName(), resModifier)
210214
.receiver(ClassName(packageName, "Res", type.typeName))
211215
.addAnnotation(experimentalAnnotation)
212216
.getter(FunSpec.getterBuilder().addStatement("return $chunkClassName.$resName").build())

gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,20 @@ class ResourcesTest : GradlePluginTestBase() {
126126
file("src/commonMain/composeResources/drawable/vector_3.xml").renameTo(
127127
file("src/commonMain/composeResources/drawable/vector_2.xml")
128128
)
129+
130+
file("build.gradle.kts").modify { txt ->
131+
txt + """
132+
compose.resources {
133+
publicResClass = true
134+
packageOfResClass = "my.lib.res"
135+
}
136+
""".trimIndent()
137+
}
138+
129139
gradle("generateComposeResClass").checks {
130140
assertDirectoriesContentEquals(
131-
file("build/generated/compose/resourceGenerator/kotlin/app/group/resources_test/generated/resources"),
132-
file("expected")
141+
file("build/generated/compose/resourceGenerator/kotlin/my/lib/res"),
142+
file("expected-open-res")
133143
)
134144
}
135145
}
@@ -155,7 +165,7 @@ class ResourcesTest : GradlePluginTestBase() {
155165
val resourcesFiles = resDir.walkTopDown()
156166
.filter { !it.isDirectory && !it.isHidden }
157167
.map { it.relativeTo(resDir).invariantSeparatorsPath }
158-
val subdir = "me.sample.library.cmplib"
168+
val subdir = "me.sample.library.resources"
159169

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

0 commit comments

Comments
 (0)