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

Support multimodule projects and libraries publication with compose resources #4454

Merged
merged 14 commits into from
Mar 14, 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
9 changes: 7 additions & 2 deletions gradle-plugins/compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ val embeddedDependencies by configurations.creating {
isTransitive = false
}

val kgpResourcesDevVersion = "2.0.0-dev-17632"
//KMP resources API available since ^kgpResourcesDevVersion
repositories {
maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev/")
}

dependencies {
// By default, Gradle resolves plugins only via Gradle Plugin Portal.
// To avoid declaring an additional repo, all dependencies must:
Expand All @@ -57,8 +63,7 @@ dependencies {

compileOnly(gradleApi())
compileOnly(localGroovy())
compileOnly(kotlin("gradle-plugin-api"))
compileOnly(kotlin("gradle-plugin"))
compileOnly(kotlin("gradle-plugin", kgpResourcesDevVersion))
compileOnly(kotlin("native-utils"))
compileOnly(libs.plugin.android)
compileOnly(libs.plugin.android.api)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ internal abstract class GenerateResClassTask : DefaultTask() {
@get:Input
abstract val packageName: Property<String>

@get:Input
@get:Optional
abstract val moduleDir: Property<File>

@get:Input
abstract val shouldGenerateResClass: Property<Boolean>

Expand Down Expand Up @@ -56,7 +60,11 @@ internal abstract class GenerateResClassTask : DefaultTask() {
}
.groupBy { it.type }
.mapValues { (_, items) -> items.groupBy { it.name } }
getResFileSpecs(resources, packageName.get()).forEach { it.writeTo(kotlinDir) }
getResFileSpecs(
resources,
packageName.get(),
moduleDir.getOrNull()?.let { it.invariantSeparatorsPath + "/" } ?: ""
).forEach { it.writeTo(kotlinDir) }
} else {
logger.info("Generation Res class is disabled")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,100 @@ package org.jetbrains.compose.resources

import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.internal.tasks.ProcessJavaResTask
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty
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.gradle.util.GradleVersion
import org.jetbrains.compose.ComposePlugin
import org.jetbrains.compose.desktop.application.internal.ComposeProperties
import org.jetbrains.compose.internal.KOTLIN_JVM_PLUGIN_ID
import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID
import org.jetbrains.compose.internal.utils.*
import org.jetbrains.compose.internal.utils.registerTask
import org.jetbrains.compose.internal.utils.uppercaseFirstChar
import org.jetbrains.compose.resources.ios.getSyncResourcesTaskName
import org.jetbrains.kotlin.gradle.ComposeKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.mpp.*
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.KotlinTargetResourcesPublication
import org.jetbrains.kotlin.gradle.plugin.sources.android.androidSourceSetInfoOrNull
import org.jetbrains.kotlin.gradle.utils.ObservableSet
import java.io.File
import javax.inject.Inject

internal const val COMPOSE_RESOURCES_DIR = "composeResources"
internal const val RES_GEN_DIR = "generated/compose/resourceGenerator"
private const val COMPOSE_RESOURCES_DIR = "composeResources"
private const val RES_GEN_DIR = "generated/compose/resourceGenerator"
private const val KMP_RES_EXT = "multiplatformResourcesPublication"
private const val MIN_GRADLE_VERSION_FOR_KMP_RESOURCES = "7.6"
private val androidPluginIds = listOf(
"com.android.application",
"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
}

plugins.withId(KOTLIN_MPP_PLUGIN_ID) {
val kotlinExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java)
configureComposeResources(kotlinExtension, KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME)

//when applied AGP then configure android resources
androidPluginIds.forEach { pluginId ->
plugins.withId(pluginId) {
val androidExtension = project.extensions.getByType(BaseExtension::class.java)
configureAndroidComposeResources(kotlinExtension, androidExtension)
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)
} else {
if (!hasKmpResources) {
logger.info(
"""
Compose resources publication requires Kotlin Gradle Plugin >= 2.0
Current Kotlin Gradle Plugin is ${KotlinVersion.CURRENT}
""".trimIndent()
)
}
if (currentGradleVersion < minGradleVersion) {
logger.info(
"""
Compose resources publication requires Gradle >= $MIN_GRADLE_VERSION_FOR_KMP_RESOURCES
Current Gradle is ${currentGradleVersion.version}
""".trimIndent()
)
}

//current KGP doesn't have KPM resources
configureComposeResources(kotlinExtension, KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME, projectId)

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

private fun Project.configureComposeResources(kotlinExtension: KotlinProjectExtension, commonSourceSetName: String) {
private fun Project.configureComposeResources(
kotlinExtension: KotlinProjectExtension,
commonSourceSetName: String,
projectId: Provider<String>
) {
logger.info("Configure compose resources")
kotlinExtension.sourceSets.all { sourceSet ->
val sourceSetName = sourceSet.name
val composeResourcesPath = project.projectDir.resolve("src/$sourceSetName/$COMPOSE_RESOURCES_DIR")
Expand All @@ -63,7 +105,103 @@ private fun Project.configureComposeResources(kotlinExtension: KotlinProjectExte
sourceSet.resources.srcDirs(composeResourcesPath)

if (sourceSetName == commonSourceSetName) {
configureResourceGenerator(composeResourcesPath, sourceSet)
configureResourceGenerator(composeResourcesPath, sourceSet, projectId, false)
}
}
}

@OptIn(ComposeKotlinGradlePluginApi::class)
private fun Project.configureKmpResources(
kotlinExtension: KotlinProjectExtension,
kmpResources: Any,
projectId: Provider<String>
) {
kotlinExtension as KotlinMultiplatformExtension
kmpResources as KotlinTargetResourcesPublication

logger.info("Configure KMP resources")

//configure KMP resources publishing for each supported target
kotlinExtension.targets
.matching { target -> kmpResources.canPublishResources(target) }
.all { target ->
logger.info("Configure resources publication for '${target.targetName}' target")
kmpResources.publishResourcesAsKotlinComponent(
target,
{ sourceSet ->
KotlinTargetResourcesPublication.ResourceRoot(
project.provider { project.file("src/${sourceSet.name}/$COMPOSE_RESOURCES_DIR") },
emptyList(),
//for android target exclude fonts
if (target is KotlinAndroidTarget) listOf("**/font*/*") else emptyList()
)
},
projectId.asModuleDir()
)

if (target is KotlinAndroidTarget) {
//for android target publish fonts in assets
logger.info("Configure fonts relocation for '${target.targetName}' target")
kmpResources.publishInAndroidAssets(
target,
{ sourceSet ->
KotlinTargetResourcesPublication.ResourceRoot(
project.provider { project.file("src/${sourceSet.name}/$COMPOSE_RESOURCES_DIR") },
listOf("**/font*/*"),
emptyList()
)
},
projectId.asModuleDir()
)
}
}

//generate accessors for common resources
kotlinExtension.sourceSets.all { sourceSet ->
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)
}
}

//add all resolved resources for browser and native compilations
val platformsForSetupCompilation = listOf(KotlinPlatformType.native, KotlinPlatformType.js, KotlinPlatformType.wasm)
kotlinExtension.targets
.matching { target -> target.platformType in platformsForSetupCompilation }
.all { target: KotlinTarget ->
val allResources = kmpResources.resolveResources(target)
target.compilations.all { compilation ->
if (compilation.name == KotlinCompilation.MAIN_COMPILATION_NAME) {
configureResourcesForCompilation(compilation, allResources)
}
}
}
}

/**
* Add resolved resources to a kotlin compilation to include it into a resulting platform artefact
* It is required for JS and Native targets.
* For JVM and Android it works automatically via jar files
*/
private fun Project.configureResourcesForCompilation(
compilation: KotlinCompilation<*>,
directoryWithAllResourcesForCompilation: Provider<File>
) {
logger.info("Add all resolved resources to '${compilation.target.targetName}' target '${compilation.name}' compilation")
compilation.defaultSourceSet.resources.srcDir(directoryWithAllResourcesForCompilation)
if (compilation is KotlinJsCompilation) {
tasks.named(compilation.processResourcesTaskName).configure { processResourcesTask ->
processResourcesTask.dependsOn(directoryWithAllResourcesForCompilation)
}
}
if (compilation is KotlinNativeCompilation) {
compilation.target.binaries.withType(Framework::class.java).all { framework ->
tasks.configureEach { task ->
if (task.name == framework.getSyncResourcesTaskName()) {
task.dependsOn(directoryWithAllResourcesForCompilation)
}
}
}
}
}
Expand Down Expand Up @@ -110,16 +248,15 @@ private fun Project.configureAndroidComposeResources(
}
}

private fun Project.configureResourceGenerator(commonComposeResourcesDir: File, commonSourceSet: KotlinSourceSet) {
val packageName = provider {
buildString {
val group = project.group.toString().lowercase().asUnderscoredIdentifier()
append(group)
if (group.isNotEmpty()) append(".")
append(project.name.lowercase().asUnderscoredIdentifier())
append(".generated.resources")
}
}
private fun Project.configureResourceGenerator(
commonComposeResourcesDir: File,
commonSourceSet: KotlinSourceSet,
projectId: Provider<String>,
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) })

Expand All @@ -142,11 +279,15 @@ private fun Project.configureResourceGenerator(commonComposeResourcesDir: File,
val genTask = tasks.register(
"generateComposeResClass",
GenerateResClassTask::class.java
) {
it.packageName.set(packageName)
it.shouldGenerateResClass.set(shouldGenerateResClass)
it.resDir.set(commonComposeResourcesDir)
it.codeDir.set(buildDir("$RES_GEN_DIR/kotlin"))
) { task ->
task.packageName.set(packageName)
task.shouldGenerateResClass.set(shouldGenerateResClass)
task.resDir.set(commonComposeResourcesDir)
task.codeDir.set(buildDir("$RES_GEN_DIR/kotlin"))

if (generateModulePath) {
task.moduleDir.set(projectId.asModuleDir())
}
}

//register generated source set
Expand All @@ -160,6 +301,8 @@ private fun Project.configureResourceGenerator(commonComposeResourcesDir: File,
}
}

private fun Provider<String>.asModuleDir() = map { File("$COMPOSE_RESOURCES_DIR/$it") }

//Copy task doesn't work with 'variant.sources?.assets?.addGeneratedSourceDirectory' API
internal abstract class CopyAndroidFontsToAssetsTask : DefaultTask() {
@get:Inject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ private const val ITEMS_PER_FILE_LIMIT = 500
internal fun getResFileSpecs(
//type -> id -> items
resources: Map<ResourceType, Map<String, List<ResourceItem>>>,
packageName: String
packageName: String,
moduleDir: String
): List<FileSpec> {
val files = mutableListOf<FileSpec>()
val resClass = FileSpec.builder(packageName, "Res").also { file ->
Expand Down Expand Up @@ -147,7 +148,7 @@ internal fun getResFileSpecs(
.addParameter("path", String::class)
.addModifiers(KModifier.SUSPEND)
.returns(ByteArray::class)
.addStatement("return %M(path)", readResourceBytes) //todo: add module ID here
.addStatement("""return %M("$moduleDir" + path)""", readResourceBytes)
.build()
)
ResourceType.values().forEach { type ->
Expand All @@ -163,7 +164,13 @@ internal fun getResFileSpecs(

chunks.forEachIndexed { index, ids ->
files.add(
getChunkFileSpec(type, index, packageName, idToResources.subMap(ids.first(), true, ids.last(), true))
getChunkFileSpec(
type,
index,
packageName,
moduleDir,
idToResources.subMap(ids.first(), true, ids.last(), true)
)
)
}
}
Expand All @@ -175,6 +182,7 @@ private fun getChunkFileSpec(
type: ResourceType,
index: Int,
packageName: String,
moduleDir: String,
idToResources: Map<String, List<ResourceItem>>
): FileSpec {
val chunkClassName = type.typeName.uppercaseFirstChar() + index
Expand Down Expand Up @@ -220,7 +228,7 @@ private fun getChunkFileSpec(
add("%T(", resourceItemClass)
add("setOf(").addQualifiers(item).add("), ")
//file separator should be '/' on all platforms
add("\"${item.path.invariantSeparatorsPathString}\"") //todo: add module ID here
add("\"$moduleDir${item.path.invariantSeparatorsPathString}\"")
add("),\n")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ private fun SyncIosResourcesContext.configureSyncResourcesTasks() {
val lazyTasksDependencies = LazyTasksDependencyConfigurator(project.tasks)
configureEachIosFramework { framework ->
val frameworkClassifier = framework.namePrefix.uppercaseFirstChar()
val syncResourcesTaskName = "sync${frameworkClassifier}ComposeResourcesForIos"
val syncResourcesTaskName = framework.getSyncResourcesTaskName()
val checkSyncResourcesTaskName = "checkCanSync${frameworkClassifier}ComposeResourcesForIos"
val checkNoSandboxTask = framework.project.tasks.registerOrConfigure<CheckCanAccessComposeResourcesDirectory>(checkSyncResourcesTaskName) {}
val syncTask = framework.project.tasks.registerOrConfigure<SyncComposeResourcesForIosTask>(syncResourcesTaskName) {
Expand Down Expand Up @@ -191,6 +191,9 @@ private fun SyncIosResourcesContext.configureSyncResourcesTasks() {
}
}

internal fun Framework.getSyncResourcesTaskName() =
"sync${namePrefix.uppercaseFirstChar()}ComposeResourcesForIos"

private val Framework.isCocoapodsFramework: Boolean
get() = name.startsWith("pod")

Expand Down
Loading
Loading