403 lines
12 KiB
Kotlin
403 lines
12 KiB
Kotlin
import org.jackhuang.hmcl.gradle.ci.GitHubActionUtils
|
|
import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
|
|
import org.jackhuang.hmcl.gradle.l10n.CheckTranslations
|
|
import org.jackhuang.hmcl.gradle.l10n.CreateLanguageList
|
|
import org.jackhuang.hmcl.gradle.l10n.CreateLocaleNamesResourceBundle
|
|
import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
|
|
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
|
|
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
|
|
import java.net.URI
|
|
import java.nio.file.FileSystems
|
|
import java.nio.file.Files
|
|
import java.security.KeyFactory
|
|
import java.security.MessageDigest
|
|
import java.security.Signature
|
|
import java.security.spec.PKCS8EncodedKeySpec
|
|
import java.util.zip.ZipFile
|
|
|
|
plugins {
|
|
alias(libs.plugins.shadow)
|
|
}
|
|
|
|
val projectConfig = PropertiesUtils.load(rootProject.file("config/project.properties").toPath())
|
|
|
|
val isOfficial = JenkinsUtils.IS_ON_CI || GitHubActionUtils.IS_ON_OFFICIAL_REPO
|
|
|
|
val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" else "unofficial"
|
|
val versionRoot = System.getenv("VERSION_ROOT") ?: projectConfig.getProperty("versionRoot") ?: "3"
|
|
|
|
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
|
|
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
|
|
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
|
|
|
|
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE") ?: ""
|
|
|
|
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt()
|
|
if (buildNumber != null) {
|
|
version = if (JenkinsUtils.IS_ON_CI && versionType == "dev") {
|
|
"$versionRoot.0.$buildNumber"
|
|
} else {
|
|
"$versionRoot.$buildNumber"
|
|
}
|
|
} else {
|
|
val shortCommit = System.getenv("GITHUB_SHA")?.lowercase()?.substring(0, 7)
|
|
version = if (shortCommit.isNullOrBlank()) {
|
|
"$versionRoot.SNAPSHOT"
|
|
} else if (isOfficial) {
|
|
"$versionRoot.dev-$shortCommit"
|
|
} else {
|
|
"$versionRoot.unofficial-$shortCommit"
|
|
}
|
|
}
|
|
|
|
val embedResources by configurations.registering
|
|
|
|
dependencies {
|
|
implementation(project(":HMCLCore"))
|
|
implementation(project(":HMCLBoot"))
|
|
implementation("libs:JFoenix")
|
|
implementation(libs.twelvemonkeys.imageio.webp)
|
|
implementation(libs.java.info)
|
|
|
|
if (launcherExe.isBlank()) {
|
|
implementation(libs.hmclauncher)
|
|
}
|
|
|
|
embedResources(libs.authlib.injector)
|
|
}
|
|
|
|
fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes)
|
|
|
|
fun createChecksum(file: File) {
|
|
val algorithms = linkedMapOf(
|
|
"SHA-1" to "sha1",
|
|
"SHA-256" to "sha256",
|
|
"SHA-512" to "sha512"
|
|
)
|
|
|
|
algorithms.forEach { (algorithm, ext) ->
|
|
File(file.parentFile, "${file.name}.$ext").writeText(
|
|
digest(algorithm, file.readBytes()).joinToString(separator = "", postfix = "\n") { "%02x".format(it) }
|
|
)
|
|
}
|
|
}
|
|
|
|
fun attachSignature(jar: File) {
|
|
val keyLocation = System.getenv("HMCL_SIGNATURE_KEY")
|
|
if (keyLocation == null) {
|
|
logger.warn("Missing signature key")
|
|
return
|
|
}
|
|
|
|
val privatekey = KeyFactory.getInstance("RSA").generatePrivate(PKCS8EncodedKeySpec(File(keyLocation).readBytes()))
|
|
val signer = Signature.getInstance("SHA512withRSA")
|
|
signer.initSign(privatekey)
|
|
ZipFile(jar).use { zip ->
|
|
zip.stream()
|
|
.sorted(Comparator.comparing { it.name })
|
|
.filter { it.name != "META-INF/hmcl_signature" }
|
|
.forEach {
|
|
signer.update(digest("SHA-512", it.name.toByteArray()))
|
|
signer.update(digest("SHA-512", zip.getInputStream(it).readBytes()))
|
|
}
|
|
}
|
|
val signature = signer.sign()
|
|
FileSystems.newFileSystem(URI.create("jar:" + jar.toURI()), emptyMap<String, Any>()).use { zipfs ->
|
|
Files.newOutputStream(zipfs.getPath("META-INF/hmcl_signature")).use { it.write(signature) }
|
|
}
|
|
}
|
|
|
|
tasks.withType<JavaCompile> {
|
|
sourceCompatibility = "17"
|
|
targetCompatibility = "17"
|
|
}
|
|
|
|
tasks.checkstyleMain {
|
|
// Third-party code is not checked
|
|
exclude("**/org/jackhuang/hmcl/ui/image/apng/**")
|
|
}
|
|
|
|
tasks.compileJava {
|
|
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
|
|
}
|
|
|
|
val addOpens = listOf(
|
|
"java.base/java.lang",
|
|
"java.base/java.lang.reflect",
|
|
"java.base/jdk.internal.loader",
|
|
"javafx.base/com.sun.javafx.binding",
|
|
"javafx.base/com.sun.javafx.event",
|
|
"javafx.base/com.sun.javafx.runtime",
|
|
"javafx.graphics/javafx.css",
|
|
"javafx.graphics/com.sun.javafx.stage",
|
|
"javafx.graphics/com.sun.prism",
|
|
"javafx.controls/com.sun.javafx.scene.control",
|
|
"javafx.controls/com.sun.javafx.scene.control.behavior",
|
|
"javafx.controls/javafx.scene.control.skin",
|
|
"jdk.attach/sun.tools.attach",
|
|
)
|
|
|
|
val hmclProperties = buildList {
|
|
add("hmcl.version" to project.version.toString())
|
|
add("hmcl.add-opens" to addOpens.joinToString(" "))
|
|
System.getenv("GITHUB_SHA")?.let {
|
|
add("hmcl.version.hash" to it)
|
|
}
|
|
add("hmcl.version.type" to versionType)
|
|
add("hmcl.microsoft.auth.id" to microsoftAuthId)
|
|
add("hmcl.microsoft.auth.secret" to microsoftAuthSecret)
|
|
add("hmcl.curseforge.apikey" to curseForgeApiKey)
|
|
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
|
|
}
|
|
|
|
val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
|
|
val createPropertiesFile by tasks.registering {
|
|
outputs.file(hmclPropertiesFile)
|
|
hmclProperties.forEach { (k, v) -> inputs.property(k, v) }
|
|
|
|
doLast {
|
|
val targetFile = hmclPropertiesFile.get().asFile
|
|
targetFile.parentFile.mkdir()
|
|
targetFile.bufferedWriter().use {
|
|
for ((k, v) in hmclProperties) {
|
|
it.write("$k=$v\n")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
tasks.jar {
|
|
enabled = false
|
|
dependsOn(tasks["shadowJar"])
|
|
}
|
|
|
|
val jarPath = tasks.jar.get().archiveFile.get().asFile
|
|
|
|
tasks.shadowJar {
|
|
dependsOn(createPropertiesFile)
|
|
|
|
archiveClassifier.set(null as String?)
|
|
|
|
exclude("**/package-info.class")
|
|
exclude("META-INF/maven/**")
|
|
|
|
exclude("META-INF/services/javax.imageio.spi.ImageReaderSpi")
|
|
exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi")
|
|
|
|
listOf(
|
|
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*", "darwin-*",
|
|
"*-ppc", "*-ppc64le", "*-s390x", "*-armel",
|
|
).forEach { exclude("com/sun/jna/$it/**") }
|
|
|
|
minimize {
|
|
exclude(dependency("com.google.code.gson:.*:.*"))
|
|
exclude(dependency("net.java.dev.jna:jna:.*"))
|
|
exclude(dependency("libs:JFoenix:.*"))
|
|
exclude(project(":HMCLBoot"))
|
|
}
|
|
|
|
manifest.attributes(
|
|
"Created-By" to "Copyright(c) 2013-2025 huangyuhui.",
|
|
"Implementation-Version" to project.version.toString(),
|
|
"Main-Class" to "org.jackhuang.hmcl.Main",
|
|
"Multi-Release" to "true",
|
|
"Add-Opens" to addOpens.joinToString(" "),
|
|
"Enable-Native-Access" to "ALL-UNNAMED"
|
|
)
|
|
|
|
if (launcherExe.isNotBlank()) {
|
|
into("assets") {
|
|
from(file(launcherExe))
|
|
}
|
|
}
|
|
|
|
doLast {
|
|
attachSignature(jarPath)
|
|
createChecksum(jarPath)
|
|
}
|
|
}
|
|
|
|
tasks.processResources {
|
|
dependsOn(createPropertiesFile)
|
|
dependsOn(upsideDownTranslate)
|
|
dependsOn(createLocaleNamesResourceBundle)
|
|
dependsOn(createLanguageList)
|
|
|
|
into("assets/") {
|
|
from(hmclPropertiesFile)
|
|
from(embedResources)
|
|
}
|
|
|
|
into("assets/lang") {
|
|
from(createLanguageList.map { it.outputFile })
|
|
from(upsideDownTranslate.map { it.outputFile })
|
|
from(createLocaleNamesResourceBundle.map { it.outputDirectory })
|
|
}
|
|
}
|
|
|
|
val makeExecutables by tasks.registering {
|
|
val extensions = listOf("exe", "sh")
|
|
|
|
dependsOn(tasks.jar)
|
|
|
|
inputs.file(jarPath)
|
|
outputs.files(extensions.map { File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + it) })
|
|
|
|
doLast {
|
|
val jarContent = jarPath.readBytes()
|
|
|
|
ZipFile(jarPath).use { zipFile ->
|
|
for (extension in extensions) {
|
|
val output = File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + extension)
|
|
val entry = zipFile.getEntry("assets/HMCLauncher.$extension")
|
|
?: throw GradleException("HMCLauncher.$extension not found")
|
|
|
|
output.outputStream().use { outputStream ->
|
|
zipFile.getInputStream(entry).use { it.copyTo(outputStream) }
|
|
outputStream.write(jarContent)
|
|
}
|
|
|
|
createChecksum(output)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
tasks.build {
|
|
dependsOn(makeExecutables)
|
|
}
|
|
|
|
fun parseToolOptions(options: String?): MutableList<String> {
|
|
if (options == null)
|
|
return mutableListOf()
|
|
|
|
val builder = StringBuilder()
|
|
val result = mutableListOf<String>()
|
|
|
|
var offset = 0
|
|
|
|
loop@ while (offset < options.length) {
|
|
val ch = options[offset]
|
|
if (Character.isWhitespace(ch)) {
|
|
if (builder.isNotEmpty()) {
|
|
result += builder.toString()
|
|
builder.clear()
|
|
}
|
|
|
|
while (offset < options.length && Character.isWhitespace(options[offset])) {
|
|
offset++
|
|
}
|
|
|
|
continue@loop
|
|
}
|
|
|
|
if (ch == '\'' || ch == '"') {
|
|
offset++
|
|
|
|
while (offset < options.length) {
|
|
val ch2 = options[offset++]
|
|
if (ch2 != ch) {
|
|
builder.append(ch2)
|
|
} else {
|
|
continue@loop
|
|
}
|
|
}
|
|
|
|
throw GradleException("Unmatched quote in $options")
|
|
}
|
|
|
|
builder.append(ch)
|
|
offset++
|
|
}
|
|
|
|
if (builder.isNotEmpty()) {
|
|
result += builder.toString()
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// For IntelliJ IDEA
|
|
tasks.withType<JavaExec> {
|
|
if (name != "run") {
|
|
jvmArgs(addOpens.map { "--add-opens=$it=ALL-UNNAMED" })
|
|
// if (javaVersion >= JavaVersion.VERSION_24) {
|
|
// jvmArgs("--enable-native-access=ALL-UNNAMED")
|
|
// }
|
|
}
|
|
}
|
|
|
|
tasks.register<JavaExec>("run") {
|
|
dependsOn(tasks.jar)
|
|
|
|
group = "application"
|
|
|
|
classpath = files(jarPath)
|
|
workingDir = rootProject.rootDir
|
|
|
|
val vmOptions = parseToolOptions(System.getenv("HMCL_JAVA_OPTS"))
|
|
if (vmOptions.none { it.startsWith("-Dhmcl.offline.auth.restricted=") })
|
|
vmOptions += "-Dhmcl.offline.auth.restricted=false"
|
|
|
|
jvmArgs(vmOptions)
|
|
|
|
val hmclJavaHome = System.getenv("HMCL_JAVA_HOME")
|
|
if (hmclJavaHome != null) {
|
|
this.executable(
|
|
file(hmclJavaHome).resolve("bin")
|
|
.resolve(if (System.getProperty("os.name").lowercase().startsWith("windows")) "java.exe" else "java")
|
|
)
|
|
}
|
|
|
|
doFirst {
|
|
logger.quiet("HMCL_JAVA_OPTS: {}", vmOptions)
|
|
logger.quiet("HMCL_JAVA_HOME: {}", hmclJavaHome ?: System.getProperty("java.home"))
|
|
}
|
|
}
|
|
|
|
// Check Translations
|
|
|
|
tasks.register<CheckTranslations>("checkTranslations") {
|
|
val dir = layout.projectDirectory.dir("src/main/resources/assets/lang")
|
|
|
|
englishFile.set(dir.file("I18N.properties"))
|
|
simplifiedChineseFile.set(dir.file("I18N_zh_CN.properties"))
|
|
traditionalChineseFile.set(dir.file("I18N_zh.properties"))
|
|
classicalChineseFile.set(dir.file("I18N_lzh.properties"))
|
|
}
|
|
|
|
// l10n
|
|
|
|
val generatedDir = layout.buildDirectory.dir("generated")
|
|
|
|
val upsideDownTranslate by tasks.registering(UpsideDownTranslate::class) {
|
|
inputFile.set(layout.projectDirectory.file("src/main/resources/assets/lang/I18N.properties"))
|
|
outputFile.set(generatedDir.map { it.file("generated/i18n/I18N_en_Qabs.properties") })
|
|
}
|
|
|
|
val createLanguageList by tasks.registering(CreateLanguageList::class) {
|
|
resourceBundleDir.set(layout.projectDirectory.dir("src/main/resources/assets/lang"))
|
|
resourceBundleBaseName.set("I18N")
|
|
additionalLanguages.set(listOf("en-Qabs"))
|
|
outputFile.set(generatedDir.map { it.file("languages.json") })
|
|
}
|
|
|
|
val createLocaleNamesResourceBundle by tasks.registering(CreateLocaleNamesResourceBundle::class) {
|
|
dependsOn(createLanguageList)
|
|
|
|
languagesFile.set(createLanguageList.flatMap { it.outputFile })
|
|
outputDirectory.set(generatedDir.map { it.dir("generated/LocaleNames") })
|
|
}
|
|
|
|
// mcmod data
|
|
|
|
tasks.register<ParseModDataTask>("parseModData") {
|
|
inputFile.set(layout.projectDirectory.file("mod.json"))
|
|
outputFile.set(layout.projectDirectory.file("src/main/resources/assets/mod_data.txt"))
|
|
}
|
|
|
|
tasks.register<ParseModDataTask>("parseModPackData") {
|
|
inputFile.set(layout.projectDirectory.file("modpack.json"))
|
|
outputFile.set(layout.projectDirectory.file("src/main/resources/assets/modpack_data.txt"))
|
|
}
|