Skip to content

Commit b161950

Browse files
committed
fix: make GenerateCurveTables and GenerateEmojiList config-cache compatible
- GenerateCurveTables: replace `project.hasProperty()` in onlyIf with a serializable Property wired via providers.gradleProperty() - GenerateEmojiList: extract inline doLast task into a proper abstract task class in buildSrc to avoid capturing script object references - Add kotlinx-serialization-json to buildSrc deps for emoji JSON parsing
1 parent 0467a97 commit b161950

5 files changed

Lines changed: 242 additions & 213 deletions

File tree

buildSrc/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ repositories {
77
mavenCentral()
88
gradlePluginPortal()
99
}
10+
11+
dependencies {
12+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
13+
}

buildSrc/src/main/java/GenerateCurveTables.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@ abstract class GenerateCurveTables : DefaultTask() {
2424
@get:Input
2525
abstract val forceRegenerate: Property<Boolean>
2626

27+
@get:Input
28+
abstract val forceViaGradleProperty: Property<Boolean>
29+
2730
init {
2831
// Default to false
2932
forceRegenerate.convention(false)
33+
forceViaGradleProperty.convention(false)
3034

3135
onlyIf {
32-
val force = project.hasProperty("forceCurveTables") || forceRegenerate.get()
36+
val force = forceViaGradleProperty.get() || forceRegenerate.get()
3337

3438
if (force) {
3539
true
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import kotlinx.serialization.json.Json
2+
import kotlinx.serialization.json.jsonArray
3+
import kotlinx.serialization.json.jsonObject
4+
import kotlinx.serialization.json.jsonPrimitive
5+
import org.gradle.api.DefaultTask
6+
import org.gradle.api.file.DirectoryProperty
7+
import org.gradle.api.provider.Property
8+
import org.gradle.api.tasks.Input
9+
import org.gradle.api.tasks.Internal
10+
import org.gradle.api.tasks.OutputDirectory
11+
import org.gradle.api.tasks.TaskAction
12+
import java.io.File
13+
import java.net.URI
14+
import java.nio.file.Files
15+
import java.nio.file.StandardCopyOption
16+
17+
abstract class GenerateEmojiList : DefaultTask() {
18+
19+
@get:Input
20+
abstract val emojiUrl: Property<String>
21+
22+
@get:Input
23+
abstract val emojiKeywordsUrl: Property<String>
24+
25+
@get:Internal
26+
abstract val emojiCacheDir: DirectoryProperty
27+
28+
@get:OutputDirectory
29+
abstract val outputDir: DirectoryProperty
30+
31+
init {
32+
description = "Fetches Unicode emoji list and generates categorized Kotlin source file if needed"
33+
group = "emoji"
34+
35+
onlyIf {
36+
!outputDir.get().asFile.resolve("Emojis.kt").exists()
37+
}
38+
}
39+
40+
@TaskAction
41+
fun generate() {
42+
val outDir = outputDir.get().asFile
43+
val cacheDir = emojiCacheDir.get().asFile
44+
val emojiFile = File(cacheDir, "emoji-test.txt")
45+
val keywordsFile = File(cacheDir, "en-keywords.json")
46+
47+
outDir.mkdirs()
48+
if (!emojiFile.exists()) {
49+
logger.lifecycle("Downloading emoji-test.txt")
50+
URI(emojiUrl.get()).toURL().openStream().use { input ->
51+
Files.copy(input, emojiFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
52+
}
53+
}
54+
55+
if (!keywordsFile.exists()) {
56+
logger.lifecycle("Downloading CLDR annotations")
57+
URI(emojiKeywordsUrl.get()).toURL().openStream().use { input ->
58+
Files.copy(input, keywordsFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
59+
}
60+
}
61+
62+
val json = Json { ignoreUnknownKeys = true }
63+
val cldrData = json.parseToJsonElement(keywordsFile.readText()).jsonObject
64+
val cldrAnnotations = cldrData["annotations"]?.jsonObject?.get("annotations")?.jsonObject
65+
66+
val emojiText = emojiFile.readText()
67+
val emojiCategories =
68+
mutableMapOf<String, MutableMap<String, MutableList<MutableMap<String, Any>>>>()
69+
val emojiCategoriesNoSkinTones =
70+
mutableMapOf<String, MutableMap<String, MutableList<MutableMap<String, Any>>>>()
71+
var currentGroup = "Uncategorized"
72+
var currentSubgroup = "Uncategorized"
73+
74+
emojiText.lines().forEach { line ->
75+
when {
76+
line.startsWith("# group:") -> {
77+
val groupName = line.removePrefix("# group:").trim()
78+
currentGroup = when (groupName) {
79+
"Smileys & Emotion", "People & Body" -> "Smileys & People"
80+
else -> groupName
81+
}
82+
emojiCategories.getOrPut(currentGroup) { mutableMapOf() }
83+
emojiCategoriesNoSkinTones.getOrPut(currentGroup) { mutableMapOf() }
84+
}
85+
86+
line.startsWith("# subgroup:") -> {
87+
currentSubgroup = line.removePrefix("# subgroup:").trim()
88+
emojiCategories[currentGroup]?.getOrPut(currentSubgroup) { mutableListOf() }
89+
emojiCategoriesNoSkinTones[currentGroup]?.getOrPut(currentSubgroup) { mutableListOf() }
90+
}
91+
92+
line.isNotBlank() && !line.startsWith("#") -> {
93+
val parts = line.split(";").map { it.trim() }
94+
if (parts.size > 1 && parts[1].contains("fully-qualified")) {
95+
val codePoints = parts[0].split(" ").map { it.toInt(16) }
96+
val unicode = codePoints.map { codePoint ->
97+
if (codePoint <= 0xFFFF) {
98+
codePoint.toChar().toString()
99+
} else {
100+
String(Character.toChars(codePoint))
101+
}
102+
}.joinToString("")
103+
val nameParts = line.split("#")[1].trim().split(" ")
104+
val name = nameParts.drop(2).joinToString(" ")
105+
val baseKeywords = name.split(" ").filter { it.isNotBlank() }
106+
107+
val cldrAnnotation = cldrAnnotations?.get(unicode)?.jsonObject
108+
val cldrKeywords = cldrAnnotation?.get("default")?.jsonArray
109+
?.mapNotNull { it.jsonPrimitive.toString().removeSurrounding("\"") }
110+
.orEmpty()
111+
val allKeywords = (baseKeywords + cldrKeywords).distinct()
112+
113+
val emojiEntry = mutableMapOf(
114+
"unicode" to unicode,
115+
"name" to name,
116+
"keywords" to allKeywords
117+
)
118+
emojiCategories[currentGroup]?.get(currentSubgroup)?.add(emojiEntry)
119+
120+
val hasSkinTone = codePoints.any { it in 0x1F3FB..0x1F3FF }
121+
if (!hasSkinTone) {
122+
emojiCategoriesNoSkinTones[currentGroup]?.get(currentSubgroup)
123+
?.add(emojiEntry)
124+
}
125+
}
126+
}
127+
}
128+
}
129+
130+
val nonEmptyCategories = emojiCategories.filter { (_, subgroups) ->
131+
subgroups.any { (_, emojis) -> emojis.isNotEmpty() }
132+
}
133+
val nonEmptyCategoriesNoSkinTones =
134+
emojiCategoriesNoSkinTones.filter { (_, subgroups) ->
135+
subgroups.any { (_, emojis) -> emojis.isNotEmpty() }
136+
}
137+
138+
val mainFile = File(outDir, "Emojis.kt")
139+
val mainCode = buildString {
140+
appendLine("// Generated file - Do not edit manually")
141+
appendLine("package com.getcode.libs.emojis.generated")
142+
appendLine()
143+
appendLine("data class Emoji(")
144+
appendLine(" val unicode: String,")
145+
appendLine(" val name: String,")
146+
appendLine(" val keywords: List<String>")
147+
appendLine(")")
148+
appendLine()
149+
appendLine("enum class Category(val displayName: String) {")
150+
nonEmptyCategories.keys.forEach { group ->
151+
val enumName = group.replace("[^A-Za-z0-9]".toRegex(), "").uppercase()
152+
appendLine(" $enumName(\"$group\"),")
153+
}
154+
appendLine(" FREQUENT(\"Frequently Used\"),")
155+
appendLine("}")
156+
appendLine()
157+
appendLine("object Emojis {")
158+
appendLine(" val categorized = mapOf(")
159+
nonEmptyCategories.forEach { (group, subgroups) ->
160+
val enumName = group.replace("[^A-Za-z0-9]".toRegex(), "").uppercase()
161+
appendLine(" Category.$enumName to mapOf(")
162+
subgroups.forEach { (subgroup, _) ->
163+
val safeGroupName = group.replace("[^A-Za-z0-9]".toRegex(), "")
164+
val safeSubgroupName = subgroup.replace("[^A-Za-z0-9]".toRegex(), "")
165+
appendLine(" \"$subgroup\" to ${safeGroupName}${safeSubgroupName}Emojis.categorized,")
166+
}
167+
appendLine(" ),")
168+
}
169+
appendLine(" )")
170+
appendLine()
171+
appendLine(" val categorizedNoSkinTones = mapOf(")
172+
nonEmptyCategoriesNoSkinTones.forEach { (group, subgroups) ->
173+
val enumName = group.replace("[^A-Za-z0-9]".toRegex(), "").uppercase()
174+
appendLine(" Category.$enumName to mapOf(")
175+
subgroups.forEach { (subgroup, _) ->
176+
val safeGroupName = group.replace("[^A-Za-z0-9]".toRegex(), "")
177+
val safeSubgroupName = subgroup.replace("[^A-Za-z0-9]".toRegex(), "")
178+
appendLine(" \"$subgroup\" to ${safeGroupName}${safeSubgroupName}Emojis.categorizedNoSkinTones,")
179+
}
180+
appendLine(" ),")
181+
}
182+
appendLine(" )")
183+
appendLine("}")
184+
}.trimIndent()
185+
mainFile.writeText(mainCode)
186+
187+
emojiCategories.forEach { (group, subgroups) ->
188+
subgroups.forEach { (subgroup, emojis) ->
189+
val safeGroupName = group.replace("[^A-Za-z0-9]".toRegex(), "")
190+
val safeSubgroupName = subgroup.replace("[^A-Za-z0-9]".toRegex(), "")
191+
val subgroupFile =
192+
File(outDir, "${safeGroupName}${safeSubgroupName}Emojis.kt")
193+
@Suppress("UNCHECKED_CAST")
194+
val subgroupCode = buildString {
195+
appendLine("// Generated file - Do not edit manually")
196+
appendLine("package com.getcode.libs.emojis.generated")
197+
appendLine()
198+
appendLine("object ${safeGroupName}${safeSubgroupName}Emojis {")
199+
appendLine(" val categorized = ${if (emojis.isEmpty()) "emptyList<Emoji>()" else "listOf("}")
200+
if (emojis.isNotEmpty()) {
201+
appendLine(" ${emojis.joinToString(",\n ") { "Emoji(\"${it["unicode"]}\", \"${it["name"]}\", listOf(${(it["keywords"] as List<String>).joinToString { "\"$it\"" }}))" }}")
202+
appendLine(" )")
203+
}
204+
appendLine()
205+
appendLine(
206+
" val categorizedNoSkinTones = ${
207+
if (emojiCategoriesNoSkinTones[group]?.get(
208+
subgroup
209+
)?.isEmpty() != false
210+
) "emptyList<Emoji>()" else "listOf("
211+
}"
212+
)
213+
val noSkinTones =
214+
emojiCategoriesNoSkinTones[group]?.get(subgroup) ?: emptyList()
215+
if (noSkinTones.isNotEmpty()) {
216+
appendLine(" ${noSkinTones.joinToString(",\n ") { "Emoji(\"${it["unicode"]}\", \"${it["name"]}\", listOf(${(it["keywords"] as List<String>).joinToString { "\"$it\"" }}))" }}")
217+
appendLine(" )")
218+
}
219+
appendLine("}")
220+
}.trimIndent()
221+
subgroupFile.writeText(subgroupCode)
222+
}
223+
}
224+
val totalEmojis = emojiCategories.values.sumOf { it.values.sumOf { e -> e.size } }
225+
logger.lifecycle("Generated $totalEmojis emojis across ${emojiCategories.size} categories")
226+
}
227+
}

libs/currency-math/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ dependencies {
2626
val generateCurveTables by tasks.registering(GenerateCurveTables::class) {
2727
rustTableUrl.set("https://raw.githubusercontent.com/code-payments/flipcash-program/refs/heads/main/api/src/table.rs")
2828
outputDir.set(layout.projectDirectory.dir("src/main/assets"))
29+
forceViaGradleProperty.set(providers.gradleProperty("forceCurveTables").map { true }.orElse(false))
2930

3031
// Always regenerate (useful for CI or development)
3132
// TODO: enable for CI when repo is public

0 commit comments

Comments
 (0)