Publish multiplatform resources and assets for Android target

^KT-65540
This commit is contained in:
Timofey Solonin
2024-02-15 16:21:17 +01:00
committed by Space Team
parent 43a4fd5ddf
commit a2989a3711
3 changed files with 351 additions and 1 deletions
@@ -0,0 +1,189 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
@file:Suppress("DEPRECATION")
package org.jetbrains.kotlin.gradle.plugin.mpp.resources.publication
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.Variant
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.tasks.*
import org.gradle.work.DisableCachingByDefault
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.AndroidGradlePluginVersion
import org.jetbrains.kotlin.gradle.plugin.KotlinProjectSetupAction
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.Companion.kotlinPropertiesProvider
import org.jetbrains.kotlin.gradle.plugin.launch
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.disambiguateName
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.KotlinTargetResourcesPublicationImpl
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.assembleHierarchicalResources
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.resourcesPublicationExtension
import org.jetbrains.kotlin.gradle.plugin.sources.android.androidSourceSetInfo
import org.jetbrains.kotlin.gradle.tasks.registerTask
import org.jetbrains.kotlin.gradle.utils.androidExtension
import org.jetbrains.kotlin.gradle.utils.androidPluginIds
import org.jetbrains.kotlin.gradle.utils.findByType
import org.jetbrains.kotlin.gradle.utils.getOrPut
import javax.inject.Inject
private val Project.kotlinMultiplatformAndroidResourcesPublication: KotlinAndroidTargetResourcesPublication
get() {
return extraProperties.getOrPut("kotlinMultiplatformAndroidResourcesPublication") {
KotlinAndroidTargetResourcesPublication()
}
}
internal val SetUpMultiplatformAndroidAssetsAndResourcesPublicationAction = KotlinProjectSetupAction {
if (!kotlinPropertiesProvider.mppResourcesPublication) return@KotlinProjectSetupAction
project.launchInStage(KotlinPluginLifecycle.Stage.AfterFinaliseCompilations) {
multiplatformExtension.awaitTargets()
.filterIsInstance<KotlinAndroidTarget>()
.forEach { target ->
target.compilations.forEach { compilation ->
target.setUpMultiplatformResourcesAndAssets(compilation)
}
}
}
var isConfigured = false
androidPluginIds.forEach { pluginId ->
pluginManager.withPlugin(pluginId) {
if (isConfigured) {
return@withPlugin
}
isConfigured = true
// Bundle assets and resources only if AGP is 7.3.0+ due to "addGeneratedSourceDirectory" API availability. And the resources
// API is only supported in CMP since 7.3.1
if (AndroidGradlePluginVersion.current >= KotlinAndroidTargetResourcesPublication.MIN_AGP_VERSION) {
project.extensions.findByType<AndroidComponentsExtension<*, *, *>>()?.onVariants { newAgpVariant ->
kotlinMultiplatformAndroidResourcesPublication.setUpCopyAssetsTasksForNewAgpVariants(
project,
newAgpVariant,
)
}
}
}
}
}
internal class KotlinAndroidTargetResourcesPublication {
@DisableCachingByDefault
internal abstract class AssetsCopyForAGPTask : DefaultTask() {
@get:Inject
abstract val fileSystem: FileSystemOperations
@get:SkipWhenEmpty
@get:PathSensitive(PathSensitivity.RELATIVE)
@get:InputDirectory
abstract val inputDirectory: DirectoryProperty
@get:OutputDirectory
abstract val outputDirectory: DirectoryProperty
@TaskAction
fun copy() {
fileSystem.copy {
it.from(inputDirectory)
it.into(outputDirectory)
}
}
}
private val copyTaskFromVariantName: MutableMap<String, TaskProvider<AssetsCopyForAGPTask>> = mutableMapOf()
private val subscribers: MutableMap<String, MutableList<(TaskProvider<AssetsCopyForAGPTask>) -> (Unit)>> = mutableMapOf()
internal fun setUpCopyAssetsTasksForNewAgpVariants(
project: Project,
newAgpVariant: Variant,
) {
val variantName = newAgpVariant.name
if (copyTaskFromVariantName[variantName] != null) {
error("Setting up multiple copy assets tasks for variant $variantName")
}
// Prepare these copy tasks in advance because onVariants API has a specific lifecycle requirement and only set up the input for
// this task if assets publication is requested
val copyTask = project.registerTask<AssetsCopyForAGPTask>("${variantName}AssetsCopyForAGP") {
// Specify output just in case
it.outputDirectory.convention(
project.layout.buildDirectory.dir(
"${KotlinTargetResourcesPublicationImpl.MULTIPLATFORM_RESOURCES_DIRECTORY}/assemble-hierarchically/${variantName}-tmp-for-agp"
)
)
}
newAgpVariant.sources.assets?.addGeneratedSourceDirectory(
taskProvider = copyTask,
wiredWith = { task -> task.outputDirectory }
)
copyTaskFromVariantName[variantName] = copyTask
subscribers[variantName].orEmpty().forEach { notify ->
notify(copyTask)
}
}
internal fun subscribeOnCopyAssetsTaskForVariant(
variantName: String,
notify: (TaskProvider<AssetsCopyForAGPTask>) -> (Unit),
) {
// Associate between BaseVariant and Variant by name
copyTaskFromVariantName[variantName]?.let(notify)
subscribers.getOrPut(variantName, { mutableListOf() }).add(notify)
}
companion object {
const val MIN_AGP_VERSION = "7.3.1"
}
}
internal fun KotlinAndroidTarget.setUpMultiplatformResourcesAndAssets(
compilation: KotlinJvmAndroidCompilation,
) {
if (project.multiplatformExtensionOrNull == null) return
val legacyAgpVariant = compilation.androidVariant
val legacyAgpVariantName = getVariantName(legacyAgpVariant)
project.multiplatformExtension.resourcesPublicationExtension?.subscribeOnPublishResources(this) { resources ->
val copyResourcesTask = compilation.assembleHierarchicalResources(
disambiguateName("${legacyAgpVariantName}Resources"),
resources,
)
if (AndroidGradlePluginVersion.current < "8.0.0") {
// AGP [7.3-8) requires explicit dependsOn
legacyAgpVariant.processJavaResourcesProvider.configure {
it.dependsOn(copyResourcesTask)
}
}
project.androidExtension.sourceSets.getByName(
compilation.defaultSourceSet.androidSourceSetInfo.androidSourceSetName
).resources.srcDir(copyResourcesTask)
}
project.multiplatformExtension.resourcesPublicationExtension?.subscribeOnAndroidPublishAssets(this) { assets ->
project.kotlinMultiplatformAndroidResourcesPublication.subscribeOnCopyAssetsTaskForVariant(legacyAgpVariantName) { copyTaskForAgp ->
val copyAssetsTask = compilation.assembleHierarchicalResources(
disambiguateName("${legacyAgpVariantName}Assets"),
assets,
)
copyTaskForAgp.configure {
it.inputDirectory.set(copyAssetsTask)
}
}
}
}
@@ -20,8 +20,9 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.apple.AddBuildListenerForXCodeSetu
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XcodeVersionSetupAction
import org.jetbrains.kotlin.gradle.plugin.mpp.compilationImpl.*
import org.jetbrains.kotlin.gradle.plugin.mpp.internal.DeprecatedMppGradlePropertiesMigrationSetupAction
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.RegisterMultiplatformResourcesPublicationExtensionAction
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.publication.SetUpMultiplatformAndroidAssetsAndResourcesPublicationAction
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.publication.SetUpMultiplatformJvmResourcesPublicationAction
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.RegisterMultiplatformResourcesPublicationExtensionAction
import org.jetbrains.kotlin.gradle.plugin.sources.KotlinMultiplatformSourceSetSetupAction
import org.jetbrains.kotlin.gradle.plugin.sources.LanguageSettingsSetupAction
import org.jetbrains.kotlin.gradle.plugin.statistics.MultiplatformBuildStatsReportSetupAction
@@ -84,6 +85,7 @@ internal fun Project.registerKotlinPluginExtensions() {
register(project, SetupConsistentMetadataDependenciesResolution)
register(project, RegisterMultiplatformResourcesPublicationExtensionAction)
register(project, SetUpMultiplatformJvmResourcesPublicationAction)
register(project, SetUpMultiplatformAndroidAssetsAndResourcesPublicationAction)
}
}
@@ -0,0 +1,159 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.unitTests
import com.android.build.gradle.LibraryExtension
import org.gradle.api.Project
import org.gradle.api.internal.project.ProjectInternal
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.KotlinTargetResourcesPublication
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.resourcesPublicationExtension
import org.jetbrains.kotlin.gradle.util.buildProjectWithMPP
import org.jetbrains.kotlin.gradle.util.kotlin
import org.jetbrains.kotlin.gradle.utils.androidExtension
import org.junit.Test
import java.io.File
class KotlinAndroidTargetResourcesPublicationTests {
private val Project.expectedReleaseResourcePath
get() = layout.buildDirectory.dir(
"kotlin-multiplatform-resources/assemble-hierarchically/androidReleaseResources"
).get().asFile
private val Project.expectedDebugResourcePath
get() = layout.buildDirectory.dir(
"kotlin-multiplatform-resources/assemble-hierarchically/androidDebugResources"
).get().asFile
private val Project.expectedDemoReleaseResourcePath
get() = layout.buildDirectory.dir(
"kotlin-multiplatform-resources/assemble-hierarchically/androidDemoReleaseResources"
).get().asFile
private val Project.expectedFullReleaseResourcePath
get() = layout.buildDirectory.dir(
"kotlin-multiplatform-resources/assemble-hierarchically/androidFullReleaseResources"
).get().asFile
@Test
fun `test publishing android resources - reflects in android resource directories`() {
val project = mppProjectWithAndroidTarget()
project.evaluate()
project.publishFakeResources(project.multiplatformExtension.androidTarget())
project.publishFakeAssets(project.multiplatformExtension.androidTarget())
val androidVariantSourceSets = project.multiplatformExtension.androidTarget()
.compilations.getByName("release")
.androidVariant.sourceSets
val actualResourcesDirectories = androidVariantSourceSets.flatMapTo(linkedSetOf()) { it.resourcesDirectories }
assert(
actualResourcesDirectories.contains(project.expectedReleaseResourcePath)
) { actualResourcesDirectories.dumpPaths() }
assert(
!actualResourcesDirectories.contains(project.expectedDebugResourcePath)
) { actualResourcesDirectories.dumpPaths() }
}
@Test
fun `test publishing android resources with flavors - reflects in android resource directories`() {
val project = mppProjectWithAndroidTarget {
val dimension = "version"
flavorDimensions += dimension
productFlavors {
with(create("demo")) { this.dimension = dimension }
with(create("full")) { this.dimension = dimension }
}
}
project.evaluate()
project.publishFakeResources(project.multiplatformExtension.androidTarget())
val androidVariantSourceSets = project.multiplatformExtension.androidTarget()
.compilations.getByName("demoRelease")
.androidVariant.sourceSets
val actualResourcesDirectories = androidVariantSourceSets.flatMapTo(linkedSetOf()) {
it.resourcesDirectories
}
assert(
actualResourcesDirectories.contains(project.expectedDemoReleaseResourcePath)
) { actualResourcesDirectories.dumpPaths() }
assert(
!actualResourcesDirectories.contains(project.expectedFullReleaseResourcePath)
) { actualResourcesDirectories.dumpPaths() }
assert(
!actualResourcesDirectories.contains(project.expectedReleaseResourcePath)
) { actualResourcesDirectories.dumpPaths() }
}
@Test
fun `test publishing android resources - doesn't show up in resources - when there is no publication`() {
val project = mppProjectWithAndroidTarget()
project.evaluate()
val actualSourcesDirectories = project.multiplatformExtension.androidTarget()
.compilations.getByName("release")
.androidVariant.sourceSets.flatMapTo(linkedSetOf()) { it.resourcesDirectories }
assert(
!actualSourcesDirectories.contains(project.expectedReleaseResourcePath)
) { actualSourcesDirectories.dumpPaths() }
}
private fun mppProjectWithAndroidTarget(
configure: LibraryExtension.(Unit) -> (Unit) = { },
): ProjectInternal {
val project = buildProjectWithMPP {
plugins.apply("com.android.library")
kotlin {
androidTarget()
}
}
val libraryExt = (project.extensions.getByName("android") as LibraryExtension)
libraryExt.compileSdk = 30
libraryExt.configure(Unit)
return project
}
private fun Project.publishFakeResources(target: KotlinTarget) {
project.multiplatformExtension.resourcesPublicationExtension?.publishResourcesAsKotlinComponent(
target,
resourcePathForSourceSet = {
KotlinTargetResourcesPublication.ResourceRoot(
project.provider { File(it.name) },
emptyList(),
emptyList(),
)
},
relativeResourcePlacement = project.provider { File("test") },
)
}
private fun Project.publishFakeAssets(target: KotlinAndroidTarget) {
project.multiplatformExtension.resourcesPublicationExtension?.publishInAndroidAssets(
target,
resourcePathForSourceSet = {
KotlinTargetResourcesPublication.ResourceRoot(
project.provider { File(it.name) },
emptyList(),
emptyList(),
)
},
relativeResourcePlacement = project.provider { File("test") },
)
}
private fun Collection<File>.dumpPaths(): String {
return "Actual paths:" + if (isEmpty()) " <Empty>" else joinToString("") { "\n${it}" }
}
}