Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import dev.icerock.gradle.metadata.container.ResourceType
import dev.icerock.gradle.metadata.resource.ResourceMetadata
import dev.icerock.gradle.utils.filterClass
import org.gradle.api.tasks.util.PatternFilterable
import java.io.File
import kotlin.reflect.KClass

@Suppress("LongParameterList", "TooManyFunctions", "UnusedPrivateMember")
Expand All @@ -28,7 +29,11 @@ internal class ResourceTypeGenerator<T : ResourceMetadata>(
private val filter: PatternFilterable.() -> Unit,
) {
fun generateMetadata(files: ResourcesFiles): List<T> {
return generator.generateMetadata(files.matching(filter).ownSourceSet.fileTree.files)
// Sort for deterministic output; FileTree.files iteration is filesystem-dependent.
val sortedFiles: Set<File> = files.matching(filter).ownSourceSet.fileTree.files
.sortedBy { it.absolutePath }
.toCollection(LinkedHashSet())
return generator.generateMetadata(sortedFiles)
}

fun getImports(): List<ClassName> = platformResourceGenerator.imports()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ internal data class StringMetadata(
)

@Suppress("MagicNumber")
override fun contentHash(): String = values.hashCode().toString(16)
override fun contentHash(): String = values.toString().hashCode().toString(16)
}

@Serializable
Expand Down Expand Up @@ -94,7 +94,7 @@ internal data class PluralMetadata(
}

@Suppress("MagicNumber")
override fun contentHash(): String = values.hashCode().toString(16)
override fun contentHash(): String = values.toString().hashCode().toString(16)
}

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,12 @@ abstract class GenerateMultiplatformResourcesTask : DefaultTask() {
)
val serializer: KSerializer<List<ContainerMetadata>> =
ListSerializer(ContainerMetadata.serializer())
val inputMetadata: List<ContainerMetadata> = inputMetadataFiles.files.flatMap { file ->
json.decodeFromString(serializer, file.readText())
}
// Sort for deterministic output.
val inputMetadata: List<ContainerMetadata> = inputMetadataFiles.files
.sortedBy { it.absolutePath }
.flatMap { file ->
json.decodeFromString(serializer, file.readText())
}

val outputMetadata: List<ContainerMetadata> = if (kotlinPlatformType.isCommon) {
generator.generateCommonKotlin(files, inputMetadata)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ import java.io.InputStream
import java.io.SequenceInputStream

internal fun File.calculateResourcesHash(): String {
val root: File = this
// Sort for deterministic output; walkTopDown() order is filesystem-dependent.
val inputStreams: List<InputStream> = walkTopDown()
.filterNot { it.isDirectory }
.sortedBy { it.relativeTo(root).path }
.map { it.inputStream() }.toList()
val singleInputStream: InputStream = SequenceInputStream(inputStreams.toEnumeration())

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2026 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.icerock.gradle.generator

import dev.icerock.gradle.metadata.resource.PluralMetadata
import dev.icerock.gradle.metadata.resource.PluralMetadata.PluralItem
import dev.icerock.gradle.metadata.resource.PluralMetadata.PluralItem.Quantity
import dev.icerock.gradle.metadata.resource.StringMetadata
import dev.icerock.gradle.utils.calculateResourcesHash
import java.io.File
import java.nio.file.Files
import kotlin.test.Test
import kotlin.test.assertEquals

class DeterministicOutputTest {

private val plural = PluralMetadata(
key = "apples",
values = listOf(
PluralMetadata.LocaleItem(
locale = "base",
values = listOf(
PluralItem(Quantity.ONE, "one apple"),
PluralItem(Quantity.OTHER, "%d apples"),
),
),
PluralMetadata.LocaleItem(
locale = "ru",
values = listOf(
PluralItem(Quantity.ONE, "яблоко"),
PluralItem(Quantity.FEW, "яблока"),
PluralItem(Quantity.MANY, "яблок"),
),
),
),
)

private val string = StringMetadata(
key = "hello",
values = listOf(
StringMetadata.LocaleItem(locale = "base", value = "Hello, world"),
StringMetadata.LocaleItem(locale = "ru", value = "Привет, мир"),
),
)

@Test
fun pluralContentHashIsStable() {
assertEquals(expected = "-9167ca0", actual = plural.contentHash())
}

@Test
fun stringContentHashIsStable() {
assertEquals(expected = "367b4e74", actual = string.contentHash())
}

@Test
fun equivalentMetadataProduceEqualContentHash() {
assertEquals(expected = plural.contentHash(), actual = plural.copy().contentHash())
}

@Test
fun directoryHashIgnoresFilesystemOrder() {
val forward = createTree(listOf("a.txt" to "A", "b.txt" to "B", "c.txt" to "C"))
val reverse = createTree(listOf("c.txt" to "C", "b.txt" to "B", "a.txt" to "A"))
try {
assertEquals(expected = forward.calculateResourcesHash(), actual = reverse.calculateResourcesHash())
} finally {
forward.deleteRecursively()
reverse.deleteRecursively()
}
}

private fun createTree(entries: List<Pair<String, String>>): File {
val dir = Files.createTempDirectory("moko-hash-test").toFile()
entries.forEach { (name, content) -> File(dir, name).writeText(content) }
return dir
}
}
Loading