diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/KotlinToolingDiagnostics.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/KotlinToolingDiagnostics.kt index 9fc9bac00a7..52dc9323b16 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/KotlinToolingDiagnostics.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/KotlinToolingDiagnostics.kt @@ -765,6 +765,17 @@ object KotlinToolingDiagnostics { ) } + private val resourcesBugReportRequest get() = "This is likely a bug in Kotlin Gradle Plugin configuration. Please report this issue to https://kotl.in/issue." + object ResourcePublishedMoreThanOncePerTarget : ToolingDiagnosticFactory(ERROR) { + operator fun invoke(targetName: String) = build( + """ + Only one resources publication per target $targetName is allowed. + + $resourcesBugReportRequest + """.trimIndent() + ) + } + object DependencyDoesNotPhysicallyExist : ToolingDiagnosticFactory(WARNING) { operator fun invoke(dependency: File) = build( """ diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/AssembleHierarchicalResourcesTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/AssembleHierarchicalResourcesTask.kt new file mode 100644 index 00000000000..aa69f409cd7 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/AssembleHierarchicalResourcesTask.kt @@ -0,0 +1,176 @@ +/* + * 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 + +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.file.FileTree +import org.gradle.api.file.ProjectLayout +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.* +import org.gradle.work.DisableCachingByDefault +import org.jetbrains.kotlin.incremental.deleteDirectoryContents +import java.io.File +import javax.inject.Inject + +@DisableCachingByDefault +internal abstract class AssembleHierarchicalResourcesTask : DefaultTask() { + + internal class Resource( + @get:Internal + val resourcesBaseDirectory: Provider, + + includes: List, + excludes: List, + layout: ProjectLayout, + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + val resourcesFileTree: Provider = layout.dir(resourcesBaseDirectory).map { baseDirectory -> + baseDirectory.asFileTree.matching { patternFilter -> + includes.forEach { patternFilter.include(it) } + excludes.forEach { patternFilter.exclude(it) } + } + } + ) + + @get:Inject + abstract val fileSystem: FileSystemOperations + + @get:Inject + abstract val projectLayout: ProjectLayout + + @get:Nested + abstract val resourceDirectoriesByLevel: ListProperty> + + /** + * Relative resource placement is part of this task to prepare the output directory as is for consumption by the .zip publication or the + * self-resources resolution tasks + */ + @get:Input + abstract val relativeResourcePlacement: Property + + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + @TaskAction + fun action() { + val directoriesToCopy = resourcesSourceSetWalk.directoriesToCopy( + resourceDirectoriesByLevel.get() + ).unwrapOrThrow() + + val outputDirectoryFile = outputDirectory.get().asFile + outputDirectoryFile.deleteDirectoryContents() + + fileSystem.copy { copy -> + directoriesToCopy.forEach { dir -> + copy.from(dir) + } + copy.into(outputDirectoryFile.resolve(relativeResourcePlacement.get())) + copy.duplicatesStrategy = DuplicatesStrategy.INCLUDE + } + + if (outputDirectoryFile.listFiles()?.isEmpty() != false) { + // Output an empty directory for the zip task + outputDirectoryFile.resolve(relativeResourcePlacement.get()).mkdirs() + } + } + + private val resourcesSourceSetWalk = SourceSetWalk( + fileSystem = object : FileSystem { + override fun walk(root: Resource): Sequence = root.resourcesFileTree.get().asSequence() + }, + basePathFromResource = { it.resourcesBaseDirectory.get() }, + fileTreeToCopyFromResource = { it.resourcesFileTree.get() }, + ) + + internal interface FileSystem { + fun walk(root: RootEntity): Sequence + fun exists(file: File): Boolean = file.exists() + fun isDirectory(file: File): Boolean = file.isDirectory + } + + internal class SourceSetWalk( + val fileSystem: FileSystem, + val basePathFromResource: (Resource) -> (File), + val fileTreeToCopyFromResource: (Resource) -> (FileTreeToCopy), + ) { + sealed class Result { + data class ToCopy(val value: T) : Result() + data class Collision(val left: File, val right: File) : Result() + data class NotDirectory(val path: File) : Result() + + fun unwrapOrThrow(): T { + when (this) { + is Collision -> error("There is a duplicate resource in a source set level:\n${left.canonicalPath}\n${right.canonicalPath}") + is NotDirectory -> error("Path $path is not a directory") + is ToCopy -> return value + } + } + } + + fun directoriesToCopy( + leveledResources: List> + ): Result> { + // 1. Is case of a collision between levels, files seen at next levels overwrite files from previous levels. E.g. iosMain/res/foo + // resources overwrites commonMain/res/foo + val directoriesToCopy = mutableListOf() + leveledResources.onEach { level -> + val relativePathsSeenAtThisLevel: MutableMap = mutableMapOf() + level.onEach { resource -> + when ( + val result = discoverAndAppendResourceDirectory( + resource = resource, + relativePathsSeenAtThisLevel = relativePathsSeenAtThisLevel, + ) + ) { + is Result.Collision -> return Result.Collision(result.left, result.right) + is Result.NotDirectory -> return Result.NotDirectory(result.path) + is Result.ToCopy -> directoriesToCopy.add(result.value) + } + } + } + + return Result.ToCopy(directoriesToCopy) + } + + private fun discoverAndAppendResourceDirectory( + resource: Resource, + relativePathsSeenAtThisLevel: MutableMap, + ): Result? { + val resourcesDirectory = basePathFromResource(resource) + if (!fileSystem.exists(resourcesDirectory)) return null + if (!fileSystem.isDirectory(resourcesDirectory)) return Result.NotDirectory(resourcesDirectory) + + val collisions: List> = + fileSystem.walk(resource).map?>{ child -> + if (fileSystem.isDirectory(child)) { + return@map null + } + val relativePath = child.toRelativeString(resourcesDirectory) + relativePathsSeenAtThisLevel[relativePath]?.let { alreadySeenFile -> + return@map Result.Collision( + child, + alreadySeenFile, + ) + } + relativePathsSeenAtThisLevel[relativePath] = child + return@map null + }.mapNotNull { it }.toList() + + if (collisions.isNotEmpty()) { + return collisions.first() + } + + return Result.ToCopy(fileTreeToCopyFromResource(resource)) + } + + } + +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/registerAssembleHierarchicalResourcesTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/registerAssembleHierarchicalResourcesTask.kt new file mode 100644 index 00000000000..706e02924df --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/registerAssembleHierarchicalResourcesTask.kt @@ -0,0 +1,75 @@ +/* + * 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 + +import org.gradle.api.file.Directory +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.diagnostics.KotlinToolingDiagnostics +import org.jetbrains.kotlin.gradle.plugin.diagnostics.reportDiagnostic +import org.jetbrains.kotlin.gradle.tasks.locateTask +import org.jetbrains.kotlin.gradle.tasks.registerTask +import org.jetbrains.kotlin.tooling.core.withClosureGroupingByDistance + +internal fun KotlinCompilation<*>.assembleHierarchicalResources( + targetNamePrefix: String, + resources: KotlinTargetResourcesPublicationImpl.TargetResources, +): Provider = registerAssembleHierarchicalResourcesTaskProvider( + targetNamePrefix, + resources, +).flatMap { it.outputDirectory } + +internal fun KotlinCompilation<*>.registerAssembleHierarchicalResourcesTaskProvider( + targetNamePrefix: String, + resources: KotlinTargetResourcesPublicationImpl.TargetResources, +): TaskProvider { + val taskName = "${targetNamePrefix}CopyHierarchicalMultiplatformResources" + val existingTask = project.tasks.locateTask(taskName) + if (existingTask != null) { + project.reportDiagnostic(KotlinToolingDiagnostics.ResourcePublishedMoreThanOncePerTarget(targetNamePrefix)) + return existingTask + } + + return project.registerTask(taskName) { assembleResources -> + project.launchInStage(KotlinPluginLifecycle.Stage.AfterFinaliseRefinesEdges) { + val resourceDirectoriesByLevel = splitResourceDirectoriesBySourceSetLevel( + resources = resources, + rootSourceSets = kotlinSourceSets.sortedBy { it.name }, + ) + val outputDirectory = project.layout.buildDirectory.dir( + "${KotlinTargetResourcesPublicationImpl.MULTIPLATFORM_RESOURCES_DIRECTORY}/assemble-hierarchically/${targetNamePrefix}" + ) + + resourceDirectoriesByLevel.forEach { level -> + assembleResources.resourceDirectoriesByLevel.add( + level.map { + AssembleHierarchicalResourcesTask.Resource( + it.resourcesBaseDirectory, + it.includes, + it.excludes, + project.layout, + ) + } + ) + } + assembleResources.relativeResourcePlacement.set(resources.relativeResourcePlacement) + assembleResources.outputDirectory.set(outputDirectory) + } + } +} + +private fun splitResourceDirectoriesBySourceSetLevel( + resources: KotlinTargetResourcesPublicationImpl.TargetResources, + rootSourceSets: List, +): List> { + return rootSourceSets.withClosureGroupingByDistance { it.dependsOn }.map { sourceSets -> + sourceSets.map { sourceSet -> + resources.resourcePathForSourceSet(sourceSet) + } + }.reversed() +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/AssembleHierarchicalResourcesTaskSourceSetWalkTests.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/AssembleHierarchicalResourcesTaskSourceSetWalkTests.kt new file mode 100644 index 00000000000..2e9f57ec5c5 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/AssembleHierarchicalResourcesTaskSourceSetWalkTests.kt @@ -0,0 +1,332 @@ +/* + * 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("UNCHECKED_CAST") + +package org.jetbrains.kotlin.gradle.unitTests + +import org.jetbrains.kotlin.gradle.plugin.mpp.resources.AssembleHierarchicalResourcesTask +import org.jetbrains.kotlin.utils.addToStdlib.popLast +import org.junit.Test +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.test.assertEquals + +class AssembleHierarchicalResourcesTaskSourceSetWalkTests { + + @Test + fun `test simple resource root`() { + assertEquals( + AssembleHierarchicalResourcesTask.SourceSetWalk.Result.ToCopy( + listOf( + absolutePath("commonMain") + ) + ), + directoriesToCopy( + hashMapOf( + "commonMain" to mapOf( + "res" to mapOf( + "foo" to null, + ) + ) + ), + listOf( + listOf( + absolutePath("commonMain") + ) + ) + ), + ) + } + + @Test + fun `test collision on the top level`() { + assertEquals( + AssembleHierarchicalResourcesTask.SourceSetWalk.Result.Collision( + absolutePath("commonMain2", "res", "collision").toFile(), + absolutePath("commonMain", "res", "collision").toFile(), + ), + directoriesToCopy( + hashMapOf( + "commonMain" to mapOf( + "res" to mapOf( + "collision" to null, + ) + ), + "commonMain2" to mapOf( + "res" to mapOf( + "collision" to null, + ) + ), + ), + listOf( + listOf( + absolutePath("commonMain"), + absolutePath("commonMain2"), + ) + ) + ), + ) + } + + @Test + fun `test collision across levels`() { + assertEquals( + AssembleHierarchicalResourcesTask.SourceSetWalk.Result.ToCopy( + listOf( + absolutePath("iosMain"), + absolutePath("commonMain"), + ) + ), + directoriesToCopy( + hashMapOf( + "commonMain" to mapOf( + "res" to mapOf( + "collision" to null, + ) + ), + "iosMain" to mapOf( + "res" to mapOf( + "collision" to null, + ) + ) + ), + listOf( + listOf( + absolutePath("iosMain"), + ), + listOf( + absolutePath("commonMain"), + ), + ) + ), + ) + } + + @Test + fun `test collision only in directories`() { + assertEquals( + AssembleHierarchicalResourcesTask.SourceSetWalk.Result.ToCopy( + listOf( + absolutePath("commonMain"), + absolutePath("commonMain2"), + ) + ), + directoriesToCopy( + hashMapOf( + "commonMain" to mapOf( + "res" to mapOf( + "foo" to null, + ) + ), + "commonMain2" to mapOf( + "res" to mapOf( + "bar" to null, + ) + ), + ), + listOf( + listOf( + absolutePath("commonMain"), + absolutePath("commonMain2"), + ) + ) + ), + ) + } + + @Test + fun `test collision at higher level`() { + assertEquals( + AssembleHierarchicalResourcesTask.SourceSetWalk.Result.Collision( + absolutePath("commonMain2", "res", "collision").toFile(), + absolutePath("commonMain", "res", "collision").toFile(), + ), + directoriesToCopy( + hashMapOf( + "commonMain" to mapOf( + "res" to mapOf( + "collision" to null, + ) + ), + "commonMain2" to mapOf( + "res" to mapOf( + "collision" to null, + ) + ), + "iosMain" to mapOf( + "res" to mapOf( + "collision" to null, + ) + ), + ), + listOf( + listOf( + absolutePath("iosMain") + ), + listOf( + absolutePath("commonMain"), + absolutePath("commonMain2"), + ) + ) + ), + ) + } + + @Test + fun `test non-existent directories`() { + assertEquals( + AssembleHierarchicalResourcesTask.SourceSetWalk.Result.ToCopy( + emptyList() + ), + directoriesToCopy( + hashMapOf(), + listOf( + listOf( + absolutePath("commonMain") + ), + ) + ), + ) + } + + @Test + fun `test empty directories`() { + assertEquals( + AssembleHierarchicalResourcesTask.SourceSetWalk.Result.ToCopy( + listOf( + absolutePath("commonMain") + ) + ), + directoriesToCopy( + hashMapOf( + "commonMain" to emptyMap() + ), + listOf( + listOf( + absolutePath("commonMain") + ), + ) + ), + ) + } + + @Test + fun `test not a directory`() { + assertEquals( + AssembleHierarchicalResourcesTask.SourceSetWalk.Result.NotDirectory( + absolutePath("commonMain").toFile(), + ), + directoriesToCopy( + hashMapOf( + "commonMain" to null, + ), + listOf( + listOf( + absolutePath("commonMain") + ), + ) + ), + ) + } + + private fun absolutePath(first: String, vararg next: String): Path { + return Paths.get("").toAbsolutePath().root.resolve(Paths.get(first, *next)) + } + + private fun directoriesToCopy( + fileSystem: HashMap, + resourcesSplitByLevel: List>, + ): AssembleHierarchicalResourcesTask.SourceSetWalk.Result> { + val fakeFs = buildFakeFileSystem(contents = fileSystem) + return AssembleHierarchicalResourcesTask.SourceSetWalk( + fileSystem = object : AssembleHierarchicalResourcesTask.FileSystem { + override fun walk(root: File): Sequence = fakeFs.walkFileSystemFrom(root.toPath()).map { + it.path.toFile() + } + override fun exists(file: File): Boolean = fakeFs.exists(file.toPath()) + override fun isDirectory(file: File): Boolean = fakeFs.fileSystemAt(file.toPath()) is FakeFileSystem.FakeDirectory + }, + basePathFromResource = { it }, + fileTreeToCopyFromResource = { it.toPath() } + ).directoriesToCopy( + resourcesSplitByLevel.map { it.map { it.toFile() } }, + ) + } + + fun buildFakeFileSystem( + path: Path = Paths.get("").toAbsolutePath().root, + contents: Map + ): FakeFileSystem { + return FakeFileSystem.FakeDirectory( + path, + contents.mapValues { + val value = it.value + return@mapValues when (value) { + is Map<*, *> -> buildFakeFileSystem( + path.resolve(it.key), + value as Map, + ) + null -> FakeFileSystem.FakeFile(path.resolve(it.key)) + else -> error("Unexpected component ${value} in fake fs") + } + } + ) + } + + sealed class FakeFileSystem(val path: Path) { + class FakeFile(path: Path) : FakeFileSystem(path) + class FakeDirectory(path: Path, val contents: Map) : FakeFileSystem(path) + + fun fileSystemAt(path: Path): FakeFileSystem? { + if (this is FakeFile) error("File has no sub filesystem") + (this as FakeDirectory) + + val components: MutableList = path.map { it.fileName?.toString().orEmpty() }.reversed().toMutableList() + var current = contents + var value: FakeFileSystem? = null + + while (components.isNotEmpty()) { + value = current[components.popLast()] + + when (value) { + is FakeDirectory -> current = value.contents + is FakeFile -> if (components.isNotEmpty()) return null + null -> return null + } + } + return value + } + + fun subdirectoryAt(path: Path): FakeDirectory { + when (val directory = fileSystemAt(path)) { + is FakeDirectory -> return directory + is FakeFile -> error("Path $path is a file") + null -> error("Subdirectory $path doesn't exist") + } + } + + fun exists(path: Path): Boolean = fileSystemAt(path) != null + + fun walkFileSystemFrom(path: Path): Sequence { + return sequence { + walkFileSystem(subdirectoryAt(path)) + } + } + + private suspend fun SequenceScope.walkFileSystem( + file: FakeFileSystem, + ) { + if (file is FakeFile) return yield(file) + (file as FakeDirectory) + yield(file) + file.contents.values.forEach { + walkFileSystem(it) + } + } + } + +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/AssembleHierarchicalResourcesTaskTests.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/AssembleHierarchicalResourcesTaskTests.kt new file mode 100644 index 00000000000..0a3f115f74a --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/AssembleHierarchicalResourcesTaskTests.kt @@ -0,0 +1,147 @@ +/* + * 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 org.gradle.api.Project +import org.gradle.api.tasks.TaskProvider +import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.diagnostics.KotlinToolingDiagnostics +import org.jetbrains.kotlin.gradle.plugin.mpp.resources.AssembleHierarchicalResourcesTask +import org.jetbrains.kotlin.gradle.plugin.mpp.resources.KotlinTargetResourcesPublication +import org.jetbrains.kotlin.gradle.plugin.mpp.resources.KotlinTargetResourcesPublicationImpl +import org.jetbrains.kotlin.gradle.plugin.mpp.resources.registerAssembleHierarchicalResourcesTaskProvider +import org.jetbrains.kotlin.gradle.util.assertContainsDiagnostic +import org.jetbrains.kotlin.gradle.util.buildProjectWithMPP +import org.jetbrains.kotlin.gradle.util.runLifecycleAwareTest +import org.junit.Test +import java.io.File +import kotlin.test.assertEquals + +class AssembleHierarchicalResourcesTaskTests { + + @Test + fun `test copying order - matches default target hierarchy`() { + with(buildProjectWithMPP()) { + val kotlin = multiplatformExtension + val compilation = kotlin.linuxArm64().compilations.getByName("main") + + evaluate() + + assertEquals( + listOf( + listOf("commonMain"), + listOf("nativeMain"), + listOf("linuxMain"), + listOf("linuxArm64Main"), + ), + resourceDirectoriesCopyingOrder( + compilation + ) + ) + } + } + + @Test + fun `test copying order - with additional source sets in platform source set`() { + with(buildProjectWithMPP()) { + val kotlin = multiplatformExtension + val compilation = kotlin.jvm().compilations.getByName("main") + + val a = kotlin.sourceSets.create("a") + + compilation.defaultSourceSet.dependsOn(a) + + evaluate() + + assertEquals( + listOf( + listOf("a", "commonMain"), + listOf("jvmMain"), + ), + resourceDirectoriesCopyingOrder( + compilation + ) + ) + } + } + + @Test + fun `test copying order - with source sets more remote than common`() { + with(buildProjectWithMPP()) { + val kotlin = multiplatformExtension + val compilation = kotlin.jvm().compilations.getByName("main") + + val a = kotlin.sourceSets.create("a") + val b = kotlin.sourceSets.create("b") + val c = kotlin.sourceSets.create("c") + val d = kotlin.sourceSets.create("d") + val e = kotlin.sourceSets.create("e") + + compilation.defaultSourceSet.dependsOn(a) + compilation.defaultSourceSet.dependsOn(b) + a.dependsOn(c) + b.dependsOn(d) + d.dependsOn(e) + + evaluate() + + assertEquals( + listOf( + listOf("e"), + listOf("c", "d"), + listOf("a", "b", "commonMain"), + listOf("jvmMain"), + ), + resourceDirectoriesCopyingOrder( + compilation + ) + ) + } + } + + @Test + fun `test registering multiple resources assembling tasks - results in a diagnostic`() { + with(buildProjectWithMPP()) { + val kotlin = multiplatformExtension + val compilation = kotlin.jvm().compilations.getByName("main") + + evaluate() + + registerFakeResourcesTask(compilation) + registerFakeResourcesTask(compilation) + + assertContainsDiagnostic(KotlinToolingDiagnostics.ResourcePublishedMoreThanOncePerTarget) + } + } + + private fun Project.resourceDirectoriesCopyingOrder( + compilation: KotlinCompilation<*>, + ): List> { + return registerFakeResourcesTask(compilation).get().resourceDirectoriesByLevel.get().map { resourcesLevel -> + resourcesLevel.map { resource -> + resource.resourcesBaseDirectory.get().name + } + } + } + + private fun Project.registerFakeResourcesTask(compilation: KotlinCompilation<*>): TaskProvider { + return compilation.registerAssembleHierarchicalResourcesTaskProvider( + "test", + resources = KotlinTargetResourcesPublicationImpl.TargetResources( + { ss -> + KotlinTargetResourcesPublication.ResourceRoot( + provider { File(ss.name) }, + emptyList(), + emptyList(), + ) + }, + provider { File("stub") } + ) + ) + } + +} \ No newline at end of file