Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/check-codes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
with:
cache-cleanup: never
- name: Check Codes
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel --stacktrace
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel --configuration-cache --stacktrace
- if: failure() && github.event_name == 'pull_request'
name: Setup reviewdog
uses: reviewdog/action-setup@d8edfce3dd5e1ec6978745e801f9c50b5ef80252 # v1.4.0
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
with:
cache-cleanup: never
- name: Build with Gradle
run: xvfb-run --auto-servernum ./gradlew build --no-daemon --parallel
run: xvfb-run --auto-servernum ./gradlew build --no-daemon --parallel --configuration-cache
env:
JAVA_TOOL_OPTIONS: -Djava.awt.headless=true
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
distribution: 'temurin'
java-version: '25'
- name: Fetch last version
run: ./gradlew ${{ matrix.task }} --no-daemon --info --stacktrace
run: ./gradlew ${{ matrix.task }} --no-daemon --configuration-cache --info --stacktrace
- name: Check for existing tags
run: if [ -z "$(git tag -l "$HMCL_TAG_NAME")" ]; then echo "continue=true" >> $GITHUB_ENV; fi
- name: Download artifacts
Expand Down
145 changes: 54 additions & 91 deletions HMCL/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,29 +1,23 @@
import org.jackhuang.hmcl.gradle.TerracottaConfigUpgradeTask
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.SyncTranslations
import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
import org.jackhuang.hmcl.gradle.l10n.*
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
import org.jackhuang.hmcl.gradle.pack.CreateDeb
import org.jackhuang.hmcl.gradle.pack.ReleaseType
import org.jackhuang.hmcl.gradle.terracotta.AbstractTerracottaTask
import org.jackhuang.hmcl.gradle.terracotta.TerracottaConfigUpgradeTask
import org.jackhuang.hmcl.gradle.terracotta.TerracottaConfigValidateTask
import org.jackhuang.hmcl.gradle.utils.ArtifactUtils.Companion.resolveArtifactFile
import org.jackhuang.hmcl.gradle.utils.ArtifactUtils.Companion.attachSignature
import org.jackhuang.hmcl.gradle.utils.ArtifactUtils.Companion.createChecksum
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 projectConfig = PropertiesUtils.load(providers.fileContents(rootProject.layout.projectDirectory.file("config/project.properties")))

val isOfficial = JenkinsUtils.IS_ON_CI || GitHubActionUtils.IS_ON_OFFICIAL_REPO

Expand Down Expand Up @@ -76,47 +70,6 @@ dependencies {
embedResources(libs.lwjgl.unsafe.agent)
}

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"
Expand Down Expand Up @@ -153,29 +106,30 @@ tasks.compileJava {
options.compilerArgs.addAll(addOpens.map { "--add-exports=$it=ALL-UNNAMED" })
}

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.curseforge.apikey" to curseForgeApiKey)
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
add("hmcl.lwjgl-unsafe-agent.version" to libs.lwjgl.unsafe.agent.get().version!!)
}
val hmclProperties: Map<String, String> = mapOf(
"hmcl.version" to project.version.toString(),
"hmcl.add-opens" to addOpens.joinToString(" "),
"hmcl.version.hash" to System.getenv("GITHUB_SHA"),
"hmcl.version.type" to versionType,
"hmcl.microsoft.auth.id" to microsoftAuthId,
"hmcl.curseforge.apikey" to curseForgeApiKey,
"hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!,
"hmcl.lwjgl-unsafe-agent.version" to libs.lwjgl.unsafe.agent.get().version!!
).filterValues { it != null }

val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
val createPropertiesFile = tasks.register("createPropertiesFile") {
outputs.file(hmclPropertiesFile)
hmclProperties.forEach { (k, v) -> inputs.property(k, v) }
inputs.properties(hmclProperties)

val file = hmclPropertiesFile
val prop = hmclProperties

doLast {
val targetFile = hmclPropertiesFile.get().asFile
val targetFile = file.get().asFile
targetFile.parentFile.mkdir()
targetFile.bufferedWriter().use {
for ((k, v) in hmclProperties) {
for ((k, v) in prop) {
it.write("$k=$v\n")
}
}
Expand Down Expand Up @@ -228,9 +182,16 @@ tasks.shadowJar {
}
}

val jarFile = jarPath

doLast {
attachSignature(jarPath)
createChecksum(jarPath)
val keyLocation = System.getenv("HMCL_SIGNATURE_KEY")
if (keyLocation != null) {
attachSignature(jarFile, keyLocation)
} else {
logger.warn("Missing signature key")
}
createChecksum(jarFile)
}
}

Expand All @@ -239,6 +200,7 @@ tasks.processResources {
dependsOn(upsideDownTranslate)
dependsOn(createLocaleNamesResourceBundle)
dependsOn(createLanguageList)
dependsOn(validateTerracottaConfig)

into("assets/") {
from(hmclPropertiesFile)
Expand All @@ -250,29 +212,24 @@ tasks.processResources {
from(upsideDownTranslate.map { it.outputFile })
from(createLocaleNamesResourceBundle.map { it.outputDirectory })
}

inputs.property("terracotta_version", libs.versions.terracotta)
doLast {
upgradeTerracottaConfig.get().checkValid()
}
}

fun artifactFile(ext: String) = jarPath.resolveSibling(jarPath.nameWithoutExtension + '.' + ext)

val makeExecutables = tasks.register("makeExecutables") {
val extensions = listOf("exe", "sh")

dependsOn(tasks.jar)

inputs.file(jarPath)
outputs.files(extensions.map { artifactFile(it) })
outputs.files(extensions.map { resolveArtifactFile(jarPath, it) })

val jarFile = jarPath

doLast {
val jarContent = jarPath.readBytes()
val jarContent = jarFile.readBytes()

ZipFile(jarPath).use { zipFile ->
ZipFile(jarFile).use { zipFile ->
for (extension in extensions) {
val output = artifactFile(extension)
val output = resolveArtifactFile(jarFile, extension)
val entry = zipFile.getEntry("assets/HMCLauncher.$extension")
?: throw GradleException("HMCLauncher.$extension not found")

Expand All @@ -290,7 +247,7 @@ val makeExecutables = tasks.register("makeExecutables") {
val makeDeb = tasks.register("makeDeb", CreateDeb::class) {
dependsOn(makeExecutables)

val debFile = layout.file(provider { artifactFile("deb") })
val debFile = layout.file(provider { resolveArtifactFile(jarPath, "deb") })

val debChannel = when (versionType) {
"stable" -> ReleaseType.STABLE
Expand All @@ -301,7 +258,7 @@ val makeDeb = tasks.register("makeDeb", CreateDeb::class) {
version.set(project.version.toString())
releaseType.set(debChannel)
launcherClassName.set("org.jackhuang.hmcl.Launcher")
appShFile.set(layout.file(provider { artifactFile("sh") }))
appShFile.set(layout.file(provider { resolveArtifactFile(jarPath, "sh") }))
iconFile.set(layout.projectDirectory.file("image/hmcl.png"))
outputFile.set(debFile)

Expand Down Expand Up @@ -405,10 +362,9 @@ tasks.register<JavaExec>("run") {

// terracotta

val upgradeTerracottaConfig = tasks.register<TerracottaConfigUpgradeTask>("upgradeTerracottaConfig") {
val destination = layout.projectDirectory.file("src/main/resources/assets/terracotta.json")
val source = layout.projectDirectory.file("terracotta-template.json");
val terracottaConfigFile = layout.projectDirectory.file("src/main/resources/assets/terracotta.json")

val upgradeTerracottaConfig = tasks.register<TerracottaConfigUpgradeTask>("upgradeTerracottaConfig") {
classifiers.set(
listOf(
"windows-x86_64", "windows-arm64",
Expand All @@ -418,11 +374,18 @@ val upgradeTerracottaConfig = tasks.register<TerracottaConfigUpgradeTask>("upgra
)
)

version.set(libs.versions.terracotta)
downloadURL.set($$"https://github.com/burningtnt/Terracotta/releases/download/v${version}/terracotta-${version}-${classifier}-pkg.tar.gz")

templateFile.set(source)
outputFile.set(destination)
templateFile.set(layout.projectDirectory.file("terracotta-template.json"))
}

val validateTerracottaConfig = tasks.register<TerracottaConfigValidateTask>("validateTerracottaConfig") {
upgradeTaskPath.set(upgradeTerracottaConfig.get().path)
}

tasks.withType<AbstractTerracottaTask> {
version.set(libs.versions.terracotta)
outputFile.set(terracottaConfigFile)
}

// Check Translations
Expand Down
9 changes: 7 additions & 2 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
plugins {
`kotlin-dsl`
}

repositories {
System.getenv("MAVEN_CENTRAL_REPO").let { repo ->
if (repo.isNullOrBlank())
Expand All @@ -20,8 +24,9 @@ dependencies {
}

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

tasks.withType<JavaCompile> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.gradle.terracotta;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.OutputFile;
import org.jetbrains.annotations.NotNull;

public abstract class AbstractTerracottaTask extends DefaultTask {

protected static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();

@Input
public abstract Property<@NotNull String> getVersion();

@OutputFile
public abstract RegularFileProperty getOutputFile();

}
Loading
Loading