Skip to content

Commit 6a91a64

Browse files
committed
Add RPM packaging task
1 parent 5c17783 commit 6a91a64

9 files changed

Lines changed: 542 additions & 50 deletions

File tree

.github/workflows/gradle.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,49 @@ jobs:
5454
name: HMCL-${{ env.SHORT_SHA }}-deb
5555
path: HMCL/build/libs/HMCL-*.deb
5656
archive: false
57+
58+
rpm:
59+
60+
runs-on: ubuntu-latest
61+
62+
steps:
63+
- uses: actions/checkout@v6
64+
- name: Build RPM in RHEL-compatible container
65+
run: |
66+
HOST_UID="$(id -u)"
67+
HOST_GID="$(id -g)"
68+
docker run --rm \
69+
-e GRADLE_USER_HOME=/tmp/hmcl-gradle \
70+
-e MICROSOFT_AUTH_ID="${MICROSOFT_AUTH_ID}" \
71+
-e CURSEFORGE_API_KEY="${CURSEFORGE_API_KEY}" \
72+
-e HOST_UID="$HOST_UID" \
73+
-e HOST_GID="$HOST_GID" \
74+
-v "$PWD":/workspace \
75+
-w /workspace \
76+
rockylinux:9 \
77+
bash -lc '
78+
dnf install -y java-17-openjdk-devel rpm-build &&
79+
./gradlew :HMCL:makeRpm --no-daemon --stacktrace
80+
status=$?
81+
chown -R "$HOST_UID:$HOST_GID" \
82+
.gradle \
83+
HMCL/build \
84+
HMCLCore/build \
85+
HMCLBoot/build \
86+
HMCLMultiMCBootstrap/build \
87+
HMCLTransformerDiscoveryService/build \
88+
buildSrc/build \
89+
2>/dev/null || true
90+
exit "$status"
91+
'
92+
env:
93+
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
94+
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
95+
- name: Get short SHA
96+
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
97+
- name: Upload RPM
98+
uses: actions/upload-artifact@v7
99+
with:
100+
name: HMCL-${{ env.SHORT_SHA }}-rpm
101+
path: HMCL/build/libs/HMCL-*.rpm
102+
archive: false

HMCL/build.gradle.kts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import org.jackhuang.hmcl.gradle.l10n.CreateLocaleNamesResourceBundle
77
import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
88
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
99
import org.jackhuang.hmcl.gradle.pack.CreateDeb
10+
import org.jackhuang.hmcl.gradle.pack.CreateRpm
1011
import org.jackhuang.hmcl.gradle.pack.ReleaseType
1112
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
1213
import java.net.URI
@@ -308,6 +309,28 @@ val makeDeb by tasks.registering(CreateDeb::class) {
308309
}
309310
}
310311

312+
val makeRpm by tasks.registering(CreateRpm::class) {
313+
dependsOn(makeExecutables)
314+
315+
val rpmFile = layout.file(provider { artifactFile("rpm") })
316+
317+
val rpmChannel = when (versionType) {
318+
"stable" -> ReleaseType.STABLE
319+
"dev" -> ReleaseType.DEVELOPMENT
320+
else -> ReleaseType.NIGHTLY
321+
}
322+
323+
version.set(project.version.toString())
324+
releaseType.set(rpmChannel)
325+
appShFile.set(layout.file(provider { artifactFile("sh") }))
326+
iconFile.set(layout.projectDirectory.file("image/hmcl.png"))
327+
outputFile.set(rpmFile)
328+
329+
doLast {
330+
createChecksum(rpmFile.get().asFile)
331+
}
332+
}
333+
311334
tasks.build {
312335
dependsOn(makeExecutables)
313336
dependsOn(makeDeb)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Hello Minecraft! Launcher
3+
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
package org.jackhuang.hmcl.upgrade;
19+
20+
import org.jetbrains.annotations.NotNullByDefault;
21+
import org.jetbrains.annotations.Nullable;
22+
23+
/// Detects launchers installed and updated by an external system package manager.
24+
///
25+
/// Package-managed installations should not overwrite their own executable
26+
/// files because that would make the system package database disagree with the
27+
/// files installed on disk. The Debian and RPM wrappers set this flag before
28+
/// launching HMCL.
29+
@NotNullByDefault
30+
public final class PackageManagerIntegration {
31+
/// System property used by package wrappers to mark a package-managed launch.
32+
public static final String PACKAGE_MANAGED_PROPERTY = "hmcl.package_managed";
33+
34+
/// Environment variable used by package wrappers to mark a package-managed launch.
35+
public static final String PACKAGE_MANAGED_ENV = "HMCL_PACKAGE_MANAGED";
36+
37+
/// Utility class constructor.
38+
private PackageManagerIntegration() {
39+
}
40+
41+
/// Returns whether the current process was launched from a package-managed installation.
42+
public static boolean isPackageManaged() {
43+
return isEnabled(System.getProperty(PACKAGE_MANAGED_PROPERTY))
44+
|| isEnabled(System.getenv(PACKAGE_MANAGED_ENV));
45+
}
46+
47+
/// Parses a package-managed marker value.
48+
private static boolean isEnabled(@Nullable String value) {
49+
return value != null
50+
&& !value.isBlank()
51+
&& !"0".equals(value)
52+
&& !"false".equalsIgnoreCase(value);
53+
}
54+
}

HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ private UpdateChecker() {
5656
private static final ReadOnlyBooleanWrapper checkingUpdate = new ReadOnlyBooleanWrapper(false);
5757

5858
public static void init() {
59+
if (PackageManagerIntegration.isPackageManaged())
60+
return;
61+
5962
requestCheckUpdate(UpdateChannel.getChannel(), settings().acceptPreviewUpdateProperty().get());
6063
}
6164

@@ -102,6 +105,9 @@ private static boolean isDevelopmentVersion(String version) {
102105
}
103106

104107
public static void requestCheckUpdate(UpdateChannel channel, boolean preview) {
108+
if (PackageManagerIntegration.isPackageManaged())
109+
return;
110+
105111
Platform.runLater(() -> {
106112
if (isCheckingUpdate())
107113
return;

HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ public static boolean processArguments(String[] args) {
100100
public static void updateFrom(RemoteVersion version) {
101101
checkFxUserThread();
102102

103+
if (PackageManagerIntegration.isPackageManaged()) {
104+
return;
105+
}
106+
103107
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && !OperatingSystem.isWindows7OrLater()) {
104108
Controllers.dialog(i18n("fatal.apply_update_need_win7", Metadata.PUBLISH_URL), i18n("message.error"), MessageType.ERROR);
105109
return;

buildSrc/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dependencies {
1212
implementation(libs.jna)
1313
implementation(libs.kala.compress.tar)
1414
implementation(libs.kala.compress.ar)
15+
compileOnly(libs.jetbrains.annotations)
1516
}
1617

1718
java {

buildSrc/src/main/java/org/jackhuang/hmcl/gradle/pack/CreateDeb.java

Lines changed: 13 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -92,24 +92,25 @@ private ReleaseType getCurrentType() {
9292
return getReleaseType().get();
9393
}
9494

95-
private String getCurrentTypeName() {
96-
return getCurrentType().getName();
97-
}
98-
9995
private String getLauncherPath() {
100-
return "/usr/bin/hmcl-" + getCurrentTypeName();
96+
return getPackageFiles().launcherPath();
10197
}
10298

10399
private String getTargetPath() {
104-
return "/usr/share/java/hmcl/" + getAppShFile().getAsFile().get().getName();
100+
return getPackageFiles().targetPath();
105101
}
106102

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

111107
private String getIconTargetPath() {
112-
return "/usr/share/icons/hicolor/256x256/apps/hmcl-%s.png".formatted(getCurrentTypeName());
108+
return getPackageFiles().iconTargetPath();
109+
}
110+
111+
/// Creates the shared Linux package path helper for this task.
112+
private LinuxPackageFiles getPackageFiles() {
113+
return new LinuxPackageFiles(getCurrentType(), getAppShFile().getAsFile().get().getName(), "deb");
113114
}
114115

115116
/// Ensures parent directories exist in the tar stream before child entries are written.
@@ -168,8 +169,9 @@ public void run() throws IOException {
168169
if (iconBytes.length == 0)
169170
throw new IOException("Empty icon file: " + iconFile);
170171

171-
byte[] launcherScriptBytes = getLauncherScript().getBytes(StandardCharsets.UTF_8);
172-
byte[] desktopInfoBytes = getDesktopInfo().getBytes(StandardCharsets.UTF_8);
172+
LinuxPackageFiles files = getPackageFiles();
173+
byte[] launcherScriptBytes = files.launcherScript().getBytes(StandardCharsets.UTF_8);
174+
byte[] desktopInfoBytes = files.desktopInfo().getBytes(StandardCharsets.UTF_8);
173175

174176
LOGGER.lifecycle("Creating control.tar.gz");
175177
var controlData = new ByteArrayOutputStream();
@@ -234,8 +236,6 @@ private String getControl(long appSize, long launcherScriptSize, long desktopInf
234236
""".formatted(getCurrentType().getPackageName(), getVersion().get(), Math.max(installedSize, 1)) + "\n";
235237
}
236238

237-
private static final String COMMON_LAUNCHER_PATH = "/usr/bin/hmcl";
238-
239239
/// Registers the channel command into the shared `hmcl` alternatives group.
240240
private String getPostinst() {
241241
return """
@@ -245,7 +245,7 @@ private String getPostinst() {
245245
if [ "$1" = configure ]; then
246246
update-alternatives --install %s hmcl %s %d
247247
fi
248-
""".formatted(COMMON_LAUNCHER_PATH, getLauncherPath(), getCurrentType().getAlternativesPriority());
248+
""".formatted(LinuxPackageFiles.COMMON_LAUNCHER_PATH, getLauncherPath(), getCurrentType().getAlternativesPriority());
249249
}
250250

251251
/// Removes the channel command from the shared `hmcl` alternatives group.
@@ -260,41 +260,4 @@ private String getPrerm() {
260260
""".formatted(getLauncherPath());
261261
}
262262

263-
/// Creates a tiny wrapper that launches the bundled shell script from the user's home directory.
264-
private String getLauncherScript() {
265-
return """
266-
#!/usr/bin/env bash
267-
cd "$HOME"
268-
if [ -z "${HMCL_USER_HOME:-}" ]; then
269-
if [ -z "${XDG_DATA_HOME:-}" ]; then
270-
export HMCL_USER_HOME="$HOME/.local/share/hmcl"
271-
else
272-
export HMCL_USER_HOME="$XDG_DATA_HOME/hmcl"
273-
fi
274-
fi
275-
if [ -z "${HMCL_LOCAL_HOME:-}" ]; then
276-
export HMCL_LOCAL_HOME="$HMCL_USER_HOME/local-%s"
277-
fi
278-
if [ -z "${HMCL_DEPENDENCIES_DIR:-}" ]; then
279-
export HMCL_DEPENDENCIES_DIR="$HMCL_USER_HOME/dependencies"
280-
fi
281-
exec %s "$@"
282-
""".formatted(getCurrentTypeName(), getTargetPath());
283-
}
284-
285-
/// Generates the desktop entry that points to the channel-specific launcher command.
286-
private String getDesktopInfo() {
287-
return """
288-
[Desktop Entry]
289-
Type=Application
290-
Name=%s
291-
Comment=Hello Minecraft! Launcher
292-
Exec=%s
293-
Icon=%s
294-
Terminal=false
295-
StartupNotify=false
296-
Categories=Game;
297-
Keywords=mc;minecraft;
298-
""".formatted(getCurrentType().getDisplayName(), getLauncherPath(), getIconTargetPath());
299-
}
300263
}

0 commit comments

Comments
 (0)