Compare commits

..
35 changed files with 876 additions and 256 deletions

No files matched your search

+27 -18
View File
@@ -1,8 +1,11 @@
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
@@ -16,30 +19,36 @@ plugins {
alias(libs.plugins.shadow)
}
val isOfficial = System.getenv("HMCL_SIGNATURE_KEY") != null
|| (System.getenv("GITHUB_REPOSITORY_OWNER") == "HMCL-dev" && System.getenv("GITHUB_BASE_REF")
.isNullOrEmpty())
val projectConfig = PropertiesUtils.load(rootProject.file("config/project.properties").toPath())
val isOfficial = JenkinsUtils.IS_ON_CI || GitHubActionUtils.IS_ON_OFFICIAL_REPO
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
val offset = System.getenv("BUILD_NUMBER_OFFSET")?.toInt() ?: 0
if (number != null) {
(number - offset).toString()
} else {
val shortCommit = System.getenv("GITHUB_SHA")?.lowercase()?.substring(0, 7)
val prefix = if (isOfficial) "dev" else "unofficial"
if (!shortCommit.isNullOrEmpty()) "$prefix-$shortCommit" else "SNAPSHOT"
}
}
val versionRoot = System.getenv("VERSION_ROOT") ?: "3.6"
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 launcherExe = System.getenv("HMCL_LAUNCHER_EXE") ?: ""
version = "$versionRoot.$buildNumber"
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
@@ -50,7 +59,7 @@ dependencies {
implementation(libs.twelvemonkeys.imageio.webp)
implementation(libs.java.info)
if (launcherExe == null) {
if (launcherExe.isBlank()) {
implementation(libs.hmclauncher)
}
@@ -195,7 +204,7 @@ tasks.shadowJar {
"Enable-Native-Access" to "ALL-UNNAMED"
)
if (launcherExe != null) {
if (launcherExe.isNotBlank()) {
into("assets") {
from(file(launcherExe))
}
@@ -46,21 +46,7 @@ public final class EntryPoint {
createHMCLDirectories();
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU")))
System.getProperties().putIfAbsent("prism.forceGPU", "true");
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
if (animationFrameRate != null) {
try {
if (Integer.parseInt(animationFrameRate) <= 0)
throw new NumberFormatException(animationFrameRate);
System.getProperties().putIfAbsent("javafx.animation.pulse", animationFrameRate);
} catch (NumberFormatException e) {
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
}
}
setupJavaFXVMOptions();
checkDirectoryPath();
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
@@ -80,6 +66,71 @@ public final class EntryPoint {
System.exit(exitCode);
}
private static void setupJavaFXVMOptions() {
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU"))) {
LOG.info("HMCL_FORCE_GPU: true");
System.getProperties().putIfAbsent("prism.forceGPU", "true");
}
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
if (animationFrameRate != null) {
LOG.info("HMCL_ANIMATION_FRAME_RATE: " + animationFrameRate);
try {
if (Integer.parseInt(animationFrameRate) <= 0)
throw new NumberFormatException(animationFrameRate);
System.getProperties().putIfAbsent("javafx.animation.pulse", animationFrameRate);
} catch (NumberFormatException e) {
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
}
}
String uiScale = System.getProperty("hmcl.uiScale", System.getenv("HMCL_UI_SCALE"));
if (uiScale != null) {
uiScale = uiScale.trim();
LOG.info("HMCL_UI_SCALE: " + uiScale);
try {
float scaleValue;
if (uiScale.endsWith("%")) {
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 1)) / 100.0f;
} else if (uiScale.endsWith("dpi") || uiScale.endsWith("DPI")) {
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 3)) / 96.0f;
} else {
scaleValue = Float.parseFloat(uiScale);
}
float lowerBound;
float upperBound;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
// JavaFX behavior may be abnormal when the DPI scaling factor is too high
lowerBound = 0.25f;
upperBound = 4f;
} else {
lowerBound = 0.01f;
upperBound = 10f;
}
if (scaleValue >= lowerBound && scaleValue <= upperBound) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
System.getProperties().putIfAbsent("glass.win.uiScale", uiScale);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
LOG.warning("macOS does not support setting UI scale, so it will be ignored");
} else {
System.getProperties().putIfAbsent("glass.gtk.uiScale", uiScale);
}
} else {
LOG.warning("UI scale out of range: " + uiScale);
}
} catch (Throwable e) {
LOG.warning("Invalid UI scale: " + uiScale);
}
}
}
private static void createHMCLDirectories() {
if (!Files.isDirectory(Metadata.HMCL_CURRENT_DIRECTORY)) {
try {
@@ -58,6 +58,7 @@ import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import java.lang.ref.WeakReference;
@@ -706,6 +707,7 @@ public final class LauncherHelper {
*/
private final class HMCLProcessListener implements ProcessListener {
private final ReentrantLock lock = new ReentrantLock();
private final HMCLGameRepository repository;
private final Version version;
private final LaunchOptions launchOptions;
@@ -849,21 +851,27 @@ public final class LauncherHelper {
level = Lang.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO);
logBuffer.add(new Log(log, level));
} else {
synchronized (this) {
lock.lock();
try {
logs.addLast(new Log(log, level));
if (logs.size() > Log.getLogLines())
logs.removeFirst();
} finally {
lock.unlock();
}
}
if (!lwjgl) {
String lowerCaseLog = log.toLowerCase(Locale.ROOT);
if (!detectWindow || lowerCaseLog.contains("lwjgl version") || lowerCaseLog.contains("lwjgl openal")) {
synchronized (this) {
lock.lock();
try {
if (!lwjgl) {
lwjgl = true;
finishLaunch();
}
} finally {
lock.unlock();
}
}
}
@@ -888,9 +896,12 @@ public final class LauncherHelper {
// Game crashed before opening the game window.
if (!lwjgl) {
synchronized (this) {
lock.lock();
try {
if (!lwjgl)
finishLaunch();
} finally {
lock.unlock();
}
}
@@ -771,6 +771,7 @@ public final class VersionSetting implements Cloneable, Observable {
obj.addProperty("wrapper", src.getWrapper());
obj.addProperty("fullscreen", src.isFullscreen());
obj.addProperty("noJVMArgs", src.isNoJVMArgs());
obj.addProperty("noOptimizingJVMArgs", src.isNoOptimizingJVMArgs());
obj.addProperty("notCheckGame", src.isNotCheckGame());
obj.addProperty("notCheckJVM", src.isNotCheckJVM());
obj.addProperty("notPatchNatives", src.isNotPatchNatives());
@@ -841,6 +842,7 @@ public final class VersionSetting implements Cloneable, Observable {
vs.setNativesDir(Optional.ofNullable(obj.get("nativesDir")).map(JsonElement::getAsString).orElse(""));
vs.setFullscreen(Optional.ofNullable(obj.get("fullscreen")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNoJVMArgs(Optional.ofNullable(obj.get("noJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNoOptimizingJVMArgs(Optional.ofNullable(obj.get("noOptimizingJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckJVM(Optional.ofNullable(obj.get("notCheckJVM")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotPatchNatives(Optional.ofNullable(obj.get("notPatchNatives")).map(JsonElement::getAsBoolean).orElse(false));
@@ -25,6 +25,8 @@ import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
@@ -61,7 +63,12 @@ public class OptionToggleButton extends StackPane {
toggleButton.setSize(8);
FXUtils.setLimitHeight(toggleButton, 30);
FXUtils.onClicked(container, () -> toggleButton.setSelected(!toggleButton.isSelected()));
container.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
if (e.getButton() == MouseButton.PRIMARY) {
toggleButton.setSelected(!toggleButton.isSelected());
e.consume();
}
});
FXUtils.onChangeAndOperate(subtitleProperty(), subtitle -> {
if (StringUtils.isNotBlank(subtitle)) {
@@ -40,6 +40,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class TaskExecutorDialogPane extends BorderPane {
private TaskExecutor executor;
private TaskCancellationAction onCancel;
@SuppressWarnings({"unused", "FieldCanBeLocal"})
private final Consumer<FetchTask.SpeedEvent> speedEventHandler;
private final Label lblTitle;
@@ -84,7 +85,7 @@ public class TaskExecutorDialogPane extends BorderPane {
}
});
speedEventHandler = speedEvent -> {
speedEventHandler = FetchTask.SPEED_EVENT.registerWeak(speedEvent -> {
String unit = "B/s";
double speed = speedEvent.getSpeed();
if (speed > 1024) {
@@ -95,11 +96,10 @@ public class TaskExecutorDialogPane extends BorderPane {
speed /= 1024;
unit = "MiB/s";
}
double finalSpeed = speed;
String finalUnit = unit;
Platform.runLater(() -> lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit)));
};
FileDownloadTask.speedEvent.channel(FetchTask.SpeedEvent.class).registerWeak(speedEventHandler);
String message = String.format("%.1f %s", speed, unit);
Platform.runLater(() -> lblProgress.setText(message));
});
onEscPressed(this, btnCancel::fire);
}
@@ -287,7 +287,7 @@ public class PartialInflaterInputStream extends FilterInputStream {
* the mark position becomes invalid.
* @see InputStream#reset()
*/
public synchronized void mark(int readlimit) {
public void mark(int readlimit) {
}
/**
@@ -302,7 +302,7 @@ public class PartialInflaterInputStream extends FilterInputStream {
* @see InputStream#mark(int)
* @see IOException
*/
public synchronized void reset() throws IOException {
public void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
}
@@ -72,6 +72,7 @@ import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
@@ -115,6 +116,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
ImageView titleIcon = new ImageView(FXUtils.newBuiltinImage("/assets/img/icon-title.png"));
Label titleLabel = new Label(Metadata.FULL_TITLE);
if (I18n.isUpsideDown()) {
titleIcon.setRotate(180);
titleLabel.setRotate(180);
}
titleLabel.getStyleClass().add("jfx-decorator-title");
titleNode.getChildren().setAll(titleIcon, titleLabel);
@@ -174,6 +174,7 @@ public class PersonalizationPage extends StackPane {
}
Label textOpacity = new Label();
FXUtils.setLimitWidth(textOpacity, 35);
StringBinding valueBinding = Bindings.createStringBinding(() -> ((int) slider.getValue()) + "%", slider.valueProperty());
textOpacity.textProperty().bind(valueBinding);
@@ -43,6 +43,10 @@ public final class I18n {
return locale;
}
public static boolean isUpsideDown() {
return LocaleUtils.getScript(locale.getLocale()).equals("Qabs");
}
public static boolean isUseChinese() {
return LocaleUtils.isChinese(locale.getLocale());
}
@@ -17,37 +17,46 @@
*/
package org.jackhuang.hmcl.util.i18n;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// @author Glavo
public final class UpsideDownUtils {
private static final Map<Integer, Integer> MAPPER = new LinkedHashMap<>();
private static final Map<Integer, Integer> MAPPER = loadMap();
private static void putChars(char baseChar, String upsideDownChars) {
for (int i = 0; i < upsideDownChars.length(); i++) {
MAPPER.put(baseChar + i, (int) upsideDownChars.charAt(i));
private static Map<Integer, Integer> loadMap() {
var map = new LinkedHashMap<Integer, Integer>();
InputStream inputStream = UpsideDownUtils.class.getResourceAsStream("/assets/lang/upside_down.txt");
if (inputStream != null) {
try (inputStream) {
new String(inputStream.readAllBytes(), StandardCharsets.UTF_8).lines().forEach(line -> {
if (line.isBlank() || line.startsWith("#"))
return;
if (line.length() != 2) {
LOG.warning("Invalid line: " + line);
return;
}
map.put((int) line.charAt(0), (int) line.charAt(1));
});
} catch (IOException e) {
LOG.warning("Failed to load upside_down.txt", e);
}
} else {
LOG.warning("upside_down.txt not found");
}
}
private static void putChars(String baseChars, String upsideDownChars) {
if (baseChars.length() != upsideDownChars.length()) {
throw new IllegalArgumentException("baseChars and upsideDownChars must have same length");
}
for (int i = 0; i < baseChars.length(); i++) {
MAPPER.put((int) baseChars.charAt(i), (int) upsideDownChars.charAt(i));
}
}
static {
putChars('a', "ɐqɔpǝɟbɥıظʞןɯuodbɹsʇnʌʍxʎz");
putChars('A', "ⱯᗺƆᗡƎℲ⅁HIſʞꞀWNOԀὉᴚS⟘∩ΛMXʎZ");
putChars('0', "0ƖᘔƐㄣϛ9ㄥ86");
putChars("_,;.?!/\\'", "‾'⸵˙¿¡/\\,");
return Collections.unmodifiableMap(map);
}
public static String translate(String str) {
@@ -56,7 +65,7 @@ public final class UpsideDownUtils {
return builder.reverse().toString();
}
private static DateTimeFormatter BASE_FORMATTER = DateTimeFormatter.ofPattern("MMM d, yyyy, h:mm:ss a")
private static final DateTimeFormatter BASE_FORMATTER = DateTimeFormatter.ofPattern("MMM d, yyyy, h:mm:ss a")
.withZone(ZoneId.systemDefault());
public static String formatDateTime(TemporalAccessor time) {
@@ -17,29 +17,32 @@
*/
package org.jackhuang.hmcl.event;
import java.util.concurrent.ConcurrentHashMap;
import org.jetbrains.annotations.NotNull;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
*
* @author huangyuhui
*/
public final class EventBus {
/// @author huangyuhui
public final class EventBus extends ClassValue<EventManager<?>> {
private final ConcurrentHashMap<Class<?>, EventManager<?>> events = new ConcurrentHashMap<>();
public static final EventBus EVENT_BUS = new EventBus();
private EventBus() {
}
@Override
protected EventManager<?> computeValue(@NotNull Class<?> type) {
return new EventManager<>();
}
@SuppressWarnings("unchecked")
public <T extends Event> EventManager<T> channel(Class<T> clazz) {
return (EventManager<T>) events.computeIfAbsent(clazz, ignored -> new EventManager<>());
return (EventManager<T>) get(clazz);
}
@SuppressWarnings("unchecked")
public Event.Result fireEvent(Event obj) {
LOG.info(obj + " gets fired");
return channel((Class<Event>) obj.getClass()).fireEvent(obj);
return ((EventManager<Event>) get(obj.getClass())).fireEvent(obj);
}
public static final EventBus EVENT_BUS = new EventBus();
}
@@ -18,26 +18,28 @@
package org.jackhuang.hmcl.event;
import org.jackhuang.hmcl.util.SimpleMultimap;
import org.jetbrains.annotations.Contract;
import java.lang.ref.WeakReference;
import java.util.EnumMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
/**
*
* @author huangyuhui
*/
/// @author huangyuhui
public final class EventManager<T extends Event> {
private final ReentrantLock lock = new ReentrantLock();
private final SimpleMultimap<EventPriority, Consumer<T>, CopyOnWriteArraySet<Consumer<T>>> handlers
= new SimpleMultimap<>(() -> new EnumMap<>(EventPriority.class), CopyOnWriteArraySet::new);
@Contract("_ -> param1")
public Consumer<T> registerWeak(Consumer<T> consumer) {
register(new WeakListener(consumer));
return consumer;
}
@Contract("_, _ -> param1")
public Consumer<T> registerWeak(Consumer<T> consumer, EventPriority priority) {
register(new WeakListener(consumer), priority);
return consumer;
@@ -47,9 +49,13 @@ public final class EventManager<T extends Event> {
register(consumer, EventPriority.NORMAL);
}
public synchronized void register(Consumer<T> consumer, EventPriority priority) {
if (!handlers.get(priority).contains(consumer))
handlers.put(priority, consumer);
public void register(Consumer<T> consumer, EventPriority priority) {
lock.lock();
try {
handlers.get(priority).add(consumer);
} finally {
lock.unlock();
}
}
public void register(Runnable runnable) {
@@ -60,23 +66,30 @@ public final class EventManager<T extends Event> {
register(t -> runnable.run(), priority);
}
public synchronized Event.Result fireEvent(T event) {
for (EventPriority priority : EventPriority.values()) {
for (Consumer<T> handler : handlers.get(priority))
handler.accept(event);
public Event.Result fireEvent(T event) {
lock.lock();
try {
for (EventPriority priority : EventPriority.values()) {
for (Consumer<T> handler : handlers.get(priority))
handler.accept(event);
}
} finally {
lock.unlock();
}
if (event.hasResult())
return event.getResult();
else
return Event.Result.DEFAULT;
return event.hasResult() ? event.getResult() : Event.Result.DEFAULT;
}
public synchronized void unregister(Consumer<T> consumer) {
handlers.removeValue(consumer);
public void unregister(Consumer<T> consumer) {
lock.lock();
try {
handlers.removeValue(consumer);
} finally {
lock.unlock();
}
}
private class WeakListener implements Consumer<T> {
private final class WeakListener implements Consumer<T> {
private final WeakReference<Consumer<T>> ref;
public WeakListener(Consumer<T> listener) {
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.task;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.io.ContentEncoding;
import org.jackhuang.hmcl.util.io.IOUtils;
@@ -355,13 +356,13 @@ public abstract class FetchTask<T> extends Task<T> {
private static final Timer timer = new Timer("DownloadSpeedRecorder", true);
private static final AtomicLong downloadSpeed = new AtomicLong(0L);
public static final EventBus speedEvent = new EventBus();
public static final EventManager<SpeedEvent> SPEED_EVENT = EventBus.EVENT_BUS.channel(SpeedEvent.class);
static {
timer.schedule(new TimerTask() {
@Override
public void run() {
speedEvent.channel(SpeedEvent.class).fireEvent(new SpeedEvent(speedEvent, downloadSpeed.getAndSet(0)));
SPEED_EVENT.fireEvent(new SpeedEvent(SPEED_EVENT, downloadSpeed.getAndSet(0)));
}
}, 0, 1000);
}
@@ -97,7 +97,7 @@ public final class Schedulers {
return ForkJoinPool.commonPool();
}
public static synchronized void shutdown() {
public static void shutdown() {
LOG.info("Shutting down executor services.");
// shutdownNow will interrupt all threads.
@@ -25,6 +25,7 @@ import javafx.beans.value.ObservableValue;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -144,6 +145,7 @@ public abstract class BindingMapping<T, U> extends ObjectBinding<U> {
private static final class AsyncMappedBinding<T, U> extends BindingMapping<T, U> {
private final ReentrantLock lock = new ReentrantLock();
private boolean initialized = false;
private T prev;
private U value;
@@ -159,13 +161,16 @@ public abstract class BindingMapping<T, U> extends ObjectBinding<U> {
}
private void tryUpdateValue(T currentPrev) {
synchronized (this) {
lock.lock();
try {
if ((initialized && Objects.equals(prev, currentPrev))
|| isComputing(currentPrev)) {
return;
}
computing = true;
computingPrev = currentPrev;
} finally {
lock.unlock();
}
CompletableFuture<? extends U> task;
@@ -189,7 +194,8 @@ public abstract class BindingMapping<T, U> extends ObjectBinding<U> {
}
private void valueUpdate(T currentPrev, U computed) {
synchronized (this) {
lock.lock();
try {
if (isComputing(currentPrev)) {
computing = false;
computingPrev = null;
@@ -197,15 +203,20 @@ public abstract class BindingMapping<T, U> extends ObjectBinding<U> {
value = computed;
initialized = true;
}
} finally {
lock.unlock();
}
}
private void valueUpdateFailed(T currentPrev) {
synchronized (this) {
lock.lock();
try {
if (isComputing(currentPrev)) {
computing = false;
computingPrev = null;
}
} finally {
lock.unlock();
}
}
@@ -24,6 +24,7 @@ import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
@@ -37,6 +38,7 @@ import javafx.beans.binding.ObjectBinding;
*/
public final class ObservableCache<K, V, E extends Exception> {
private final ReentrantLock lock = new ReentrantLock();
private final ExceptionalFunction<K, V, E> source;
private final BiConsumer<K, Throwable> exceptionHandler;
private final V fallbackValue;
@@ -54,22 +56,29 @@ public final class ObservableCache<K, V, E extends Exception> {
}
public Optional<V> getImmediately(K key) {
synchronized (this) {
lock.lock();
try {
return Optional.ofNullable(cache.get(key));
} finally {
lock.unlock();
}
}
public void put(K key, V value) {
synchronized (this) {
lock.lock();
try {
cache.put(key, value);
invalidated.remove(key);
} finally {
lock.unlock();
}
Platform.runLater(observable::invalidate);
}
private CompletableFuture<V> query(K key, Executor executor) {
CompletableFuture<V> future;
synchronized (this) {
lock.lock();
try {
CompletableFuture<V> prev = pendings.get(key);
if (prev != null) {
return prev;
@@ -77,6 +86,8 @@ public final class ObservableCache<K, V, E extends Exception> {
future = new CompletableFuture<>();
pendings.put(key, future);
}
} finally {
lock.unlock();
}
executor.execute(() -> {
@@ -84,18 +95,24 @@ public final class ObservableCache<K, V, E extends Exception> {
try {
result = source.apply(key);
} catch (Throwable ex) {
synchronized (this) {
lock.lock();
try {
pendings.remove(key);
} finally {
lock.unlock();
}
exceptionHandler.accept(key, ex);
future.completeExceptionally(ex);
return;
}
synchronized (this) {
lock.lock();
try {
cache.put(key, result);
invalidated.remove(key);
pendings.remove(key, future);
} finally {
lock.unlock();
}
future.complete(result);
Platform.runLater(observable::invalidate);
@@ -106,11 +123,14 @@ public final class ObservableCache<K, V, E extends Exception> {
public V get(K key) {
V cached;
synchronized (this) {
lock.lock();
try {
cached = cache.get(key);
if (cached != null && !invalidated.containsKey(key)) {
return cached;
}
} finally {
lock.unlock();
}
try {
@@ -143,7 +163,9 @@ public final class ObservableCache<K, V, E extends Exception> {
return Bindings.createObjectBinding(() -> {
V result;
boolean refresh;
synchronized (this) {
lock.lock();
try {
result = cache.get(key);
if (result == null) {
result = fallbackValue;
@@ -151,6 +173,8 @@ public final class ObservableCache<K, V, E extends Exception> {
} else {
refresh = invalidated.containsKey(key);
}
} finally {
lock.unlock();
}
if (!quiet && refresh) {
query(key, executor);
@@ -160,10 +184,13 @@ public final class ObservableCache<K, V, E extends Exception> {
}
public void invalidate(K key) {
synchronized (this) {
lock.lock();
try {
if (cache.containsKey(key)) {
invalidated.put(key, Boolean.TRUE);
}
} finally {
lock.unlock();
}
Platform.runLater(observable::invalidate);
}
@@ -22,17 +22,17 @@ import org.jackhuang.hmcl.util.Lang;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* The managed process.
*
* @author huangyuhui
* @see org.jackhuang.hmcl.launch.ExitWaiter
* @see org.jackhuang.hmcl.launch.StreamPump
*/
/// The managed process.
///
/// @author huangyuhui
/// <!-- @see org.jackhuang.hmcl.launch.ExitWaiter -->
/// @see org.jackhuang.hmcl.launch.StreamPump
public final class ManagedProcess {
private final ReentrantLock lock = new ReentrantLock();
private final Process process;
private final List<String> commands;
private final String classpath;
@@ -54,7 +54,7 @@ public final class ManagedProcess {
*/
public ManagedProcess(Process process, List<String> commands) {
this.process = process;
this.commands = Collections.unmodifiableList(new ArrayList<>(commands));
this.commands = List.copyOf(commands);
this.classpath = null;
}
@@ -67,7 +67,7 @@ public final class ManagedProcess {
*/
public ManagedProcess(Process process, List<String> commands, String classpath) {
this.process = process;
this.commands = Collections.unmodifiableList(new ArrayList<>(commands));
this.commands = List.copyOf(commands);
this.classpath = classpath;
}
@@ -111,9 +111,11 @@ public final class ManagedProcess {
*
* @see #addLine
*/
public synchronized List<String> getLines(Predicate<String> lineFilter) {
public List<String> getLines(Predicate<String> lineFilter) {
lock.lock();
if (lineFilter == null)
return Collections.unmodifiableList(Arrays.asList(lines.toArray(new String[0])));
return List.copyOf(lines);
ArrayList<String> res = new ArrayList<>();
for (String line : this.lines) {
@@ -123,8 +125,13 @@ public final class ManagedProcess {
return Collections.unmodifiableList(res);
}
public synchronized void addLine(String line) {
lines.add(line);
public void addLine(String line) {
lock.lock();
try {
lines.add(line);
} finally {
lock.unlock();
}
}
/**
@@ -133,15 +140,20 @@ public final class ManagedProcess {
* If a thread is monitoring this raw process,
* you are required to add the instance by this method.
*/
public synchronized void addRelatedThread(Thread thread) {
relatedThreads.add(thread);
public void addRelatedThread(Thread thread) {
lock.lock();
try {
relatedThreads.add(thread);
} finally {
lock.unlock();
}
}
public synchronized void pumpInputStream(Consumer<String> onLogLine) {
public void pumpInputStream(Consumer<String> onLogLine) {
addRelatedThread(Lang.thread(new StreamPump(process.getInputStream(), onLogLine, OperatingSystem.NATIVE_CHARSET), "ProcessInputStreamPump", true));
}
public synchronized void pumpErrorStream(Consumer<String> onLogLine) {
public void pumpErrorStream(Consumer<String> onLogLine) {
addRelatedThread(Lang.thread(new StreamPump(process.getErrorStream(), onLogLine, OperatingSystem.NATIVE_CHARSET), "ProcessErrorStreamPump", true));
}
@@ -172,8 +184,13 @@ public final class ManagedProcess {
destroyRelatedThreads();
}
public synchronized void destroyRelatedThreads() {
relatedThreads.forEach(Thread::interrupt);
public void destroyRelatedThreads() {
lock.lock();
try {
relatedThreads.forEach(Thread::interrupt);
} finally {
lock.unlock();
}
}
@Override
@@ -24,15 +24,20 @@ import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnmodifiableView;
import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.FileTime;
import java.util.*;
/**
* @author Glavo
*/
public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Closeable {
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// @author Glavo
public abstract class ArchiveFileTree<R, E extends ArchiveEntry> implements Closeable {
public static ArchiveFileTree<?, ?> open(Path file) throws IOException {
Path namePath = file.getFileName();
@@ -50,23 +55,55 @@ public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Clos
}
}
protected final F file;
protected final Dir<E> root = new Dir<>("");
protected final R reader;
protected final Dir<E> root = new Dir<>("", "");
public ArchiveFileTree(F file) {
this.file = file;
public ArchiveFileTree(R reader) {
this.reader = reader;
}
public F getFile() {
return file;
public R getReader() {
return reader;
}
public Dir<E> getRoot() {
return root;
}
public @Nullable E getEntry(@NotNull String entryPath) {
Dir<E> dir = root;
String fileName;
if (entryPath.indexOf('/') < 0) {
fileName = entryPath;
if (fileName.isEmpty())
return root.getEntry();
} else {
String[] path = entryPath.split("/");
if (path.length == 0)
return root.getEntry();
for (int i = 0; i < path.length - 1; i++) {
String item = path[i];
if (item.isEmpty())
continue;
dir = dir.getSubDirs().get(item);
if (dir == null)
return null;
}
fileName = path[path.length - 1];
E entry = dir.getFiles().get(fileName);
if (entry != null)
return entry;
}
Dir<E> subDir = dir.getSubDirs().get(fileName);
return subDir != null ? subDir.getEntry() : null;
}
protected void addEntry(E entry) throws IOException {
String[] path = entry.getName().split("/");
List<String> pathList = Arrays.asList(path);
Dir<E> dir = root;
@@ -81,7 +118,9 @@ public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Clos
throw new IOException("A file and a directory have the same name: " + entry.getName());
}
dir = dir.subDirs.computeIfAbsent(item, Dir::new);
final int nameEnd = i + 1;
dir = dir.subDirs.computeIfAbsent(item, name ->
new Dir<>(name, String.join("/", pathList.subList(0, nameEnd))));
}
if (entry.isDirectory()) {
@@ -106,6 +145,55 @@ public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Clos
public abstract InputStream getInputStream(E entry) throws IOException;
public @NotNull InputStream getInputStream(String entryPath) throws IOException {
E entry = getEntry(entryPath);
if (entry == null)
throw new FileNotFoundException("Entry not found: " + entryPath);
return getInputStream(entry);
}
public byte[] readBinaryEntry(@NotNull E entry) throws IOException {
try (InputStream input = getInputStream(entry)) {
return input.readAllBytes();
}
}
public String readTextEntry(@NotNull String entryPath) throws IOException {
E entry = getEntry(entryPath);
if (entry == null)
throw new FileNotFoundException("Entry not found: " + entryPath);
return readTextEntry(entry);
}
public String readTextEntry(@NotNull E entry) throws IOException {
return new String(readBinaryEntry(entry), StandardCharsets.UTF_8);
}
protected void copyAttributes(@NotNull E source, @NotNull Path targetFile) throws IOException {
FileTime lastModifiedTime = source.getLastModifiedTime();
if (lastModifiedTime != null)
Files.setLastModifiedTime(targetFile, lastModifiedTime);
}
public void extractTo(@NotNull String entryPath, @NotNull Path targetFile) throws IOException {
E entry = getEntry(entryPath);
if (entry == null)
throw new FileNotFoundException("Entry not found: " + entryPath);
extractTo(entry, targetFile);
}
public void extractTo(@NotNull E entry, @NotNull Path targetFile) throws IOException {
try (InputStream input = getInputStream(entry)) {
Files.copy(input, targetFile, StandardCopyOption.REPLACE_EXISTING);
}
try {
copyAttributes(entry, targetFile);
} catch (Throwable e) {
LOG.warning("Failed to copy attributes to " + targetFile, e);
}
}
public abstract boolean isLink(E entry);
public abstract String getLink(E entry) throws IOException;
@@ -117,13 +205,15 @@ public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Clos
public static final class Dir<E extends ArchiveEntry> {
private final String name;
private final String fullName;
private E entry;
final Map<String, Dir<E>> subDirs = new HashMap<>();
final Map<String, E> files = new HashMap<>();
public Dir(String name) {
public Dir(String name, String fullName) {
this.name = name;
this.fullName = fullName;
}
public boolean isRoot() {
@@ -134,6 +224,11 @@ public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Clos
return name;
}
/// Get the normalized full path. Leading `/` and all `.` in the path will be removed.
public @NotNull String getFullName() {
return fullName;
}
public @Nullable E getEntry() {
return entry;
}
@@ -20,11 +20,13 @@ package org.jackhuang.hmcl.util.tree;
import kala.compress.archivers.tar.TarArchiveEntry;
import kala.compress.archivers.tar.TarArchiveReader;
import org.jetbrains.annotations.NotNull;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributeView;
import java.util.zip.GZIPInputStream;
/**
@@ -98,9 +100,22 @@ public final class TarFileTree extends ArchiveFileTree<TarArchiveReader, TarArch
this.shutdownHook = null;
}
@Override
protected void copyAttributes(@NotNull TarArchiveEntry source, @NotNull Path targetFile) throws IOException {
var fileAttributeView = Files.getFileAttributeView(targetFile, BasicFileAttributeView.class);
if (fileAttributeView == null)
return;
fileAttributeView.setTimes(
source.getLastModifiedTime(),
source.getLastAccessTime(),
source.getCreationTime()
);
}
@Override
public InputStream getInputStream(TarArchiveEntry entry) throws IOException {
return file.getInputStream(entry);
return reader.getInputStream(entry);
}
@Override
@@ -121,7 +136,7 @@ public final class TarFileTree extends ArchiveFileTree<TarArchiveReader, TarArch
@Override
public void close() throws IOException {
try {
file.close();
reader.close();
} finally {
if (tempFile != null) {
Runtime.getRuntime().removeShutdownHook(shutdownHook);
@@ -20,9 +20,17 @@ package org.jackhuang.hmcl.util.tree;
import kala.compress.archivers.zip.ZipArchiveEntry;
import kala.compress.archivers.zip.ZipArchiveReader;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFilePermission;
import java.util.EnumSet;
import java.util.Set;
/**
* @author Glavo
@@ -51,12 +59,53 @@ public final class ZipFileTree extends ArchiveFileTree<ZipArchiveReader, ZipArch
@Override
public void close() throws IOException {
if (closeReader)
file.close();
reader.close();
}
@Override
@SuppressWarnings("OctalInteger")
protected void copyAttributes(@NotNull ZipArchiveEntry source, @NotNull Path targetFile) throws IOException {
BasicFileAttributeView targetView = Files.getFileAttributeView(targetFile, PosixFileAttributeView.class);
// target might not support posix even if source does
if (targetView == null)
targetView = Files.getFileAttributeView(targetFile, BasicFileAttributeView.class);
if (targetView == null)
return;
targetView.setTimes(
source.getLastModifiedTime(),
source.getLastAccessTime(),
source.getCreationTime()
);
int unixMode = source.getUnixMode();
if (unixMode != 0 && targetView instanceof PosixFileAttributeView posixView) {
Set<PosixFilePermission> permissions = EnumSet.noneOf(PosixFilePermission.class);
// Owner permissions
if ((unixMode & 0400) != 0) permissions.add(PosixFilePermission.OWNER_READ);
if ((unixMode & 0200) != 0) permissions.add(PosixFilePermission.OWNER_WRITE);
if ((unixMode & 0100) != 0) permissions.add(PosixFilePermission.OWNER_EXECUTE);
// Group permissions
if ((unixMode & 0040) != 0) permissions.add(PosixFilePermission.GROUP_READ);
if ((unixMode & 0020) != 0) permissions.add(PosixFilePermission.GROUP_WRITE);
if ((unixMode & 0010) != 0) permissions.add(PosixFilePermission.GROUP_EXECUTE);
// Others permissions
if ((unixMode & 0004) != 0) permissions.add(PosixFilePermission.OTHERS_READ);
if ((unixMode & 0002) != 0) permissions.add(PosixFilePermission.OTHERS_WRITE);
if ((unixMode & 0001) != 0) permissions.add(PosixFilePermission.OTHERS_EXECUTE);
posixView.setPermissions(permissions);
}
}
@Override
public InputStream getInputStream(ZipArchiveEntry entry) throws IOException {
return getFile().getInputStream(entry);
return getReader().getInputStream(entry);
}
@Override
@@ -66,7 +115,7 @@ public final class ZipFileTree extends ArchiveFileTree<ZipArchiveReader, ZipArch
@Override
public String getLink(ZipArchiveEntry entry) throws IOException {
return getFile().getUnixSymlink(entry);
return getReader().getUnixSymlink(entry);
}
@Override
@@ -831,4 +831,19 @@
1.21.7-rc2
1.21.7
1.21.8-rc1
1.21.8
1.21.8
25w31a
25w32a
25w33a
25w34a
25w34b
25w35a
25w36a
25w36b
25w37a
1.21.9-pre1
1.21.9-pre2
1.21.9-pre3
1.21.9-pre4
1.21.9-rc1
1.21.9
@@ -0,0 +1,71 @@
aɐ
bq
cɔ
dp
eǝ
fɟ
gᵷ
hɥ
iᴉ
jɾ
kʞ
lꞁ
mɯ
nu
oo
pd
qb
rɹ
ss
tʇ
un
vʌ
wʍ
xx
yʎ
zz
AⱯ
Bᗺ
CƆ
Dᗡ
EƎ
FℲ
G⅁
HH
II
Jſ
Kʞ
LꞀ
MW
NN
OO
PԀ
QὉ
Rᴚ
SS
T⟘
U∩
VΛ
WM
XX
Yʎ
ZZ
00
1Ɩ
2ᘔ
3Ɛ
4ㄣ
5ϛ
69
7ㄥ
88
96
_‾
,'
;⸵
.˙
?¿
!¡
//
\\
',
+10 -3
View File
@@ -21,7 +21,14 @@ subprojects {
name = "libs"
dirs = setOf(rootProject.file("lib"))
}
mavenCentral()
System.getenv("MAVEN_CENTRAL_REPO").let { repo ->
if (repo.isNullOrBlank())
mavenCentral()
else
maven(url = repo)
}
maven(url = "https://jitpack.io")
maven(url = "https://libraries.minecraft.net")
}
@@ -71,10 +78,10 @@ tasks.register<UpdateDocuments>("updateDocuments") {
tasks.register<CheckUpdate>("checkUpdateDev") {
tagPrefix.set("v")
api.set("https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/api/json")
uri.set("https://ci.huangyuhui.net/job/HMCL")
}
tasks.register<CheckUpdate>("checkUpdateStable") {
tagPrefix.set("release-")
api.set("https://ci.huangyuhui.net/job/HMCL-stable/lastSuccessfulBuild/api/json")
uri.set("https://ci.huangyuhui.net/job/HMCL-stable")
}
+6 -1
View File
@@ -1,5 +1,10 @@
repositories {
mavenCentral()
System.getenv("MAVEN_CENTRAL_REPO").let { repo ->
if (repo.isNullOrBlank())
mavenCentral()
else
maven(url = repo)
}
}
dependencies {
@@ -18,6 +18,8 @@
package org.jackhuang.hmcl.gradle.ci;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.logging.Logger;
@@ -35,6 +37,7 @@ import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.function.BiConsumer;
@@ -45,8 +48,12 @@ import java.util.regex.Pattern;
public abstract class CheckUpdate extends DefaultTask {
private static final Logger LOGGER = Logging.getLogger(CheckUpdate.class);
private static final String WORKFLOW_JOB = "org.jenkinsci.plugins.workflow.job.WorkflowJob";
private static final String WORKFLOW_MULTI_BRANCH_PROJECT = "org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject";
private static final String FREE_STYLE_PROJECT = "hudson.model.FreeStyleProject";
@Input
public abstract Property<String> getApi();
public abstract Property<String> getUri();
@Input
public abstract Property<String> getTagPrefix();
@@ -55,81 +62,150 @@ public abstract class CheckUpdate extends DefaultTask {
getOutputs().upToDateWhen(task -> false);
}
private static <T> T fetch(URI uri, Class<T> type) throws IOException, InterruptedException {
// // HttpClient implements Closeable since Java 21
//noinspection resource
var client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build();
HttpResponse<String> response = client.send(HttpRequest.newBuilder(uri).build(), HttpResponse.BodyHandlers.ofString());
if (response.statusCode() / 100 != 2) {
throw new IOException("Bad status code: " + response.statusCode());
}
return new Gson().fromJson(response.body(), type);
private static URI toURI(String baseUri, String suffix) {
return URI.create(baseUri.endsWith("/")
? baseUri + suffix
: baseUri + "/" + suffix
);
}
@TaskAction
public void run() throws IOException, InterruptedException {
public void run() throws Exception {
String uri = getUri().get();
URI apiUri = toURI(uri, "api/json");
LOGGER.quiet("Fetching metadata from {}", apiUri);
BuildMetadata buildMetadata;
try (var helper = new Helper()) {
JsonObject body = Objects.requireNonNull(helper.fetch(apiUri, JsonObject.class));
String jobType = Objects.requireNonNull(body.getAsJsonPrimitive("_class"), "Missing _class property")
.getAsString();
if (WORKFLOW_MULTI_BRANCH_PROJECT.equals(jobType)) {
Pattern namePattern = Pattern.compile("release%2F3\\.\\d+");
List<BuildMetadata> metadatas = Objects.requireNonNull(helper.gson.fromJson(body.get("jobs"), new TypeToken<List<SubJobInfo>>() {
}), "jobs")
.stream()
.filter(it -> WORKFLOW_JOB.equals(it._class()))
.filter(it -> namePattern.matcher(it.name()).matches())
.filter(it -> !it.color().equals("disabled"))
.map(it -> {
try {
return fetchBuildInfo(helper, toURI(it.url, "lastSuccessfulBuild/api/json"));
} catch (Throwable e) {
throw new GradleException("Failed to retrieve build info from " + it.url(), e);
}
}).sorted(Comparator.comparing(BuildMetadata::timestamp))
.toList();
if (metadatas.isEmpty())
throw new GradleException("Failed to retrieve build metadata from " + apiUri);
buildMetadata = metadatas.get(metadatas.size() - 1);
} else if (WORKFLOW_JOB.equals(jobType) || FREE_STYLE_PROJECT.equals(jobType)) {
buildMetadata = fetchBuildInfo(helper, toURI(uri, "lastSuccessfulBuild/api/json"));
} else {
throw new GradleException("Unsupported job type: " + jobType);
}
}
LOGGER.quiet("Build metadata found: {}", buildMetadata);
String githubEnv = Objects.requireNonNullElse(System.getenv("GITHUB_ENV"), "");
if (githubEnv.isBlank())
LOGGER.warn("GITHUB_ENV is not set");
String uri = getApi().get();
LOGGER.info("Fetching metadata from {}", uri);
BuildInfo buildInfo = Objects.requireNonNull(fetch(URI.create(uri), BuildInfo.class),
"Could not fetch build info");
try (PrintWriter writer = githubEnv.isBlank()
? null
: new PrintWriter(Files.newBufferedWriter(Path.of(githubEnv), StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND))) {
BiConsumer<String, String> addEnv = (name, value) -> {
String item = name + "=" + value;
LOGGER.info(item);
LOGGER.quiet(item);
if (writer != null)
writer.println(item);
};
String revision = Objects.requireNonNullElse(buildInfo.actions(), List.<BuildInfo.ActionInfo>of())
.stream()
.filter(action -> "hudson.plugins.git.util.BuildData".equals(action._class))
.map(BuildInfo.ActionInfo::lastBuiltRevision)
.map(BuildInfo.ActionInfo.BuiltRevision::SHA1)
.findFirst()
.orElseThrow(() -> new GradleException("Could not find revision"));
if (revision.matches("[0-9a-z]{40}"))
addEnv.accept("HMCL_COMMIT_SHA", revision);
else
throw new GradleException("Invalid revision: " + revision);
Pattern fileNamePattern = Pattern.compile("HMCL-(?<version>\\d+(?:\\.\\d+)+)\\.jar");
String version = Objects.requireNonNullElse(buildInfo.artifacts(), List.<BuildInfo.ArtifactInfo>of())
.stream()
.map(BuildInfo.ArtifactInfo::fileName)
.map(fileNamePattern::matcher)
.filter(Matcher::matches)
.map(matcher -> matcher.group("version"))
.findFirst()
.orElseThrow(() -> new GradleException("Could not find .jar artifact"));
addEnv.accept("HMCL_VERSION", version);
addEnv.accept("HMCL_TAG_NAME", getTagPrefix().get() + version);
addEnv.accept("HMCL_COMMIT_SHA", buildMetadata.revision());
addEnv.accept("HMCL_VERSION", buildMetadata.version());
addEnv.accept("HMCL_TAG_NAME", getTagPrefix().get() + buildMetadata.version());
}
}
private record BuildInfo(long number,
List<ArtifactInfo> artifacts,
List<ActionInfo> actions
) {
private record BuildMetadata(String version, String revision, long timestamp) {
}
record ArtifactInfo(String fileName) {
private BuildMetadata fetchBuildInfo(Helper helper, URI uri) throws IOException, InterruptedException {
LOGGER.quiet("Fetching build info from {}", uri);
BuildInfo buildInfo = Objects.requireNonNull(helper.fetch(uri, BuildInfo.class), "build info");
String revision = Objects.requireNonNullElse(buildInfo.actions(), List.<ActionInfo>of())
.stream()
.filter(action -> "hudson.plugins.git.util.BuildData".equals(action._class()))
.map(ActionInfo::lastBuiltRevision)
.map(BuiltRevision::SHA1)
.findFirst()
.orElseThrow(() -> new GradleException("Could not find revision"));
if (!revision.matches("[0-9a-z]{40}"))
throw new GradleException("Invalid revision: " + revision);
Pattern fileNamePattern = Pattern.compile("HMCL-(?<version>\\d+(?:\\.\\d+)+)\\.jar");
String version = Objects.requireNonNullElse(buildInfo.artifacts(), List.<ArtifactInfo>of())
.stream()
.map(ArtifactInfo::fileName)
.map(fileNamePattern::matcher)
.filter(Matcher::matches)
.map(matcher -> matcher.group("version"))
.findFirst()
.orElseThrow(() -> new GradleException("Could not find .jar artifact"));
return new BuildMetadata(version, revision, buildInfo.timestamp());
}
private static final class Helper implements AutoCloseable {
private final HttpClient client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build();
private final Gson gson = new Gson();
private <T> T fetch(URI uri, Class<T> type) throws IOException, InterruptedException {
HttpResponse<String> response = client.send(HttpRequest.newBuilder(uri).build(), HttpResponse.BodyHandlers.ofString());
if (response.statusCode() / 100 != 2) {
throw new IOException("Bad status code " + response.statusCode() + " for " + uri);
}
return gson.fromJson(response.body(), type);
}
record ActionInfo(String _class, BuiltRevision lastBuiltRevision) {
record BuiltRevision(String SHA1) {
@Override
public void close() throws Exception {
// HttpClient implements AutoCloseable since Java 21
if (((Object) client) instanceof AutoCloseable closeable) {
closeable.close();
}
}
}
private record SubJobInfo(String _class, String name, String url, String color) {
}
private record BuildInfo(long number,
long timestamp,
List<ArtifactInfo> artifacts,
List<ActionInfo> actions
) {
}
record ArtifactInfo(String fileName) {
}
record ActionInfo(String _class, BuiltRevision lastBuiltRevision) {
}
record BuiltRevision(String SHA1) {
}
}
@@ -0,0 +1,32 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 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.ci;
import java.util.Objects;
/// @author Glavo
public final class GitHubActionUtils {
private static final String OFFICIAL_ORGANIZATION = "HMCL-dev";
public static final boolean IS_ON_OFFICIAL_REPO =
OFFICIAL_ORGANIZATION.equalsIgnoreCase(System.getenv("GITHUB_REPOSITORY_OWNER"))
&& Objects.requireNonNullElse(System.getenv("GITHUB_BASE_REF"), "").isBlank();
private GitHubActionUtils() {
}
}
@@ -0,0 +1,27 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 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.ci;
/// @author Glavo
public final class JenkinsUtils {
public static final boolean IS_ON_CI = "1".equals(System.getenv("HMCL_CI"));
private JenkinsUtils() {
}
}
@@ -18,18 +18,18 @@
package org.jackhuang.hmcl.gradle.l10n;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -75,29 +75,31 @@ public abstract class UpsideDownTranslate extends DefaultTask {
}
static final class Translator {
private static final Map<Integer, Integer> MAPPER = new LinkedHashMap<>();
private static final Map<Integer, Integer> MAPPER = loadMap();
private static void putChars(char baseChar, String upsideDownChars) {
for (int i = 0; i < upsideDownChars.length(); i++) {
MAPPER.put(baseChar + i, (int) upsideDownChars.charAt(i));
private static Map<Integer, Integer> loadMap() {
var map = new LinkedHashMap<Integer, Integer>();
InputStream inputStream = UpsideDownTranslate.class.getResourceAsStream("upside_down.txt");
if (inputStream != null) {
try (inputStream) {
new String(inputStream.readAllBytes(), StandardCharsets.UTF_8).lines().forEach(line -> {
if (line.isBlank() || line.startsWith("#"))
return;
if (line.length() != 2) {
throw new GradleException("Invalid line: " + line);
}
map.put((int) line.charAt(0), (int) line.charAt(1));
});
} catch (IOException e) {
throw new GradleException("Failed to load upside_down.txt", e);
}
} else {
throw new GradleException("upside_down.txt not found");
}
}
private static void putChars(String baseChars, String upsideDownChars) {
if (baseChars.length() != upsideDownChars.length()) {
throw new IllegalArgumentException("baseChars and upsideDownChars must have same length");
}
for (int i = 0; i < baseChars.length(); i++) {
MAPPER.put((int) baseChars.charAt(i), (int) upsideDownChars.charAt(i));
}
}
static {
putChars('a', "ɐqɔpǝɟbɥıظʞןɯuodbɹsʇnʌʍxʎz");
putChars('A', "ⱯᗺƆᗡƎℲ⅁HIſʞꞀWNOԀὉᴚS⟘∩ΛMXʎZ");
putChars('0', "0ƖᘔƐㄣϛ9ㄥ86");
putChars("_,;.?!/\\'", "‾'⸵˙¿¡/\\,");
return Collections.unmodifiableMap(map);
}
private static final Pattern FORMAT_PATTERN = Pattern.compile("^%(\\d\\$)?(\\d+)?(\\.\\d+)?([sdf])");
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2025 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
@@ -15,33 +15,25 @@
* 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.event;
package org.jackhuang.hmcl.gradle.utils;
/**
*
* @author huang
*/
public class FailedEvent<T> extends Event {
import org.jetbrains.annotations.NotNull;
private final int failedTime;
private T newResult;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;
public FailedEvent(Object source, int failedTime, T newResult) {
super(source);
this.failedTime = failedTime;
this.newResult = newResult;
/// @author Glavo
public final class PropertiesUtils {
public static @NotNull Properties load(Path path) throws IOException {
Properties properties = new Properties();
try (var reader = Files.newBufferedReader(path)) {
properties.load(reader);
}
return properties;
}
public int getFailedTime() {
return failedTime;
private PropertiesUtils() {
}
public T getNewResult() {
return newResult;
}
public void setNewResult(T newResult) {
this.newResult = newResult;
}
}
-5
View File
@@ -1,5 +0,0 @@
#!/usr/bin/env bash
_HMCL_PROJECT_ROOT=$(dirname "${BASH_SOURCE[0]}")
sed -i 's,//services.gradle.org/distributions/,//mirrors.cloud.tencent.com/gradle/,g' "$_HMCL_PROJECT_ROOT/gradle/wrapper/gradle-wrapper.properties"
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
sed -i 's,//services.gradle.org/distributions/,//mirrors.cloud.tencent.com/gradle/,g' "$PWD/gradle/wrapper/gradle-wrapper.properties"
+23
View File
@@ -0,0 +1,23 @@
pipeline {
agent any
environment {
HMCL_CI = '1'
VERSION_TYPE = 'dev'
MICROSOFT_AUTH_ID = credentials('microsoft_auth_id')
MICROSOFT_AUTH_SECRET = credentials('microsoft_auth_secret')
CURSEFORGE_API_KEY = credentials('curseforge_api_key')
HMCL_SIGNATURE_KEY = credentials('hmcl_signature_key')
}
stages {
stage('Build') {
steps {
sh 'bash ./config/jenkins/config-jenkins.sh'
sh './gradlew clean makeExecutables --stacktrace --no-daemon'
archiveArtifacts artifacts: 'HMCL/build/libs/*'
}
}
}
}
+23
View File
@@ -0,0 +1,23 @@
pipeline {
agent any
environment {
HMCL_CI = '1'
VERSION_TYPE = 'stable'
MICROSOFT_AUTH_ID = credentials('microsoft_auth_id')
MICROSOFT_AUTH_SECRET = credentials('microsoft_auth_secret')
CURSEFORGE_API_KEY = credentials('curseforge_api_key')
HMCL_SIGNATURE_KEY = credentials('hmcl_signature_key')
}
stages {
stage('Build') {
steps {
sh 'bash ./config/jenkins/config-jenkins.sh'
sh './gradlew clean makeExecutables --stacktrace --no-daemon'
archiveArtifacts artifacts: 'HMCL/build/libs/*'
}
}
}
}
+18
View File
@@ -0,0 +1,18 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2025 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/>.
#
versionRoot=3.7