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
46 changes: 46 additions & 0 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,49 @@ jobs:
name: HMCL-${{ env.SHORT_SHA }}-deb
path: HMCL/build/libs/HMCL-*.deb
archive: false

rpm:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6
- name: Build RPM in RHEL-compatible container
run: |
HOST_UID="$(id -u)"
HOST_GID="$(id -g)"
docker run --rm \
-e GRADLE_USER_HOME=/tmp/hmcl-gradle \
-e MICROSOFT_AUTH_ID="${MICROSOFT_AUTH_ID}" \
-e CURSEFORGE_API_KEY="${CURSEFORGE_API_KEY}" \
-e HOST_UID="$HOST_UID" \
-e HOST_GID="$HOST_GID" \
-v "$PWD":/workspace \
-w /workspace \
rockylinux:9 \
bash -lc '
dnf install -y java-17-openjdk-devel rpm-build &&
./gradlew :HMCL:makeRpm --no-daemon --stacktrace
status=$?
chown -R "$HOST_UID:$HOST_GID" \
.gradle \
HMCL/build \
HMCLCore/build \
HMCLBoot/build \
HMCLMultiMCBootstrap/build \
HMCLTransformerDiscoveryService/build \
buildSrc/build \
2>/dev/null || true
exit "$status"
'
env:
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
- name: Get short SHA
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
- name: Upload RPM
uses: actions/upload-artifact@v7
with:
name: HMCL-${{ env.SHORT_SHA }}-rpm
path: HMCL/build/libs/HMCL-*.rpm
archive: false
23 changes: 23 additions & 0 deletions HMCL/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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.CreateRpm
import org.jackhuang.hmcl.gradle.pack.ReleaseType
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
import java.net.URI
Expand Down Expand Up @@ -308,6 +309,28 @@ val makeDeb by tasks.registering(CreateDeb::class) {
}
}

val makeRpm by tasks.registering(CreateRpm::class) {
dependsOn(makeExecutables)

val rpmFile = layout.file(provider { artifactFile("rpm") })

val rpmChannel = when (versionType) {
"stable" -> ReleaseType.STABLE
"dev" -> ReleaseType.DEVELOPMENT
else -> ReleaseType.NIGHTLY
}

version.set(project.version.toString())
releaseType.set(rpmChannel)
appShFile.set(layout.file(provider { artifactFile("sh") }))
iconFile.set(layout.projectDirectory.file("image/hmcl.png"))
outputFile.set(rpmFile)

doLast {
createChecksum(rpmFile.get().asFile)
}
}

tasks.build {
dependsOn(makeExecutables)
dependsOn(makeDeb)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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.upgrade;

import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;

/// Detects launchers installed and updated by an external system package manager.
///
/// Package-managed installations should not overwrite their own executable
/// files because that would make the system package database disagree with the
/// files installed on disk. The Debian and RPM wrappers set this flag before
/// launching HMCL.
@NotNullByDefault
public final class PackageManagerIntegration {
/// System property used by package wrappers to mark a package-managed launch.
public static final String PACKAGE_MANAGED_PROPERTY = "hmcl.package_managed";

/// Environment variable used by package wrappers to mark a package-managed launch.
public static final String PACKAGE_MANAGED_ENV = "HMCL_PACKAGE_MANAGED";

/// Utility class constructor.
private PackageManagerIntegration() {
}

/// Returns whether the current process was launched from a package-managed installation.
public static boolean isPackageManaged() {
return isEnabled(System.getProperty(PACKAGE_MANAGED_PROPERTY))
|| isEnabled(System.getenv(PACKAGE_MANAGED_ENV));
}

/// Parses a package-managed marker value.
private static boolean isEnabled(@Nullable String value) {
return value != null
&& !value.isBlank()
&& !"0".equals(value)
&& !"false".equalsIgnoreCase(value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ private UpdateChecker() {
private static final ReadOnlyBooleanWrapper checkingUpdate = new ReadOnlyBooleanWrapper(false);

public static void init() {
if (PackageManagerIntegration.isPackageManaged())
return;

requestCheckUpdate(UpdateChannel.getChannel(), settings().acceptPreviewUpdateProperty().get());
}

Expand Down Expand Up @@ -102,6 +105,9 @@ private static boolean isDevelopmentVersion(String version) {
}

public static void requestCheckUpdate(UpdateChannel channel, boolean preview) {
if (PackageManagerIntegration.isPackageManaged())
return;

Platform.runLater(() -> {
if (isCheckingUpdate())
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ public static boolean processArguments(String[] args) {
public static void updateFrom(RemoteVersion version) {
checkFxUserThread();

if (PackageManagerIntegration.isPackageManaged()) {
return;
}

if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && !OperatingSystem.isWindows7OrLater()) {
Controllers.dialog(i18n("fatal.apply_update_need_win7", Metadata.PUBLISH_URL), i18n("message.error"), MessageType.ERROR);
return;
Expand Down
1 change: 1 addition & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies {
implementation(libs.jna)
implementation(libs.kala.compress.tar)
implementation(libs.kala.compress.ar)
compileOnly(libs.jetbrains.annotations)
}

java {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,24 +92,25 @@ private ReleaseType getCurrentType() {
return getReleaseType().get();
}

private String getCurrentTypeName() {
return getCurrentType().getName();
}

private String getLauncherPath() {
return "/usr/bin/hmcl-" + getCurrentTypeName();
return getPackageFiles().launcherPath();
}

private String getTargetPath() {
return "/usr/share/java/hmcl/" + getAppShFile().getAsFile().get().getName();
return getPackageFiles().targetPath();
}

private String getDesktopFilePath() {
return "/usr/share/applications/hmcl-%s.desktop".formatted(getCurrentTypeName());
return getPackageFiles().desktopFilePath();
}

private String getIconTargetPath() {
return "/usr/share/icons/hicolor/256x256/apps/hmcl-%s.png".formatted(getCurrentTypeName());
return getPackageFiles().iconTargetPath();
}

/// Creates the shared Linux package path helper for this task.
private LinuxPackageFiles getPackageFiles() {
return new LinuxPackageFiles(getCurrentType(), getAppShFile().getAsFile().get().getName(), "deb");
}

/// Ensures parent directories exist in the tar stream before child entries are written.
Expand Down Expand Up @@ -168,8 +169,9 @@ public void run() throws IOException {
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);
LinuxPackageFiles files = getPackageFiles();
byte[] launcherScriptBytes = files.launcherScript().getBytes(StandardCharsets.UTF_8);
byte[] desktopInfoBytes = files.desktopInfo().getBytes(StandardCharsets.UTF_8);

LOGGER.lifecycle("Creating control.tar.gz");
var controlData = new ByteArrayOutputStream();
Expand Down Expand Up @@ -234,8 +236,6 @@ private String getControl(long appSize, long launcherScriptSize, long desktopInf
""".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 """
Expand All @@ -245,7 +245,7 @@ private String getPostinst() {
if [ "$1" = configure ]; then
update-alternatives --install %s hmcl %s %d
fi
""".formatted(COMMON_LAUNCHER_PATH, getLauncherPath(), getCurrentType().getAlternativesPriority());
""".formatted(LinuxPackageFiles.COMMON_LAUNCHER_PATH, getLauncherPath(), getCurrentType().getAlternativesPriority());
}

/// Removes the channel command from the shared `hmcl` alternatives group.
Expand All @@ -260,41 +260,4 @@ private String getPrerm() {
""".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());
}
}
Loading