Skip to content

Commit 062c9eb

Browse files
authored
Support source set's hierarchy for compose resources (JetBrains#4589)
Compose resources can be located in different KMP source sets in the `composeResources` directory. For each resource an accessor will be generated in the suitable kotlin source set.
1 parent 96f1ceb commit 062c9eb

File tree

39 files changed

+714
-365
lines changed

39 files changed

+714
-365
lines changed

Diff for: gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidResources.kt

+25-20
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import com.android.build.gradle.BaseExtension
55
import org.gradle.api.DefaultTask
66
import org.gradle.api.Project
77
import org.gradle.api.file.DirectoryProperty
8+
import org.gradle.api.file.FileCollection
89
import org.gradle.api.file.FileSystemOperations
910
import org.gradle.api.provider.Property
10-
import org.gradle.api.provider.Provider
1111
import org.gradle.api.tasks.*
1212
import org.jetbrains.compose.internal.utils.registerTask
1313
import org.jetbrains.compose.internal.utils.uppercaseFirstChar
@@ -18,40 +18,31 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget
1818
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation
1919
import org.jetbrains.kotlin.gradle.plugin.sources.android.androidSourceSetInfoOrNull
2020
import org.jetbrains.kotlin.gradle.utils.ObservableSet
21-
import java.io.File
2221
import javax.inject.Inject
2322

2423
@OptIn(ExperimentalKotlinGradlePluginApi::class)
2524
internal fun Project.configureAndroidComposeResources(
2625
kotlinExtension: KotlinMultiplatformExtension,
27-
androidExtension: BaseExtension,
28-
preparedCommonResources: Provider<File>
26+
androidExtension: BaseExtension
2927
) {
30-
val commonMain = KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME
31-
val commonResourcesDir = projectDir.resolve("src/$commonMain/$COMPOSE_RESOURCES_DIR")
32-
3328
// 1) get the Kotlin Android Target Compilation -> [A]
3429
// 2) get default source set name for the 'A'
3530
// 3) find the associated Android SourceSet in the AndroidExtension -> [B]
3631
// 4) get all source sets in the 'A' and add its resources to the 'B'
3732
kotlinExtension.targets.withType(KotlinAndroidTarget::class.java).all { androidTarget ->
3833
androidTarget.compilations.all { compilation: KotlinJvmAndroidCompilation ->
39-
40-
//fix for AGP < 8.0
41-
//usually 'androidSourceSet.resources.srcDir(preparedCommonResources)' should be enough
42-
compilation.androidVariant.processJavaResourcesProvider.configure { it.dependsOn(preparedCommonResources) }
43-
4434
compilation.defaultSourceSet.androidSourceSetInfoOrNull?.let { kotlinAndroidSourceSet ->
4535
androidExtension.sourceSets
4636
.matching { it.name == kotlinAndroidSourceSet.androidSourceSetName }
4737
.all { androidSourceSet ->
4838
(compilation.allKotlinSourceSets as? ObservableSet<KotlinSourceSet>)?.forAll { kotlinSourceSet ->
49-
if (kotlinSourceSet.name == commonMain) {
50-
androidSourceSet.resources.srcDir(preparedCommonResources)
51-
} else {
52-
androidSourceSet.resources.srcDir(
53-
projectDir.resolve("src/${kotlinSourceSet.name}/$COMPOSE_RESOURCES_DIR")
54-
)
39+
val preparedComposeResources = getPreparedComposeResourcesDir(kotlinSourceSet)
40+
androidSourceSet.resources.srcDirs(preparedComposeResources)
41+
42+
//fix for AGP < 8.0
43+
//usually 'androidSourceSet.resources.srcDir(preparedCommonResources)' should be enough
44+
compilation.androidVariant.processJavaResourcesProvider.configure {
45+
it.dependsOn(preparedComposeResources)
5546
}
5647
}
5748
}
@@ -62,10 +53,24 @@ internal fun Project.configureAndroidComposeResources(
6253
//copy fonts from the compose resources dir to android assets
6354
val androidComponents = project.extensions.findByType(AndroidComponentsExtension::class.java) ?: return
6455
androidComponents.onVariants { variant ->
56+
val variantResources = project.files()
57+
58+
kotlinExtension.targets.withType(KotlinAndroidTarget::class.java).all { androidTarget ->
59+
androidTarget.compilations.all { compilation: KotlinJvmAndroidCompilation ->
60+
if (compilation.androidVariant.name == variant.name) {
61+
project.logger.info("Configure fonts for variant ${variant.name}")
62+
(compilation.allKotlinSourceSets as? ObservableSet<KotlinSourceSet>)?.forAll { kotlinSourceSet ->
63+
val preparedComposeResources = getPreparedComposeResourcesDir(kotlinSourceSet)
64+
variantResources.from(preparedComposeResources)
65+
}
66+
}
67+
}
68+
}
69+
6570
val copyFonts = registerTask<CopyAndroidFontsToAssetsTask>(
6671
"copy${variant.name.uppercaseFirstChar()}FontsToAndroidAssets"
6772
) {
68-
from.set(commonResourcesDir)
73+
from.set(variantResources)
6974
}
7075
variant.sources?.assets?.addGeneratedSourceDirectory(
7176
taskProvider = copyFonts,
@@ -83,7 +88,7 @@ internal abstract class CopyAndroidFontsToAssetsTask : DefaultTask() {
8388

8489
@get:InputFiles
8590
@get:IgnoreEmptyDirectories
86-
abstract val from: Property<File>
91+
abstract val from: Property<FileCollection>
8792

8893
@get:OutputDirectory
8994
abstract val outputDirectory: DirectoryProperty
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package org.jetbrains.compose.resources
22

33
import com.android.build.gradle.BaseExtension
4+
import com.android.build.gradle.internal.lint.AndroidLintAnalysisTask
5+
import com.android.build.gradle.internal.lint.LintModelWriterTask
46
import org.gradle.api.Project
57
import org.gradle.api.provider.Provider
68
import org.gradle.api.tasks.SourceSet
79
import org.gradle.api.tasks.TaskProvider
810
import org.gradle.util.GradleVersion
11+
import org.jetbrains.compose.ComposePlugin
912
import org.jetbrains.compose.internal.KOTLIN_JVM_PLUGIN_ID
1013
import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID
1114
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
@@ -33,22 +36,18 @@ internal fun Project.configureComposeResources(extension: ResourcesExtension) {
3336
private fun Project.onKgpApplied(config: Provider<ResourcesExtension>) {
3437
val kotlinExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java)
3538

36-
//common resources must be converted (XML -> CVR)
37-
val commonMain = KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME
38-
val preparedCommonResources = prepareCommonResources(commonMain)
39-
4039
val hasKmpResources = extraProperties.has(KMP_RES_EXT)
4140
val currentGradleVersion = GradleVersion.current()
4241
val minGradleVersion = GradleVersion.version(MIN_GRADLE_VERSION_FOR_KMP_RESOURCES)
4342
val kmpResourcesAreAvailable = hasKmpResources && currentGradleVersion >= minGradleVersion
4443

4544
if (kmpResourcesAreAvailable) {
46-
configureKmpResources(kotlinExtension, extraProperties.get(KMP_RES_EXT)!!, preparedCommonResources, config)
45+
configureKmpResources(kotlinExtension, extraProperties.get(KMP_RES_EXT)!!, config)
4746
} else {
4847
if (!hasKmpResources) logger.info(
4948
"""
5049
Compose resources publication requires Kotlin Gradle Plugin >= 2.0
51-
Current Kotlin Gradle Plugin is ${KotlinVersion.CURRENT}
50+
Current Kotlin Gradle Plugin is ${kotlinExtension.coreLibrariesVersion}
5251
""".trimIndent()
5352
)
5453
if (currentGradleVersion < minGradleVersion) logger.info(
@@ -58,13 +57,31 @@ private fun Project.onKgpApplied(config: Provider<ResourcesExtension>) {
5857
""".trimIndent()
5958
)
6059

61-
configureComposeResources(kotlinExtension, commonMain, preparedCommonResources, config)
60+
val commonMain = KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME
61+
configureComposeResources(kotlinExtension, commonMain, config)
6262

6363
//when applied AGP then configure android resources
6464
androidPluginIds.forEach { pluginId ->
6565
plugins.withId(pluginId) {
6666
val androidExtension = project.extensions.getByType(BaseExtension::class.java)
67-
configureAndroidComposeResources(kotlinExtension, androidExtension, preparedCommonResources)
67+
configureAndroidComposeResources(kotlinExtension, androidExtension)
68+
69+
70+
/*
71+
There is a dirty fix for the problem:
72+
73+
Reason: Task ':generateDemoDebugUnitTestLintModel' uses this output of task ':generateResourceAccessorsForAndroidUnitTest' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed.
74+
75+
Possible solutions:
76+
1. Declare task ':generateResourceAccessorsForAndroidUnitTest' as an input of ':generateDemoDebugUnitTestLintModel'.
77+
2. Declare an explicit dependency on ':generateResourceAccessorsForAndroidUnitTest' from ':generateDemoDebugUnitTestLintModel' using Task#dependsOn.
78+
3. Declare an explicit dependency on ':generateResourceAccessorsForAndroidUnitTest' from ':generateDemoDebugUnitTestLintModel' using Task#mustRunAfter.
79+
*/
80+
tasks.matching {
81+
it is AndroidLintAnalysisTask || it is LintModelWriterTask
82+
}.configureEach {
83+
it.mustRunAfter(tasks.withType(GenerateResourceAccessorsTask::class.java))
84+
}
6885
}
6986
}
7087
}
@@ -75,36 +92,20 @@ private fun Project.onKgpApplied(config: Provider<ResourcesExtension>) {
7592
private fun Project.onKotlinJvmApplied(config: Provider<ResourcesExtension>) {
7693
val kotlinExtension = project.extensions.getByType(KotlinProjectExtension::class.java)
7794
val main = SourceSet.MAIN_SOURCE_SET_NAME
78-
val preparedCommonResources = prepareCommonResources(main)
79-
configureComposeResources(kotlinExtension, main, preparedCommonResources, config)
80-
}
81-
82-
//common resources must be converted (XML -> CVR)
83-
private fun Project.prepareCommonResources(commonSourceSetName: String): Provider<File> {
84-
val preparedResourcesTask = registerPrepareComposeResourcesTask(
85-
project.projectDir.resolve("src/$commonSourceSetName/$COMPOSE_RESOURCES_DIR"),
86-
layout.buildDirectory.dir("$RES_GEN_DIR/preparedResources/$commonSourceSetName/$COMPOSE_RESOURCES_DIR")
87-
)
88-
return preparedResourcesTask.flatMap { it.outputDir }
95+
configureComposeResources(kotlinExtension, main, config)
8996
}
9097

9198
// sourceSet.resources.srcDirs doesn't work for Android targets.
9299
// Android resources should be configured separately
93100
private fun Project.configureComposeResources(
94101
kotlinExtension: KotlinProjectExtension,
95-
commonSourceSetName: String,
96-
preparedCommonResources: Provider<File>,
102+
resClassSourceSetName: String,
97103
config: Provider<ResourcesExtension>
98104
) {
99105
logger.info("Configure compose resources")
106+
configureComposeResourcesGeneration(kotlinExtension, resClassSourceSetName, config, false)
107+
100108
kotlinExtension.sourceSets.all { sourceSet ->
101-
val sourceSetName = sourceSet.name
102-
val resourcesDir = project.projectDir.resolve("src/$sourceSetName/$COMPOSE_RESOURCES_DIR")
103-
if (sourceSetName == commonSourceSetName) {
104-
sourceSet.resources.srcDirs(preparedCommonResources)
105-
configureGenerationComposeResClass(preparedCommonResources, sourceSet, config, false)
106-
} else {
107-
sourceSet.resources.srcDirs(resourcesDir)
108-
}
109+
sourceSet.resources.srcDirs(getPreparedComposeResourcesDir(sourceSet))
109110
}
110111
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package org.jetbrains.compose.resources
2+
3+
import org.gradle.api.Project
4+
import org.gradle.api.provider.Provider
5+
import org.jetbrains.compose.ComposePlugin
6+
import org.jetbrains.compose.internal.utils.uppercaseFirstChar
7+
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
8+
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
9+
import java.io.File
10+
11+
internal fun Project.configureComposeResourcesGeneration(
12+
kotlinExtension: KotlinProjectExtension,
13+
resClassSourceSetName: String,
14+
config: Provider<ResourcesExtension>,
15+
generateModulePath: Boolean
16+
) {
17+
logger.info("Configure compose resources generation")
18+
19+
//lazy check a dependency on the Resources library
20+
val shouldGenerateCode = config.map {
21+
when (it.generateResClass) {
22+
ResourcesExtension.ResourceClassGeneration.Auto -> {
23+
configurations.run {
24+
val commonSourceSet = kotlinExtension.sourceSets.getByName(resClassSourceSetName)
25+
//because the implementation configuration doesn't extend the api in the KGP ¯\_(ツ)_/¯
26+
getByName(commonSourceSet.implementationConfigurationName).allDependencies +
27+
getByName(commonSourceSet.apiConfigurationName).allDependencies
28+
}.any { dep ->
29+
val depStringNotation = dep.let { "${it.group}:${it.name}:${it.version}" }
30+
depStringNotation == ComposePlugin.CommonComponentsDependencies.resources
31+
}
32+
}
33+
34+
ResourcesExtension.ResourceClassGeneration.Always -> true
35+
ResourcesExtension.ResourceClassGeneration.Never -> false
36+
}
37+
}
38+
val packageName = config.getResourcePackage(project)
39+
val makeAccessorsPublic = config.map { it.publicResClass }
40+
val packagingDir = config.getModuleResourcesDir(project)
41+
42+
kotlinExtension.sourceSets.all { sourceSet ->
43+
if (sourceSet.name == resClassSourceSetName) {
44+
configureResClassGeneration(
45+
sourceSet,
46+
shouldGenerateCode,
47+
packageName,
48+
makeAccessorsPublic,
49+
packagingDir,
50+
generateModulePath
51+
)
52+
}
53+
54+
//common resources must be converted (XML -> CVR)
55+
val preparedResourcesTask = registerPrepareComposeResourcesTask(sourceSet)
56+
val preparedResources = preparedResourcesTask.flatMap { it.outputDir.asFile }
57+
configureResourceAccessorsGeneration(
58+
sourceSet,
59+
preparedResources,
60+
shouldGenerateCode,
61+
packageName,
62+
makeAccessorsPublic,
63+
packagingDir,
64+
generateModulePath
65+
)
66+
}
67+
68+
//setup task execution during IDE import
69+
tasks.configureEach { importTask ->
70+
if (importTask.name == "prepareKotlinIdeaImport") {
71+
importTask.dependsOn(tasks.withType(CodeGenerationTask::class.java))
72+
}
73+
}
74+
}
75+
76+
private fun Project.configureResClassGeneration(
77+
resClassSourceSet: KotlinSourceSet,
78+
shouldGenerateCode: Provider<Boolean>,
79+
packageName: Provider<String>,
80+
makeAccessorsPublic: Provider<Boolean>,
81+
packagingDir: Provider<File>,
82+
generateModulePath: Boolean
83+
) {
84+
logger.info("Configure Res class generation for ${resClassSourceSet.name}")
85+
86+
val genTask = tasks.register(
87+
"generateComposeResClass",
88+
GenerateResClassTask::class.java
89+
) { task ->
90+
task.packageName.set(packageName)
91+
task.shouldGenerateCode.set(shouldGenerateCode)
92+
task.makeAccessorsPublic.set(makeAccessorsPublic)
93+
task.codeDir.set(layout.buildDirectory.dir("$RES_GEN_DIR/kotlin/commonResClass"))
94+
95+
if (generateModulePath) {
96+
task.packagingDir.set(packagingDir)
97+
}
98+
}
99+
100+
//register generated source set
101+
resClassSourceSet.kotlin.srcDir(genTask.map { it.codeDir })
102+
}
103+
104+
private fun Project.configureResourceAccessorsGeneration(
105+
sourceSet: KotlinSourceSet,
106+
resourcesDir: Provider<File>,
107+
shouldGenerateCode: Provider<Boolean>,
108+
packageName: Provider<String>,
109+
makeAccessorsPublic: Provider<Boolean>,
110+
packagingDir: Provider<File>,
111+
generateModulePath: Boolean
112+
) {
113+
logger.info("Configure resource accessors generation for ${sourceSet.name}")
114+
115+
val genTask = tasks.register(
116+
"generateResourceAccessorsFor${sourceSet.name.uppercaseFirstChar()}",
117+
GenerateResourceAccessorsTask::class.java
118+
) { task ->
119+
task.packageName.set(packageName)
120+
task.sourceSetName.set(sourceSet.name)
121+
task.shouldGenerateCode.set(shouldGenerateCode)
122+
task.makeAccessorsPublic.set(makeAccessorsPublic)
123+
task.resDir.set(resourcesDir)
124+
task.codeDir.set(layout.buildDirectory.dir("$RES_GEN_DIR/kotlin/${sourceSet.name}ResourceAccessors"))
125+
126+
if (generateModulePath) {
127+
task.packagingDir.set(packagingDir)
128+
}
129+
}
130+
131+
//register generated source set
132+
sourceSet.kotlin.srcDir(genTask.map { it.codeDir })
133+
}

0 commit comments

Comments
 (0)