Skip to content

Commit

Permalink
Support source set's hierarchy for compose resources (#4589)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
terrakok committed Apr 10, 2024
1 parent 96f1ceb commit 062c9eb
Show file tree
Hide file tree
Showing 39 changed files with 714 additions and 365 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import com.android.build.gradle.BaseExtension
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileCollection
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.*
import org.jetbrains.compose.internal.utils.registerTask
import org.jetbrains.compose.internal.utils.uppercaseFirstChar
Expand All @@ -18,40 +18,31 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation
import org.jetbrains.kotlin.gradle.plugin.sources.android.androidSourceSetInfoOrNull
import org.jetbrains.kotlin.gradle.utils.ObservableSet
import java.io.File
import javax.inject.Inject

@OptIn(ExperimentalKotlinGradlePluginApi::class)
internal fun Project.configureAndroidComposeResources(
kotlinExtension: KotlinMultiplatformExtension,
androidExtension: BaseExtension,
preparedCommonResources: Provider<File>
androidExtension: BaseExtension
) {
val commonMain = KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME
val commonResourcesDir = projectDir.resolve("src/$commonMain/$COMPOSE_RESOURCES_DIR")

// 1) get the Kotlin Android Target Compilation -> [A]
// 2) get default source set name for the 'A'
// 3) find the associated Android SourceSet in the AndroidExtension -> [B]
// 4) get all source sets in the 'A' and add its resources to the 'B'
kotlinExtension.targets.withType(KotlinAndroidTarget::class.java).all { androidTarget ->
androidTarget.compilations.all { compilation: KotlinJvmAndroidCompilation ->

//fix for AGP < 8.0
//usually 'androidSourceSet.resources.srcDir(preparedCommonResources)' should be enough
compilation.androidVariant.processJavaResourcesProvider.configure { it.dependsOn(preparedCommonResources) }

compilation.defaultSourceSet.androidSourceSetInfoOrNull?.let { kotlinAndroidSourceSet ->
androidExtension.sourceSets
.matching { it.name == kotlinAndroidSourceSet.androidSourceSetName }
.all { androidSourceSet ->
(compilation.allKotlinSourceSets as? ObservableSet<KotlinSourceSet>)?.forAll { kotlinSourceSet ->
if (kotlinSourceSet.name == commonMain) {
androidSourceSet.resources.srcDir(preparedCommonResources)
} else {
androidSourceSet.resources.srcDir(
projectDir.resolve("src/${kotlinSourceSet.name}/$COMPOSE_RESOURCES_DIR")
)
val preparedComposeResources = getPreparedComposeResourcesDir(kotlinSourceSet)
androidSourceSet.resources.srcDirs(preparedComposeResources)

//fix for AGP < 8.0
//usually 'androidSourceSet.resources.srcDir(preparedCommonResources)' should be enough
compilation.androidVariant.processJavaResourcesProvider.configure {
it.dependsOn(preparedComposeResources)
}
}
}
Expand All @@ -62,10 +53,24 @@ internal fun Project.configureAndroidComposeResources(
//copy fonts from the compose resources dir to android assets
val androidComponents = project.extensions.findByType(AndroidComponentsExtension::class.java) ?: return
androidComponents.onVariants { variant ->
val variantResources = project.files()

kotlinExtension.targets.withType(KotlinAndroidTarget::class.java).all { androidTarget ->
androidTarget.compilations.all { compilation: KotlinJvmAndroidCompilation ->
if (compilation.androidVariant.name == variant.name) {
project.logger.info("Configure fonts for variant ${variant.name}")
(compilation.allKotlinSourceSets as? ObservableSet<KotlinSourceSet>)?.forAll { kotlinSourceSet ->
val preparedComposeResources = getPreparedComposeResourcesDir(kotlinSourceSet)
variantResources.from(preparedComposeResources)
}
}
}
}

val copyFonts = registerTask<CopyAndroidFontsToAssetsTask>(
"copy${variant.name.uppercaseFirstChar()}FontsToAndroidAssets"
) {
from.set(commonResourcesDir)
from.set(variantResources)
}
variant.sources?.assets?.addGeneratedSourceDirectory(
taskProvider = copyFonts,
Expand All @@ -83,7 +88,7 @@ internal abstract class CopyAndroidFontsToAssetsTask : DefaultTask() {

@get:InputFiles
@get:IgnoreEmptyDirectories
abstract val from: Property<File>
abstract val from: Property<FileCollection>

@get:OutputDirectory
abstract val outputDirectory: DirectoryProperty
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package org.jetbrains.compose.resources

import com.android.build.gradle.BaseExtension
import com.android.build.gradle.internal.lint.AndroidLintAnalysisTask
import com.android.build.gradle.internal.lint.LintModelWriterTask
import org.gradle.api.Project
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.TaskProvider
import org.gradle.util.GradleVersion
import org.jetbrains.compose.ComposePlugin
import org.jetbrains.compose.internal.KOTLIN_JVM_PLUGIN_ID
import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
Expand Down Expand Up @@ -33,22 +36,18 @@ internal fun Project.configureComposeResources(extension: ResourcesExtension) {
private fun Project.onKgpApplied(config: Provider<ResourcesExtension>) {
val kotlinExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java)

//common resources must be converted (XML -> CVR)
val commonMain = KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME
val preparedCommonResources = prepareCommonResources(commonMain)

val hasKmpResources = extraProperties.has(KMP_RES_EXT)
val currentGradleVersion = GradleVersion.current()
val minGradleVersion = GradleVersion.version(MIN_GRADLE_VERSION_FOR_KMP_RESOURCES)
val kmpResourcesAreAvailable = hasKmpResources && currentGradleVersion >= minGradleVersion

if (kmpResourcesAreAvailable) {
configureKmpResources(kotlinExtension, extraProperties.get(KMP_RES_EXT)!!, preparedCommonResources, config)
configureKmpResources(kotlinExtension, extraProperties.get(KMP_RES_EXT)!!, config)
} else {
if (!hasKmpResources) logger.info(
"""
Compose resources publication requires Kotlin Gradle Plugin >= 2.0
Current Kotlin Gradle Plugin is ${KotlinVersion.CURRENT}
Current Kotlin Gradle Plugin is ${kotlinExtension.coreLibrariesVersion}
""".trimIndent()
)
if (currentGradleVersion < minGradleVersion) logger.info(
Expand All @@ -58,13 +57,31 @@ private fun Project.onKgpApplied(config: Provider<ResourcesExtension>) {
""".trimIndent()
)

configureComposeResources(kotlinExtension, commonMain, preparedCommonResources, config)
val commonMain = KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME
configureComposeResources(kotlinExtension, commonMain, config)

//when applied AGP then configure android resources
androidPluginIds.forEach { pluginId ->
plugins.withId(pluginId) {
val androidExtension = project.extensions.getByType(BaseExtension::class.java)
configureAndroidComposeResources(kotlinExtension, androidExtension, preparedCommonResources)
configureAndroidComposeResources(kotlinExtension, androidExtension)


/*
There is a dirty fix for the problem:
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.
Possible solutions:
1. Declare task ':generateResourceAccessorsForAndroidUnitTest' as an input of ':generateDemoDebugUnitTestLintModel'.
2. Declare an explicit dependency on ':generateResourceAccessorsForAndroidUnitTest' from ':generateDemoDebugUnitTestLintModel' using Task#dependsOn.
3. Declare an explicit dependency on ':generateResourceAccessorsForAndroidUnitTest' from ':generateDemoDebugUnitTestLintModel' using Task#mustRunAfter.
*/
tasks.matching {
it is AndroidLintAnalysisTask || it is LintModelWriterTask
}.configureEach {
it.mustRunAfter(tasks.withType(GenerateResourceAccessorsTask::class.java))
}
}
}
}
Expand All @@ -75,36 +92,20 @@ private fun Project.onKgpApplied(config: Provider<ResourcesExtension>) {
private fun Project.onKotlinJvmApplied(config: Provider<ResourcesExtension>) {
val kotlinExtension = project.extensions.getByType(KotlinProjectExtension::class.java)
val main = SourceSet.MAIN_SOURCE_SET_NAME
val preparedCommonResources = prepareCommonResources(main)
configureComposeResources(kotlinExtension, main, preparedCommonResources, config)
}

//common resources must be converted (XML -> CVR)
private fun Project.prepareCommonResources(commonSourceSetName: String): Provider<File> {
val preparedResourcesTask = registerPrepareComposeResourcesTask(
project.projectDir.resolve("src/$commonSourceSetName/$COMPOSE_RESOURCES_DIR"),
layout.buildDirectory.dir("$RES_GEN_DIR/preparedResources/$commonSourceSetName/$COMPOSE_RESOURCES_DIR")
)
return preparedResourcesTask.flatMap { it.outputDir }
configureComposeResources(kotlinExtension, main, config)
}

// sourceSet.resources.srcDirs doesn't work for Android targets.
// Android resources should be configured separately
private fun Project.configureComposeResources(
kotlinExtension: KotlinProjectExtension,
commonSourceSetName: String,
preparedCommonResources: Provider<File>,
resClassSourceSetName: String,
config: Provider<ResourcesExtension>
) {
logger.info("Configure compose resources")
configureComposeResourcesGeneration(kotlinExtension, resClassSourceSetName, config, false)

kotlinExtension.sourceSets.all { sourceSet ->
val sourceSetName = sourceSet.name
val resourcesDir = project.projectDir.resolve("src/$sourceSetName/$COMPOSE_RESOURCES_DIR")
if (sourceSetName == commonSourceSetName) {
sourceSet.resources.srcDirs(preparedCommonResources)
configureGenerationComposeResClass(preparedCommonResources, sourceSet, config, false)
} else {
sourceSet.resources.srcDirs(resourcesDir)
}
sourceSet.resources.srcDirs(getPreparedComposeResourcesDir(sourceSet))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package org.jetbrains.compose.resources

import org.gradle.api.Project
import org.gradle.api.provider.Provider
import org.jetbrains.compose.ComposePlugin
import org.jetbrains.compose.internal.utils.uppercaseFirstChar
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import java.io.File

internal fun Project.configureComposeResourcesGeneration(
kotlinExtension: KotlinProjectExtension,
resClassSourceSetName: String,
config: Provider<ResourcesExtension>,
generateModulePath: Boolean
) {
logger.info("Configure compose resources generation")

//lazy check a dependency on the Resources library
val shouldGenerateCode = config.map {
when (it.generateResClass) {
ResourcesExtension.ResourceClassGeneration.Auto -> {
configurations.run {
val commonSourceSet = kotlinExtension.sourceSets.getByName(resClassSourceSetName)
//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
ResourcesExtension.ResourceClassGeneration.Never -> false
}
}
val packageName = config.getResourcePackage(project)
val makeAccessorsPublic = config.map { it.publicResClass }
val packagingDir = config.getModuleResourcesDir(project)

kotlinExtension.sourceSets.all { sourceSet ->
if (sourceSet.name == resClassSourceSetName) {
configureResClassGeneration(
sourceSet,
shouldGenerateCode,
packageName,
makeAccessorsPublic,
packagingDir,
generateModulePath
)
}

//common resources must be converted (XML -> CVR)
val preparedResourcesTask = registerPrepareComposeResourcesTask(sourceSet)
val preparedResources = preparedResourcesTask.flatMap { it.outputDir.asFile }
configureResourceAccessorsGeneration(
sourceSet,
preparedResources,
shouldGenerateCode,
packageName,
makeAccessorsPublic,
packagingDir,
generateModulePath
)
}

//setup task execution during IDE import
tasks.configureEach { importTask ->
if (importTask.name == "prepareKotlinIdeaImport") {
importTask.dependsOn(tasks.withType(CodeGenerationTask::class.java))
}
}
}

private fun Project.configureResClassGeneration(
resClassSourceSet: KotlinSourceSet,
shouldGenerateCode: Provider<Boolean>,
packageName: Provider<String>,
makeAccessorsPublic: Provider<Boolean>,
packagingDir: Provider<File>,
generateModulePath: Boolean
) {
logger.info("Configure Res class generation for ${resClassSourceSet.name}")

val genTask = tasks.register(
"generateComposeResClass",
GenerateResClassTask::class.java
) { task ->
task.packageName.set(packageName)
task.shouldGenerateCode.set(shouldGenerateCode)
task.makeAccessorsPublic.set(makeAccessorsPublic)
task.codeDir.set(layout.buildDirectory.dir("$RES_GEN_DIR/kotlin/commonResClass"))

if (generateModulePath) {
task.packagingDir.set(packagingDir)
}
}

//register generated source set
resClassSourceSet.kotlin.srcDir(genTask.map { it.codeDir })
}

private fun Project.configureResourceAccessorsGeneration(
sourceSet: KotlinSourceSet,
resourcesDir: Provider<File>,
shouldGenerateCode: Provider<Boolean>,
packageName: Provider<String>,
makeAccessorsPublic: Provider<Boolean>,
packagingDir: Provider<File>,
generateModulePath: Boolean
) {
logger.info("Configure resource accessors generation for ${sourceSet.name}")

val genTask = tasks.register(
"generateResourceAccessorsFor${sourceSet.name.uppercaseFirstChar()}",
GenerateResourceAccessorsTask::class.java
) { task ->
task.packageName.set(packageName)
task.sourceSetName.set(sourceSet.name)
task.shouldGenerateCode.set(shouldGenerateCode)
task.makeAccessorsPublic.set(makeAccessorsPublic)
task.resDir.set(resourcesDir)
task.codeDir.set(layout.buildDirectory.dir("$RES_GEN_DIR/kotlin/${sourceSet.name}ResourceAccessors"))

if (generateModulePath) {
task.packagingDir.set(packagingDir)
}
}

//register generated source set
sourceSet.kotlin.srcDir(genTask.map { it.codeDir })
}

0 comments on commit 062c9eb

Please sign in to comment.