重构 Java 管理 (#2988)

* update

* update

* Update task name

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* Update logo
This commit is contained in:
Glavo
2024-10-05 23:50:14 +08:00
committed by GitHub
parent 37d6857b82
commit 7e4d437a1d
74 changed files with 5232 additions and 1343 deletions

View File

@@ -40,7 +40,7 @@ import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.NotNull;
@@ -129,7 +129,7 @@ public class ForgeNewInstallTask extends Task<Version> {
throw new Exception("Game processor jar does not have main class " + jar);
List<String> command = new ArrayList<>();
command.add(JavaVersion.fromCurrentEnvironment().getBinary().toString());
command.add(JavaRuntime.getDefault().getBinary().toString());
command.add("-cp");
List<String> classpath = new ArrayList<>(processor.getClasspath().size() + 1);

View File

@@ -0,0 +1,36 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.download.java;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.platform.Platform;
import java.util.Set;
import java.util.TreeMap;
/**
* @author Glavo
*/
public interface JavaDistribution<V extends JavaRemoteVersion> {
String getDisplayName();
Set<JavaPackageType> getSupportedPackageTypes();
Task<TreeMap<Integer, V>> getFetchJavaVersionsTask(DownloadProvider provider, Platform platform, JavaPackageType packageType);
}

View File

@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2024 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,23 +15,30 @@
* 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.game;
package org.jackhuang.hmcl.download.java;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.junit.jupiter.api.Test;
/**
* @author Glavo
*/
public enum JavaPackageType {
JDK(true, false),
JRE(false, false),
JDKFX(true, true),
JREFX(false, true);
import static org.junit.jupiter.api.Assertions.*;
private final boolean jdk;
private final boolean javafx;
public class JavaVersionConstraintTest {
JavaPackageType(boolean jdk, boolean javafx) {
this.jdk = jdk;
this.javafx = javafx;
}
@Test
public void vanillaJava16() {
JavaVersionConstraint.VersionRanges range = JavaVersionConstraint.findSuitableJavaVersionRange(
GameVersionNumber.asGameVersion("1.17"),
null
);
public boolean isJDK() {
return jdk;
}
assertEquals(VersionNumber.atLeast("16"), range.getMandatory());
public boolean isJavaFXBundled() {
return javafx;
}
}

View File

@@ -0,0 +1,29 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.download.java;
/**
* @author Glavo
*/
public interface JavaRemoteVersion {
int getJdkVersion();
String getJavaVersion();
String getDistributionVersion();
}

View File

@@ -1,168 +0,0 @@
package org.jackhuang.hmcl.download.java;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.game.GameJavaVersion;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class JavaRepository {
private JavaRepository() {
}
public static Task<JavaVersion> downloadJava(GameJavaVersion javaVersion, DownloadProvider downloadProvider) {
return new JavaDownloadTask(javaVersion, getJavaStoragePath(), downloadProvider)
.thenSupplyAsync(() -> {
String platform = getSystemJavaPlatform().orElseThrow(JavaDownloadTask.UnsupportedPlatformException::new);
return addJava(getJavaHome(javaVersion, platform));
});
}
public static JavaVersion addJava(Path javaHome) throws InterruptedException, IOException {
if (Files.isDirectory(javaHome)) {
Path executable = JavaVersion.getExecutable(javaHome);
if (Files.isRegularFile(executable)) {
JavaVersion javaVersion = JavaVersion.fromExecutable(executable);
JavaVersion.getJavas().add(javaVersion);
return javaVersion;
}
}
throw new IOException("Incorrect java home " + javaHome);
}
public static Stream<Optional<Path>> findMinecraftRuntimeDirs() {
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
return Stream.of(
FileUtils.tryGetPath(System.getenv("localappdata"),
"Packages\\Microsoft.4297127D64EC6_8wekyb3d8bbwe\\LocalCache\\Local\\runtime"),
FileUtils.tryGetPath(
Optional.ofNullable(System.getenv("ProgramFiles(x86)")).orElse("C:\\Program Files (x86)"),
"Minecraft Launcher\\runtime"));
case LINUX:
case FREEBSD:
return Stream.of(FileUtils.tryGetPath(System.getProperty("user.home"), ".minecraft/runtime"));
case OSX:
return Stream.of(FileUtils.tryGetPath(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"));
default:
return Stream.empty();
}
}
public static Stream<Path> findJavaHomeInMinecraftRuntimeDir(Path runtimeDir) {
if (!Files.isDirectory(runtimeDir))
return Stream.empty();
// Examples:
// $HOME/Library/Application Support/minecraft/runtime/java-runtime-beta/mac-os/java-runtime-beta/jre.bundle/Contents/Home
// $HOME/.minecraft/runtime/java-runtime-beta/linux/java-runtime-beta
List<Path> javaHomes = new ArrayList<>();
Consumer<String> action = platform -> {
try (DirectoryStream<Path> dir = Files.newDirectoryStream(runtimeDir)) {
// component can be jre-legacy, java-runtime-alpha, java-runtime-beta, java-runtime-gamma or any other being added in the future.
for (Path component : dir) {
findJavaHomeInComponentDir(platform, component).ifPresent(javaHomes::add);
}
} catch (IOException e) {
LOG.warning("Failed to list java-runtime directory " + runtimeDir, e);
}
};
getSystemJavaPlatform().ifPresent(action);
// Workaround, which will be removed in the future
if (Platform.SYSTEM_PLATFORM == Platform.OSX_ARM64)
action.accept("mac-os-arm64");
return javaHomes.stream();
}
private static Optional<Path> findJavaHomeInComponentDir(String platform, Path component) {
Path sha1File = component.resolve(platform).resolve(component.getFileName() + ".sha1");
if (!Files.isRegularFile(sha1File))
return Optional.empty();
Path dir = component.resolve(platform).resolve(component.getFileName());
try (BufferedReader reader = Files.newBufferedReader(sha1File)) {
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) continue;
int idx = line.indexOf(" /#//");
if (idx <= 0)
throw new IOException("Illegal line: " + line);
Path file = dir.resolve(line.substring(0, idx));
// Should we check the sha1 of files? This will take a lot of time.
if (Files.notExists(file))
throw new NoSuchFileException(file.toAbsolutePath().toString());
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
Path macPath = dir.resolve("jre.bundle/Contents/Home");
if (Files.exists(macPath))
return Optional.of(macPath);
else
LOG.warning("The Java is not in 'jre.bundle/Contents/Home'");
}
return Optional.of(dir);
} catch (IOException e) {
LOG.warning("Failed to verify Java in " + component, e);
return Optional.empty();
}
}
public static Optional<String> getSystemJavaPlatform() {
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
if (Architecture.SYSTEM_ARCH == Architecture.X86) {
return Optional.of("linux-i386");
} else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return Optional.of("linux");
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64 || Architecture.SYSTEM_ARCH == Architecture.ARM64) {
return Optional.of("mac-os");
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
if (Architecture.SYSTEM_ARCH == Architecture.X86) {
return Optional.of("windows-x86");
} else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return Optional.of("windows-x64");
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277) {
return Optional.of("windows-x64");
} else {
return Optional.of("windows-x86");
}
}
}
return Optional.empty();
}
public static Path getJavaStoragePath() {
return CacheRepository.getInstance().getCacheDirectory().resolve("java");
}
public static Path getJavaHome(GameJavaVersion javaVersion, String platform) {
Path javaHome = getJavaStoragePath().resolve(javaVersion.getComponent()).resolve(platform).resolve(javaVersion.getComponent());
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
javaHome = javaHome.resolve("jre.bundle/Contents/Home");
return javaHome;
}
}

View File

@@ -0,0 +1,95 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.download.java.disco;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.java.JavaPackageType;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.util.*;
/**
* @author Glavo
*/
public final class DiscoFetchJavaListTask extends Task<TreeMap<Integer, DiscoJavaRemoteVersion>> {
public static final String API_ROOT = System.getProperty("hmcl.discoapi.override", "https://api.foojay.io/disco/v3.0");
private static String getOperatingSystemName(OperatingSystem os) {
return os == OperatingSystem.OSX ? "macos" : os.getCheckedName();
}
private static String getArchitectureName(Architecture arch) {
return arch.getCheckedName();
}
private final DiscoJavaDistribution distribution;
private final Task<String> fetchPackagesTask;
public DiscoFetchJavaListTask(DownloadProvider downloadProvider, DiscoJavaDistribution distribution, Platform platform, JavaPackageType packageType) {
this.distribution = distribution;
HashMap<String, String> params = new HashMap<>();
params.put("distribution", distribution.getApiParameter());
params.put("package", packageType.isJDK() ? "jdk" : "jre");
params.put("javafx_bundled", Boolean.toString(packageType.isJavaFXBundled()));
params.put("operating_system", getOperatingSystemName(platform.getOperatingSystem()));
params.put("architecture", getArchitectureName(platform.getArchitecture()));
params.put("archive_type", platform.getOperatingSystem() == OperatingSystem.WINDOWS ? "zip" : "tar.gz");
params.put("directly_downloadable", "true");
if (platform.getOperatingSystem() == OperatingSystem.LINUX) {
params.put("lib_c_type", "glibc");
}
this.fetchPackagesTask = new GetTask(downloadProvider.injectURLWithCandidates(NetworkUtils.withQuery(API_ROOT + "/packages", params)));
}
@Override
public Collection<Task<?>> getDependents() {
return Collections.singleton(fetchPackagesTask);
}
@Override
public void execute() throws Exception {
String json = fetchPackagesTask.getResult();
List<DiscoJavaRemoteVersion> result = JsonUtils.fromNonNullJson(json, DiscoResult.typeOf(DiscoJavaRemoteVersion.class)).getResult();
TreeMap<Integer, DiscoJavaRemoteVersion> map = new TreeMap<>();
for (DiscoJavaRemoteVersion version : result) {
if (!distribution.getApiParameter().equals(version.getDistribution()))
continue;
int jdkVersion = version.getJdkVersion();
DiscoJavaRemoteVersion oldVersion = map.get(jdkVersion);
if (oldVersion == null || VersionNumber.compare(version.getDistributionVersion(), oldVersion.getDistributionVersion()) > 0) {
map.put(jdkVersion, version);
}
}
setResult(map);
}
}

View File

@@ -0,0 +1,116 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.download.java.disco;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.java.JavaDistribution;
import org.jackhuang.hmcl.download.java.JavaPackageType;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import java.util.*;
import static org.jackhuang.hmcl.download.java.JavaPackageType.*;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.platform.Architecture.*;
import static org.jackhuang.hmcl.util.platform.OperatingSystem.*;
/**
* @author Glavo
*/
public enum DiscoJavaDistribution implements JavaDistribution<DiscoJavaRemoteVersion> {
TEMURIN("Eclipse Temurin", "temurin", "Adoptium",
EnumSet.of(JDK, JRE),
pair(WINDOWS, EnumSet.of(X86_64, X86, ARM64)),
pair(LINUX, EnumSet.of(X86_64, X86, ARM64, ARM32, RISCV64, PPC64, PPC64LE, S390X, SPARCV9)),
pair(OSX, EnumSet.of(X86_64, ARM64))),
LIBERICA("Liberica", "liberica", "BellSoft",
EnumSet.of(JDK, JRE, JDKFX, JREFX),
pair(WINDOWS, EnumSet.of(X86_64, X86, ARM64)),
pair(LINUX, EnumSet.of(X86_64, X86, ARM64, ARM32, RISCV64, PPC64LE)),
pair(OSX, EnumSet.of(X86_64, ARM64))),
ZULU("Zulu", "zulu", "Azul",
EnumSet.of(JDK, JRE, JDKFX, JREFX),
pair(WINDOWS, EnumSet.of(X86_64, X86, ARM64)),
pair(LINUX, EnumSet.of(X86_64, X86, ARM64, ARM32, RISCV64, PPC64LE)),
pair(OSX, EnumSet.of(X86_64, ARM64))),
GRAALVM("GraalVM", "graalvm", "Oracle",
EnumSet.of(JDK),
pair(WINDOWS, EnumSet.of(X86_64, X86)),
pair(LINUX, EnumSet.of(X86_64, X86, ARM64, ARM32, RISCV64, PPC64LE)),
pair(OSX, EnumSet.of(X86_64, ARM64)));
public static DiscoJavaDistribution of(String name) {
for (DiscoJavaDistribution distribution : values()) {
if (distribution.apiParameter.equalsIgnoreCase(name) || distribution.name().equalsIgnoreCase(name)) {
return distribution;
}
}
return null;
}
private final String displayName;
private final String apiParameter;
private final String vendor;
private final Set<JavaPackageType> supportedPackageTypes;
private final Map<OperatingSystem, EnumSet<Architecture>> supportedPlatforms = new EnumMap<>(OperatingSystem.class);
@SafeVarargs
DiscoJavaDistribution(String displayName, String apiParameter, String vendor, Set<JavaPackageType> supportedPackageTypes, Pair<OperatingSystem, EnumSet<Architecture>>... supportedPlatforms) {
this.displayName = displayName;
this.apiParameter = apiParameter;
this.vendor = vendor;
this.supportedPackageTypes = supportedPackageTypes;
for (Pair<OperatingSystem, EnumSet<Architecture>> platform : supportedPlatforms) {
this.supportedPlatforms.put(platform.getKey(), platform.getValue());
}
}
@Override
public String getDisplayName() {
return displayName;
}
public String getApiParameter() {
return apiParameter;
}
public String getVendor() {
return vendor;
}
@Override
public Set<JavaPackageType> getSupportedPackageTypes() {
return supportedPackageTypes;
}
public boolean isSupport(Platform platform) {
EnumSet<Architecture> architectures = supportedPlatforms.get(platform.getOperatingSystem());
return architectures != null && architectures.contains(platform.getArchitecture());
}
@Override
public Task<TreeMap<Integer, DiscoJavaRemoteVersion>> getFetchJavaVersionsTask(DownloadProvider provider, Platform platform, JavaPackageType packageType) {
return new DiscoFetchJavaListTask(provider, this, platform, packageType);
}
}

View File

@@ -0,0 +1,259 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.download.java.disco;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.download.java.JavaRemoteVersion;
import org.jackhuang.hmcl.util.gson.JsonUtils;
/**
* @author Glavo
*/
public final class DiscoJavaRemoteVersion implements JavaRemoteVersion {
@SerializedName("id")
private final String id;
@SerializedName("archive_type")
private final String archiveType;
@SerializedName("distribution")
private final String distribution;
@SerializedName("major_version")
private final int majorVersion;
@SerializedName("java_version")
private final String javaVersion;
@SerializedName("distribution_version")
private final String distributionVersion;
@SerializedName("jdk_version")
private final int jdkVersion;
@SerializedName("latest_build_available")
private final boolean latestBuildAvailable;
@SerializedName("release_status")
private final String releaseStatus;
@SerializedName("term_of_support")
private final String termOfSupport;
@SerializedName("operating_system")
private final String operatingSystem;
@SerializedName("lib_c_type")
private final String libCType;
@SerializedName("architecture")
private final String architecture;
@SerializedName("fpu")
private final String fpu;
@SerializedName("package_type")
private final String packageType;
@SerializedName("javafx_bundled")
private final boolean javafxBundled;
@SerializedName("directly_downloadable")
private final boolean directlyDownloadable;
@SerializedName("filename")
private final String fileName;
@SerializedName("links")
private final Links links;
@SerializedName("free_use_in_production")
private final boolean freeUseInProduction;
@SerializedName("tck_tested")
private final String tckTested;
@SerializedName("tck_cert_uri")
private final String tckCertUri;
@SerializedName("aqavit_certified")
private final String aqavitCertified;
@SerializedName("aqavit_cert_uri")
private final String aqavitCertUri;
@SerializedName("size")
private final long size;
public DiscoJavaRemoteVersion(String id, String archiveType, String distribution, int majorVersion, String javaVersion, String distributionVersion, int jdkVersion, boolean latestBuildAvailable, String releaseStatus, String termOfSupport, String operatingSystem, String libCType, String architecture, String fpu, String packageType, boolean javafxBundled, boolean directlyDownloadable, String fileName, Links links, boolean freeUseInProduction, String tckTested, String tckCertUri, String aqavitCertified, String aqavitCertUri, long size) {
this.id = id;
this.archiveType = archiveType;
this.distribution = distribution;
this.majorVersion = majorVersion;
this.javaVersion = javaVersion;
this.distributionVersion = distributionVersion;
this.jdkVersion = jdkVersion;
this.latestBuildAvailable = latestBuildAvailable;
this.releaseStatus = releaseStatus;
this.termOfSupport = termOfSupport;
this.operatingSystem = operatingSystem;
this.libCType = libCType;
this.architecture = architecture;
this.fpu = fpu;
this.packageType = packageType;
this.javafxBundled = javafxBundled;
this.directlyDownloadable = directlyDownloadable;
this.fileName = fileName;
this.links = links;
this.freeUseInProduction = freeUseInProduction;
this.tckTested = tckTested;
this.tckCertUri = tckCertUri;
this.aqavitCertified = aqavitCertified;
this.aqavitCertUri = aqavitCertUri;
this.size = size;
}
public String getId() {
return id;
}
public String getArchiveType() {
return archiveType;
}
public String getDistribution() {
return distribution;
}
public int getMajorVersion() {
return majorVersion;
}
@Override
public String getJavaVersion() {
return javaVersion;
}
@Override
public String getDistributionVersion() {
return distributionVersion;
}
@Override
public int getJdkVersion() {
return jdkVersion;
}
public boolean isLatestBuildAvailable() {
return latestBuildAvailable;
}
public String getReleaseStatus() {
return releaseStatus;
}
public String getTermOfSupport() {
return termOfSupport;
}
public String getOperatingSystem() {
return operatingSystem;
}
public String getLibCType() {
return libCType;
}
public String getArchitecture() {
return architecture;
}
public String getFpu() {
return fpu;
}
public String getPackageType() {
return packageType;
}
public boolean isJavafxBundled() {
return javafxBundled;
}
public boolean isDirectlyDownloadable() {
return directlyDownloadable;
}
public String getFileName() {
return fileName;
}
public Links getLinks() {
return links;
}
public boolean isFreeUseInProduction() {
return freeUseInProduction;
}
public String getTckTested() {
return tckTested;
}
public String getTckCertUri() {
return tckCertUri;
}
public String getAqavitCertified() {
return aqavitCertified;
}
public String getAqavitCertUri() {
return aqavitCertUri;
}
public long getSize() {
return size;
}
@Override
public String toString() {
return "DiscoJavaRemoteVersion " + JsonUtils.GSON.toJson(this);
}
public static final class Links {
@SerializedName("pkg_info_uri")
private final String pkgInfoUri;
@SerializedName("pkg_download_redirect")
private final String pkgDownloadRedirect;
public Links(String pkgInfoUri, String pkgDownloadRedirect) {
this.pkgInfoUri = pkgInfoUri;
this.pkgDownloadRedirect = pkgDownloadRedirect;
}
public String getPkgInfoUri() {
return pkgInfoUri;
}
public String getPkgDownloadRedirect() {
return pkgDownloadRedirect;
}
}
}

View File

@@ -0,0 +1,68 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.download.java.disco;
import com.google.gson.annotations.SerializedName;
/**
* @author Glavo
*/
public final class DiscoRemoteFileInfo {
@SerializedName("filename")
private final String fileName;
@SerializedName("direct_download_uri")
private final String directDownloadUri;
@SerializedName("checksum_type")
private final String checksumType;
@SerializedName("checksum")
private final String checksum;
@SerializedName("checksum_uri")
private final String checksumUri;
public DiscoRemoteFileInfo(String fileName, String directDownloadUri, String checksumType, String checksum, String checksumUri) {
this.fileName = fileName;
this.directDownloadUri = directDownloadUri;
this.checksumType = checksumType;
this.checksum = checksum;
this.checksumUri = checksumUri;
}
public String getFileName() {
return fileName;
}
public String getDirectDownloadUri() {
return directDownloadUri;
}
public String getChecksumType() {
return checksumType;
}
public String getChecksum() {
return checksum;
}
public String getChecksumUri() {
return checksumUri;
}
}

View File

@@ -0,0 +1,49 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.download.java.disco;
import com.google.gson.reflect.TypeToken;
import java.util.List;
/**
* @author Glavo
*/
public final class DiscoResult<T> {
@SuppressWarnings("unchecked")
public static <T> TypeToken<DiscoResult<T>> typeOf(Class<T> argType) {
return (TypeToken<DiscoResult<T>>) TypeToken.getParameterized(DiscoResult.class, argType);
}
private final List<T> result;
private final String message;
private DiscoResult(List<T> result, String message) {
this.result = result;
this.message = message;
}
public List<T> getResult() {
return result;
}
public String getMessage() {
return message;
}
}

View File

@@ -0,0 +1,55 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.download.java.mojang;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.java.JavaDistribution;
import org.jackhuang.hmcl.download.java.JavaPackageType;
import org.jackhuang.hmcl.download.java.JavaRemoteVersion;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.platform.Platform;
import java.util.Collections;
import java.util.Set;
import java.util.TreeMap;
/**
* @author Glavo
*/
public final class MojangJavaDistribution implements JavaDistribution<JavaRemoteVersion> {
public static final MojangJavaDistribution DISTRIBUTION = new MojangJavaDistribution();
private MojangJavaDistribution() {
}
@Override
public String getDisplayName() {
return "Mojang";
}
@Override
public Set<JavaPackageType> getSupportedPackageTypes() {
return Collections.singleton(JavaPackageType.JRE);
}
@Override
public Task<TreeMap<Integer, JavaRemoteVersion>> getFetchJavaVersionsTask(DownloadProvider provider, Platform platform, JavaPackageType packageType) {
return null;
}
}

View File

@@ -15,20 +15,19 @@
* 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.download.java;
package org.jackhuang.hmcl.download.java.mojang;
import org.jackhuang.hmcl.download.ArtifactMalformedException;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.game.DownloadInfo;
import org.jackhuang.hmcl.game.GameJavaVersion;
import org.jackhuang.hmcl.java.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException;
import org.tukaani.xz.LZMAInputStream;
import java.io.File;
@@ -40,50 +39,39 @@ import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class JavaDownloadTask extends Task<Void> {
private final GameJavaVersion javaVersion;
private final Path rootDir;
private String platform;
private final Task<RemoteFiles> javaDownloadsTask;
private JavaDownloads.JavaDownload download;
private final List<Task<?>> dependencies = new ArrayList<>();
private final DownloadProvider downloadProvider;
public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Result> {
public JavaDownloadTask(GameJavaVersion javaVersion, Path rootDir, DownloadProvider downloadProvider) {
this.javaVersion = javaVersion;
this.rootDir = rootDir;
private final DownloadProvider downloadProvider;
private final Path target;
private final Task<MojangJavaRemoteFiles> javaDownloadsTask;
private final List<Task<?>> dependencies = new ArrayList<>();
private volatile MojangJavaDownloads.JavaDownload download;
public MojangJavaDownloadTask(DownloadProvider downloadProvider, Path target, GameJavaVersion javaVersion, String platform) {
this.target = target;
this.downloadProvider = downloadProvider;
this.javaDownloadsTask = new GetTask(downloadProvider.injectURLWithCandidates(
"https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json"))
.thenComposeAsync(javaDownloadsJson -> {
JavaDownloads allDownloads = JsonUtils.fromNonNullJson(javaDownloadsJson, JavaDownloads.class);
if (!allDownloads.getDownloads().containsKey(platform)) throw new UnsupportedPlatformException();
Map<String, List<JavaDownloads.JavaDownload>> osDownloads = allDownloads.getDownloads().get(platform);
if (!osDownloads.containsKey(javaVersion.getComponent())) throw new UnsupportedPlatformException();
List<JavaDownloads.JavaDownload> candidates = osDownloads.get(javaVersion.getComponent());
for (JavaDownloads.JavaDownload download : candidates) {
if (VersionNumber.compare(download.getVersion().getName(), Integer.toString(javaVersion.getMajorVersion())) >= 0) {
MojangJavaDownloads allDownloads = JsonUtils.fromNonNullJson(javaDownloadsJson, MojangJavaDownloads.class);
Map<String, List<MojangJavaDownloads.JavaDownload>> osDownloads = allDownloads.getDownloads().get(platform);
if (osDownloads == null || !osDownloads.containsKey(javaVersion.getComponent()))
throw new UnsupportedPlatformException("Unsupported platform: " + platform);
List<MojangJavaDownloads.JavaDownload> candidates = osDownloads.get(javaVersion.getComponent());
for (MojangJavaDownloads.JavaDownload download : candidates) {
if (JavaInfo.parseVersion(download.getVersion().getName()) >= javaVersion.getMajorVersion()) {
this.download = download;
return new GetTask(downloadProvider.injectURLWithCandidates(download.getManifest().getUrl()));
}
}
throw new UnsupportedPlatformException();
throw new UnsupportedPlatformException("Candidates: " + JsonUtils.GSON.toJson(candidates));
})
.thenApplyAsync(javaDownloadJson -> JsonUtils.fromNonNullJson(javaDownloadJson, RemoteFiles.class));
}
@Override
public boolean doPreExecute() {
return true;
}
@Override
public void preExecute() throws Exception {
this.platform = JavaRepository.getSystemJavaPlatform().orElseThrow(UnsupportedPlatformException::new);
.thenApplyAsync(javaDownloadJson -> JsonUtils.fromNonNullJson(javaDownloadJson, MojangJavaRemoteFiles.class));
}
@Override
@@ -93,11 +81,10 @@ public class JavaDownloadTask extends Task<Void> {
@Override
public void execute() throws Exception {
Path jvmDir = rootDir.resolve(javaVersion.getComponent()).resolve(platform).resolve(javaVersion.getComponent());
for (Map.Entry<String, RemoteFiles.Remote> entry : javaDownloadsTask.getResult().getFiles().entrySet()) {
Path dest = jvmDir.resolve(entry.getKey());
if (entry.getValue() instanceof RemoteFiles.RemoteFile) {
RemoteFiles.RemoteFile file = ((RemoteFiles.RemoteFile) entry.getValue());
for (Map.Entry<String, MojangJavaRemoteFiles.Remote> entry : javaDownloadsTask.getResult().getFiles().entrySet()) {
Path dest = target.resolve(entry.getKey());
if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteFile) {
MojangJavaRemoteFiles.RemoteFile file = ((MojangJavaRemoteFiles.RemoteFile) entry.getValue());
// Use local file if it already exists
try {
@@ -115,11 +102,11 @@ public class JavaDownloadTask extends Task<Void> {
if (file.getDownloads().containsKey("lzma")) {
DownloadInfo download = file.getDownloads().get("lzma");
File tempFile = jvmDir.resolve(entry.getKey() + ".lzma").toFile();
File tempFile = target.resolve(entry.getKey() + ".lzma").toFile();
FileDownloadTask task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(download.getUrl()), tempFile, new FileDownloadTask.IntegrityCheck("SHA-1", download.getSha1()));
task.setName(entry.getKey());
dependencies.add(task.thenRunAsync(() -> {
Path decompressed = jvmDir.resolve(entry.getKey() + ".tmp");
Path decompressed = target.resolve(entry.getKey() + ".tmp");
try (LZMAInputStream input = new LZMAInputStream(new FileInputStream(tempFile))) {
Files.copy(input, decompressed, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
@@ -144,10 +131,10 @@ public class JavaDownloadTask extends Task<Void> {
} else {
continue;
}
} else if (entry.getValue() instanceof RemoteFiles.RemoteDirectory) {
} else if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteDirectory) {
Files.createDirectories(dest);
} else if (entry.getValue() instanceof RemoteFiles.RemoteLink) {
RemoteFiles.RemoteLink link = ((RemoteFiles.RemoteLink) entry.getValue());
} else if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteLink) {
MojangJavaRemoteFiles.RemoteLink link = ((MojangJavaRemoteFiles.RemoteLink) entry.getValue());
Files.deleteIfExists(dest);
Files.createSymbolicLink(dest, Paths.get(link.getTarget()));
}
@@ -166,16 +153,16 @@ public class JavaDownloadTask extends Task<Void> {
@Override
public void postExecute() throws Exception {
FileUtils.writeText(rootDir.resolve(javaVersion.getComponent()).resolve(platform).resolve(".version").toFile(), download.getVersion().getName());
FileUtils.writeText(rootDir.resolve(javaVersion.getComponent()).resolve(platform).resolve(javaVersion.getComponent() + ".sha1").toFile(),
javaDownloadsTask.getResult().getFiles().entrySet().stream()
.filter(entry -> entry.getValue() instanceof RemoteFiles.RemoteFile)
.map(entry -> {
RemoteFiles.RemoteFile file = (RemoteFiles.RemoteFile) entry.getValue();
return entry.getKey() + " /#// " + file.getDownloads().get("raw").getSha1() + " " + file.getDownloads().get("raw").getSize();
})
.collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)));
setResult(new Result(download, javaDownloadsTask.getResult()));
}
public static class UnsupportedPlatformException extends Exception {}
public static final class Result {
public final MojangJavaDownloads.JavaDownload download;
public final MojangJavaRemoteFiles remoteFiles;
public Result(MojangJavaDownloads.JavaDownload download, MojangJavaRemoteFiles remoteFiles) {
this.download = download;
this.remoteFiles = remoteFiles;
}
}
}

View File

@@ -15,7 +15,7 @@
* 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.download.java;
package org.jackhuang.hmcl.download.java.mojang;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
@@ -27,12 +27,12 @@ import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
@JsonAdapter(JavaDownloads.Adapter.class)
public class JavaDownloads {
@JsonAdapter(MojangJavaDownloads.Adapter.class)
public class MojangJavaDownloads {
private final Map<String, Map<String, List<JavaDownload>>> downloads;
public JavaDownloads(Map<String, Map<String, List<JavaDownload>>> downloads) {
public MojangJavaDownloads(Map<String, Map<String, List<JavaDownload>>> downloads) {
this.downloads = downloads;
}
@@ -40,16 +40,16 @@ public class JavaDownloads {
return downloads;
}
public static class Adapter implements JsonSerializer<JavaDownloads>, JsonDeserializer<JavaDownloads> {
public static class Adapter implements JsonSerializer<MojangJavaDownloads>, JsonDeserializer<MojangJavaDownloads> {
@Override
public JsonElement serialize(JavaDownloads src, Type typeOfSrc, JsonSerializationContext context) {
public JsonElement serialize(MojangJavaDownloads src, Type typeOfSrc, JsonSerializationContext context) {
return context.serialize(src.downloads);
}
@Override
public JavaDownloads deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return new JavaDownloads(context.deserialize(json, new TypeToken<Map<String, Map<String, List<JavaDownload>>>>() {
public MojangJavaDownloads deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return new MojangJavaDownloads(context.deserialize(json, new TypeToken<Map<String, Map<String, List<JavaDownload>>>>() {
}.getType()));
}
}

View File

@@ -15,7 +15,7 @@
* 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.download.java;
package org.jackhuang.hmcl.download.java.mojang;
import org.jackhuang.hmcl.game.DownloadInfo;
import org.jackhuang.hmcl.util.gson.JsonSubtype;
@@ -24,10 +24,10 @@ import org.jackhuang.hmcl.util.gson.JsonType;
import java.util.Collections;
import java.util.Map;
public class RemoteFiles {
public final class MojangJavaRemoteFiles {
private final Map<String, Remote> files;
public RemoteFiles(Map<String, Remote> files) {
public MojangJavaRemoteFiles(Map<String, Remote> files) {
this.files = files;
}

View File

@@ -0,0 +1,56 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.download.java.mojang;
import org.jackhuang.hmcl.download.java.JavaRemoteVersion;
import org.jackhuang.hmcl.game.GameJavaVersion;
/**
* @author Glavo
*/
public final class MojangJavaRemoteVersion implements JavaRemoteVersion {
private final GameJavaVersion gameJavaVersion;
public MojangJavaRemoteVersion(GameJavaVersion gameJavaVersion) {
this.gameJavaVersion = gameJavaVersion;
}
public GameJavaVersion getGameJavaVersion() {
return gameJavaVersion;
}
@Override
public int getJdkVersion() {
return gameJavaVersion.getMajorVersion();
}
@Override
public String getJavaVersion() {
return String.valueOf(getJdkVersion());
}
@Override
public String getDistributionVersion() {
return String.valueOf(getJdkVersion());
}
@Override
public String toString() {
return "MojangJavaRemoteVersion[gameJavaVersion=" + gameJavaVersion + "]";
}
}

View File

@@ -36,7 +36,7 @@ import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.NotNull;
@@ -125,7 +125,7 @@ public class NeoForgeOldInstallTask extends Task<Version> {
throw new Exception("Game processor jar does not have main class " + jar);
List<String> command = new ArrayList<>();
command.add(JavaVersion.fromCurrentEnvironment().getBinary().toString());
command.add(JavaRuntime.getDefault().getBinary().toString());
command.add("-cp");
List<String> classpath = new ArrayList<>(processor.getClasspath().size() + 1);

View File

@@ -27,7 +27,7 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jenkinsci.constant_pool_scanner.ConstantPool;
@@ -144,7 +144,7 @@ public final class OptiFineInstallTask extends Task<Version> {
Path optiFineLibraryPath = gameRepository.getLibraryFile(version, optiFineLibrary).toPath();
if (Files.exists(fs.getPath("optifine/Patcher.class"))) {
String[] command = {
JavaVersion.fromCurrentEnvironment().getBinary().toString(),
JavaRuntime.getDefault().getBinary().toString(),
"-cp",
dest.toString(),
"optifine.Patcher",

View File

@@ -17,7 +17,91 @@
*/
package org.jackhuang.hmcl.game;
public class GameJavaVersion {
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.util.*;
public final class GameJavaVersion {
public static final GameJavaVersion JAVA_21 = new GameJavaVersion("java-runtime-delta", 21);
public static final GameJavaVersion JAVA_17 = new GameJavaVersion("java-runtime-beta", 17);
public static final GameJavaVersion JAVA_16 = new GameJavaVersion("java-runtime-alpha", 16);
public static final GameJavaVersion JAVA_8 = new GameJavaVersion("jre-legacy", 8);
public static final GameJavaVersion LATEST = JAVA_21;
public static GameJavaVersion getMinimumJavaVersion(GameVersionNumber gameVersion) {
if (gameVersion.compareTo("1.21") >= 0)
return JAVA_21;
if (gameVersion.compareTo("1.18") >= 0)
return JAVA_17;
if (gameVersion.compareTo("1.17") >= 0)
return JAVA_16;
if (gameVersion.compareTo("1.13") >= 0)
return JAVA_8;
return null;
}
public static GameJavaVersion get(int major) {
switch (major) {
case 8:
return JAVA_8;
case 16:
return JAVA_16;
case 17:
return JAVA_17;
case 21:
return JAVA_21;
default:
return null;
}
}
public static boolean isSupportedPlatform(Platform platform) {
OperatingSystem os = platform.getOperatingSystem();
Architecture arch = platform.getArchitecture();
switch (arch) {
case X86:
return os == OperatingSystem.WINDOWS || os == OperatingSystem.LINUX;
case X86_64:
return os == OperatingSystem.WINDOWS || os == OperatingSystem.LINUX || os == OperatingSystem.OSX;
case ARM64:
return os == OperatingSystem.WINDOWS || os == OperatingSystem.OSX;
default:
return false;
}
}
public static List<GameJavaVersion> getSupportedVersions(Platform platform) {
OperatingSystem operatingSystem = platform.getOperatingSystem();
Architecture architecture = platform.getArchitecture();
if (architecture == Architecture.X86) {
switch (operatingSystem) {
case WINDOWS:
return Arrays.asList(JAVA_8, JAVA_16, JAVA_17);
case LINUX:
return Collections.singletonList(JAVA_8);
}
} else if (architecture == Architecture.X86_64) {
switch (operatingSystem) {
case WINDOWS:
case LINUX:
case OSX:
return Arrays.asList(JAVA_8, JAVA_16, JAVA_17, JAVA_21);
}
} else if (architecture == Architecture.ARM64) {
switch (operatingSystem) {
case WINDOWS:
case OSX:
return Arrays.asList(JAVA_17, JAVA_21);
}
}
return Collections.emptyList();
}
private final String component;
private final int majorVersion;
@@ -38,8 +122,16 @@ public class GameJavaVersion {
return majorVersion;
}
public static final GameJavaVersion JAVA_21 = new GameJavaVersion("java-runtime-delta", 21);
public static final GameJavaVersion JAVA_17 = new GameJavaVersion("java-runtime-beta", 17);
public static final GameJavaVersion JAVA_16 = new GameJavaVersion("java-runtime-alpha", 16);
public static final GameJavaVersion JAVA_8 = new GameJavaVersion("jre-legacy", 8);
@Override
public int hashCode() {
return getMajorVersion();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof GameJavaVersion)) return false;
GameJavaVersion that = (GameJavaVersion) o;
return majorVersion == that.majorVersion;
}
}

View File

@@ -20,7 +20,7 @@ package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
@@ -33,48 +33,47 @@ import java.util.Objects;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LAUNCH_WRAPPER_MAIN;
public enum JavaVersionConstraint {
// Minecraft>=1.13 requires Java 8
VANILLA_JAVA_8(JavaVersionConstraint.RULE_MANDATORY, GameVersionNumber.atLeast("1.13"), VersionNumber.atLeast("1.8")),
// Minecraft 1.17 requires Java 16
VANILLA_JAVA_16(JavaVersionConstraint.RULE_MANDATORY, GameVersionNumber.atLeast("1.17"), VersionNumber.atLeast("16")),
// Minecraft>=1.18 requires Java 17
VANILLA_JAVA_17(JavaVersionConstraint.RULE_MANDATORY, GameVersionNumber.atLeast("1.18"), VersionNumber.atLeast("17")),
// Minecraft>=1.20.5 requires Java 21
VANILLA_JAVA_21(JavaVersionConstraint.RULE_MANDATORY, GameVersionNumber.atLeast("1.20.5"), VersionNumber.atLeast("21")),
VANILLA(true, VersionRange.all(), VersionRange.all()) {
@Override
public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java) {
GameJavaVersion minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersionNumber);
return minimumJavaVersion == null || java.getParsedVersion() >= minimumJavaVersion.getMajorVersion();
}
},
// Minecraft<=1.7.2+Forge requires Java<=7, But LegacyModFixer may fix that problem. So only suggest user using Java 7.
MODDED_JAVA_7(JavaVersionConstraint.RULE_SUGGESTED, GameVersionNumber.atMost("1.7.2"), VersionNumber.atMost("1.7.999")) {
MODDED_JAVA_7(false, GameVersionNumber.atMost("1.7.2"), VersionNumber.atMost("1.7.999")) {
@Override
protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version,
@Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) {
@Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) {
return version != null && analyzer != null && analyzer.has(LibraryAnalyzer.LibraryType.FORGE);
}
},
MODDED_JAVA_8(JavaVersionConstraint.RULE_SUGGESTED, GameVersionNumber.between("1.7.10", "1.16.999"), VersionNumber.between("1.8", "1.8.999")) {
MODDED_JAVA_8(false, GameVersionNumber.between("1.7.10", "1.16.999"), VersionNumber.between("1.8", "1.8.999")) {
@Override
protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version,
@Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) {
@Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) {
return analyzer != null && analyzer.has(LibraryAnalyzer.LibraryType.FORGE);
}
},
MODDED_JAVA_16(JavaVersionConstraint.RULE_SUGGESTED, GameVersionNumber.between("1.17", "1.17.999"), VersionNumber.between("16", "16.999")) {
MODDED_JAVA_16(false, GameVersionNumber.between("1.17", "1.17.999"), VersionNumber.between("16", "16.999")) {
@Override
protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version,
@Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) {
@Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) {
return analyzer != null && analyzer.has(LibraryAnalyzer.LibraryType.FORGE);
}
},
MODDED_JAVA_17(JavaVersionConstraint.RULE_SUGGESTED, GameVersionNumber.atLeast("1.18"), VersionNumber.between("17", "17.999")) {
MODDED_JAVA_17(false, GameVersionNumber.atLeast("1.18"), VersionNumber.between("17", "17.999")) {
@Override
protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version,
@Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) {
@Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) {
return analyzer != null && analyzer.has(LibraryAnalyzer.LibraryType.FORGE);
}
},
// LaunchWrapper<=1.12 will crash because LaunchWrapper assumes the system class loader is an instance of URLClassLoader (Java 8)
LAUNCH_WRAPPER(JavaVersionConstraint.RULE_MANDATORY, GameVersionNumber.atMost("1.12.999"), VersionNumber.atMost("1.8.999")) {
LAUNCH_WRAPPER(true, GameVersionNumber.atMost("1.12.999"), VersionNumber.atMost("1.8.999")) {
@Override
protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version,
@Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) {
@Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) {
if (version == null) return false;
return LAUNCH_WRAPPER_MAIN.equals(version.getMainClass()) &&
version.getLibraries().stream()
@@ -83,12 +82,12 @@ public enum JavaVersionConstraint {
}
},
// Minecraft>=1.13 may crash when generating world on Java [1.8,1.8.0_51)
VANILLA_JAVA_8_51(JavaVersionConstraint.RULE_SUGGESTED, GameVersionNumber.atLeast("1.13"), VersionNumber.atLeast("1.8.0_51")),
VANILLA_JAVA_8_51(false, GameVersionNumber.atLeast("1.13"), VersionNumber.atLeast("1.8.0_51")),
// Minecraft with suggested java version recorded in game json is restrictedly constrained.
GAME_JSON(JavaVersionConstraint.RULE_MANDATORY, VersionRange.all(), VersionRange.all()) {
GAME_JSON(true, VersionRange.all(), VersionRange.all()) {
@Override
protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version,
@Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) {
@Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) {
if (version == null) return false;
// We only checks for 1.7.10 and above, since 1.7.2 with Forge can only run on Java 7, but it is recorded Java 8 in game json, which is not correct.
return gameVersionNumber.compareTo("1.7.10") >= 0 && version.getJavaVersion() != null;
@@ -107,26 +106,26 @@ public enum JavaVersionConstraint {
},
// On Linux, JDK 9+ cannot launch Minecraft<=1.12.2, since JDK 9+ does not accept loading native library built in different arch.
// For example, JDK 9+ 64-bit cannot load 32-bit lwjgl native library.
VANILLA_LINUX_JAVA_8(JavaVersionConstraint.RULE_MANDATORY, GameVersionNumber.atMost("1.12.999"), VersionNumber.atMost("1.8.999")) {
VANILLA_LINUX_JAVA_8(true, GameVersionNumber.atMost("1.12.999"), VersionNumber.atMost("1.8.999")) {
@Override
protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version,
@Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) {
@Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) {
return OperatingSystem.CURRENT_OS == OperatingSystem.LINUX
&& Architecture.SYSTEM_ARCH == Architecture.X86_64
&& (javaVersion == null || javaVersion.getArchitecture() == Architecture.X86_64);
&& (java == null || java.getArchitecture() == Architecture.X86_64);
}
@Override
public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaVersion javaVersion) {
return javaVersion.getArchitecture() != Architecture.X86_64 || super.checkJava(gameVersionNumber, version, javaVersion);
public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java) {
return java.getArchitecture() != Architecture.X86_64 || super.checkJava(gameVersionNumber, version, java);
}
},
// Minecraft currently does not provide official support for architectures other than x86 and x86-64.
VANILLA_X86(JavaVersionConstraint.RULE_SUGGESTED, VersionRange.all(), VersionRange.all()) {
VANILLA_X86(false, VersionRange.all(), VersionRange.all()) {
@Override
protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version,
@Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) {
if (javaVersion == null || javaVersion.getArchitecture() != Architecture.ARM64)
@Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) {
if (java == null || java.getArchitecture() != Architecture.ARM64)
return false;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
@@ -136,16 +135,16 @@ public enum JavaVersionConstraint {
}
@Override
public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaVersion javaVersion) {
return javaVersion.getArchitecture().isX86();
public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java) {
return java.getArchitecture().isX86();
}
},
// Minecraft 1.16+Forge with crash because JDK-8273826
MODLAUNCHER_8(JavaVersionConstraint.RULE_SUGGESTED, GameVersionNumber.between("1.16.3", "1.17.1"), VersionRange.all()) {
MODLAUNCHER_8(false, GameVersionNumber.between("1.16.3", "1.17.1"), VersionRange.all()) {
@Override
protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version,
@Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) {
if (version == null || javaVersion == null || analyzer == null) return false;
@Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) {
if (version == null || java == null || analyzer == null) return false;
VersionNumber forgePatchVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.FORGE)
.map(VersionNumber::asVersion)
.orElse(null);
@@ -167,36 +166,36 @@ public enum JavaVersionConstraint {
}
@Override
public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaVersion javaVersion) {
int parsedJavaVersion = javaVersion.getParsedVersion();
public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java) {
int parsedJavaVersion = java.getParsedVersion();
if (parsedJavaVersion > 17) {
return false;
} else if (parsedJavaVersion == 8) {
return javaVersion.getVersionNumber().compareTo(VersionNumber.asVersion("1.8.0_321")) < 0;
return java.getVersionNumber().compareTo(VersionNumber.asVersion("1.8.0_321")) < 0;
} else if (parsedJavaVersion == 11) {
return javaVersion.getVersionNumber().compareTo(VersionNumber.asVersion("11.0.14")) < 0;
return java.getVersionNumber().compareTo(VersionNumber.asVersion("11.0.14")) < 0;
} else if (parsedJavaVersion == 15) {
return javaVersion.getVersionNumber().compareTo(VersionNumber.asVersion("15.0.6")) < 0;
return java.getVersionNumber().compareTo(VersionNumber.asVersion("15.0.6")) < 0;
} else if (parsedJavaVersion == 17) {
return javaVersion.getVersionNumber().compareTo(VersionNumber.asVersion("17.0.2")) < 0;
return java.getVersionNumber().compareTo(VersionNumber.asVersion("17.0.2")) < 0;
} else {
return true;
}
}
};
private final int type;
private final boolean isMandatory;
private final VersionRange<GameVersionNumber> gameVersionRange;
private final VersionRange<VersionNumber> javaVersionRange;
JavaVersionConstraint(int type, VersionRange<GameVersionNumber> gameVersionRange, VersionRange<VersionNumber> javaVersionRange) {
this.type = type;
JavaVersionConstraint(boolean isMandatory, VersionRange<GameVersionNumber> gameVersionRange, VersionRange<VersionNumber> javaVersionRange) {
this.isMandatory = isMandatory;
this.gameVersionRange = gameVersionRange;
this.javaVersionRange = javaVersionRange;
}
public int getType() {
return type;
public boolean isMandatory() {
return isMandatory;
}
public VersionRange<GameVersionNumber> getGameVersionRange() {
@@ -208,112 +207,20 @@ public enum JavaVersionConstraint {
}
public final boolean appliesToVersion(@Nullable GameVersionNumber gameVersionNumber, @Nullable Version version,
@Nullable JavaVersion javaVersion, LibraryAnalyzer analyzer) {
@Nullable JavaRuntime java, LibraryAnalyzer analyzer) {
return gameVersionRange.contains(gameVersionNumber)
&& appliesToVersionImpl(gameVersionNumber, version, javaVersion, analyzer);
&& appliesToVersionImpl(gameVersionNumber, version, java, analyzer);
}
protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version,
@Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) {
@Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) {
return true;
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaVersion javaVersion) {
return getJavaVersionRange(version).contains(javaVersion.getVersionNumber());
public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java) {
return getJavaVersionRange(version).contains(java.getVersionNumber());
}
public static final List<JavaVersionConstraint> ALL = Lang.immutableListOf(values());
public static VersionRanges findSuitableJavaVersionRange(GameVersionNumber gameVersion, Version version) {
VersionRange<VersionNumber> mandatoryJavaRange = VersionRange.all();
VersionRange<VersionNumber> suggestedJavaRange = VersionRange.all();
LibraryAnalyzer analyzer = version != null ? LibraryAnalyzer.analyze(version, gameVersion != null ? gameVersion.toString() : null) : null;
for (JavaVersionConstraint java : ALL) {
if (java.appliesToVersion(gameVersion, version, null, analyzer)) {
VersionRange<VersionNumber> javaVersionRange = java.getJavaVersionRange(version);
if (java.type == RULE_MANDATORY) {
mandatoryJavaRange = mandatoryJavaRange.intersectionWith(javaVersionRange);
suggestedJavaRange = suggestedJavaRange.intersectionWith(javaVersionRange);
} else if (java.type == RULE_SUGGESTED) {
suggestedJavaRange = suggestedJavaRange.intersectionWith(javaVersionRange);
}
}
}
return new VersionRanges(mandatoryJavaRange, suggestedJavaRange);
}
@Nullable
public static JavaVersion findSuitableJavaVersion(GameVersionNumber gameVersion, Version version) throws InterruptedException {
VersionRanges range = findSuitableJavaVersionRange(gameVersion, version);
boolean forceX86 = Architecture.SYSTEM_ARCH == Architecture.ARM64
&& (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
&& gameVersion.compareTo("1.6") < 0;
JavaVersion mandatory = null;
JavaVersion suggested = null;
for (JavaVersion javaVersion : JavaVersion.getJavas()) {
// Do not automatically select 32-bit Java
if (Architecture.SYSTEM_ARCH == Architecture.X86_64 && javaVersion.getArchitecture() == Architecture.X86)
continue;
// select the latest x86 java that this version accepts.
if (forceX86 && !javaVersion.getArchitecture().isX86())
continue;
VersionNumber javaVersionNumber = javaVersion.getVersionNumber();
if (range.getMandatory().contains(javaVersionNumber)) {
if (mandatory == null) mandatory = javaVersion;
else if (compareJavaVersion(javaVersion, mandatory) > 0) {
mandatory = javaVersion;
}
}
if (range.getSuggested().contains(javaVersionNumber)) {
if (suggested == null) suggested = javaVersion;
else if (compareJavaVersion(javaVersion, suggested) > 0) {
suggested = javaVersion;
}
}
}
if (suggested != null) return suggested;
else return mandatory;
}
private static int compareJavaVersion(JavaVersion javaVersion1, JavaVersion javaVersion2) {
Architecture arch1 = javaVersion1.getArchitecture();
Architecture arch2 = javaVersion2.getArchitecture();
if (arch1 != arch2) {
if (arch1 == Architecture.X86_64) {
return 1;
}
if (arch2 == Architecture.X86_64) {
return -1;
}
}
return javaVersion1.getVersionNumber().compareTo(javaVersion2.getVersionNumber());
}
public static final int RULE_MANDATORY = 1;
public static final int RULE_SUGGESTED = 2;
public static final class VersionRanges {
private final VersionRange<VersionNumber> mandatory;
private final VersionRange<VersionNumber> suggested;
public VersionRanges(VersionRange<VersionNumber> mandatory, VersionRange<VersionNumber> suggested) {
this.mandatory = mandatory;
this.suggested = suggested;
}
public VersionRange<VersionNumber> getMandatory() {
return mandatory;
}
public VersionRange<VersionNumber> getSuggested() {
return suggested;
}
}
}

View File

@@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jetbrains.annotations.NotNull;
import java.io.File;
@@ -32,7 +32,7 @@ import java.util.*;
public class LaunchOptions implements Serializable {
private File gameDir;
private JavaVersion java;
private JavaRuntime java;
private String versionName;
private String versionType;
private String profileName;
@@ -73,7 +73,7 @@ public class LaunchOptions implements Serializable {
/**
* The Java Environment that Minecraft runs on.
*/
public JavaVersion getJava() {
public JavaRuntime getJava() {
return java;
}
@@ -312,7 +312,7 @@ public class LaunchOptions implements Serializable {
return this;
}
public Builder setJava(JavaVersion java) {
public Builder setJava(JavaRuntime java) {
options.java = java;
return this;
}

View File

@@ -0,0 +1,280 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.java;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.jackhuang.hmcl.util.KeyValuePairProperties;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* @author Glavo
*/
public final class JavaInfo {
public static int parseVersion(String version) {
try {
int idx = version.indexOf('.');
if (idx < 0) {
idx = version.indexOf('u');
return idx > 0 ? Integer.parseInt(version.substring(0, idx)) : Integer.parseInt(version);
} else {
int major = Integer.parseInt(version.substring(0, idx));
if (major != 1) {
return major;
} else {
int idx2 = version.indexOf('.', idx + 1);
if (idx2 < 0) {
return -1;
}
return Integer.parseInt(version.substring(idx + 1, idx2));
}
}
} catch (NumberFormatException e) {
return -1;
}
}
public static JavaInfo fromReleaseFile(BufferedReader reader) throws IOException {
KeyValuePairProperties properties = KeyValuePairProperties.load(reader);
String osName = properties.get("OS_NAME");
String osArch = properties.get("OS_ARCH");
String vendor = properties.get("IMPLEMENTOR");
OperatingSystem os = "".equals(osName) && "OpenJDK BSD Porting Team".equals(vendor)
? OperatingSystem.FREEBSD
: OperatingSystem.parseOSName(osName);
Architecture arch = Architecture.parseArchName(osArch);
String javaVersion = properties.get("JAVA_VERSION");
if (os == OperatingSystem.UNKNOWN)
throw new IOException("Unknown operating system: " + osName);
if (arch == Architecture.UNKNOWN)
throw new IOException("Unknown architecture: " + osArch);
if (javaVersion == null)
throw new IOException("Missing Java version");
return new JavaInfo(Platform.getPlatform(os, arch), javaVersion, vendor);
}
public static JavaInfo fromReleaseFile(Path releaseFile) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(releaseFile)) {
return fromReleaseFile(reader);
}
}
public static <F, E extends ArchiveEntry> JavaInfo fromArchive(ArchiveFileTree<F, E> tree) throws IOException {
if (tree.getRoot().getSubDirs().size() != 1 || !tree.getRoot().getFiles().isEmpty())
throw new IOException();
ArchiveFileTree.Dir<E> jdkRoot = tree.getRoot().getSubDirs().values().iterator().next();
E releaseEntry = jdkRoot.getFiles().get("release");
if (releaseEntry == null)
throw new IOException("Missing release file");
JavaInfo info;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(tree.getInputStream(releaseEntry), StandardCharsets.UTF_8))) {
info = JavaInfo.fromReleaseFile(reader);
}
ArchiveFileTree.Dir<E> binDir = jdkRoot.getSubDirs().get("bin");
if (binDir == null || binDir.getFiles().get(info.getPlatform().getOperatingSystem().getJavaExecutable()) == null)
throw new IOException("Missing java executable file");
return info;
}
public static String normalizeVendor(String vendor) {
if (vendor == null)
return null;
switch (vendor) {
case "N/A":
return null;
case "Oracle Corporation":
return "Oracle";
case "Azul Systems, Inc.":
return "Azul";
case "IBM Corporation":
case "International Business Machines Corporation":
return "IBM";
case "Eclipse Adoptium":
return "Adoptium";
default:
return vendor;
}
}
private static final String OS_ARCH = "os.arch = ";
private static final String JAVA_VERSION = "java.version = ";
private static final String JAVA_VENDOR = "java.vendor = ";
private static final String VERSION_PREFIX = "version \"";
public static JavaInfo fromExecutable(Path executable) throws IOException {
return fromExecutable(executable, true);
}
public static JavaInfo fromExecutable(Path executable, boolean tryFindReleaseFile) throws IOException {
assert executable.isAbsolute();
Path parent = executable.getParent();
if (tryFindReleaseFile && parent != null && parent.getFileName() != null && parent.getFileName().toString().equals("bin")) {
Path javaHome = parent.getParent();
if (javaHome != null && javaHome.getFileName() != null) {
Path releaseFile = javaHome.resolve("release");
String javaHomeName = javaHome.getFileName().toString();
if ((javaHomeName.contains("jre") || javaHomeName.contains("jdk") || javaHomeName.contains("openj9")) && Files.isRegularFile(releaseFile)) {
try {
return fromReleaseFile(releaseFile);
} catch (IOException ignored) {
}
}
}
}
String osArch = null;
String version = null;
String vendor = null;
Platform platform = null;
String executablePath = executable.toString();
Process process = new ProcessBuilder(executablePath, "-XshowSettings:properties", "-version").start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream(), OperatingSystem.NATIVE_CHARSET))) {
for (String line; (line = reader.readLine()) != null; ) {
int idx = line.indexOf(OS_ARCH);
if (idx >= 0) {
osArch = line.substring(idx + OS_ARCH.length()).trim();
if (version != null && vendor != null)
break;
else
continue;
}
idx = line.indexOf(JAVA_VERSION);
if (idx >= 0) {
version = line.substring(idx + JAVA_VERSION.length()).trim();
if (osArch != null && vendor != null)
break;
else
continue;
}
idx = line.indexOf(JAVA_VENDOR);
if (idx >= 0) {
vendor = line.substring(idx + JAVA_VENDOR.length()).trim();
if (osArch != null && version != null)
break;
else
//noinspection UnnecessaryContinue
continue;
}
}
}
if (osArch != null)
platform = Platform.getPlatform(OperatingSystem.CURRENT_OS, Architecture.parseArchName(osArch));
// Java 6
if (version == null) {
boolean is64Bit = false;
process = new ProcessBuilder(executablePath, "-version").start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream(), OperatingSystem.NATIVE_CHARSET))) {
for (String line; (line = reader.readLine()) != null; ) {
if (version == null) {
int idx = line.indexOf(VERSION_PREFIX);
if (idx >= 0) {
int begin = idx + VERSION_PREFIX.length();
int end = line.indexOf('"', begin);
if (end >= 0) {
version = line.substring(begin, end);
}
}
}
if (line.contains("64-Bit"))
is64Bit = true;
}
}
if (platform == null)
platform = Platform.getPlatform(OperatingSystem.CURRENT_OS, is64Bit ? Architecture.X86_64 : Architecture.X86);
if (version == null)
throw new IOException("Cannot determine version");
}
return new JavaInfo(platform, version, vendor);
}
public static final JavaInfo CURRENT_ENVIRONMENT = new JavaInfo(Platform.CURRENT_PLATFORM, System.getProperty("java.version"), System.getProperty("java.vendor"));
private final Platform platform;
private final String version;
private final @Nullable String vendor;
private final transient int parsedVersion;
private final transient VersionNumber versionNumber;
public JavaInfo(Platform platform, String version, @Nullable String vendor) {
this.platform = platform;
this.version = version;
this.parsedVersion = parseVersion(version);
this.versionNumber = VersionNumber.asVersion(version);
this.vendor = vendor;
}
public Platform getPlatform() {
return platform;
}
public String getVersion() {
return version;
}
public VersionNumber getVersionNumber() {
return versionNumber;
}
public int getParsedVersion() {
return parsedVersion;
}
public @Nullable String getVendor() {
return vendor;
}
@Override
public String toString() {
return JsonUtils.GSON.toJson(this);
}
}

View File

@@ -0,0 +1,44 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.java;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.game.GameJavaVersion;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.platform.Platform;
import java.nio.file.Path;
import java.util.Collection;
/**
* @author Glavo
*/
public interface JavaRepository {
Path getJavaDir(Platform platform, String name);
Path getManifestFile(Platform platform, String name);
Collection<JavaRuntime> getAllJava(Platform platform);
Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion);
Task<Void> getUninstallJavaTask(Platform platform, String name);
Task<Void> getUninstallJavaTask(JavaRuntime java);
}

View File

@@ -0,0 +1,156 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.java;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.Bits;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* @author Glavo
*/
public final class JavaRuntime implements Comparable<JavaRuntime> {
public static JavaRuntime of(Path binary, JavaInfo info, boolean isManaged) {
String javacName = info.getPlatform().getOperatingSystem() == OperatingSystem.WINDOWS ? "javac.exe" : "javac";
return new JavaRuntime(binary, info, isManaged, Files.isRegularFile(binary.resolveSibling(javacName)));
}
private final Path binary;
private final JavaInfo info;
private final boolean isManaged;
private final boolean isJDK;
public JavaRuntime(Path binary, JavaInfo info, boolean isManaged, boolean isJDK) {
this.binary = binary;
this.info = info;
this.isManaged = isManaged;
this.isJDK = isJDK;
}
public boolean isManaged() {
return isManaged;
}
public Path getBinary() {
return binary;
}
public String getVersion() {
return info.getVersion();
}
public Platform getPlatform() {
return info.getPlatform();
}
public Architecture getArchitecture() {
return getPlatform().getArchitecture();
}
public Bits getBits() {
return getPlatform().getBits();
}
public VersionNumber getVersionNumber() {
return info.getVersionNumber();
}
/**
* The major version of Java installation.
*/
public int getParsedVersion() {
return info.getParsedVersion();
}
public String getVendor() {
return info.getVendor();
}
public boolean isJDK() {
return isJDK;
}
@Override
public int compareTo(@NotNull JavaRuntime that) {
if (this.isManaged != that.isManaged) {
return this.isManaged ? -1 : 1;
}
int c = Integer.compare(this.getParsedVersion(), that.getParsedVersion());
if (c != 0)
return c;
c = this.getVersionNumber().compareTo(that.getVersionNumber());
if (c != 0)
return c;
c = this.getArchitecture().compareTo(that.getArchitecture());
if (c != 0)
return c;
return this.getBinary().compareTo(that.getBinary());
}
@Override
public int hashCode() {
return binary.hashCode();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof JavaRuntime)) return false;
JavaRuntime that = (JavaRuntime) o;
return this.getBinary().equals(that.getBinary());
}
public static final JavaRuntime CURRENT_JAVA;
public static final int CURRENT_VERSION;
public static JavaRuntime getDefault() {
return CURRENT_JAVA;
}
static {
String javaHome = System.getProperty("java.home");
Path executable = null;
if (javaHome != null) {
executable = Paths.get(javaHome, "bin", OperatingSystem.CURRENT_OS.getJavaExecutable());
try {
executable = executable.toRealPath();
} catch (IOException ignored) {
}
if (!Files.isRegularFile(executable)) {
executable = null;
}
}
CURRENT_JAVA = executable != null ? JavaRuntime.of(executable, JavaInfo.CURRENT_ENVIRONMENT, false) : null;
CURRENT_VERSION = JavaInfo.CURRENT_ENVIRONMENT.getParsedVersion();
}
}

View File

@@ -133,7 +133,7 @@ public class DefaultLauncher extends Launcher {
res.addDefault("-Xms", options.getMinMemory() + "m");
if (options.getMetaspace() != null && options.getMetaspace() > 0)
if (options.getJava().getParsedVersion() < JavaVersion.JAVA_8)
if (options.getJava().getParsedVersion() < 8)
res.addDefault("-XX:PermSize=", options.getMetaspace() + "m");
else
res.addDefault("-XX:MetaspaceSize=", options.getMetaspace() + "m");
@@ -186,7 +186,7 @@ public class DefaultLauncher extends Launcher {
res.addDefault("-Duser.home=", options.getGameDir().getParent());
// Using G1GC with its settings by default
if (options.getJava().getParsedVersion() >= JavaVersion.JAVA_8
if (options.getJava().getParsedVersion() >= 8
&& res.noneMatch(arg -> "-XX:-UseG1GC".equals(arg) || (arg.startsWith("-XX:+Use") && arg.endsWith("GC")))) {
res.addUnstableDefault("UnlockExperimentalVMOptions", true);
res.addUnstableDefault("UseG1GC", true);
@@ -206,7 +206,7 @@ public class DefaultLauncher extends Launcher {
res.addDefault("-Xss", "1m");
}
if (options.getJava().getParsedVersion() == JavaVersion.JAVA_16)
if (options.getJava().getParsedVersion() == 16)
res.addDefault("--illegal-access=", "permit");
res.addDefault("-Dfml.ignoreInvalidMinecraftCertificates=", "true");
@@ -308,7 +308,7 @@ public class DefaultLauncher extends Launcher {
}
private final Map<String, Supplier<Boolean>> forbiddens = mapOf(
pair("-Xincgc", () -> options.getJava().getParsedVersion() >= JavaVersion.JAVA_9)
pair("-Xincgc", () -> options.getJava().getParsedVersion() >= 9)
);
protected Map<String, Supplier<Boolean>> getForbiddens() {

View File

@@ -0,0 +1,94 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.util;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
/**
* @author Glavo
*/
public final class KeyValuePairProperties extends LinkedHashMap<String, String> {
public static KeyValuePairProperties load(Path file) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(file)) {
return load(reader);
}
}
public static KeyValuePairProperties load(BufferedReader reader) throws IOException {
KeyValuePairProperties result = new KeyValuePairProperties();
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("#"))
continue;
int idx = line.indexOf('=');
if (idx <= 0)
continue;
String name = line.substring(0, idx);
String value;
if (line.length() > idx + 2 && line.charAt(idx + 1) == '"' && line.charAt(line.length() - 1) == '"') {
if (line.indexOf('\\', idx + 1) < 0) {
value = line.substring(idx + 2, line.length() - 1);
} else {
StringBuilder builder = new StringBuilder();
for (int i = idx + 2, end = line.length() - 1; i < end; i++) {
char ch = line.charAt(i);
if (ch == '\\' && i < end - 1) {
char nextChar = line.charAt(++i);
switch (nextChar) {
case 'n':
builder.append('\n');
break;
case 'r':
builder.append('\r');
break;
case 't':
builder.append('\t');
break;
case 'f':
builder.append('\f');
break;
case 'b':
builder.append('\b');
break;
default:
builder.append(nextChar);
break;
}
} else {
builder.append(ch);
}
}
value = builder.toString();
}
} else {
value = line.substring(idx + 1);
}
result.put(name, value);
}
return result;
}
}

View File

@@ -377,15 +377,15 @@ public final class StringUtils {
return result.toString();
}
public static int MAX_SHORT_STRING_LENGTH = 77;
public static String truncate(String str, int limit) {
assert limit > 5;
public static Optional<String> truncate(String str) {
if (str.length() <= MAX_SHORT_STRING_LENGTH) {
return Optional.empty();
if (str.length() <= limit) {
return str;
}
final int halfLength = (MAX_SHORT_STRING_LENGTH - 5) / 2;
return Optional.of(str.substring(0, halfLength) + " ... " + str.substring(str.length() - halfLength));
final int halfLength = (limit - 5) / 2;
return str.substring(0, halfLength) + " ... " + str.substring(str.length() - halfLength);
}
public static boolean isASCII(String cs) {

View File

@@ -21,6 +21,7 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import java.io.File;
import java.io.IOException;
@@ -73,6 +74,13 @@ public final class JsonUtils {
return parsed;
}
public static <T> T fromNonNullJson(String json, TypeToken<T> type) throws JsonParseException {
T parsed = GSON.fromJson(json, type);
if (parsed == null)
throw new JsonParseException("Json object cannot be null.");
return parsed;
}
public static <T> T fromNonNullJsonFully(InputStream json, Class<T> classOfT) throws IOException, JsonParseException {
try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) {
T parsed = GSON.fromJson(reader, classOfT);

View File

@@ -90,4 +90,21 @@ public final class IOUtils {
public static InputStream wrapFromGZip(InputStream inputStream) throws IOException {
return new GZIPInputStream(inputStream);
}
public static void closeQuietly(AutoCloseable closeable) {
try {
if (closeable != null)
closeable.close();
} catch (Throwable ignored) {
}
}
public static void closeQuietly(AutoCloseable closeable, Throwable exception) {
try {
if (closeable != null)
closeable.close();
} catch (Throwable e) {
exception.addSuppressed(e);
}
}
}

View File

@@ -176,6 +176,9 @@ public enum Architecture {
return LOONGARCH64_OW;
return LOONGARCH64;
}
case "loongarch64_ow": {
return LOONGARCH64_OW;
}
default:
if (value.startsWith("armv7")) {
return ARM32;

View File

@@ -1,449 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 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.util.platform;
import org.jackhuang.hmcl.download.java.JavaRepository;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* Represents a Java installation.
*
* @author huangyuhui
*/
public final class JavaVersion {
private final Path binary;
private final String longVersion;
private final Platform platform;
private final int version;
private final VersionNumber versionNumber;
public JavaVersion(Path binary, String longVersion, Platform platform) {
this.binary = binary;
this.longVersion = longVersion;
this.platform = platform;
if (longVersion != null) {
version = parseVersion(longVersion);
versionNumber = VersionNumber.asVersion(longVersion);
} else {
version = UNKNOWN;
versionNumber = null;
}
}
public String toString() {
return "JavaVersion {" + binary + ", " + longVersion + "(" + version + ")" + ", " + platform + "}";
}
public Path getBinary() {
return binary;
}
public String getVersion() {
return longVersion;
}
public Platform getPlatform() {
return platform;
}
public Architecture getArchitecture() {
return platform.getArchitecture();
}
public Bits getBits() {
return platform.getBits();
}
public VersionNumber getVersionNumber() {
return versionNumber;
}
/**
* The major version of Java installation.
*
* @see org.jackhuang.hmcl.util.platform.JavaVersion#JAVA_9
* @see org.jackhuang.hmcl.util.platform.JavaVersion#JAVA_8
* @see org.jackhuang.hmcl.util.platform.JavaVersion#JAVA_7
* @see org.jackhuang.hmcl.util.platform.JavaVersion#UNKNOWN
*/
public int getParsedVersion() {
return version;
}
private static final Pattern REGEX = Pattern.compile("version \"(?<version>(.*?))\"");
private static final Pattern VERSION = Pattern.compile("^(?<version>[0-9]+)");
private static final Pattern OS_ARCH = Pattern.compile("os\\.arch = (?<arch>.*)");
private static final Pattern JAVA_VERSION = Pattern.compile("java\\.version = (?<version>.*)");
private static final String JAVA_EXECUTABLE = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "java.exe" : "java";
public static final int UNKNOWN = -1;
public static final int JAVA_6 = 6;
public static final int JAVA_7 = 7;
public static final int JAVA_8 = 8;
public static final int JAVA_9 = 9;
public static final int JAVA_16 = 16;
public static final int JAVA_17 = 17;
private static int parseVersion(String version) {
Matcher matcher = VERSION.matcher(version);
if (matcher.find()) {
int head = Lang.parseInt(matcher.group(), -1);
if (head > 1) return head;
}
if (version.contains("1.8"))
return JAVA_8;
else if (version.contains("1.7"))
return JAVA_7;
else if (version.contains("1.6"))
return JAVA_6;
else
return UNKNOWN;
}
private static final Map<Path, JavaVersion> fromExecutableCache = new ConcurrentHashMap<>();
public static JavaVersion fromExecutable(Path executable) throws IOException {
executable = executable.toRealPath();
JavaVersion cachedJavaVersion = fromExecutableCache.get(executable);
if (cachedJavaVersion != null)
return cachedJavaVersion;
String osArch = null;
String version = null;
Platform platform = null;
Process process = new ProcessBuilder(executable.toString(), "-XshowSettings:properties", "-version").start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream(), OperatingSystem.NATIVE_CHARSET))) {
for (String line; (line = reader.readLine()) != null; ) {
Matcher m;
m = OS_ARCH.matcher(line);
if (m.find()) {
osArch = m.group("arch");
if (version != null) {
break;
} else {
continue;
}
}
m = JAVA_VERSION.matcher(line);
if (m.find()) {
version = m.group("version");
if (osArch != null) {
break;
} else {
//noinspection UnnecessaryContinue
continue;
}
}
}
}
if (osArch != null) {
platform = Platform.getPlatform(OperatingSystem.CURRENT_OS, Architecture.parseArchName(osArch));
}
if (version == null) {
boolean is64Bit = false;
process = new ProcessBuilder(executable.toString(), "-version").start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream(), OperatingSystem.NATIVE_CHARSET))) {
for (String line; (line = reader.readLine()) != null; ) {
Matcher m = REGEX.matcher(line);
if (m.find())
version = m.group("version");
if (line.contains("64-Bit"))
is64Bit = true;
}
}
if (platform == null) {
platform = Platform.getPlatform(OperatingSystem.CURRENT_OS, is64Bit ? Architecture.X86_64 : Architecture.X86);
}
}
JavaVersion javaVersion = new JavaVersion(executable, version, platform);
if (javaVersion.getParsedVersion() == UNKNOWN)
throw new IOException("Unrecognized Java version " + version + " at " + executable);
fromExecutableCache.put(executable, javaVersion);
return javaVersion;
}
public static Path getExecutable(Path javaHome) {
return javaHome.resolve("bin").resolve(JAVA_EXECUTABLE);
}
public static JavaVersion fromCurrentEnvironment() {
return CURRENT_JAVA;
}
public static final JavaVersion CURRENT_JAVA;
static {
Path currentExecutable = getExecutable(Paths.get(System.getProperty("java.home")).toAbsolutePath());
try {
currentExecutable = currentExecutable.toRealPath();
} catch (IOException e) {
LOG.warning("Failed to resolve current Java path: " + currentExecutable, e);
}
CURRENT_JAVA = new JavaVersion(
currentExecutable,
System.getProperty("java.version"),
Platform.CURRENT_PLATFORM
);
}
private static Collection<JavaVersion> JAVAS;
private static final CountDownLatch LATCH = new CountDownLatch(1);
public static Collection<JavaVersion> getJavas() throws InterruptedException {
if (JAVAS != null)
return JAVAS;
LATCH.await();
return JAVAS;
}
public static synchronized void initialize() {
if (JAVAS != null)
throw new IllegalStateException("JavaVersions have already been initialized.");
List<JavaVersion> javaVersions;
try (Stream<Path> stream = searchPotentialJavaExecutables()) {
javaVersions = lookupJavas(stream);
} catch (IOException e) {
LOG.warning("Failed to search Java homes", e);
javaVersions = new ArrayList<>();
}
// insert current java to the list
if (!javaVersions.contains(CURRENT_JAVA)) {
javaVersions.add(CURRENT_JAVA);
}
JAVAS = Collections.newSetFromMap(new ConcurrentHashMap<>());
JAVAS.addAll(javaVersions);
LOG.trace("Finished Java installation lookup, found " + JAVAS.size());
LATCH.countDown();
}
private static List<JavaVersion> lookupJavas(Stream<Path> javaExecutables) {
return javaExecutables
.filter(Files::isExecutable)
.flatMap(executable -> { // resolve symbolic links
try {
return Stream.of(executable.toRealPath());
} catch (IOException e) {
LOG.warning("Failed to lookup Java executable at " + executable, e);
return Stream.empty();
}
})
.distinct() // remove duplicated javas
.flatMap(executable -> {
if (executable.equals(CURRENT_JAVA.getBinary())) {
return Stream.of(CURRENT_JAVA);
}
try {
LOG.trace("Looking for Java:" + executable);
Future<JavaVersion> future = Schedulers.io().submit(() -> fromExecutable(executable));
JavaVersion javaVersion = future.get(5, TimeUnit.SECONDS);
LOG.trace("Found Java (" + javaVersion.getVersion() + ") " + javaVersion.getBinary().toString());
return Stream.of(javaVersion);
} catch (ExecutionException | InterruptedException | TimeoutException e) {
LOG.warning("Failed to determine Java at " + executable, e);
return Stream.empty();
}
})
.collect(toList());
}
private static Stream<Path> searchPotentialJavaExecutables() throws IOException {
// Add order:
// 1. System-defined locations
// 2. Minecraft-installed locations
// 3. PATH
List<Stream<Path>> javaExecutables = new ArrayList<>();
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
javaExecutables.add(queryJavaHomesInRegistryKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Runtime Environment\\").stream().map(JavaVersion::getExecutable));
javaExecutables.add(queryJavaHomesInRegistryKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit\\").stream().map(JavaVersion::getExecutable));
javaExecutables.add(queryJavaHomesInRegistryKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JRE\\").stream().map(JavaVersion::getExecutable));
javaExecutables.add(queryJavaHomesInRegistryKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\\").stream().map(JavaVersion::getExecutable));
for (Optional<Path> programFiles : Arrays.asList(
FileUtils.tryGetPath(Optional.ofNullable(System.getenv("ProgramFiles")).orElse("C:\\Program Files")),
FileUtils.tryGetPath(Optional.ofNullable(System.getenv("ProgramFiles(x86)")).orElse("C:\\Program Files (x86)")),
FileUtils.tryGetPath(Optional.ofNullable(System.getenv("ProgramFiles(ARM)")).orElse("C:\\Program Files (ARM)"))
)) {
if (!programFiles.isPresent())
continue;
for (String vendor : new String[]{"Java", "BellSoft", "AdoptOpenJDK", "Zulu", "Microsoft", "Eclipse Foundation", "Semeru"}) {
javaExecutables.add(listDirectory(programFiles.get().resolve(vendor)).map(JavaVersion::getExecutable));
}
}
break;
case LINUX:
case FREEBSD:
javaExecutables.add(listDirectory(Paths.get("/usr/java")).map(JavaVersion::getExecutable)); // Oracle RPMs
javaExecutables.add(listDirectory(Paths.get("/usr/lib/jvm")).map(JavaVersion::getExecutable)); // General locations
javaExecutables.add(listDirectory(Paths.get("/usr/lib32/jvm")).map(JavaVersion::getExecutable)); // General locations
javaExecutables.add(listDirectory(Paths.get(System.getProperty("user.home"), ".sdkman/candidates/java")).map(JavaVersion::getExecutable)); // SDKMAN!
break;
case OSX:
javaExecutables.add(listDirectory(Paths.get("/Library/Java/JavaVirtualMachines"))
.flatMap(dir -> Stream.of(dir.resolve("Contents/Home"), dir.resolve("Contents/Home/jre")))
.map(JavaVersion::getExecutable));
javaExecutables.add(listDirectory(Paths.get(System.getProperty("user.home"), "Library/Java/JavaVirtualMachines"))
.flatMap(dir -> Stream.of(dir.resolve("Contents/Home"), dir.resolve("Contents/Home/jre")))
.map(JavaVersion::getExecutable));
javaExecutables.add(listDirectory(Paths.get("/System/Library/Java/JavaVirtualMachines"))
.map(dir -> dir.resolve("Contents/Home"))
.map(JavaVersion::getExecutable));
javaExecutables.add(Stream.of(Paths.get("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java")));
javaExecutables.add(Stream.of(Paths.get("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java")));
// Homebrew
javaExecutables.add(Stream.of(Paths.get("/opt/homebrew/opt/java/bin/java")));
javaExecutables.add(listDirectory(Paths.get("/opt/homebrew/Cellar/openjdk"))
.map(JavaVersion::getExecutable));
break;
default:
break;
}
// Search Minecraft bundled runtimes.
javaExecutables.add(Stream.concat(Stream.of(Optional.of(JavaRepository.getJavaStoragePath())), JavaRepository.findMinecraftRuntimeDirs())
.flatMap(Lang::toStream)
.flatMap(JavaRepository::findJavaHomeInMinecraftRuntimeDir)
.map(JavaVersion::getExecutable));
// Search in PATH.
if (System.getenv("PATH") != null) {
javaExecutables.add(Arrays.stream(System.getenv("PATH").split(OperatingSystem.PATH_SEPARATOR))
.flatMap(path -> Lang.toStream(FileUtils.tryGetPath(path, JAVA_EXECUTABLE))));
}
// Search in HMCL_JRES, convenient environment variable for users to add JRE in global
// May be removed when we implement global Java configuration.
if (System.getenv("HMCL_JRES") != null) {
javaExecutables.add(Arrays.stream(System.getenv("HMCL_JRES").split(OperatingSystem.PATH_SEPARATOR))
.flatMap(path -> Lang.toStream(FileUtils.tryGetPath(path, "bin", JAVA_EXECUTABLE))));
}
return javaExecutables.parallelStream().flatMap(stream -> stream);
}
private static Stream<Path> listDirectory(Path directory) throws IOException {
if (Files.isDirectory(directory)) {
try (final DirectoryStream<Path> subDirs = Files.newDirectoryStream(directory)) {
final ArrayList<Path> paths = new ArrayList<>();
for (Path subDir : subDirs) {
paths.add(subDir);
}
return paths.stream();
}
} else {
return Stream.empty();
}
}
// ==== Windows Registry Support ====
private static List<Path> queryJavaHomesInRegistryKey(String location) throws IOException {
List<Path> homes = new ArrayList<>();
for (String java : querySubFolders(location)) {
if (!querySubFolders(java).contains(java + "\\MSI"))
continue;
String home = queryRegisterValue(java, "JavaHome");
if (home != null) {
try {
homes.add(Paths.get(home));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path in system registry: " + home);
}
}
}
return homes;
}
private static List<String> querySubFolders(String location) throws IOException {
List<String> res = new ArrayList<>();
Process process = Runtime.getRuntime().exec(new String[] { "cmd", "/c", "reg", "query", location });
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) {
for (String line; (line = reader.readLine()) != null;) {
if (line.startsWith(location) && !line.equals(location)) {
res.add(line);
}
}
}
return res;
}
private static String queryRegisterValue(String location, String name) throws IOException {
boolean last = false;
Process process = Runtime.getRuntime().exec(new String[] { "cmd", "/c", "reg", "query", location, "/v", name });
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) {
for (String line; (line = reader.readLine()) != null;) {
if (StringUtils.isNotBlank(line)) {
if (last && line.trim().startsWith(name)) {
int begins = line.indexOf(name);
if (begins > 0) {
String s2 = line.substring(begins + name.length());
begins = s2.indexOf("REG_SZ");
if (begins > 0) {
return s2.substring(begins + "REG_SZ".length()).trim();
}
}
}
if (location.equals(line.trim())) {
last = true;
}
}
}
}
return null;
}
}

View File

@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.util.platform;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.launch.StreamPump;
import org.jackhuang.hmcl.util.Lang;
@@ -90,7 +91,7 @@ public final class ManagedProcess {
* @return PID
*/
public long getPID() throws UnsupportedOperationException {
if (JavaVersion.CURRENT_JAVA.getParsedVersion() >= 9) {
if (JavaRuntime.CURRENT_VERSION >= 9) {
// Method Process.pid() is provided (Java 9 or later). Invoke it to get the pid.
try {
return (long) MethodHandles.publicLookup()

View File

@@ -74,6 +74,10 @@ public enum OperatingSystem {
return this == LINUX || this == FREEBSD;
}
public String getJavaExecutable() {
return this == WINDOWS ? "java.exe" : "java";
}
/**
* The current operating system.
*/
@@ -215,10 +219,10 @@ public enum OperatingSystem {
name = name.trim().toLowerCase(Locale.ROOT);
if (name.contains("win"))
return WINDOWS;
else if (name.contains("mac"))
if (name.contains("mac") || name.contains("darwin") || name.contains("osx"))
return OSX;
else if (name.contains("win"))
return WINDOWS;
else if (name.contains("solaris") || name.contains("linux") || name.contains("unix") || name.contains("sunos"))
return LINUX;
else if (name.equals("freebsd"))

View File

@@ -5,6 +5,7 @@ import java.util.Objects;
public final class Platform {
public static final Platform UNKNOWN = new Platform(OperatingSystem.UNKNOWN, Architecture.UNKNOWN);
public static final Platform WINDOWS_X86 = new Platform(OperatingSystem.WINDOWS, Architecture.X86);
public static final Platform WINDOWS_X86_64 = new Platform(OperatingSystem.WINDOWS, Architecture.X86_64);
public static final Platform WINDOWS_ARM64 = new Platform(OperatingSystem.WINDOWS, Architecture.ARM64);
@@ -78,6 +79,10 @@ public final class Platform {
return Objects.hash(os, arch);
}
public boolean equals(OperatingSystem os, Architecture arch) {
return this.os == os && this.arch == arch;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@@ -17,6 +17,8 @@
*/
package org.jackhuang.hmcl.util.platform;
import org.jackhuang.hmcl.java.JavaRuntime;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
@@ -42,8 +44,7 @@ public final class SystemUtils {
}
public static boolean supportJVMAttachment() {
return JavaVersion.CURRENT_JAVA.getParsedVersion() >= 9
&& Thread.currentThread().getContextClassLoader().getResource("com/sun/tools/attach/VirtualMachine.class") != null;
return JavaRuntime.CURRENT_VERSION >= 9 && Thread.currentThread().getContextClassLoader().getResource("com/sun/tools/attach/VirtualMachine.class") != null;
}
private static void onLogLine(String log) {

View File

@@ -0,0 +1,30 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.util.platform;
/**
* @author Glavo
*/
public final class UnsupportedPlatformException extends Exception {
public UnsupportedPlatformException() {
}
public UnsupportedPlatformException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,129 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.util.tree;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.*;
/**
* @author Glavo
*/
public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Closeable {
public static ArchiveFileTree<?, ?> open(Path file) throws IOException {
Path namePath = file.getFileName();
if (namePath == null) {
throw new IOException(file + " is not a valid archive file");
}
String name = namePath.toString();
if (name.endsWith(".jar") || name.endsWith(".zip")) {
return new ZipFileTree(new ZipFile(file));
} else if (name.endsWith(".tar") || name.endsWith(".tar.gz") || name.endsWith(".tgz")) {
return TarFileTree.open(file);
} else {
throw new IOException(file + " is not a valid archive file");
}
}
protected final F file;
protected final Dir<E> root = new Dir<>();
public ArchiveFileTree(F file) {
this.file = file;
}
public F getFile() {
return file;
}
public Dir<E> getRoot() {
return root;
}
public void addEntry(E entry) throws IOException {
String[] path = entry.getName().split("/");
Dir<E> dir = root;
for (int i = 0, end = entry.isDirectory() ? path.length : path.length - 1; i < end; i++) {
String item = path[i];
if (item.equals("."))
continue;
if (item.equals("..") || item.isEmpty())
throw new IOException("Invalid entry: " + entry.getName());
if (dir.files.containsKey(item)) {
throw new IOException("A file and a directory have the same name: " + entry.getName());
}
dir = dir.subDirs.computeIfAbsent(item, name -> new Dir<>());
}
if (entry.isDirectory()) {
if (dir.entry != null) {
throw new IOException("Duplicate entry: " + entry.getName());
}
dir.entry = entry;
} else {
String fileName = path[path.length - 1];
if (dir.subDirs.containsKey(fileName)) {
throw new IOException("A file and a directory have the same name: " + entry.getName());
}
if (dir.files.containsKey(fileName)) {
throw new IOException("Duplicate entry: " + entry.getName());
}
dir.files.put(fileName, entry);
}
}
public abstract InputStream getInputStream(E entry) throws IOException;
public abstract boolean isLink(E entry);
public abstract String getLink(E entry) throws IOException;
public abstract boolean isExecutable(E entry);
@Override
public abstract void close() throws IOException;
public static final class Dir<E extends ArchiveEntry> {
E entry;
final Map<String, Dir<E>> subDirs = new HashMap<>();
final Map<String, E> files = new HashMap<>();
public Map<String, Dir<E>> getSubDirs() {
return subDirs;
}
public Map<String, E> getFiles() {
return files;
}
}
}

View File

@@ -0,0 +1,133 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.util.tree;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarFile;
import org.jackhuang.hmcl.util.io.IOUtils;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.zip.GZIPInputStream;
/**
* @author Glavo
*/
public final class TarFileTree extends ArchiveFileTree<TarFile, TarArchiveEntry> {
public static TarFileTree open(Path file) throws IOException {
String fileName = file.getFileName().toString();
if (fileName.endsWith(".tar.gz") || fileName.endsWith(".tgz")) {
Path tempFile = Files.createTempFile("hmcl-", ".tar");
TarFile tarFile;
try (GZIPInputStream input = new GZIPInputStream(Files.newInputStream(file));
OutputStream output = Files.newOutputStream(tempFile, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE)
) {
IOUtils.copyTo(input, output);
tarFile = new TarFile(tempFile.toFile());
} catch (Throwable e) {
try {
Files.deleteIfExists(tempFile);
} catch (Throwable e2) {
e.addSuppressed(e2);
}
throw e;
}
return new TarFileTree(tarFile, tempFile);
} else {
return new TarFileTree(new TarFile(file), null);
}
}
private final Path tempFile;
private final Thread shutdownHook;
public TarFileTree(TarFile file, Path tempFile) throws IOException {
super(file);
this.tempFile = tempFile;
try {
for (TarArchiveEntry entry : file.getEntries()) {
addEntry(entry);
}
} catch (Throwable e) {
try {
file.close();
} catch (Throwable e2) {
e.addSuppressed(e2);
}
if (tempFile != null) {
try {
Files.deleteIfExists(tempFile);
} catch (Throwable e2) {
e.addSuppressed(e2);
}
}
throw e;
}
if (tempFile != null) {
this.shutdownHook = new Thread(() -> {
try {
Files.deleteIfExists(tempFile);
} catch (Throwable ignored) {
}
});
Runtime.getRuntime().addShutdownHook(shutdownHook);
} else
this.shutdownHook = null;
}
@Override
public InputStream getInputStream(TarArchiveEntry entry) throws IOException {
return file.getInputStream(entry);
}
@Override
public boolean isLink(TarArchiveEntry entry) {
return entry.isSymbolicLink();
}
@Override
public String getLink(TarArchiveEntry entry) throws IOException {
return entry.getLinkName();
}
@Override
public boolean isExecutable(TarArchiveEntry entry) {
return entry.isFile() && (entry.getMode() & 0b1000000) != 0;
}
@Override
public void close() throws IOException {
try {
file.close();
} finally {
if (tempFile != null) {
Runtime.getRuntime().removeShutdownHook(shutdownHook);
Files.deleteIfExists(tempFile);
}
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.util.tree;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
/**
* @author Glavo
*/
public final class ZipFileTree extends ArchiveFileTree<ZipFile, ZipArchiveEntry> {
public ZipFileTree(ZipFile file) throws IOException {
super(file);
try {
Enumeration<ZipArchiveEntry> entries = file.getEntries();
while (entries.hasMoreElements()) {
addEntry(entries.nextElement());
}
} catch (Throwable e) {
try {
file.close();
} catch (Throwable e2) {
e.addSuppressed(e2);
}
throw e;
}
}
@Override
public void close() throws IOException {
file.close();
}
@Override
public InputStream getInputStream(ZipArchiveEntry entry) throws IOException {
return getFile().getInputStream(entry);
}
@Override
public boolean isLink(ZipArchiveEntry entry) {
return entry.isUnixSymlink();
}
@Override
public String getLink(ZipArchiveEntry entry) throws IOException {
return getFile().getUnixSymlink(entry);
}
@Override
public boolean isExecutable(ZipArchiveEntry entry) {
return !entry.isDirectory() && !entry.isUnixSymlink() && (entry.getUnixMode() & 0b1000000) != 0;
}
}

View File

@@ -0,0 +1,31 @@
package org.jackhuang.hmcl.util;
import org.junit.jupiter.api.Test;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* @author Glavo
*/
public final class KeyValuePairPropertiesTest {
@Test
public void test() throws IOException {
String content = "#test: key0=value0\n \n" +
"key1=value1\n" +
"key2=\"value2\"\n" +
"key3=\"\\\" \\n\"\n";
KeyValuePairProperties properties = KeyValuePairProperties.load(new BufferedReader(new StringReader(content)));
assertEquals(Lang.mapOf(
pair("key1", "value1"),
pair("key2", "value2"),
pair("key3", "\" \n")
), properties);
}
}

View File

@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.util;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
@@ -79,10 +78,11 @@ public class TaskTest {
@EnabledIf("org.jackhuang.hmcl.JavaFXLauncher#isStarted")
public void testThenAccept() {
AtomicBoolean flag = new AtomicBoolean();
boolean result = Task.supplyAsync(JavaVersion::fromCurrentEnvironment)
.thenAcceptAsync(Schedulers.io(), javaVersion -> {
Object obj = new Object();
boolean result = Task.supplyAsync(() -> obj)
.thenAcceptAsync(Schedulers.io(), o -> {
flag.set(true);
assertEquals(javaVersion, JavaVersion.fromCurrentEnvironment());
assertSame(obj, o);
})
.test();

View File

@@ -0,0 +1,18 @@
package org.jackhuang.hmcl.util.platform;
import org.junit.jupiter.api.Test;
import static org.jackhuang.hmcl.java.JavaInfo.parseVersion;
import static org.junit.jupiter.api.Assertions.*;
/**
* @author Glavo
*/
public final class JavaRuntimeTest {
@Test
public void testParseVersion() {
assertEquals(8, parseVersion("1.8.0_302"));
assertEquals(11, parseVersion("11"));
assertEquals(11, parseVersion("11.0.12"));
}
}