Skip to content

Commit 24ae82e

Browse files
authored
Better CLI experience (#70)
1 parent 9dafbb6 commit 24ae82e

12 files changed

Lines changed: 874 additions & 77 deletions

File tree

.github/workflows/pr.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: PR Build
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- master
7+
8+
jobs:
9+
test:
10+
name: Run Tests
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@v4
16+
17+
- name: Set up JDK 25
18+
uses: actions/setup-java@v4
19+
with:
20+
java-version: 25
21+
distribution: temurin
22+
23+
- name: Grant Gradle permissions
24+
run: chmod +x ./gradlew
25+
26+
- name: Run tests
27+
run: ./gradlew test

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ dependencies {
4545
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin'
4646
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml'
4747
implementation 'com.github.Frotty:SimpleRegistry:f96dda96bd'
48+
implementation 'org.jline:jline:3.30.13'
4849
implementation group: 'org.slf4j', name: 'slf4j-api', version: '2.0.17'
4950
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.5.32'
5051
testImplementation 'org.testng:testng:7.12.0'

src/main/kotlin/config/DAOs.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import kotlin.collections.ArrayList
77
const val CONFIG_FILE_NAME = "wurst.build"
88

99
enum class ScriptMode { LUA, JASS }
10-
enum class Wc3Patch { REFORGED, PRE_129 }
1110

1211
/**
1312
* The root DAO that contains the child DAOs.
@@ -19,7 +18,7 @@ data class WurstProjectConfigData(
1918
val dependencies: ArrayList<String> = ArrayList(),
2019
val buildMapData: WurstProjectBuildMapData = WurstProjectBuildMapData(),
2120
val scriptMode: ScriptMode? = null,
22-
val wc3Patch: Wc3Patch? = null
21+
var wc3Patch: String? = null
2322
) {
2423
constructor() : this("unnamed")
2524
}

src/main/kotlin/file/CLICommand.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package file
22

33
import config.ScriptMode
4-
import config.Wc3Patch
54

65
enum class CLICommand {
76
HELP,
@@ -76,10 +75,7 @@ enum class GlobalOptions(val optionName: String = "", val argCount: Int = 0) {
7675
},
7776
WC3_PATCH("--wc3-patch", 1) {
7877
override fun runOption(setupMain: SetupMain, args: List<String>) {
79-
setupMain.wc3Patch = when (args[0].lowercase()) {
80-
"pre1.29" -> Wc3Patch.PRE_129
81-
else -> Wc3Patch.REFORGED
82-
}
78+
setupMain.wc3Patch = CoreJassProvider.normalizePatchInput(args[0])
8379
}
8480
};
8581

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
package file
2+
3+
import logging.KotlinLogging
4+
import java.net.URI
5+
import java.nio.file.AtomicMoveNotSupportedException
6+
import java.nio.file.Files
7+
import java.nio.file.Path
8+
import java.nio.file.StandardCopyOption
9+
import java.util.jar.JarFile
10+
11+
object CoreJassProvider {
12+
const val DEFAULT_PATCH = "v1.36"
13+
const val PRE_129_PATCH = "v1.28"
14+
15+
private const val JASS_HISTORY_RAW = "https://raw.githubusercontent.com/Luashine/jass-history"
16+
private const val JASS_HISTORY_REF = "master"
17+
private val log = KotlinLogging.logger {}
18+
19+
private val PATCH_TO_JASS_HISTORY_FOLDER = linkedMapOf(
20+
"v1.36" to "Reforged-v1.36.1.20719-w3-51d40ee",
21+
"v1.35" to "Reforged-v1.35.0.20093-w3-5ec1b77",
22+
"v1.34" to "Reforged-v1.34.0.19632-w3-31590bf",
23+
"v1.33" to "Reforged-v1.33.0.19378-w3-e94d62c",
24+
"v1.32" to "Reforged-v1.32.10.19202",
25+
"v1.31" to "TFT-v1.31.1.12173",
26+
"v1.30" to "TFT-v1.30.4.11274",
27+
"v1.29" to "TFT-v1.29.2.9231",
28+
"v1.28" to "TFT-v1.28.2.7395",
29+
"v1.27b" to "TFT-v1.27b-ru",
30+
"v1.27a" to "TFT-v1.27a-ru",
31+
"v1.26a" to "TFT-v1.26a-ru",
32+
"v1.25b" to "TFT-v1.25b-ru",
33+
"v1.24e" to "TFT-v1.24e-ru",
34+
"v1.24d" to "TFT-v1.24d-ru",
35+
"v1.24c" to "TFT-v1.24c-ru",
36+
"v1.24b" to "TFT-v1.24b-ru",
37+
"v1.24a" to "TFT-v1.24a-ru",
38+
"v1.23a" to "TFT-v1.23a-ru",
39+
"v1.22a" to "TFT-v1.22a-ru",
40+
"v1.21b" to "TFT-v1.21b-ru",
41+
"v1.21a" to "TFT-v1.21a-ru",
42+
"v1.21" to "Beta-ROC-v1.21",
43+
"v1.20e" to "TFT-v1.20e-ru",
44+
"v1.20d" to "TFT-v1.20d-ru",
45+
"v1.20c" to "TFT-v1.20c-ru",
46+
"v1.20b" to "TFT-v1.20b-ru",
47+
"v1.20a" to "TFT-v1.20a-ru",
48+
"v1.20" to "Beta-ROC-v1.20",
49+
"v1.19b" to "TFT-v1.19b-ru",
50+
"v1.19a" to "TFT-v1.19a-ru",
51+
"v1.18a" to "TFT-v1.18a-ru",
52+
"v1.17a" to "TFT-v1.17a-ru",
53+
"v1.16a" to "TFT-v1.16a-ru",
54+
"v1.15" to "TFT-v1.15-ru",
55+
"v1.14b" to "TFT-v1.14b-ru",
56+
"v1.14" to "TFT-v1.14-ru",
57+
"v1.13b" to "TFT-v1.13b-ru",
58+
"v1.13" to "TFT-v1.13-ru",
59+
"v1.12" to "TFT-v1.12-ru",
60+
"v1.11" to "TFT-v1.11-ru",
61+
"v1.10" to "TFT-v1.10-ru",
62+
"v1.07" to "TFT-v1.07-ru",
63+
"v1.06" to "ROC-v1.06-ru",
64+
"v1.05" to "ROC-v1.05-ru",
65+
"v1.04" to "ROC-v1.04-ru",
66+
"v1.03" to "ROC-v1.03-ru",
67+
"v1.02a" to "ROC-v1.02a-ru",
68+
"v1.02" to "ROC-v1.02-ru",
69+
"v1.01b" to "ROC-v1.01b-ru",
70+
"v1.01" to "ROC-v1.01-ru",
71+
"v1.00" to "ROC-v1.00-ru"
72+
)
73+
74+
val supportedPatches: List<String> = PATCH_TO_JASS_HISTORY_FOLDER.keys.toList()
75+
76+
fun describePatch(patch: String): String {
77+
val normalizedPatch = normalizePatchInput(patch)
78+
val label = when (normalizedPatch) {
79+
DEFAULT_PATCH -> "latest Reforged / WC3 2.x core JASS"
80+
"v1.31" -> "latest classic TFT"
81+
PRE_129_PATCH -> "legacy pre-1.29"
82+
else -> {
83+
val minor = Regex("""v1\.(\d+)""").find(normalizedPatch)?.groupValues?.get(1)?.toIntOrNull()
84+
when {
85+
minor != null && minor >= 32 -> "Reforged"
86+
minor != null && minor >= 7 -> "classic TFT"
87+
minor != null -> "classic ROC"
88+
else -> null
89+
}
90+
}
91+
}
92+
return if (label == null) normalizedPatch else "$normalizedPatch ($label)"
93+
}
94+
95+
fun jassHistoryFolderForPatch(patch: String): String? {
96+
return PATCH_TO_JASS_HISTORY_FOLDER[normalizePatchInput(patch)]
97+
}
98+
99+
fun isSupportedPatch(patch: String): Boolean {
100+
return PATCH_TO_JASS_HISTORY_FOLDER.containsKey(normalizePatchInput(patch))
101+
}
102+
103+
fun normalizePatchInput(input: String?): String {
104+
val patch = input?.trim().orEmpty()
105+
val normalizedAlias = when (patch.lowercase()) {
106+
"", "reforged", "latest" -> DEFAULT_PATCH
107+
"pre1.29", "pre-1.29", "pre_129", "pre-129" -> PRE_129_PATCH
108+
else -> patch
109+
}
110+
val withPrefix = if (normalizedAlias.matches(Regex("""\d+\.\d+.*"""))) "v$normalizedAlias" else normalizedAlias
111+
val canonicalCase = PATCH_TO_JASS_HISTORY_FOLDER.keys.firstOrNull { it.equals(withPrefix, ignoreCase = true) }
112+
if (canonicalCase != null) {
113+
return canonicalCase
114+
}
115+
return PATCH_TO_JASS_HISTORY_FOLDER.entries.firstOrNull { it.value.equals(withPrefix, ignoreCase = true) }?.key
116+
?: withPrefix
117+
}
118+
119+
fun isPre129Patch(input: String?): Boolean {
120+
val patch = normalizePatchInput(input)
121+
if (patch == PRE_129_PATCH) {
122+
return true
123+
}
124+
val version = Regex("""v(\d+)\.(\d+)""").find(patch)?.groupValues ?: return false
125+
return version[1].toIntOrNull() == 1 && (version[2].toIntOrNull() ?: 29) < 29
126+
}
127+
128+
fun ensureFiles(projectRoot: Path, wc3Patch: String?): List<Path> {
129+
val buildFolder = projectRoot.resolve("_build")
130+
Files.createDirectories(buildFolder)
131+
val patch = normalizePatchInput(wc3Patch)
132+
val previousPatch = readProvenance(buildFolder)
133+
val materializedFiles = listOf(
134+
materializeFile(buildFolder, "common.j", patch, previousPatch),
135+
materializeFile(buildFolder, "blizzard.j", patch, previousPatch)
136+
)
137+
if (materializedFiles.all { it.managedByGrill }) {
138+
Files.writeString(
139+
buildFolder.resolve("core-jass.provenance"),
140+
"wc3Patch: $patch\njassHistoryFolder: ${jassHistoryFolderForPatch(patch).orEmpty()}\n"
141+
)
142+
} else {
143+
log.warn(
144+
"Existing _build core JASS files have no Grill provenance; leaving them project-owned. " +
145+
"Delete _build/common.j and _build/blizzard.j to let Grill regenerate them for $patch."
146+
)
147+
}
148+
return materializedFiles.map { it.path }
149+
}
150+
151+
fun fetchJassHistoryVersions(): List<String> {
152+
return supportedPatches
153+
}
154+
155+
fun recommendedPatchOptions(versions: List<String>): List<String> {
156+
return (listOf(DEFAULT_PATCH, "v1.31", PRE_129_PATCH) + versions.take(1)).distinct()
157+
}
158+
159+
private data class MaterializedFile(val path: Path, val managedByGrill: Boolean)
160+
161+
private fun materializeFile(buildFolder: Path, fileName: String, patch: String, previousPatch: String?): MaterializedFile {
162+
val target = buildFolder.resolve(fileName)
163+
val jassHistoryFolder = PATCH_TO_JASS_HISTORY_FOLDER[patch]
164+
if (jassHistoryFolder == null) {
165+
throw IllegalArgumentException("Unsupported WC3 patch <$patch>. Supported values: ${supportedPatches.joinToString()}")
166+
}
167+
168+
if (previousPatch == null && Files.exists(target)) {
169+
log.info("Keeping existing _build/$fileName because it has no Grill provenance.")
170+
return MaterializedFile(target, managedByGrill = false)
171+
}
172+
173+
val canKeepExisting = previousPatch == patch
174+
if (canKeepExisting && isValidCoreJassFile(target)) {
175+
log.info("Using cached _build/$fileName for $patch.")
176+
return MaterializedFile(target, managedByGrill = true)
177+
}
178+
179+
try {
180+
downloadJassHistoryFile(fileName, patch, jassHistoryFolder, target)
181+
return MaterializedFile(target, managedByGrill = true)
182+
} catch (e: Exception) {
183+
if (canKeepExisting && isValidCoreJassFile(target)) {
184+
log.warn("Could not refresh $fileName for $patch; keeping existing _build copy. Reason: ${e.message}")
185+
return MaterializedFile(target, managedByGrill = true)
186+
}
187+
if (patch == DEFAULT_PATCH || patch == PRE_129_PATCH) {
188+
log.warn("Could not download $fileName for $patch; falling back to bundled core JASS. Reason: ${e.message}")
189+
copyBundledCoreJass(fileName, patch, target)
190+
return MaterializedFile(target, managedByGrill = true)
191+
}
192+
throw RuntimeException("Could not download $fileName for WC3 patch <$patch> from jass-history.", e)
193+
}
194+
}
195+
196+
private fun readProvenance(buildFolder: Path): String? {
197+
val provenanceFile = buildFolder.resolve("core-jass.provenance")
198+
if (!Files.exists(provenanceFile)) {
199+
return null
200+
}
201+
return Files.readString(provenanceFile)
202+
.lineSequence()
203+
.firstOrNull { it.startsWith("wc3Patch:") }
204+
?.substringAfter(":")
205+
?.trim()
206+
?.let(::normalizePatchInput)
207+
}
208+
209+
private fun copyBundledCoreJass(fileName: String, patch: String, target: Path) {
210+
val patchFolder = when (patch) {
211+
PRE_129_PATCH -> "pre1.29"
212+
else -> "reforged"
213+
}
214+
215+
Files.createDirectories(target.parent)
216+
val resourcePath = "core-jass/$patchFolder/$fileName"
217+
javaClass.classLoader.getResourceAsStream(resourcePath)?.use { input ->
218+
Files.copy(input, target, StandardCopyOption.REPLACE_EXISTING)
219+
return
220+
}
221+
222+
JarFile(global.InstallationManager.getCompilerPath()).use { jar ->
223+
val entry = jar.getEntry(fileName)
224+
?: throw IllegalStateException("Bundled $fileName was not found for $patch.")
225+
jar.getInputStream(entry).use { input ->
226+
Files.copy(input, target, StandardCopyOption.REPLACE_EXISTING)
227+
}
228+
}
229+
}
230+
231+
private fun downloadJassHistoryFile(fileName: String, patch: String, jassHistoryFolder: String, target: Path) {
232+
Files.createDirectories(target.parent)
233+
val rawUrl = "$JASS_HISTORY_RAW/$JASS_HISTORY_REF/war3extract/$jassHistoryFolder/scripts/$fileName"
234+
val tempFile = Files.createTempFile(target.parent, "$fileName.", ".download")
235+
var replacedTarget = false
236+
try {
237+
URI(rawUrl).toURL().openStream().use { input ->
238+
Files.copy(input, tempFile, StandardCopyOption.REPLACE_EXISTING)
239+
}
240+
if (!isValidCoreJassFile(tempFile)) {
241+
throw IllegalStateException("Downloaded $fileName from jass-history did not look valid.")
242+
}
243+
moveValidatedDownload(tempFile, target)
244+
replacedTarget = true
245+
} finally {
246+
if (!replacedTarget) {
247+
Files.deleteIfExists(tempFile)
248+
}
249+
}
250+
}
251+
252+
private fun isValidCoreJassFile(path: Path): Boolean {
253+
return Files.exists(path) && Files.size(path) >= 1024L
254+
}
255+
256+
private fun moveValidatedDownload(source: Path, target: Path) {
257+
try {
258+
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE)
259+
} catch (_: AtomicMoveNotSupportedException) {
260+
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING)
261+
}
262+
}
263+
264+
}

0 commit comments

Comments
 (0)