diff --git a/libraries/tools/kotlin-gradle-plugin/api/kotlin-gradle-plugin.api b/libraries/tools/kotlin-gradle-plugin/api/kotlin-gradle-plugin.api index e68ecf82c00..9ef7d64be84 100644 --- a/libraries/tools/kotlin-gradle-plugin/api/kotlin-gradle-plugin.api +++ b/libraries/tools/kotlin-gradle-plugin/api/kotlin-gradle-plugin.api @@ -912,8 +912,10 @@ public abstract interface class org/jetbrains/kotlin/gradle/plugin/mpp/resources public static final field Companion Lorg/jetbrains/kotlin/gradle/plugin/mpp/resources/KotlinTargetResourcesPublication$Companion; public static final field EXTENSION_NAME Ljava/lang/String; public abstract fun canPublishResources (Lorg/jetbrains/kotlin/gradle/plugin/KotlinTarget;)Z + public abstract fun canResolveResources (Lorg/jetbrains/kotlin/gradle/plugin/KotlinTarget;)Z public abstract fun publishInAndroidAssets (Lorg/jetbrains/kotlin/gradle/plugin/mpp/KotlinAndroidTarget;Lkotlin/jvm/functions/Function1;Lorg/gradle/api/provider/Provider;)V public abstract fun publishResourcesAsKotlinComponent (Lorg/jetbrains/kotlin/gradle/plugin/KotlinTarget;Lkotlin/jvm/functions/Function1;Lorg/gradle/api/provider/Provider;)V + public abstract fun resolveResources (Lorg/jetbrains/kotlin/gradle/plugin/KotlinTarget;)Lorg/gradle/api/provider/Provider; } public abstract class org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/SourceSetTreeClassifier { diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/KotlinTargetResourcesPublication.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/KotlinTargetResourcesPublication.kt index 11fb49ad04a..a27f1a38bf7 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/KotlinTargetResourcesPublication.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/KotlinTargetResourcesPublication.kt @@ -34,6 +34,10 @@ interface KotlinTargetResourcesPublication { resourcePathForSourceSet: (KotlinSourceSet) -> (ResourceRoot), relativeResourcePlacement: Provider, ) + + fun canResolveResources(target: KotlinTarget): Boolean + + fun resolveResources(target: KotlinTarget): Provider companion object { const val EXTENSION_NAME = "multiplatformResourcesPublication" diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/KotlinTargetResourcesPublicationImpl.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/KotlinTargetResourcesPublicationImpl.kt index a2bc578f077..0487838a4c8 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/KotlinTargetResourcesPublicationImpl.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/KotlinTargetResourcesPublicationImpl.kt @@ -6,19 +6,21 @@ package org.jetbrains.kotlin.gradle.plugin.mpp.resources import org.gradle.api.Project -import org.gradle.api.artifacts.Configuration import org.gradle.api.provider.Provider import org.gradle.api.tasks.TaskProvider import org.jetbrains.kotlin.gradle.plugin.* import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle +import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.Companion.kotlinPropertiesProvider import org.jetbrains.kotlin.gradle.plugin.diagnostics.KotlinToolingDiagnostics import org.jetbrains.kotlin.gradle.plugin.diagnostics.reportDiagnostic import org.jetbrains.kotlin.gradle.plugin.launchInStage import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.gradle.plugin.mpp.disambiguateName -import org.jetbrains.kotlin.gradle.plugin.mpp.internal import org.jetbrains.kotlin.gradle.plugin.mpp.resources.publication.KotlinAndroidTargetResourcesPublication +import org.jetbrains.kotlin.gradle.plugin.mpp.resources.resolve.AggregateResourcesTask +import org.jetbrains.kotlin.gradle.plugin.mpp.resources.resolve.KotlinTargetResourcesResolutionStrategy +import org.jetbrains.kotlin.gradle.plugin.mpp.resources.resolve.ResolveResourcesFromDependenciesTask import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget import org.jetbrains.kotlin.gradle.tasks.locateTask @@ -42,6 +44,11 @@ internal abstract class KotlinTargetResourcesPublicationImpl @Inject constructor KotlinAndroidTarget::class, ) + private val targetsThatSupportResolution = listOf( + KotlinJsIrTarget::class, + KotlinNativeTarget::class, + ) + private val targetToResourcesMap: MutableMap = mutableMapOf() private val androidTargetAssetsMap: MutableMap = mutableMapOf() @@ -117,6 +124,86 @@ internal abstract class KotlinTargetResourcesPublicationImpl @Inject constructor } } + override fun canResolveResources(target: KotlinTarget): Boolean { + return targetsThatSupportResolution.any { it.isInstance(target) } + } + + override fun resolveResources(target: KotlinTarget): Provider { + if (!canResolveResources(target)) { + target.project.reportDiagnostic(KotlinToolingDiagnostics.ResourceMayNotBeResolvedForTarget(target.name)) + } + + val aggregateResourcesTaskName = target.disambiguateName("AggregateResources") + project.locateTask(aggregateResourcesTaskName)?.let { + return it.flatMap { it.outputDirectory.asFile } + } + + val resolveResourcesFromDependenciesTask = project.registerTask( + target.disambiguateName("ResolveResourcesFromDependencies") + ) + val aggregateResourcesTask = project.registerTask(aggregateResourcesTaskName) { aggregate -> + aggregate.resourcesFromDependenciesDirectory.set(resolveResourcesFromDependenciesTask.flatMap { it.outputDirectory }) + aggregate.outputDirectory.set( + project.layout.buildDirectory.dir("$MULTIPLATFORM_RESOURCES_DIRECTORY/aggregated-resources/${target.targetName}") + ) + } + + project.launchInStage(KotlinPluginLifecycle.Stage.AfterFinaliseCompilations) { + val mainCompilation = target.compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME) + resolveResourcesFromDependencies( + compilation = mainCompilation, + resolveResourcesFromDependenciesTask = resolveResourcesFromDependenciesTask, + targetName = target.targetName, + ) + resolveResourcesFromSelf( + compilation = mainCompilation, + target = target, + aggregateResourcesTask = aggregateResourcesTask, + ) + } + + return aggregateResourcesTask.flatMap { it.outputDirectory.asFile } + } + + private fun resolveResourcesFromDependencies( + compilation: KotlinCompilation<*>, + resolveResourcesFromDependenciesTask: TaskProvider, + targetName: String, + ) { + resolveResourcesFromDependenciesTask.configure { + it.filterResourcesByExtension.set( + project.kotlinPropertiesProvider + .mppFilterResourcesByExtension + .map { explicitlyEnabled -> + // Always filter resources configuration because it resolves klibs for dependency graph inheritance + explicitlyEnabled || project.kotlinPropertiesProvider.mppResourcesResolutionStrategy == KotlinTargetResourcesResolutionStrategy.ResourcesConfiguration + } + ) + it.archivesFromDependencies.from( + project.kotlinPropertiesProvider.mppResourcesResolutionStrategy.resourceArchives(compilation) + ) + it.outputDirectory.set( + project.layout.buildDirectory.dir("$MULTIPLATFORM_RESOURCES_DIRECTORY/resources-from-dependencies/${targetName}") + ) + } + } + + private fun resolveResourcesFromSelf( + compilation: KotlinCompilation<*>, + target: KotlinTarget, + aggregateResourcesTask: TaskProvider, + ) { + subscribeOnPublishResources(target) { resources -> + val copyResourcesTask = compilation.assembleHierarchicalResources( + target.disambiguateName("ResolveSelfResources"), + resources, + ) + aggregateResourcesTask.configure { aggregate -> + aggregate.resourcesFromSelfDirectory.set(copyResourcesTask) + } + } + } + internal companion object { const val MULTIPLATFORM_RESOURCES_DIRECTORY = "kotlin-multiplatform-resources" const val RESOURCES_CLASSIFIER = "kotlin_resources" diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/resolve/AggregateResourcesTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/resolve/AggregateResourcesTask.kt new file mode 100644 index 00000000000..0552862ac9d --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/resolve/AggregateResourcesTask.kt @@ -0,0 +1,47 @@ +/* + * 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.plugin.mpp.resources.resolve + +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.DuplicatesStrategy +import org.gradle.api.file.FileSystemOperations +import org.gradle.api.tasks.* +import org.gradle.work.DisableCachingByDefault +import org.jetbrains.kotlin.incremental.deleteDirectoryContents +import javax.inject.Inject + +@DisableCachingByDefault +internal abstract class AggregateResourcesTask : DefaultTask() { + + @get:Inject + abstract val fileSystem: FileSystemOperations + + @get:Optional + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:InputDirectory + abstract val resourcesFromDependenciesDirectory: DirectoryProperty + + @get:Optional + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:InputDirectory + abstract val resourcesFromSelfDirectory: DirectoryProperty + + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + @TaskAction + fun copyResources() { + outputDirectory.get().asFile.deleteDirectoryContents() + fileSystem.copy { copy -> + resourcesFromDependenciesDirectory.orNull?.let { copy.from(it) } + resourcesFromSelfDirectory.orNull?.let { copy.from(it) } + copy.into(outputDirectory) + copy.duplicatesStrategy = DuplicatesStrategy.FAIL + } + } + +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/resolve/ResolveResourcesFromDependenciesTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/resolve/ResolveResourcesFromDependenciesTask.kt new file mode 100644 index 00000000000..9f1d3a195b4 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/resolve/ResolveResourcesFromDependenciesTask.kt @@ -0,0 +1,51 @@ +/* + * 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.plugin.mpp.resources.resolve + +import org.gradle.api.DefaultTask +import org.gradle.api.file.* +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import org.gradle.work.DisableCachingByDefault +import org.jetbrains.kotlin.gradle.plugin.mpp.resources.KotlinTargetResourcesPublicationImpl +import org.jetbrains.kotlin.incremental.deleteDirectoryContents +import javax.inject.Inject + +@DisableCachingByDefault +internal abstract class ResolveResourcesFromDependenciesTask : DefaultTask() { + + @get:Inject + abstract val fileSystem: FileSystemOperations + + @get:Inject + abstract val archiveOperations: ArchiveOperations + + @get:Input + abstract val filterResourcesByExtension: Property + + @get:PathSensitive(PathSensitivity.NONE) + @get:InputFiles + abstract val archivesFromDependencies: ConfigurableFileCollection + + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + @TaskAction + fun copyResources() { + outputDirectory.get().asFile.deleteDirectoryContents() + fileSystem.copy { copy -> + archivesFromDependencies + .filter { it.isFile } + .filter { if (filterResourcesByExtension.get()) it.name.endsWith(KotlinTargetResourcesPublicationImpl.RESOURCES_ZIP_EXTENSION) else true } + .forEach { + copy.from(archiveOperations.zipTree(it)) + } + copy.into(outputDirectory) + copy.duplicatesStrategy = DuplicatesStrategy.FAIL + } + } + +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/KotlinTargetResourcesPublicationImplTests.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/KotlinTargetResourcesPublicationImplTests.kt index 8721f802a5f..3c61b42f737 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/KotlinTargetResourcesPublicationImplTests.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/KotlinTargetResourcesPublicationImplTests.kt @@ -3,6 +3,8 @@ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ +@file:OptIn(ExperimentalWasmDsl::class) + package org.jetbrains.kotlin.gradle.unitTests import com.android.build.gradle.LibraryExtension @@ -15,10 +17,10 @@ import org.jetbrains.kotlin.gradle.plugin.diagnostics.ToolingDiagnosticFactory 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.targets.js.dsl.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.util.* import org.jetbrains.kotlin.gradle.util.assertContainsDiagnostic import org.jetbrains.kotlin.gradle.util.assertNoDiagnostics -import org.jetbrains.kotlin.gradle.util.buildProjectWithMPP -import org.jetbrains.kotlin.gradle.util.kotlin import org.junit.Test import java.io.File import kotlin.test.assertEquals @@ -119,6 +121,49 @@ class KotlinTargetResourcesPublicationImplTests { ) } + @Test + fun `test targets that can publish resources`() { + buildProjectWithMPP { + plugins.apply("com.android.library") + enableMppResourcesPublication(true) + kotlin { + listOf( + androidTarget(), + jvm(), + wasmJs(), + wasmWasi(), + linuxArm64(), + iosArm64(), + ).forEach { target -> + assert( + resourcesPublicationExtension!!.canPublishResources(target), + { target } + ) + } + } + } + } + + @Test + fun `test targets that can resolve resources`() { + buildProjectWithMPP { + enableMppResourcesPublication(true) + kotlin { + listOf( + wasmJs(), + wasmWasi(), + linuxArm64(), + iosArm64(), + ).forEach { target -> + assert( + resourcesPublicationExtension!!.canResolveResources(target), + { target } + ) + } + } + } + } + private fun testCallbacksAfterApiCall( callback: ((Unit) -> Unit) -> Unit, apiCall: (Unit) -> Unit,