构建时创建 deb 包 (#5981)

This commit is contained in:
Glavo
2026-04-26 14:40:05 +08:00
committed by GitHub
parent 36568df5b1
commit cb9d32dc91
9 changed files with 418 additions and 10 deletions
+6
View File
@@ -48,3 +48,9 @@ jobs:
name: HMCL-${{ env.SHORT_SHA }}-sh
path: HMCL/build/libs/HMCL-*.sh
archive: false
- name: Upload DEB
uses: actions/upload-artifact@v7
with:
name: HMCL-${{ env.SHORT_SHA }}-deb
path: HMCL/build/libs/HMCL-*.deb
archive: false
+11
View File
@@ -46,16 +46,20 @@ jobs:
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar.sha256"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh.sha256"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.deb"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.deb.sha256"
- name: Verify artifacts
if: ${{ env.continue == 'true' }}
run: |
export JAR_SHA256=$(cat HMCL-$HMCL_VERSION.jar.sha256 | tr -d '\n')
export EXE_SHA256=$(cat HMCL-$HMCL_VERSION.exe.sha256 | tr -d '\n')
export SH_SHA256=$(cat HMCL-$HMCL_VERSION.sh.sha256 | tr -d '\n')
export DEB_SHA256=$(cat HMCL-$HMCL_VERSION.deb.sha256 | tr -d '\n')
echo "$JAR_SHA256 HMCL-$HMCL_VERSION.jar" | sha256sum -c
echo "$EXE_SHA256 HMCL-$HMCL_VERSION.exe" | sha256sum -c
echo "$SH_SHA256 HMCL-$HMCL_VERSION.sh" | sha256sum -c
echo "$DEB_SHA256 HMCL-$HMCL_VERSION.deb" | sha256sum -c
- name: Generate release note
if: ${{ env.continue == 'true' }}
run: |
@@ -67,6 +71,7 @@ jobs:
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.deb]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.deb) | \`$(cat HMCL-$HMCL_VERSION.deb.sha256)\` |" >> RELEASE_NOTE
# CNB Release Note
echo "[更新日志](https://docs.hmcl.net/changelog/${{ matrix.channel }}.html#HMCL-$HMCL_VERSION)" >> CNB_RELEASE_NOTE
@@ -76,6 +81,8 @@ jobs:
echo "| [HMCL-$HMCL_VERSION.exe]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> CNB_RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.jar]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> CNB_RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.sh]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> CNB_RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.deb]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.deb) | \`$(cat HMCL-$HMCL_VERSION.deb.sha256)\` |" >> CNB_RELEASE_NOTE
env:
GH_DOWNLOAD_BASE_URL: https://github.com/${{ github.repository }}/releases/download
CNB_DOWNLOAD_BASE_URL: https://cnb.cool/HMCL-dev/HMCL/-/releases/download
@@ -86,6 +93,7 @@ jobs:
"HMCL-${{ env.HMCL_VERSION }}.exe" \
"HMCL-${{ env.HMCL_VERSION }}.jar" \
"HMCL-${{ env.HMCL_VERSION }}.sh" \
"HMCL-${{ env.HMCL_VERSION }}.deb" \
--target "${{ env.HMCL_COMMIT_SHA }}" \
--title "${{ env.HMCL_TAG_NAME }}" \
--notes-file RELEASE_NOTE \
@@ -120,6 +128,9 @@ jobs:
echo "Uploading HMCL-$HMCL_VERSION.sh"
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.sh"
echo "Uploading HMCL-$HMCL_VERSION.deb"
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.deb"
env:
CNB_TOKEN: ${{ secrets.CNB_SYNC_TOKEN }}
CNB_REPO: HMCL-dev/HMCL
+37 -8
View File
@@ -6,6 +6,8 @@ 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.pack.CreateDeb
import org.jackhuang.hmcl.gradle.pack.ReleaseType
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
import java.net.URI
import java.nio.file.FileSystems
@@ -251,20 +253,22 @@ tasks.processResources {
}
}
fun artifactFile(ext: String) = jarPath.resolveSibling(jarPath.nameWithoutExtension + '.' + ext)
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) })
outputs.files(extensions.map { artifactFile(it) })
doLast {
val jarContent = jarPath.readBytes()
ZipFile(jarPath).use { zipFile ->
for (extension in extensions) {
val output = File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + extension)
val output = artifactFile(extension)
val entry = zipFile.getEntry("assets/HMCLauncher.$extension")
?: throw GradleException("HMCLauncher.$extension not found")
@@ -279,8 +283,31 @@ val makeExecutables by tasks.registering {
}
}
val makeDeb by tasks.registering(CreateDeb::class) {
dependsOn(makeExecutables)
val debFile = layout.file(provider { artifactFile("deb") })
val debChannel = when (versionType) {
"stable" -> ReleaseType.STABLE
"dev" -> ReleaseType.DEVELOPMENT
else -> ReleaseType.NIGHTLY
}
version.set(project.version.toString())
releaseType.set(debChannel)
appShFile.set(layout.file(provider { artifactFile("sh") }))
iconFile.set(layout.projectDirectory.file("image/hmcl.png"))
outputFile.set(debFile)
doLast {
createChecksum(debFile.get().asFile)
}
}
tasks.build {
dependsOn(makeExecutables)
dependsOn(makeDeb)
}
fun parseToolOptions(options: String?): MutableList<String> {
@@ -377,12 +404,14 @@ val upgradeTerracottaConfig = tasks.register<TerracottaConfigUpgradeTask>("upgra
val destination = layout.projectDirectory.file("src/main/resources/assets/terracotta.json")
val source = layout.projectDirectory.file("terracotta-template.json");
classifiers.set(listOf(
"windows-x86_64", "windows-arm64",
"macos-x86_64", "macos-arm64",
"linux-x86_64", "linux-arm64", "linux-loongarch64", "linux-riscv64",
"freebsd-x86_64"
))
classifiers.set(
listOf(
"windows-x86_64", "windows-arm64",
"macos-x86_64", "macos-arm64",
"linux-x86_64", "linux-arm64", "linux-loongarch64", "linux-riscv64",
"freebsd-x86_64"
)
)
version.set(libs.versions.terracotta)
downloadURL.set($$"https://github.com/burningtnt/Terracotta/releases/download/v${version}/terracotta-${version}-${classifier}-pkg.tar.gz")
+1
View File
@@ -11,6 +11,7 @@ dependencies {
implementation(libs.gson)
implementation(libs.jna)
implementation(libs.kala.compress.tar)
implementation(libs.kala.compress.ar)
}
java {
@@ -0,0 +1,300 @@
/*
* 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.pack;
import kala.compress.archivers.ar.ArArchiveEntry;
import kala.compress.archivers.ar.ArArchiveOutputStream;
import kala.compress.archivers.tar.TarArchiveEntry;
import kala.compress.archivers.tar.TarArchiveOutputStream;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.GZIPOutputStream;
/// Creates a Debian package for the current HMCL channel.
///
/// ## Package layout
///
/// The generated `data.tar.gz` contains four installed artifacts:
///
/// - the bundled HMCL shell launcher under `/usr/share/java/hmcl/`
/// - a channel-specific command under `/usr/bin/`
/// - a desktop entry under `/usr/share/applications/`
/// - the HMCL icon under `/usr/share/icons/hicolor/256x256/apps/`
///
/// ## Channel commands and aliases
///
/// Every package installs a channel-specific executable such as `hmcl-stable`
/// or `hmcl-beta`. The generic `hmcl` command is intentionally not shipped as a
/// plain file. Instead, maintainer scripts register the channel command into
/// the shared `hmcl` alternatives group so multiple channel packages can
/// coexist without file conflicts.
///
/// @author Glavo
public abstract class CreateDeb extends DefaultTask {
public static final Logger LOGGER = Logging.getLogger(CreateDeb.class);
private static final int DIRECTORY_MODE = 0755;
private static final int EXECUTABLE_MODE = 0755;
private static final int REGULAR_FILE_MODE = 0644;
/// Debian version written into the `control` file and output filename.
@Input
public abstract Property<String> getVersion();
/// Release type metadata that controls package name, launcher name, and alias priority.
@Input
public abstract Property<ReleaseType> getReleaseType();
/// Executable `.sh` artifact produced by `makeExecutables`.
@InputFile
public abstract RegularFileProperty getAppShFile();
/// Desktop icon installed into the hicolor icon theme.
@InputFile
public abstract RegularFileProperty getIconFile();
/// Final `.deb` archive written by this task.
@OutputFile
public abstract RegularFileProperty getOutputFile();
private ReleaseType getCurrentType() {
return getReleaseType().get();
}
private String getCurrentTypeName() {
return getCurrentType().getName();
}
private String getLauncherPath() {
return "/usr/bin/hmcl-" + getCurrentTypeName();
}
private String getTargetPath() {
return "/usr/share/java/hmcl/" + getAppShFile().getAsFile().get().getName();
}
private String getDesktopFilePath() {
return "/usr/share/applications/hmcl-%s.desktop".formatted(getCurrentTypeName());
}
private String getIconTargetPath() {
return "/usr/share/icons/hicolor/256x256/apps/hmcl-%s.png".formatted(getCurrentTypeName());
}
/// Ensures parent directories exist in the tar stream before child entries are written.
private static void makeDirectories(Set<String> directories, TarArchiveOutputStream output, String dirName) throws IOException {
if (dirName.isEmpty() || ".".equals(dirName) || directories.contains(dirName))
return;
int idx = dirName.lastIndexOf('/');
if (idx > 0) {
makeDirectories(directories, output, dirName.substring(0, idx));
}
TarArchiveEntry entry = new TarArchiveEntry(dirName + "/", true);
entry.setMode(DIRECTORY_MODE);
output.putArchiveEntry(entry);
output.closeArchiveEntry();
directories.add(dirName);
}
/// Writes UTF-8 text content as a tar entry with the requested file mode.
private static void putEntry(Set<String> directories, TarArchiveOutputStream output, String name, String content, int mode) throws IOException {
putEntry(directories, output, name, content.getBytes(StandardCharsets.UTF_8), mode);
}
/// Writes binary content as a tar entry and creates parent directories on demand.
private static void putEntry(Set<String> directories, TarArchiveOutputStream output, String name, byte[] content, int mode) throws IOException {
int idx = name.lastIndexOf('/');
if (idx > 0) {
makeDirectories(directories, output, name.substring(0, idx));
}
TarArchiveEntry entry = new TarArchiveEntry(name);
entry.setMode(mode);
entry.setSize(content.length);
output.putArchiveEntry(entry);
output.write(content);
output.closeArchiveEntry();
}
/// Builds a valid `.deb` archive with `debian-binary`, `control.tar.gz`, and `data.tar.gz`.
@TaskAction
public void run() throws IOException {
Path appShFile = getAppShFile().getAsFile().get().toPath();
if (!Files.isRegularFile(appShFile))
throw new IOException("Invalid app script file: " + appShFile);
Path iconFile = getIconFile().getAsFile().get().toPath();
if (!Files.isRegularFile(iconFile))
throw new IOException("Invalid icon file: " + iconFile);
byte[] appShBytes = Files.readAllBytes(appShFile);
if (appShBytes.length == 0)
throw new IOException("Empty app script file: " + appShFile);
byte[] iconBytes = Files.readAllBytes(iconFile);
if (iconBytes.length == 0)
throw new IOException("Empty icon file: " + iconFile);
byte[] launcherScriptBytes = getLauncherScript().getBytes(StandardCharsets.UTF_8);
byte[] desktopInfoBytes = getDesktopInfo().getBytes(StandardCharsets.UTF_8);
LOGGER.lifecycle("Creating control.tar.gz");
var controlData = new ByteArrayOutputStream();
try (var output = new TarArchiveOutputStream(new GZIPOutputStream(controlData))) {
output.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
Set<String> directories = new HashSet<>();
putEntry(directories, output, "./control", getControl(appShBytes.length, launcherScriptBytes.length, desktopInfoBytes.length, iconBytes.length), REGULAR_FILE_MODE);
putEntry(directories, output, "./postinst", getPostinst(), EXECUTABLE_MODE);
putEntry(directories, output, "./prerm", getPrerm(), EXECUTABLE_MODE);
}
Path outputFile = getOutputFile().get().getAsFile().toPath();
Files.createDirectories(outputFile.getParent());
LOGGER.lifecycle("Creating data.tar.gz");
var dataTarBuffer = new ByteArrayOutputStream(12 * 1024 * 1024);
try (var output = new TarArchiveOutputStream(new GZIPOutputStream(dataTarBuffer))) {
output.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
Set<String> directories = new HashSet<>();
putEntry(directories, output, "." + getTargetPath(), appShBytes, EXECUTABLE_MODE);
putEntry(directories, output, "." + getLauncherPath(), launcherScriptBytes, EXECUTABLE_MODE);
putEntry(directories, output, "." + getDesktopFilePath(), desktopInfoBytes, REGULAR_FILE_MODE);
putEntry(directories, output, "." + getIconTargetPath(), iconBytes, REGULAR_FILE_MODE);
}
LOGGER.lifecycle("Creating deb file");
try (var output = new ArArchiveOutputStream(Files.newOutputStream(outputFile,
StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE))) {
putArEntry(output, "debian-binary", "2.0\n".getBytes(StandardCharsets.UTF_8));
putArEntry(output, "control.tar.gz", controlData.toByteArray());
putArEntry(output, "data.tar.gz", dataTarBuffer.toByteArray());
}
}
/// Adds one member to the outer ar container used by Debian packages.
private static void putArEntry(ArArchiveOutputStream output, String name, byte[] content) throws IOException {
ArArchiveEntry entry = new ArArchiveEntry(name, content.length);
output.putArchiveEntry(entry);
output.write(content);
output.closeArchiveEntry();
}
/// Generates the package metadata file.
///
/// `Depends` is deliberately omitted so the package stays policy-light and
/// leaves runtime dependency resolution to the target environment.
private String getControl(long appSize, long launcherScriptSize, long desktopInfoSize, long iconSize) {
long installedSize = (appSize + launcherScriptSize + desktopInfoSize + iconSize + 1023) / 1024;
return """
Package: %s
Version: %s
Section: games
Priority: optional
Architecture: all
Installed-Size: %d
Maintainer: Glavo <zjx001202@gmail.com>
Description: Hello Minecraft! Launcher
Homepage: https://github.com/HMCL-dev/HMCL
""".formatted(getCurrentType().getPackageName(), getVersion().get(), Math.max(installedSize, 1)) + "\n";
}
private static final String COMMON_LAUNCHER_PATH = "/usr/bin/hmcl";
/// Registers the channel command into the shared `hmcl` alternatives group.
private String getPostinst() {
return """
#!/bin/sh
set -e
if [ "$1" = configure ]; then
update-alternatives --install %s hmcl %s %d
fi
""".formatted(COMMON_LAUNCHER_PATH, getLauncherPath(), getCurrentType().getAlternativesPriority());
}
/// Removes the channel command from the shared `hmcl` alternatives group.
private String getPrerm() {
return """
#!/bin/sh
set -e
if [ "$1" = remove ] || [ "$1" = deconfigure ]; then
update-alternatives --remove hmcl %s
fi
""".formatted(getLauncherPath());
}
/// Creates a tiny wrapper that launches the bundled shell script from the user's home directory.
private String getLauncherScript() {
return """
#!/usr/bin/env bash
cd "$HOME"
if [ -z "${HMCL_USER_HOME:-}" ]; then
if [ -z "${XDG_DATA_HOME:-}" ]; then
export HMCL_USER_HOME="$HOME/.local/share/hmcl"
else
export HMCL_USER_HOME="$XDG_DATA_HOME/hmcl"
fi
fi
if [ -z "${HMCL_LOCAL_HOME:-}" ]; then
export HMCL_LOCAL_HOME="$HMCL_USER_HOME/local-%s"
fi
if [ -z "${HMCL_DEPENDENCIES_DIR:-}" ]; then
export HMCL_DEPENDENCIES_DIR="$HMCL_USER_HOME/dependencies"
fi
exec %s "$@"
""".formatted(getCurrentTypeName(), getTargetPath());
}
/// Generates the desktop entry that points to the channel-specific launcher command.
private String getDesktopInfo() {
return """
[Desktop Entry]
Type=Application
Name=%s
Comment=Hello Minecraft! Launcher
Exec=%s
Icon=%s
Terminal=false
StartupNotify=false
Categories=Game;
Keywords=mc;minecraft;
""".formatted(getCurrentType().getDisplayName(), getLauncherPath(), getIconTargetPath());
}
}
@@ -0,0 +1,60 @@
/*
* 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.pack;
/// Debian packaging metadata for one HMCL release type.
///
/// The package name, installed command, desktop file, and alternatives
/// priority are intentionally centralized here so `CreateDeb` can stay focused
/// on archive layout instead of duplicating channel-specific branching.
public enum ReleaseType {
STABLE("stable", "hmcl", "HMCL", 100),
DEVELOPMENT("beta", "hmcl-beta", "HMCL (Beta)", 200),
NIGHTLY("nightly", "hmcl-nightly", "HMCL (Nightly)", 300);
private final String name;
private final String packageName;
private final String displayName;
private final int alternativesPriority;
ReleaseType(String name, String packageName, String displayName, int alternativesPriority) {
this.name = name;
this.packageName = packageName;
this.displayName = displayName;
this.alternativesPriority = alternativesPriority;
}
///
public String getName() {
return name;
}
/// Debian package name written into `control` and used in the output filename.
public String getPackageName() {
return packageName;
}
public String getDisplayName() {
return displayName;
}
/// Priority used when registering the generic `hmcl` alias.
public int getAlternativesPriority() {
return alternativesPriority;
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ pipeline {
stage('Build') {
steps {
sh 'bash ./config/jenkins/config-jenkins.sh'
sh './gradlew clean makeExecutables --stacktrace --no-daemon'
sh './gradlew clean makeExecutables makeDeb --stacktrace --no-daemon'
archiveArtifacts artifacts: 'HMCL/build/libs/*'
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ pipeline {
stage('Build') {
steps {
sh 'bash ./config/jenkins/config-jenkins.sh'
sh './gradlew clean makeExecutables --stacktrace --no-daemon'
sh './gradlew clean makeExecutables makeDeb --stacktrace --no-daemon'
archiveArtifacts artifacts: 'HMCL/build/libs/*'
}
}
+1
View File
@@ -36,6 +36,7 @@ hmclauncher = { module = "org.glavo.hmcl:HMCLauncher", version.ref = "hmclaunche
jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" }
kala-compress-zip = { module = "org.glavo.kala:kala-compress-archivers-zip", version.ref = "kala-compress" }
kala-compress-tar = { module = "org.glavo.kala:kala-compress-archivers-tar", version.ref = "kala-compress" }
kala-compress-ar = { module = "org.glavo.kala:kala-compress-archivers-ar", version.ref = "kala-compress" }
simple-png-javafx = { module = "org.glavo:simple-png-javafx", version.ref = "simple-png-javafx" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
tomlj = { module = "org.tomlj:tomlj", version.ref = "tomlj" }