v0.10.302 #2

Merged
didirus merged 289 commits from feature-clean into beta 2025-07-08 15:00:09 +00:00
17 changed files with 342 additions and 157 deletions
Showing only changes of commit f10e0f2bf1 - Show all commits
Generated
+1
View File
@@ -8889,6 +8889,7 @@ dependencies = [
"flate2",
"fs4",
"futures",
"hashlink",
"hickory-resolver",
"indicatif",
"notify",
+1
View File
@@ -60,6 +60,7 @@ flate2 = "1.1.2"
fs4 = { version = "0.13.1", default-features = false }
futures = { version = "0.3.31", default-features = false }
futures-util = "0.3.31"
hashlink = "0.10.0"
hex = "0.4.3"
hickory-resolver = "0.25.2"
hmac = "0.12.1"
@@ -127,7 +127,7 @@ async function handleJavaFileInput() {
const filePath = await open()
if (filePath) {
let result = await get_jre(filePath.path ?? filePath)
let result = await get_jre(filePath.path ?? filePath).catch(handleError)
if (!result) {
result = {
path: filePath.path ?? filePath,
+2 -2
View File
@@ -41,8 +41,8 @@ pub async fn jre_find_filtered_jres(
// Validates JRE at a given path
// Returns None if the path is not a valid JRE
#[tauri::command]
pub async fn jre_get_jre(path: PathBuf) -> Result<Option<JavaVersion>> {
jre::check_jre(path).await.map_err(|e| e.into())
pub async fn jre_get_jre(path: PathBuf) -> Result<JavaVersion> {
Ok(jre::check_jre(path).await?)
}
// Tests JRE of a certain version
-1
View File
@@ -30,7 +30,6 @@
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"linux": {
"deb": {
+1
View File
@@ -23,6 +23,7 @@ quick-xml = { workspace = true, features = ["async-tokio"] }
enumset.workspace = true
chardetng.workspace = true
encoding_rs.workspace = true
hashlink.workspace = true
chrono = { workspace = true, features = ["serde"] }
daedalus.workspace = true
+17 -17
View File
@@ -1,22 +1,22 @@
public final class JavaInfo {
private static final String[] CHECKED_PROPERTIES = new String[] {
"os.arch",
"java.version"
};
private static final String[] CHECKED_PROPERTIES = new String[] {
"os.arch",
"java.version"
};
public static void main(String[] args) {
int returnCode = 0;
public static void main(String[] args) {
int returnCode = 0;
for (String key : CHECKED_PROPERTIES) {
String property = System.getProperty(key);
for (String key : CHECKED_PROPERTIES) {
String property = System.getProperty(key);
if (property != null) {
System.out.println(key + "=" + property);
} else {
returnCode = 1;
}
}
System.exit(returnCode);
if (property != null) {
System.out.println(key + "=" + property);
} else {
returnCode = 1;
}
}
}
System.exit(returnCode);
}
}
Binary file not shown.
@@ -0,0 +1,118 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
public final class MinecraftLaunch {
public static void main(String[] args) throws IOException, ReflectiveOperationException {
final String mainClass = args[0];
final String[] gameArgs = Arrays.copyOfRange(args, 1, args.length);
System.setProperty("modrinth.process.args", String.join("\u001f", gameArgs));
parseInput();
relaunch(mainClass, gameArgs);
}
private static void parseInput() throws IOException {
final ByteArrayOutputStream line = new ByteArrayOutputStream();
while (true) {
final int b = System.in.read();
if (b < 0) {
throw new IllegalStateException("Stdin terminated while parsing");
}
if (b != '\n') {
line.write(b);
continue;
}
if (handleLine(line.toString("UTF-8"))) {
break;
}
line.reset();
}
}
private static boolean handleLine(String line) {
final String[] parts = line.split("\t", 2);
switch (parts[0]) {
case "property": {
final String[] keyValue = parts[1].split("\t", 2);
System.setProperty(keyValue[0], keyValue[1]);
return false;
}
case "launch":
return true;
}
System.err.println("Unknown input line " + line);
return false;
}
private static void relaunch(String mainClassName, String[] args) throws ReflectiveOperationException {
final int javaVersion = getJavaVersion();
final Class<?> mainClass = Class.forName(mainClassName);
if (javaVersion >= 25) {
Method mainMethod;
try {
mainMethod = findMainMethodJep512(mainClass);
} catch (ReflectiveOperationException e) {
System.err
.println("[MODRINTH] Unable to call JDK findMainMethod. Falling back to pre-Java 25 main method finding.");
// If the above fails due to JDK implementation details changing
try {
mainMethod = findSimpleMainMethod(mainClass);
} catch (ReflectiveOperationException innerE) {
e.addSuppressed(innerE);
throw e;
}
}
if (mainMethod == null) {
throw new IllegalArgumentException("Could not find main() method");
}
Object thisObject = null;
if (!Modifier.isStatic(mainMethod.getModifiers())) {
thisObject = mainClass.getDeclaredConstructor().newInstance();
}
final Object[] parameters = mainMethod.getParameterCount() > 0
? new Object[] { args }
: new Object[] {};
mainMethod.invoke(thisObject, parameters);
} else {
findSimpleMainMethod(mainClass).invoke(null, new Object[] { args });
}
}
private static int getJavaVersion() {
String javaVersion = System.getProperty("java.version");
final int dotIndex = javaVersion.indexOf('.');
if (dotIndex != -1) {
javaVersion = javaVersion.substring(0, dotIndex);
}
final int dashIndex = javaVersion.indexOf('-');
if (dashIndex != -1) {
javaVersion = javaVersion.substring(0, dashIndex);
}
return Integer.parseInt(javaVersion);
}
private static Method findMainMethodJep512(Class<?> mainClass) throws ReflectiveOperationException {
// BEWARE BELOW: This code may break if JDK internals to find the main method
// change.
final Class<?> methodFinderClass = Class.forName("jdk.internal.misc.MethodFinder");
final Method methodFinderMethod = methodFinderClass.getDeclaredMethod("findMainMethod", Class.class);
final Object result = methodFinderMethod.invoke(null, mainClass);
return (Method) result;
}
private static Method findSimpleMainMethod(Class<?> mainClass) throws NoSuchMethodException {
return mainClass.getMethod("main", String[].class);
}
}
+8 -8
View File
@@ -9,7 +9,7 @@ use std::path::PathBuf;
use sysinfo::{MemoryRefreshKind, RefreshKind};
use crate::util::io;
use crate::util::jre::extract_java_majorminor_version;
use crate::util::jre::extract_java_version;
use crate::{
LoadingBarType, State,
util::jre::{self},
@@ -38,9 +38,9 @@ pub async fn find_filtered_jres(
Ok(if let Some(java_version) = java_version {
jres.into_iter()
.filter(|jre| {
let jre_version = extract_java_majorminor_version(&jre.version);
let jre_version = extract_java_version(&jre.version);
if let Ok(jre_version) = jre_version {
jre_version.1 == java_version
jre_version == java_version
} else {
false
}
@@ -157,8 +157,8 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
}
// Validates JRE at a given at a given path
pub async fn check_jre(path: PathBuf) -> crate::Result<Option<JavaVersion>> {
Ok(jre::check_java_at_filepath(&path).await)
pub async fn check_jre(path: PathBuf) -> crate::Result<JavaVersion> {
jre::check_java_at_filepath(&path).await
}
// Test JRE at a given path
@@ -166,11 +166,11 @@ pub async fn test_jre(
path: PathBuf,
major_version: u32,
) -> crate::Result<bool> {
let Some(jre) = jre::check_java_at_filepath(&path).await else {
let Ok(jre) = jre::check_java_at_filepath(&path).await else {
return Ok(false);
};
let (major, _) = extract_java_majorminor_version(&jre.version)?;
Ok(major == major_version)
let version = extract_java_version(&jre.version)?;
Ok(version == major_version)
}
// Gets maximum memory in KiB.
+17 -15
View File
@@ -13,7 +13,7 @@ use daedalus::{
modded::SidedDataEntry,
};
use dunce::canonicalize;
use std::collections::HashSet;
use hashlink::LinkedHashSet;
use std::io::{BufRead, BufReader};
use std::{collections::HashMap, path::Path};
use uuid::Uuid;
@@ -24,7 +24,7 @@ const TEMPORARY_REPLACE_CHAR: &str = "\n";
pub fn get_class_paths(
libraries_path: &Path,
libraries: &[Library],
client_path: &Path,
launcher_class_path: &[&Path],
java_arch: &str,
minecraft_updated: bool,
) -> crate::Result<String> {
@@ -48,20 +48,22 @@ pub fn get_class_paths(
Some(get_lib_path(libraries_path, &library.name, false))
})
.collect::<Result<HashSet<_>, _>>()?;
.collect::<Result<LinkedHashSet<_>, _>>()?;
cps.insert(
canonicalize(client_path)
.map_err(|_| {
crate::ErrorKind::LauncherError(format!(
"Specified class path {} does not exist",
client_path.to_string_lossy()
))
.as_error()
})?
.to_string_lossy()
.to_string(),
);
for launcher_path in launcher_class_path {
cps.insert(
canonicalize(launcher_path)
.map_err(|_| {
crate::ErrorKind::LauncherError(format!(
"Specified class path {} does not exist",
launcher_path.to_string_lossy()
))
.as_error()
})?
.to_string_lossy()
.to_string(),
);
}
Ok(cps
.into_iter()
+76 -46
View File
@@ -9,7 +9,7 @@ use crate::state::{
Credentials, JavaVersion, ProcessMetadata, ProfileInstallStage,
};
use crate::util::io;
use crate::{State, process, state as st};
use crate::{State, get_resource_file, process, state as st};
use chrono::Utc;
use daedalus as d;
use daedalus::minecraft::{LoggingSide, RuleAction, VersionInfo};
@@ -19,6 +19,7 @@ use serde::Deserialize;
use st::Profile;
use std::fmt::Write;
use std::path::PathBuf;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
mod args;
@@ -124,12 +125,10 @@ pub async fn get_java_version_from_profile(
version_info: &VersionInfo,
) -> crate::Result<Option<JavaVersion>> {
if let Some(java) = profile.java_path.as_ref() {
let java = crate::api::jre::check_jre(std::path::PathBuf::from(java))
.await
.ok()
.flatten();
let java =
crate::api::jre::check_jre(std::path::PathBuf::from(java)).await;
if let Some(java) = java {
if let Ok(java) = java {
return Ok(Some(java));
}
}
@@ -289,13 +288,7 @@ pub async fn install_minecraft(
};
// Test jre version
let java_version = crate::api::jre::check_jre(java_version.clone())
.await?
.ok_or_else(|| {
crate::ErrorKind::LauncherError(format!(
"Java path invalid or non-functional: {java_version:?}"
))
})?;
let java_version = crate::api::jre::check_jre(java_version.clone()).await?;
if set_java {
java_version.upsert(&state.pool).await?;
@@ -560,14 +553,7 @@ pub async fn launch_minecraft(
// Test jre version
let java_version =
crate::api::jre::check_jre(java_version.path.clone().into())
.await?
.ok_or_else(|| {
crate::ErrorKind::LauncherError(format!(
"Java path invalid or non-functional: {}",
java_version.path
))
})?;
crate::api::jre::check_jre(java_version.path.clone().into()).await?;
let client_path = state
.directories
@@ -603,33 +589,43 @@ pub async fn launch_minecraft(
io::create_dir_all(&natives_dir).await?;
}
command
.args(
args::get_jvm_arguments(
args.get(&d::minecraft::ArgumentType::Jvm)
.map(|x| x.as_slice()),
&natives_dir,
let (main_class_keep_alive, main_class_path) =
get_resource_file!("../../library" / "MinecraftLaunch.class")?;
command.args(
args::get_jvm_arguments(
args.get(&d::minecraft::ArgumentType::Jvm)
.map(|x| x.as_slice()),
&natives_dir,
&state.directories.libraries_dir(),
&state.directories.log_configs_dir(),
&args::get_class_paths(
&state.directories.libraries_dir(),
&state.directories.log_configs_dir(),
&args::get_class_paths(
&state.directories.libraries_dir(),
version_info.libraries.as_slice(),
&client_path,
&java_version.architecture,
minecraft_updated,
)?,
&version_jar,
*memory,
Vec::from(java_args),
version_info.libraries.as_slice(),
&[main_class_path.parent().unwrap(), &client_path],
&java_version.architecture,
quick_play_type,
version_info
.logging
.as_ref()
.and_then(|x| x.get(&LoggingSide::Client)),
)?
.into_iter(),
)
minecraft_updated,
)?,
&version_jar,
*memory,
Vec::from(java_args),
&java_version.architecture,
quick_play_type,
version_info
.logging
.as_ref()
.and_then(|x| x.get(&LoggingSide::Client)),
)?
.into_iter(),
);
// The java launcher code requires internal JDK code in Java 25+ in order to support JEP 512
if java_version.parsed_version >= 25 {
command.arg("--add-opens=jdk.internal/jdk.internal.misc=ALL-UNNAMED");
}
command
.arg("MinecraftLaunch")
.arg(version_info.main_class.clone())
.args(
args::get_minecraft_arguments(
@@ -744,6 +740,40 @@ pub async fn launch_minecraft(
post_exit_hook,
state.directories.profile_logs_dir(&profile.path),
version_info.logging.is_some(),
main_class_keep_alive,
async |process: &ProcessMetadata, stdin| {
let process_start_time = process.start_time.to_rfc3339();
let profile_created_time = profile.created.to_rfc3339();
let profile_modified_time = profile.modified.to_rfc3339();
let system_properties = [
("modrinth.process.startTime", Some(&process_start_time)),
("modrinth.profile.created", Some(&profile_created_time)),
("modrinth.profile.icon", profile.icon_path.as_ref()),
(
"modrinth.profile.link.project",
profile.linked_data.as_ref().map(|x| &x.project_id),
),
(
"modrinth.profile.link.version",
profile.linked_data.as_ref().map(|x| &x.version_id),
),
("modrinth.profile.modified", Some(&profile_modified_time)),
("modrinth.profile.name", Some(&profile.name)),
];
for (key, value) in system_properties {
let Some(value) = value else {
continue;
};
stdin.write_all(b"property\t").await?;
stdin.write_all(key.as_bytes()).await?;
stdin.write_u8(b'\t').await?;
stdin.write_all(value.as_bytes()).await?;
stdin.write_u8(b'\n').await?;
}
stdin.write_all(b"launch\n").await?;
stdin.flush().await?;
Ok(())
},
)
.await
}
+4 -4
View File
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Clone)]
pub struct JavaVersion {
pub major_version: u32,
pub parsed_version: u32,
pub version: String,
pub architecture: String,
pub path: String,
@@ -30,7 +30,7 @@ impl JavaVersion {
.await?;
Ok(res.map(|x| JavaVersion {
major_version,
parsed_version: major_version,
version: x.full_version,
architecture: x.architecture,
path: x.path,
@@ -52,7 +52,7 @@ impl JavaVersion {
acc.insert(
x.major_version as u32,
JavaVersion {
major_version: x.major_version as u32,
parsed_version: x.major_version as u32,
version: x.full_version,
architecture: x.architecture,
path: x.path,
@@ -70,7 +70,7 @@ impl JavaVersion {
&self,
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
) -> crate::Result<()> {
let major_version = self.major_version as i32;
let major_version = self.parsed_version as i32;
sqlx::query!(
"
@@ -83,7 +83,7 @@ where
settings.prev_custom_dir = Some(old_launcher_root_str.clone());
for (_, legacy_version) in legacy_settings.java_globals.0 {
if let Ok(Some(java_version)) =
if let Ok(java_version) =
check_jre(PathBuf::from(legacy_version.path)).await
{
java_version.upsert(exec).await?;
+24 -2
View File
@@ -8,12 +8,14 @@ use quick_xml::Reader;
use quick_xml::events::Event;
use serde::Deserialize;
use serde::Serialize;
use std::fmt::Debug;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
use tempfile::TempDir;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::{Child, Command};
use tokio::process::{Child, ChildStdin, Command};
use uuid::Uuid;
const LAUNCHER_LOG_PATH: &str = "launcher_log.txt";
@@ -35,6 +37,7 @@ impl ProcessManager {
}
}
#[allow(clippy::too_many_arguments)]
pub async fn insert_new_process(
&self,
profile_path: &str,
@@ -42,24 +45,42 @@ impl ProcessManager {
post_exit_command: Option<String>,
logs_folder: PathBuf,
xml_logging: bool,
main_class_keep_alive: TempDir,
post_process_init: impl AsyncFnOnce(
&ProcessMetadata,
&mut ChildStdin,
) -> crate::Result<()>,
) -> crate::Result<ProcessMetadata> {
mc_command.stdout(std::process::Stdio::piped());
mc_command.stderr(std::process::Stdio::piped());
mc_command.stdin(std::process::Stdio::piped());
let mut mc_proc = mc_command.spawn().map_err(IOError::from)?;
let stdout = mc_proc.stdout.take();
let stderr = mc_proc.stderr.take();
let process = Process {
let mut process = Process {
metadata: ProcessMetadata {
uuid: Uuid::new_v4(),
start_time: Utc::now(),
profile_path: profile_path.to_string(),
},
child: mc_proc,
_main_class_keep_alive: main_class_keep_alive,
};
if let Err(e) = post_process_init(
&process.metadata,
&mut process.child.stdin.as_mut().unwrap(),
)
.await
{
tracing::error!("Failed to run post-process init: {e}");
let _ = process.child.kill().await;
return Err(e);
}
let metadata = process.metadata.clone();
if !logs_folder.exists() {
@@ -193,6 +214,7 @@ pub struct ProcessMetadata {
struct Process {
metadata: ProcessMetadata,
child: Child,
_main_class_keep_alive: TempDir,
}
#[derive(Debug, Default)]
+33 -1
View File
@@ -2,7 +2,6 @@
// A wrapper around the tokio IO functions that adds the path to the error message, instead of the uninformative std::io::Error.
use std::{io::Write, path::Path};
use tempfile::NamedTempFile;
use tokio::task::spawn_blocking;
@@ -299,3 +298,36 @@ pub async fn metadata(
path: path.to_string_lossy().to_string(),
})
}
/// Gets a resource file from the executable. Returns `theseus::Result<(TempDir, PathBuf)>`.
#[macro_export]
macro_rules! get_resource_file {
($relative_dir:literal / $file_name:literal) => {
'get_resource_file: {
let dir = match tempfile::tempdir() {
Ok(dir) => dir,
Err(e) => {
break 'get_resource_file $crate::Result::Err(
$crate::util::io::IOError::from(e).into(),
);
}
};
let path = dir.path().join($file_name);
if let Err(e) = $crate::util::io::write(
&path,
include_bytes!(concat!($relative_dir, "/", $file_name)),
)
.await
{
break 'get_resource_file $crate::Result::Err(e.into());
}
let path = match $crate::util::io::canonicalize(path) {
Ok(path) => path,
Err(e) => {
break 'get_resource_file $crate::Result::Err(e.into());
}
};
$crate::Result::Ok((dir, path))
}
};
}
+38 -59
View File
@@ -7,7 +7,7 @@ use std::process::Command;
use std::{collections::HashSet, path::Path};
use tokio::task::JoinError;
use crate::State;
use crate::{State, get_resource_file};
#[cfg(target_os = "windows")]
use winreg::{
RegKey,
@@ -183,7 +183,6 @@ pub async fn get_all_jre() -> Result<Vec<JavaVersion>, JREError> {
// Gets all JREs from the PATH env variable
#[tracing::instrument]
async fn get_all_autoinstalled_jre_path() -> Result<HashSet<PathBuf>, JREError>
{
Box::pin(async move {
@@ -239,54 +238,49 @@ pub const JAVA_BIN: &str = if cfg!(target_os = "windows") {
pub async fn check_java_at_filepaths(
paths: HashSet<PathBuf>,
) -> HashSet<JavaVersion> {
let jres = stream::iter(paths.into_iter())
stream::iter(paths.into_iter())
.map(|p: PathBuf| {
tokio::task::spawn(async move { check_java_at_filepath(&p).await })
})
.buffer_unordered(64)
.collect::<Vec<_>>()
.await;
jres.into_iter().filter_map(|x| x.ok()).flatten().collect()
.filter_map(async |x| x.ok().and_then(Result::ok))
.collect()
.await
}
// For example filepath 'path', attempt to resolve it and get a Java version at this path
// If no such path exists, or no such valid java at this path exists, returns None
#[tracing::instrument]
pub async fn check_java_at_filepath(path: &Path) -> Option<JavaVersion> {
pub async fn check_java_at_filepath(path: &Path) -> crate::Result<JavaVersion> {
// Attempt to canonicalize the potential java filepath
// If it fails, this path does not exist and None is returned (no Java here)
let Ok(path) = io::canonicalize(path) else {
return None;
};
let path = io::canonicalize(path)?;
// Checks for existence of Java at this filepath
// Adds JAVA_BIN to the end of the path if it is not already there
let java = if path.file_name()?.to_str()? != JAVA_BIN {
let java = if path
.file_name()
.and_then(|x| x.to_str())
.is_some_and(|x| x != JAVA_BIN)
{
path.join(JAVA_BIN)
} else {
path
};
if !java.exists() {
return None;
return Err(JREError::NoExecutable(java).into());
};
let bytes = include_bytes!("../../library/JavaInfo.class");
let Ok(tempdir) = tempfile::tempdir() else {
return None;
};
let file_path = tempdir.path().join("JavaInfo.class");
io::write(&file_path, bytes).await.ok()?;
let (_temp, file_path) =
get_resource_file!("../../library" / "JavaInfo.class")?;
let output = Command::new(&java)
.arg("-cp")
.arg(file_path.parent().unwrap())
.arg("JavaInfo")
.env_remove("_JAVA_OPTIONS")
.output()
.ok()?;
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
@@ -308,64 +302,49 @@ pub async fn check_java_at_filepath(path: &Path) -> Option<JavaVersion> {
// Extract version info from it
if let Some(arch) = java_arch {
if let Some(version) = java_version {
if let Ok((_, major_version)) =
extract_java_majorminor_version(version)
{
if let Ok(version) = extract_java_version(version) {
let path = java.to_string_lossy().to_string();
return Some(JavaVersion {
major_version,
return Ok(JavaVersion {
parsed_version: version,
path,
version: version.to_string(),
architecture: arch.to_string(),
});
}
return Err(JREError::InvalidJREVersion(version.to_owned()).into());
}
}
None
Err(JREError::FailedJavaCheck(java).into())
}
/// Extract major/minor version from a java version string
/// Gets the minor version or an error, and assumes 1 for major version if it could not find
/// "1.8.0_361" -> (1, 8)
/// "20" -> (1, 20)
pub fn extract_java_majorminor_version(
version: &str,
) -> Result<(u32, u32), JREError> {
pub fn extract_java_version(version: &str) -> Result<u32, JREError> {
let mut split = version.split('.');
let major_opt = split.next();
let mut major;
// Try minor. If doesn't exist, in format like "20" so use major
let mut minor = if let Some(minor) = split.next() {
major = major_opt.unwrap_or("1").parse::<u32>()?;
minor.parse::<u32>()?
} else {
// Formatted like "20", only one value means that is minor version
major = 1;
major_opt
.ok_or_else(|| JREError::InvalidJREVersion(version.to_string()))?
.parse::<u32>()?
};
// Java start should always be 1. If more than 1, it is formatted like "17.0.1.2" and starts with minor version
if major > 1 {
minor = major;
major = 1;
let version = split.next().unwrap();
let version = version.split_once('-').map_or(version, |(x, _)| x);
let mut version = version.parse::<u32>()?;
if version == 1 {
version = split.next().map_or(Ok(1), |x| x.parse::<u32>())?;
}
Ok((major, minor))
Ok(version)
}
#[derive(thiserror::Error, Debug)]
pub enum JREError {
#[error("Command error : {0}")]
#[error("Command error: {0}")]
IOError(#[from] std::io::Error),
#[error("Env error: {0}")]
EnvError(#[from] env::VarError),
#[error("No JRE found for required version: {0}")]
NoJREFound(String),
#[error("No executable found at {0}")]
NoExecutable(PathBuf),
#[error("Could not check Java version at path {0}")]
FailedJavaCheck(PathBuf),
#[error("Invalid JRE version string: {0}")]
InvalidJREVersion(String),
@@ -376,9 +355,9 @@ pub enum JREError {
#[error("Join error: {0}")]
JoinError(#[from] JoinError),
#[error("No stored tag for Minecraft Version {0}")]
#[error("No stored tag for Minecraft version {0}")]
NoMinecraftVersionFound(String),
#[error("Error getting launcher sttae")]
#[error("Error getting launcher state")]
StateError,
}