Add a task to hierarchically assemble multiplatform resources
^KT-65540
This commit is contained in:
committed by
Space Team
parent
74628c0394
commit
5ddc7b47ef
+11
@@ -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) {
|
object DependencyDoesNotPhysicallyExist : ToolingDiagnosticFactory(WARNING) {
|
||||||
operator fun invoke(dependency: File) = build(
|
operator fun invoke(dependency: File) = build(
|
||||||
"""
|
"""
|
||||||
|
|||||||
+176
@@ -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<File>,
|
||||||
|
|
||||||
|
includes: List<String>,
|
||||||
|
excludes: List<String>,
|
||||||
|
layout: ProjectLayout,
|
||||||
|
@get:InputFiles
|
||||||
|
@get:PathSensitive(PathSensitivity.RELATIVE)
|
||||||
|
val resourcesFileTree: Provider<FileTree> = 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<List<Resource>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<File>
|
||||||
|
|
||||||
|
@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<Resource> {
|
||||||
|
override fun walk(root: Resource): Sequence<File> = root.resourcesFileTree.get().asSequence()
|
||||||
|
},
|
||||||
|
basePathFromResource = { it.resourcesBaseDirectory.get() },
|
||||||
|
fileTreeToCopyFromResource = { it.resourcesFileTree.get() },
|
||||||
|
)
|
||||||
|
|
||||||
|
internal interface FileSystem<RootEntity> {
|
||||||
|
fun walk(root: RootEntity): Sequence<File>
|
||||||
|
fun exists(file: File): Boolean = file.exists()
|
||||||
|
fun isDirectory(file: File): Boolean = file.isDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SourceSetWalk<Resource, FileTreeToCopy>(
|
||||||
|
val fileSystem: FileSystem<Resource>,
|
||||||
|
val basePathFromResource: (Resource) -> (File),
|
||||||
|
val fileTreeToCopyFromResource: (Resource) -> (FileTreeToCopy),
|
||||||
|
) {
|
||||||
|
sealed class Result<T> {
|
||||||
|
data class ToCopy<T>(val value: T) : Result<T>()
|
||||||
|
data class Collision<T>(val left: File, val right: File) : Result<T>()
|
||||||
|
data class NotDirectory<T>(val path: File) : Result<T>()
|
||||||
|
|
||||||
|
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<List<Resource>>
|
||||||
|
): Result<List<FileTreeToCopy>> {
|
||||||
|
// 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<FileTreeToCopy>()
|
||||||
|
leveledResources.onEach { level ->
|
||||||
|
val relativePathsSeenAtThisLevel: MutableMap<String, File> = 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<String, File>,
|
||||||
|
): Result<FileTreeToCopy>? {
|
||||||
|
val resourcesDirectory = basePathFromResource(resource)
|
||||||
|
if (!fileSystem.exists(resourcesDirectory)) return null
|
||||||
|
if (!fileSystem.isDirectory(resourcesDirectory)) return Result.NotDirectory(resourcesDirectory)
|
||||||
|
|
||||||
|
val collisions: List<Result.Collision<FileTreeToCopy>> =
|
||||||
|
fileSystem.walk(resource).map<File, Result.Collision<FileTreeToCopy>?>{ 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+75
@@ -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<Directory> = registerAssembleHierarchicalResourcesTaskProvider(
|
||||||
|
targetNamePrefix,
|
||||||
|
resources,
|
||||||
|
).flatMap { it.outputDirectory }
|
||||||
|
|
||||||
|
internal fun KotlinCompilation<*>.registerAssembleHierarchicalResourcesTaskProvider(
|
||||||
|
targetNamePrefix: String,
|
||||||
|
resources: KotlinTargetResourcesPublicationImpl.TargetResources,
|
||||||
|
): TaskProvider<AssembleHierarchicalResourcesTask> {
|
||||||
|
val taskName = "${targetNamePrefix}CopyHierarchicalMultiplatformResources"
|
||||||
|
val existingTask = project.tasks.locateTask<AssembleHierarchicalResourcesTask>(taskName)
|
||||||
|
if (existingTask != null) {
|
||||||
|
project.reportDiagnostic(KotlinToolingDiagnostics.ResourcePublishedMoreThanOncePerTarget(targetNamePrefix))
|
||||||
|
return existingTask
|
||||||
|
}
|
||||||
|
|
||||||
|
return project.registerTask<AssembleHierarchicalResourcesTask>(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<KotlinSourceSet>,
|
||||||
|
): List<List<KotlinTargetResourcesPublication.ResourceRoot>> {
|
||||||
|
return rootSourceSets.withClosureGroupingByDistance { it.dependsOn }.map { sourceSets ->
|
||||||
|
sourceSets.map { sourceSet ->
|
||||||
|
resources.resourcePathForSourceSet(sourceSet)
|
||||||
|
}
|
||||||
|
}.reversed()
|
||||||
|
}
|
||||||
+332
@@ -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<String, Any>()
|
||||||
|
),
|
||||||
|
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<String, Any?>,
|
||||||
|
resourcesSplitByLevel: List<List<Path>>,
|
||||||
|
): AssembleHierarchicalResourcesTask.SourceSetWalk.Result<List<Path>> {
|
||||||
|
val fakeFs = buildFakeFileSystem(contents = fileSystem)
|
||||||
|
return AssembleHierarchicalResourcesTask.SourceSetWalk<File, Path>(
|
||||||
|
fileSystem = object : AssembleHierarchicalResourcesTask.FileSystem<File> {
|
||||||
|
override fun walk(root: File): Sequence<File> = 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<String, Any?>
|
||||||
|
): 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<String, Any>,
|
||||||
|
)
|
||||||
|
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<String, FakeFileSystem>) : FakeFileSystem(path)
|
||||||
|
|
||||||
|
fun fileSystemAt(path: Path): FakeFileSystem? {
|
||||||
|
if (this is FakeFile) error("File has no sub filesystem")
|
||||||
|
(this as FakeDirectory)
|
||||||
|
|
||||||
|
val components: MutableList<String> = 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<FakeFileSystem> {
|
||||||
|
return sequence {
|
||||||
|
walkFileSystem(subdirectoryAt(path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun SequenceScope<FakeFileSystem>.walkFileSystem(
|
||||||
|
file: FakeFileSystem,
|
||||||
|
) {
|
||||||
|
if (file is FakeFile) return yield(file)
|
||||||
|
(file as FakeDirectory)
|
||||||
|
yield(file)
|
||||||
|
file.contents.values.forEach {
|
||||||
|
walkFileSystem(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+147
@@ -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<List<String>> {
|
||||||
|
return registerFakeResourcesTask(compilation).get().resourceDirectoriesByLevel.get().map { resourcesLevel ->
|
||||||
|
resourcesLevel.map { resource ->
|
||||||
|
resource.resourcesBaseDirectory.get().name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Project.registerFakeResourcesTask(compilation: KotlinCompilation<*>): TaskProvider<AssembleHierarchicalResourcesTask> {
|
||||||
|
return compilation.registerAssembleHierarchicalResourcesTaskProvider(
|
||||||
|
"test",
|
||||||
|
resources = KotlinTargetResourcesPublicationImpl.TargetResources(
|
||||||
|
{ ss ->
|
||||||
|
KotlinTargetResourcesPublication.ResourceRoot(
|
||||||
|
provider { File(ss.name) },
|
||||||
|
emptyList(),
|
||||||
|
emptyList(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
provider { File("stub") }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user