aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src-tauri/src
diff options
context:
space:
mode:
Diffstat (limited to 'src-tauri/src')
-rw-r--r--src-tauri/src/core/assistant.rs31
-rw-r--r--src-tauri/src/core/auth.rs32
-rw-r--r--src-tauri/src/core/config.rs22
-rw-r--r--src-tauri/src/core/downloader.rs52
-rw-r--r--src-tauri/src/core/fabric.rs65
-rw-r--r--src-tauri/src/core/forge.rs16
-rw-r--r--src-tauri/src/core/game_version.rs66
-rw-r--r--src-tauri/src/core/instance.rs25
-rw-r--r--src-tauri/src/core/java.rs1103
-rw-r--r--src-tauri/src/core/java/detection.rs247
-rw-r--r--src-tauri/src/core/java/error.rs95
-rw-r--r--src-tauri/src/core/java/mod.rs530
-rw-r--r--src-tauri/src/core/java/persistence.rs114
-rw-r--r--src-tauri/src/core/java/priority.rs59
-rw-r--r--src-tauri/src/core/java/provider.rs58
-rw-r--r--src-tauri/src/core/java/providers/adoptium.rs334
-rw-r--r--src-tauri/src/core/java/providers/mod.rs3
-rw-r--r--src-tauri/src/core/java/validation.rs146
-rw-r--r--src-tauri/src/core/manifest.rs24
-rw-r--r--src-tauri/src/main.rs156
20 files changed, 1935 insertions, 1243 deletions
diff --git a/src-tauri/src/core/assistant.rs b/src-tauri/src/core/assistant.rs
index 9a8f7bf..6e656dc 100644
--- a/src-tauri/src/core/assistant.rs
+++ b/src-tauri/src/core/assistant.rs
@@ -4,8 +4,14 @@ use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use tauri::{Emitter, Window};
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
+use ts_rs::TS;
+
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/assistant.ts"
+)]
pub struct Message {
pub role: String,
pub content: String,
@@ -51,7 +57,12 @@ pub struct OllamaTagsResponse {
}
// Simplified model info for frontend
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/assistant.ts"
+)]
pub struct ModelInfo {
pub id: String,
pub name: String,
@@ -102,7 +113,12 @@ pub struct OpenAIModelsResponse {
}
// Streaming response structures
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/assistant.ts"
+)]
pub struct GenerationStats {
pub total_duration: u64,
pub load_duration: u64,
@@ -112,7 +128,12 @@ pub struct GenerationStats {
pub eval_duration: u64,
}
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/assistant.ts"
+)]
pub struct StreamChunk {
pub content: String,
pub done: bool,
diff --git a/src-tauri/src/core/auth.rs b/src-tauri/src/core/auth.rs
index d5e6c17..0e873e3 100644
--- a/src-tauri/src/core/auth.rs
+++ b/src-tauri/src/core/auth.rs
@@ -1,5 +1,6 @@
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
+use ts_rs::TS;
use uuid::Uuid;
// Helper to create a client with a custom User-Agent
@@ -11,8 +12,14 @@ fn get_client() -> reqwest::Client {
.unwrap_or_else(|_| reqwest::Client::new())
}
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(tag = "type")]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ tag = "type",
+ export_to = "../../packages/ui-new/src/types/bindings/auth.ts"
+)]
pub enum Account {
Offline(OfflineAccount),
Microsoft(MicrosoftAccount),
@@ -41,13 +48,17 @@ impl Account {
}
}
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../../packages/ui-new/src/types/bindings/auth.ts")]
pub struct OfflineAccount {
pub username: String,
pub uuid: String,
}
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../../packages/ui-new/src/types/bindings/auth.ts")]
pub struct MicrosoftAccount {
pub username: String,
pub uuid: String,
@@ -73,11 +84,12 @@ pub fn generate_offline_uuid(username: &str) -> String {
Uuid::new_v3(&namespace, username.as_bytes()).to_string()
}
-// const CLIENT_ID: &str = "fe165602-5410-4441-92f7-326e10a7cb82";
-const CLIENT_ID: &str = "c36a9fb6-4f2a-41ff-90bd-ae7cc92031eb"; // ATLauncher's Client ID
+const CLIENT_ID: &str = "fe165602-5410-4441-92f7-326e10a7cb82";
const SCOPE: &str = "XboxLive.SignIn XboxLive.offline_access";
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../../packages/ui-new/src/types/bindings/auth.ts")]
pub struct DeviceCodeResponse {
pub user_code: String,
pub device_code: String,
@@ -87,7 +99,9 @@ pub struct DeviceCodeResponse {
pub message: Option<String>,
}
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../../packages/ui-new/src/types/bindings/auth.ts")]
pub struct TokenResponse {
pub access_token: String,
pub refresh_token: Option<String>,
@@ -209,7 +223,9 @@ pub struct MinecraftAuthResponse {
pub expires_in: u64,
}
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../../packages/ui-new/src/types/bindings/auth.ts")]
pub struct MinecraftProfile {
pub id: String,
pub name: String,
diff --git a/src-tauri/src/core/config.rs b/src-tauri/src/core/config.rs
index e4b9381..0d0e8ff 100644
--- a/src-tauri/src/core/config.rs
+++ b/src-tauri/src/core/config.rs
@@ -3,8 +3,14 @@ use std::fs;
use std::path::PathBuf;
use std::sync::Mutex;
use tauri::{AppHandle, Manager};
+use ts_rs::TS;
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/config.ts"
+)]
#[serde(default)]
pub struct AssistantConfig {
pub enabled: bool,
@@ -43,7 +49,12 @@ impl Default for AssistantConfig {
}
/// Feature-gated arguments configuration
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/config.ts"
+)]
#[serde(default)]
pub struct FeatureFlags {
/// Demo user: enables demo-related arguments when rules require it
@@ -70,7 +81,12 @@ impl Default for FeatureFlags {
}
}
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/config.ts"
+)]
#[serde(default)]
pub struct LauncherConfig {
pub min_memory: u32, // in MB
diff --git a/src-tauri/src/core/downloader.rs b/src-tauri/src/core/downloader.rs
index 26f6ebd..d4fc782 100644
--- a/src-tauri/src/core/downloader.rs
+++ b/src-tauri/src/core/downloader.rs
@@ -7,8 +7,14 @@ use std::sync::Arc;
use tauri::{AppHandle, Emitter, Manager, Window};
use tokio::io::{AsyncSeekExt, AsyncWriteExt};
use tokio::sync::Semaphore;
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
+use ts_rs::TS;
+
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/downloader.ts"
+)]
pub struct DownloadTask {
pub url: String,
pub path: PathBuf,
@@ -19,7 +25,12 @@ pub struct DownloadTask {
}
/// Metadata for resumable downloads stored in .part.meta file
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/downloader.ts"
+)]
pub struct DownloadMetadata {
pub url: String,
pub file_name: String,
@@ -31,7 +42,12 @@ pub struct DownloadMetadata {
}
/// A download segment for multi-segment parallel downloading
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/downloader.ts"
+)]
pub struct DownloadSegment {
pub start: u64,
pub end: u64,
@@ -40,7 +56,12 @@ pub struct DownloadSegment {
}
/// Progress event for Java download
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/downloader.ts"
+)]
pub struct JavaDownloadProgress {
pub file_name: String,
pub downloaded_bytes: u64,
@@ -52,7 +73,12 @@ pub struct JavaDownloadProgress {
}
/// Pending download task for queue persistence
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/downloader.ts"
+)]
pub struct PendingJavaDownload {
pub major_version: u32,
pub image_type: String,
@@ -65,7 +91,12 @@ pub struct PendingJavaDownload {
}
/// Download queue for persistence
-#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/downloader.ts"
+)]
pub struct DownloadQueue {
pub pending_downloads: Vec<PendingJavaDownload>,
}
@@ -419,7 +450,12 @@ fn create_new_metadata(
}
}
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui/src/types/generated/downloader.ts"
+)]
pub struct ProgressEvent {
pub file: String,
pub downloaded: u64,
diff --git a/src-tauri/src/core/fabric.rs b/src-tauri/src/core/fabric.rs
index 32790c7..7385850 100644
--- a/src-tauri/src/core/fabric.rs
+++ b/src-tauri/src/core/fabric.rs
@@ -8,11 +8,17 @@
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::path::PathBuf;
+use ts_rs::TS;
const FABRIC_META_URL: &str = "https://meta.fabricmc.net/v2";
/// Represents a Fabric loader version from the Meta API.
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/fabric.ts"
+)]
pub struct FabricLoaderVersion {
pub separator: String,
pub build: i32,
@@ -22,7 +28,12 @@ pub struct FabricLoaderVersion {
}
/// Represents a Fabric intermediary mapping version.
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/fabric.ts"
+)]
pub struct FabricIntermediaryVersion {
pub maven: String,
pub version: String,
@@ -30,7 +41,12 @@ pub struct FabricIntermediaryVersion {
}
/// Represents a combined loader + intermediary version entry.
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/fabric.ts"
+)]
pub struct FabricLoaderEntry {
pub loader: FabricLoaderVersion,
pub intermediary: FabricIntermediaryVersion,
@@ -39,7 +55,12 @@ pub struct FabricLoaderEntry {
}
/// Launcher metadata from Fabric Meta API.
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/fabric.ts"
+)]
pub struct FabricLauncherMeta {
pub version: i32,
pub libraries: FabricLibraries,
@@ -48,7 +69,12 @@ pub struct FabricLauncherMeta {
}
/// Libraries required by Fabric loader.
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/fabric.ts"
+)]
pub struct FabricLibraries {
pub client: Vec<FabricLibrary>,
pub common: Vec<FabricLibrary>,
@@ -56,7 +82,12 @@ pub struct FabricLibraries {
}
/// A single Fabric library dependency.
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/fabric.ts"
+)]
pub struct FabricLibrary {
pub name: String,
pub url: Option<String>,
@@ -64,7 +95,13 @@ pub struct FabricLibrary {
/// Main class configuration for Fabric.
/// Can be either a struct with client/server fields or a simple string.
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/fabric.ts",
+ untagged
+)]
#[serde(untagged)]
pub enum FabricMainClass {
Structured { client: String, server: String },
@@ -89,14 +126,24 @@ impl FabricMainClass {
}
/// Represents a Minecraft version supported by Fabric.
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/fabric.ts"
+)]
pub struct FabricGameVersion {
pub version: String,
pub stable: bool,
}
/// Information about an installed Fabric version.
-#[derive(Debug, Serialize, Clone)]
+#[derive(Debug, Serialize, Clone, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/fabric.ts"
+)]
pub struct InstalledFabricVersion {
pub id: String,
pub minecraft_version: String,
diff --git a/src-tauri/src/core/forge.rs b/src-tauri/src/core/forge.rs
index 65bf413..1d4ae1d 100644
--- a/src-tauri/src/core/forge.rs
+++ b/src-tauri/src/core/forge.rs
@@ -12,6 +12,7 @@ use std::error::Error;
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
use std::path::PathBuf;
+use ts_rs::TS;
const FORGE_PROMOTIONS_URL: &str =
"https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json";
@@ -19,7 +20,12 @@ const FORGE_MAVEN_URL: &str = "https://maven.minecraftforge.net/";
const FORGE_FILES_URL: &str = "https://files.minecraftforge.net/";
/// Represents a Forge version entry.
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/forge.ts"
+)]
pub struct ForgeVersion {
pub version: String,
pub minecraft_version: String,
@@ -36,11 +42,17 @@ struct ForgePromotions {
}
/// Information about an installed Forge version.
-#[derive(Debug, Serialize, Clone)]
+#[derive(Debug, Serialize, Clone, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/forge.ts"
+)]
pub struct InstalledForgeVersion {
pub id: String,
pub minecraft_version: String,
pub forge_version: String,
+ #[ts(type = "string")]
pub path: PathBuf,
}
diff --git a/src-tauri/src/core/game_version.rs b/src-tauri/src/core/game_version.rs
index c62e232..7df631a 100644
--- a/src-tauri/src/core/game_version.rs
+++ b/src-tauri/src/core/game_version.rs
@@ -1,8 +1,13 @@
use serde::{Deserialize, Serialize};
+use ts_rs::TS;
/// Represents a Minecraft version JSON, supporting both vanilla and modded (Fabric/Forge) formats.
/// Modded versions use `inheritsFrom` to reference a parent vanilla version.
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, TS)]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/game_version.ts"
+)]
pub struct GameVersion {
pub id: String,
/// Optional for mod loaders that inherit from vanilla
@@ -28,13 +33,21 @@ pub struct GameVersion {
pub version_type: Option<String>,
}
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, TS)]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/game_version.ts"
+)]
pub struct Downloads {
pub client: DownloadArtifact,
pub server: Option<DownloadArtifact>,
}
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, TS)]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/game_version.ts"
+)]
pub struct DownloadArtifact {
pub sha1: Option<String>,
pub size: Option<u64>,
@@ -42,7 +55,11 @@ pub struct DownloadArtifact {
pub path: Option<String>,
}
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, TS)]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/game_version.ts"
+)]
pub struct AssetIndex {
pub id: String,
pub sha1: String,
@@ -52,43 +69,72 @@ pub struct AssetIndex {
pub total_size: Option<u64>,
}
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, TS)]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/game_version.ts"
+)]
pub struct Library {
pub downloads: Option<LibraryDownloads>,
pub name: String,
pub rules: Option<Vec<Rule>>,
+ #[ts(type = "Record<string, unknown>")]
pub natives: Option<serde_json::Value>,
/// Maven repository URL for mod loader libraries
pub url: Option<String>,
}
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, TS)]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/game_version.ts"
+)]
pub struct Rule {
pub action: String, // "allow" or "disallow"
pub os: Option<OsRule>,
+ #[ts(type = "Record<string, unknown>")]
pub features: Option<serde_json::Value>, // Feature-based rules (e.g., is_demo_user, has_quick_plays_support)
}
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, TS)]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/game_version.ts"
+)]
pub struct OsRule {
pub name: Option<String>, // "linux", "osx", "windows"
pub version: Option<String>, // Regex
pub arch: Option<String>, // "x86"
}
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, TS)]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/game_version.ts"
+)]
pub struct LibraryDownloads {
pub artifact: Option<DownloadArtifact>,
+ #[ts(type = "Record<string, unknown>")]
pub classifiers: Option<serde_json::Value>, // Complex, simplifying for now
}
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, TS)]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/game_version.ts"
+)]
pub struct Arguments {
+ #[ts(type = "Record<string, unknown>")]
pub game: Option<serde_json::Value>,
+ #[ts(type = "Record<string, unknown>")]
pub jvm: Option<serde_json::Value>,
}
-#[derive(Debug, Deserialize, Serialize, Clone)]
+#[derive(Debug, Deserialize, Serialize, Clone, TS)]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/game_version.ts"
+)]
pub struct JavaVersion {
pub component: String,
#[serde(rename = "majorVersion")]
diff --git a/src-tauri/src/core/instance.rs b/src-tauri/src/core/instance.rs
index 573273e..e842ec9 100644
--- a/src-tauri/src/core/instance.rs
+++ b/src-tauri/src/core/instance.rs
@@ -11,9 +11,15 @@ use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use tauri::{AppHandle, Manager};
+use ts_rs::TS;
/// Represents a game instance/profile
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/instance.ts"
+)]
pub struct Instance {
pub id: String, // 唯一标识符(UUID)
pub name: String, // 显示名称
@@ -28,17 +34,28 @@ pub struct Instance {
pub jvm_args_override: Option<String>, // JVM参数覆盖(可选)
#[serde(default)]
pub memory_override: Option<MemoryOverride>, // 内存设置覆盖(可选)
+ pub java_path_override: Option<String>, // 实例级Java路径覆盖(可选)
}
/// Memory settings override for an instance
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/instance.ts"
+)]
pub struct MemoryOverride {
pub min: u32, // MB
pub max: u32, // MB
}
/// Configuration for all instances
-#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/instance.ts"
+)]
pub struct InstanceConfig {
pub instances: Vec<Instance>,
pub active_instance_id: Option<String>, // 当前活动的实例ID
@@ -111,6 +128,7 @@ impl InstanceState {
mod_loader_version: None,
jvm_args_override: None,
memory_override: None,
+ java_path_override: None,
};
let mut config = self.instances.lock().unwrap();
@@ -267,6 +285,7 @@ impl InstanceState {
last_played: None,
jvm_args_override: source_instance.jvm_args_override.clone(),
memory_override: source_instance.memory_override.clone(),
+ java_path_override: source_instance.java_path_override.clone(),
};
self.update_instance(new_instance.clone())?;
diff --git a/src-tauri/src/core/java.rs b/src-tauri/src/core/java.rs
deleted file mode 100644
index 2e3c8a7..0000000
--- a/src-tauri/src/core/java.rs
+++ /dev/null
@@ -1,1103 +0,0 @@
-use serde::{Deserialize, Serialize};
-#[cfg(target_os = "windows")]
-use std::os::windows::process::CommandExt;
-use std::path::PathBuf;
-use std::process::Command;
-use tauri::AppHandle;
-use tauri::Emitter;
-use tauri::Manager;
-
-use crate::core::downloader::{self, DownloadQueue, JavaDownloadProgress, PendingJavaDownload};
-use crate::utils::zip;
-
-const ADOPTIUM_API_BASE: &str = "https://api.adoptium.net/v3";
-const CACHE_DURATION_SECS: u64 = 24 * 60 * 60; // 24 hours
-
-/// Helper to strip UNC prefix on Windows (\\?\)
-fn strip_unc_prefix(path: PathBuf) -> PathBuf {
- #[cfg(target_os = "windows")]
- {
- let s = path.to_string_lossy().to_string();
- if s.starts_with(r"\\?\") {
- return PathBuf::from(&s[4..]);
- }
- }
- path
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct JavaInstallation {
- pub path: String,
- pub version: String,
- pub is_64bit: bool,
-}
-
-/// Java image type: JRE or JDK
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "lowercase")]
-pub enum ImageType {
- Jre,
- Jdk,
-}
-
-impl Default for ImageType {
- fn default() -> Self {
- Self::Jre
- }
-}
-
-impl std::fmt::Display for ImageType {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::Jre => write!(f, "jre"),
- Self::Jdk => write!(f, "jdk"),
- }
- }
-}
-
-/// Java release information for UI display
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct JavaReleaseInfo {
- pub major_version: u32,
- pub image_type: String,
- pub version: String,
- pub release_name: String,
- pub release_date: Option<String>,
- pub file_size: u64,
- pub checksum: Option<String>,
- pub download_url: String,
- pub is_lts: bool,
- pub is_available: bool,
- pub architecture: String,
-}
-
-/// Java catalog containing all available versions
-#[derive(Debug, Clone, Serialize, Deserialize, Default)]
-pub struct JavaCatalog {
- pub releases: Vec<JavaReleaseInfo>,
- pub available_major_versions: Vec<u32>,
- pub lts_versions: Vec<u32>,
- pub cached_at: u64,
-}
-
-/// Adoptium `/v3/assets/latest/{version}/hotspot` API response structures
-#[derive(Debug, Clone, Deserialize)]
-pub struct AdoptiumAsset {
- pub binary: AdoptiumBinary,
- pub release_name: String,
- pub version: AdoptiumVersionData,
-}
-
-#[derive(Debug, Clone, Deserialize)]
-#[allow(dead_code)]
-pub struct AdoptiumBinary {
- pub os: String,
- pub architecture: String,
- pub image_type: String,
- pub package: AdoptiumPackage,
- #[serde(default)]
- pub updated_at: Option<String>,
-}
-
-#[derive(Debug, Clone, Deserialize)]
-pub struct AdoptiumPackage {
- pub name: String,
- pub link: String,
- pub size: u64,
- pub checksum: Option<String>, // SHA256
-}
-
-#[derive(Debug, Clone, Deserialize)]
-#[allow(dead_code)]
-pub struct AdoptiumVersionData {
- pub major: u32,
- pub minor: u32,
- pub security: u32,
- pub semver: String,
- pub openjdk_version: String,
-}
-
-/// Adoptium available releases response
-#[derive(Debug, Clone, Deserialize)]
-#[allow(dead_code)]
-pub struct AvailableReleases {
- pub available_releases: Vec<u32>,
- pub available_lts_releases: Vec<u32>,
- pub most_recent_lts: Option<u32>,
- pub most_recent_feature_release: Option<u32>,
-}
-
-/// Java download information from Adoptium
-#[derive(Debug, Clone, Serialize)]
-pub struct JavaDownloadInfo {
- pub version: String,
- pub release_name: String,
- pub download_url: String,
- pub file_name: String,
- pub file_size: u64,
- pub checksum: Option<String>,
- pub image_type: String,
-}
-
-/// Get the Adoptium OS name for the current platform
-pub fn get_adoptium_os() -> &'static str {
- #[cfg(target_os = "linux")]
- {
- // Check if Alpine Linux (musl libc)
- if std::path::Path::new("/etc/alpine-release").exists() {
- return "alpine-linux";
- }
- "linux"
- }
- #[cfg(target_os = "macos")]
- {
- "mac"
- }
- #[cfg(target_os = "windows")]
- {
- "windows"
- }
- #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
- {
- "linux" // fallback
- }
-}
-
-/// Get the Adoptium Architecture name for the current architecture
-pub fn get_adoptium_arch() -> &'static str {
- #[cfg(target_arch = "x86_64")]
- {
- "x64"
- }
- #[cfg(target_arch = "aarch64")]
- {
- "aarch64"
- }
- #[cfg(target_arch = "x86")]
- {
- "x86"
- }
- #[cfg(target_arch = "arm")]
- {
- "arm"
- }
- #[cfg(not(any(
- target_arch = "x86_64",
- target_arch = "aarch64",
- target_arch = "x86",
- target_arch = "arm"
- )))]
- {
- "x64" // fallback
- }
-}
-
-/// Get the default Java installation directory for DropOut
-pub fn get_java_install_dir(app_handle: &AppHandle) -> PathBuf {
- app_handle.path().app_data_dir().unwrap().join("java")
-}
-
-/// Get the cache file path for Java catalog
-fn get_catalog_cache_path(app_handle: &AppHandle) -> PathBuf {
- app_handle
- .path()
- .app_data_dir()
- .unwrap()
- .join("java_catalog_cache.json")
-}
-
-/// Load cached Java catalog if not expired
-pub fn load_cached_catalog(app_handle: &AppHandle) -> Option<JavaCatalog> {
- let cache_path = get_catalog_cache_path(app_handle);
- if !cache_path.exists() {
- return None;
- }
-
- let content = std::fs::read_to_string(&cache_path).ok()?;
- let catalog: JavaCatalog = serde_json::from_str(&content).ok()?;
-
- let now = std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap()
- .as_secs();
-
- if now - catalog.cached_at < CACHE_DURATION_SECS {
- Some(catalog)
- } else {
- None
- }
-}
-
-/// Save Java catalog to cache
-pub fn save_catalog_cache(app_handle: &AppHandle, catalog: &JavaCatalog) -> Result<(), String> {
- let cache_path = get_catalog_cache_path(app_handle);
- let content = serde_json::to_string_pretty(catalog).map_err(|e| e.to_string())?;
- std::fs::write(&cache_path, content).map_err(|e| e.to_string())?;
- Ok(())
-}
-
-/// Clear Java catalog cache
-#[allow(dead_code)]
-pub fn clear_catalog_cache(app_handle: &AppHandle) -> Result<(), String> {
- let cache_path = get_catalog_cache_path(app_handle);
- if cache_path.exists() {
- std::fs::remove_file(&cache_path).map_err(|e| e.to_string())?;
- }
- Ok(())
-}
-
-/// Fetch complete Java catalog from Adoptium API with platform availability check
-pub async fn fetch_java_catalog(
- app_handle: &AppHandle,
- force_refresh: bool,
-) -> Result<JavaCatalog, String> {
- // Check cache first unless force refresh
- if !force_refresh {
- if let Some(cached) = load_cached_catalog(app_handle) {
- return Ok(cached);
- }
- }
-
- let os = get_adoptium_os();
- let arch = get_adoptium_arch();
- let client = reqwest::Client::new();
-
- // 1. Fetch available releases
- let releases_url = format!("{}/info/available_releases", ADOPTIUM_API_BASE);
- let available: AvailableReleases = client
- .get(&releases_url)
- .header("Accept", "application/json")
- .send()
- .await
- .map_err(|e| format!("Failed to fetch available releases: {}", e))?
- .json()
- .await
- .map_err(|e| format!("Failed to parse available releases: {}", e))?;
-
- let mut releases = Vec::new();
-
- // 2. Fetch details for each major version
- for major_version in &available.available_releases {
- for image_type in &["jre", "jdk"] {
- let url = format!(
- "{}/assets/latest/{}/hotspot?os={}&architecture={}&image_type={}",
- ADOPTIUM_API_BASE, major_version, os, arch, image_type
- );
-
- match client
- .get(&url)
- .header("Accept", "application/json")
- .send()
- .await
- {
- Ok(response) => {
- if response.status().is_success() {
- if let Ok(assets) = response.json::<Vec<AdoptiumAsset>>().await {
- if let Some(asset) = assets.into_iter().next() {
- let release_date = asset.binary.updated_at.clone();
- releases.push(JavaReleaseInfo {
- major_version: *major_version,
- image_type: image_type.to_string(),
- version: asset.version.semver.clone(),
- release_name: asset.release_name.clone(),
- release_date,
- file_size: asset.binary.package.size,
- checksum: asset.binary.package.checksum,
- download_url: asset.binary.package.link,
- is_lts: available
- .available_lts_releases
- .contains(major_version),
- is_available: true,
- architecture: asset.binary.architecture.clone(),
- });
- }
- }
- } else {
- // Platform not available for this version/type
- releases.push(JavaReleaseInfo {
- major_version: *major_version,
- image_type: image_type.to_string(),
- version: format!("{}.x", major_version),
- release_name: format!("jdk-{}", major_version),
- release_date: None,
- file_size: 0,
- checksum: None,
- download_url: String::new(),
- is_lts: available.available_lts_releases.contains(major_version),
- is_available: false,
- architecture: arch.to_string(),
- });
- }
- }
- Err(_) => {
- // Network error, mark as unavailable
- releases.push(JavaReleaseInfo {
- major_version: *major_version,
- image_type: image_type.to_string(),
- version: format!("{}.x", major_version),
- release_name: format!("jdk-{}", major_version),
- release_date: None,
- file_size: 0,
- checksum: None,
- download_url: String::new(),
- is_lts: available.available_lts_releases.contains(major_version),
- is_available: false,
- architecture: arch.to_string(),
- });
- }
- }
- }
- }
-
- let now = std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap()
- .as_secs();
-
- let catalog = JavaCatalog {
- releases,
- available_major_versions: available.available_releases,
- lts_versions: available.available_lts_releases,
- cached_at: now,
- };
-
- // Save to cache
- let _ = save_catalog_cache(app_handle, &catalog);
-
- Ok(catalog)
-}
-
-/// Get Adoptium API download info for a specific Java version and image type
-///
-/// # Arguments
-/// * `major_version` - Java major version (e.g., 8, 11, 17)
-/// * `image_type` - JRE or JDK
-///
-/// # Returns
-/// * `Ok(JavaDownloadInfo)` - Download information
-/// * `Err(String)` - Error message
-pub async fn fetch_java_release(
- major_version: u32,
- image_type: ImageType,
-) -> Result<JavaDownloadInfo, String> {
- let os = get_adoptium_os();
- let arch = get_adoptium_arch();
-
- let url = format!(
- "{}/assets/latest/{}/hotspot?os={}&architecture={}&image_type={}",
- ADOPTIUM_API_BASE, major_version, os, arch, image_type
- );
-
- let client = reqwest::Client::new();
- let response = client
- .get(&url)
- .header("Accept", "application/json")
- .send()
- .await
- .map_err(|e| format!("Network request failed: {}", e))?;
-
- if !response.status().is_success() {
- return Err(format!(
- "Adoptium API returned error: {} - The version/platform might be unavailable",
- response.status()
- ));
- }
-
- let assets: Vec<AdoptiumAsset> = response
- .json()
- .await
- .map_err(|e| format!("Failed to parse API response: {}", e))?;
-
- let asset = assets
- .into_iter()
- .next()
- .ok_or_else(|| format!("Java {} {} download not found", major_version, image_type))?;
-
- Ok(JavaDownloadInfo {
- version: asset.version.semver.clone(),
- release_name: asset.release_name,
- download_url: asset.binary.package.link,
- file_name: asset.binary.package.name,
- file_size: asset.binary.package.size,
- checksum: asset.binary.package.checksum,
- image_type: asset.binary.image_type,
- })
-}
-
-/// Fetch available Java versions from Adoptium API
-pub async fn fetch_available_versions() -> Result<Vec<u32>, String> {
- let url = format!("{}/info/available_releases", ADOPTIUM_API_BASE);
-
- let response = reqwest::get(url)
- .await
- .map_err(|e| format!("Network request failed: {}", e))?;
-
- #[derive(Deserialize)]
- struct AvailableReleases {
- available_releases: Vec<u32>,
- }
-
- let releases: AvailableReleases = response
- .json()
- .await
- .map_err(|e| format!("Failed to parse response: {}", e))?;
-
- Ok(releases.available_releases)
-}
-
-/// Download and install Java with resume support and progress events
-///
-/// # Arguments
-/// * `app_handle` - Tauri app handle for accessing app directories
-/// * `major_version` - Java major version (e.g., 8, 11, 17)
-/// * `image_type` - JRE or JDK
-/// * `custom_path` - Optional custom installation path
-///
-/// # Returns
-/// * `Ok(JavaInstallation)` - Information about the successfully installed Java
-pub async fn download_and_install_java(
- app_handle: &AppHandle,
- major_version: u32,
- image_type: ImageType,
- custom_path: Option<PathBuf>,
-) -> Result<JavaInstallation, String> {
- // 1. Fetch download information
- let info = fetch_java_release(major_version, image_type).await?;
- let file_name = info.file_name.clone();
-
- // 2. Prepare installation directory
- let install_base = custom_path.unwrap_or_else(|| get_java_install_dir(app_handle));
- let version_dir = install_base.join(format!("temurin-{}-{}", major_version, image_type));
-
- std::fs::create_dir_all(&install_base)
- .map_err(|e| format!("Failed to create installation directory: {}", e))?;
-
- // 3. Add to download queue for persistence
- let mut queue = DownloadQueue::load(app_handle);
- queue.add(PendingJavaDownload {
- major_version,
- image_type: image_type.to_string(),
- download_url: info.download_url.clone(),
- file_name: info.file_name.clone(),
- file_size: info.file_size,
- checksum: info.checksum.clone(),
- install_path: install_base.to_string_lossy().to_string(),
- created_at: std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap()
- .as_secs(),
- });
- queue.save(app_handle)?;
-
- // 4. Download the archive with resume support
- let archive_path = install_base.join(&info.file_name);
-
- // Check if we need to download
- let need_download = if archive_path.exists() {
- if let Some(expected_checksum) = &info.checksum {
- let data = std::fs::read(&archive_path)
- .map_err(|e| format!("Failed to read downloaded file: {}", e))?;
- !downloader::verify_checksum(&data, Some(expected_checksum), None)
- } else {
- false
- }
- } else {
- true
- };
-
- if need_download {
- // Use resumable download
- downloader::download_with_resume(
- app_handle,
- &info.download_url,
- &archive_path,
- info.checksum.as_deref(),
- info.file_size,
- )
- .await?;
- }
-
- // 5. Emit extracting status
- let _ = app_handle.emit(
- "java-download-progress",
- JavaDownloadProgress {
- file_name: file_name.clone(),
- downloaded_bytes: info.file_size,
- total_bytes: info.file_size,
- speed_bytes_per_sec: 0,
- eta_seconds: 0,
- status: "Extracting".to_string(),
- percentage: 100.0,
- },
- );
-
- // 6. Extract
- // If the target directory exists, remove it first
- if version_dir.exists() {
- std::fs::remove_dir_all(&version_dir)
- .map_err(|e| format!("Failed to remove old version directory: {}", e))?;
- }
-
- std::fs::create_dir_all(&version_dir)
- .map_err(|e| format!("Failed to create version directory: {}", e))?;
-
- let top_level_dir = if info.file_name.ends_with(".tar.gz") || info.file_name.ends_with(".tgz") {
- zip::extract_tar_gz(&archive_path, &version_dir)?
- } else if info.file_name.ends_with(".zip") {
- zip::extract_zip(&archive_path, &version_dir)?;
- // Find the top-level directory inside the extracted folder
- find_top_level_dir(&version_dir)?
- } else {
- return Err(format!("Unsupported archive format: {}", info.file_name));
- };
-
- // 7. Clean up downloaded archive
- let _ = std::fs::remove_file(&archive_path);
-
- // 8. Locate java executable
- // macOS has a different structure: jdk-xxx/Contents/Home/bin/java
- // Linux/Windows: jdk-xxx/bin/java
- let java_home = version_dir.join(&top_level_dir);
- let java_bin = if cfg!(target_os = "macos") {
- java_home
- .join("Contents")
- .join("Home")
- .join("bin")
- .join("java")
- } else if cfg!(windows) {
- java_home.join("bin").join("java.exe")
- } else {
- java_home.join("bin").join("java")
- };
-
- if !java_bin.exists() {
- return Err(format!(
- "Installation completed but Java executable not found: {}",
- java_bin.display()
- ));
- }
-
- // Resolve symlinks and strip UNC prefix to ensure clean path
- let java_bin = std::fs::canonicalize(&java_bin).map_err(|e| e.to_string())?;
- let java_bin = strip_unc_prefix(java_bin);
-
- // 9. Verify installation
- let installation = check_java_installation(&java_bin)
- .ok_or_else(|| "Failed to verify Java installation".to_string())?;
-
- // 10. Remove from download queue
- queue.remove(major_version, &image_type.to_string());
- queue.save(app_handle)?;
-
- // 11. Emit completed status
- let _ = app_handle.emit(
- "java-download-progress",
- JavaDownloadProgress {
- file_name,
- downloaded_bytes: info.file_size,
- total_bytes: info.file_size,
- speed_bytes_per_sec: 0,
- eta_seconds: 0,
- status: "Completed".to_string(),
- percentage: 100.0,
- },
- );
-
- Ok(installation)
-}
-
-/// Find the top-level directory inside the extracted folder
-fn find_top_level_dir(extract_dir: &PathBuf) -> Result<String, String> {
- let entries: Vec<_> = std::fs::read_dir(extract_dir)
- .map_err(|e| format!("Failed to read directory: {}", e))?
- .filter_map(|e| e.ok())
- .filter(|e| e.path().is_dir())
- .collect();
-
- if entries.len() == 1 {
- Ok(entries[0].file_name().to_string_lossy().to_string())
- } else {
- // No single top-level directory, return empty string
- Ok(String::new())
- }
-}
-
-/// Detect Java installations on the system
-pub fn detect_java_installations() -> Vec<JavaInstallation> {
- let mut installations = Vec::new();
- let candidates = get_java_candidates();
-
- for candidate in candidates {
- if let Some(java) = check_java_installation(&candidate) {
- // Avoid duplicates
- if !installations
- .iter()
- .any(|j: &JavaInstallation| j.path == java.path)
- {
- installations.push(java);
- }
- }
- }
-
- // Sort by version (newer first)
- installations.sort_by(|a, b| {
- let v_a = parse_java_version(&a.version);
- let v_b = parse_java_version(&b.version);
- v_b.cmp(&v_a)
- });
-
- installations
-}
-
-/// Get list of candidate Java paths to check
-fn get_java_candidates() -> Vec<PathBuf> {
- let mut candidates = Vec::new();
-
- // Check PATH first
- let mut cmd = Command::new(if cfg!(windows) { "where" } else { "which" });
- cmd.arg("java");
- #[cfg(target_os = "windows")]
- cmd.creation_flags(0x08000000);
-
- if let Ok(output) = cmd.output() {
- if output.status.success() {
- let paths = String::from_utf8_lossy(&output.stdout);
- for line in paths.lines() {
- let path = PathBuf::from(line.trim());
- if path.exists() {
- // Resolve symlinks (important for Windows javapath wrapper)
- let resolved = std::fs::canonicalize(&path).unwrap_or(path);
- // Strip UNC prefix if present to keep paths clean
- let final_path = strip_unc_prefix(resolved);
- candidates.push(final_path);
- }
- }
- }
- }
-
- #[cfg(target_os = "linux")]
- {
- // Common Linux Java paths
- let linux_paths = [
- "/usr/lib/jvm",
- "/usr/java",
- "/opt/java",
- "/opt/jdk",
- "/opt/openjdk",
- ];
-
- for base in &linux_paths {
- if let Ok(entries) = std::fs::read_dir(base) {
- for entry in entries.flatten() {
- let java_path = entry.path().join("bin/java");
- if java_path.exists() {
- candidates.push(java_path);
- }
- }
- }
- }
-
- // Flatpak / Snap locations
- let home = std::env::var("HOME").unwrap_or_default();
- let snap_java = PathBuf::from(&home).join(".sdkman/candidates/java");
- if snap_java.exists() {
- if let Ok(entries) = std::fs::read_dir(&snap_java) {
- for entry in entries.flatten() {
- let java_path = entry.path().join("bin/java");
- if java_path.exists() {
- candidates.push(java_path);
- }
- }
- }
- }
- }
-
- #[cfg(target_os = "macos")]
- {
- // macOS Java paths
- let mac_paths = [
- "/Library/Java/JavaVirtualMachines",
- "/System/Library/Java/JavaVirtualMachines",
- "/usr/local/opt/openjdk/bin/java",
- "/opt/homebrew/opt/openjdk/bin/java",
- ];
-
- for path in &mac_paths {
- let p = PathBuf::from(path);
- if p.is_dir() {
- if let Ok(entries) = std::fs::read_dir(&p) {
- for entry in entries.flatten() {
- let java_path = entry.path().join("Contents/Home/bin/java");
- if java_path.exists() {
- candidates.push(java_path);
- }
- }
- }
- } else if p.exists() {
- candidates.push(p);
- }
- }
-
- // Homebrew ARM64
- let homebrew_arm = PathBuf::from("/opt/homebrew/Cellar/openjdk");
- if homebrew_arm.exists() {
- if let Ok(entries) = std::fs::read_dir(&homebrew_arm) {
- for entry in entries.flatten() {
- let java_path = entry
- .path()
- .join("libexec/openjdk.jdk/Contents/Home/bin/java");
- if java_path.exists() {
- candidates.push(java_path);
- }
- }
- }
- }
- }
-
- #[cfg(target_os = "windows")]
- {
- // Windows Java paths
- let program_files =
- std::env::var("ProgramFiles").unwrap_or_else(|_| "C:\\Program Files".to_string());
- let program_files_x86 = std::env::var("ProgramFiles(x86)")
- .unwrap_or_else(|_| "C:\\Program Files (x86)".to_string());
- let local_app_data = std::env::var("LOCALAPPDATA").unwrap_or_default();
-
- let win_paths = [
- format!("{}\\Java", program_files),
- format!("{}\\Java", program_files_x86),
- format!("{}\\Eclipse Adoptium", program_files),
- format!("{}\\AdoptOpenJDK", program_files),
- format!("{}\\Microsoft\\jdk", program_files),
- format!("{}\\Zulu", program_files),
- format!("{}\\Amazon Corretto", program_files),
- format!("{}\\BellSoft\\LibericaJDK", program_files),
- format!("{}\\Programs\\Eclipse Adoptium", local_app_data),
- ];
-
- for base in &win_paths {
- let base_path = PathBuf::from(base);
- if base_path.exists() {
- if let Ok(entries) = std::fs::read_dir(&base_path) {
- for entry in entries.flatten() {
- let java_path = entry.path().join("bin\\java.exe");
- if java_path.exists() {
- candidates.push(java_path);
- }
- }
- }
- }
- }
-
- // Also check JAVA_HOME
- if let Ok(java_home) = std::env::var("JAVA_HOME") {
- let java_path = PathBuf::from(&java_home).join("bin\\java.exe");
- if java_path.exists() {
- candidates.push(java_path);
- }
- }
- }
-
- // JAVA_HOME environment variable (cross-platform)
- if let Ok(java_home) = std::env::var("JAVA_HOME") {
- let bin_name = if cfg!(windows) { "java.exe" } else { "java" };
- let java_path = PathBuf::from(&java_home).join("bin").join(bin_name);
- if java_path.exists() {
- candidates.push(java_path);
- }
- }
-
- candidates
-}
-
-/// Check a specific Java installation and get its version info
-fn check_java_installation(path: &PathBuf) -> Option<JavaInstallation> {
- let mut cmd = Command::new(path);
- cmd.arg("-version");
- #[cfg(target_os = "windows")]
- cmd.creation_flags(0x08000000);
-
- let output = cmd.output().ok()?;
-
- // Java outputs version info to stderr
- let version_output = String::from_utf8_lossy(&output.stderr);
-
- // Parse version string (e.g., "openjdk version \"17.0.1\"" or "java version \"1.8.0_301\"")
- let version = parse_version_string(&version_output)?;
- let is_64bit = version_output.contains("64-Bit");
-
- Some(JavaInstallation {
- path: path.to_string_lossy().to_string(),
- version,
- is_64bit,
- })
-}
-
-/// Parse version string from java -version output
-fn parse_version_string(output: &str) -> Option<String> {
- for line in output.lines() {
- if line.contains("version") {
- // Find the quoted version string
- if let Some(start) = line.find('"') {
- if let Some(end) = line[start + 1..].find('"') {
- return Some(line[start + 1..start + 1 + end].to_string());
- }
- }
- }
- }
- None
-}
-
-/// Parse version for comparison (returns major version number)
-fn parse_java_version(version: &str) -> u32 {
- // Handle various formats:
- // - Old format: 1.8.0_xxx (Java 8 with update)
- // - New format: 17.0.1, 11.0.5+10 (Java 11+)
- // - Format with build: 21.0.3+13-Ubuntu-0ubuntu0.24.04.1
- // - Format with underscores: 1.8.0_411
-
- // First, strip build metadata (everything after '+')
- let version_only = version.split('+').next().unwrap_or(version);
-
- // Remove trailing junk (like "-Ubuntu-0ubuntu0.24.04.1")
- let version_only = version_only.split('-').next().unwrap_or(version_only);
-
- // Replace underscores with dots (1.8.0_411 -> 1.8.0.411)
- let normalized = version_only.replace('_', ".");
-
- // Split by dots
- let parts: Vec<&str> = normalized.split('.').collect();
-
- if let Some(first) = parts.first() {
- if *first == "1" {
- // Old format: 1.8.0 -> major is 8
- parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0)
- } else {
- // New format: 17.0.1 -> major is 17
- first.parse().unwrap_or(0)
- }
- } else {
- 0
- }
-}
-
-/// Get the best Java for a specific Minecraft version
-pub fn get_recommended_java(required_major_version: Option<u64>) -> Option<JavaInstallation> {
- let installations = detect_java_installations();
-
- if let Some(required) = required_major_version {
- // Find exact match or higher
- installations.into_iter().find(|java| {
- let major = parse_java_version(&java.version);
- major >= required as u32
- })
- } else {
- // Return newest
- installations.into_iter().next()
- }
-}
-
-/// Get compatible Java for a specific Minecraft version with upper bound
-/// For older Minecraft versions (1.13.x and below), we need Java 8 specifically
-/// as newer Java versions have compatibility issues with old Forge versions
-pub fn get_compatible_java(
- app_handle: &AppHandle,
- required_major_version: Option<u64>,
- max_major_version: Option<u32>,
-) -> Option<JavaInstallation> {
- let installations = detect_all_java_installations(app_handle);
-
- if let Some(max_version) = max_major_version {
- // Find Java version within the acceptable range
- installations.into_iter().find(|java| {
- let major = parse_java_version(&java.version);
- let meets_min = if let Some(required) = required_major_version {
- major >= required as u32
- } else {
- true
- };
- meets_min && major <= max_version
- })
- } else if let Some(required) = required_major_version {
- // Find exact match or higher (no upper bound)
- installations.into_iter().find(|java| {
- let major = parse_java_version(&java.version);
- major >= required as u32
- })
- } else {
- // Return newest
- installations.into_iter().next()
- }
-}
-
-/// Check if a Java installation is compatible with the required version range
-pub fn is_java_compatible(
- java_path: &str,
- required_major_version: Option<u64>,
- max_major_version: Option<u32>,
-) -> bool {
- let java_path_buf = PathBuf::from(java_path);
- if let Some(java) = check_java_installation(&java_path_buf) {
- let major = parse_java_version(&java.version);
- let meets_min = if let Some(required) = required_major_version {
- major >= required as u32
- } else {
- true
- };
- let meets_max = if let Some(max_version) = max_major_version {
- major <= max_version
- } else {
- true
- };
- meets_min && meets_max
- } else {
- false
- }
-}
-
-/// Detect all installed Java versions (including system installations and DropOut downloads)
-pub fn detect_all_java_installations(app_handle: &AppHandle) -> Vec<JavaInstallation> {
- let mut installations = detect_java_installations();
-
- // Add DropOut downloaded Java versions
- let dropout_java_dir = get_java_install_dir(app_handle);
- if dropout_java_dir.exists() {
- if let Ok(entries) = std::fs::read_dir(&dropout_java_dir) {
- for entry in entries.flatten() {
- let path = entry.path();
- if path.is_dir() {
- // Find the java executable in this directory
- let java_bin = find_java_executable(&path);
- if let Some(java_path) = java_bin {
- if let Some(java) = check_java_installation(&java_path) {
- if !installations.iter().any(|j| j.path == java.path) {
- installations.push(java);
- }
- }
- }
- }
- }
- }
- }
-
- // Sort by version
- installations.sort_by(|a, b| {
- let v_a = parse_java_version(&a.version);
- let v_b = parse_java_version(&b.version);
- v_b.cmp(&v_a)
- });
-
- installations
-}
-
-/// Find the java executable in a directory using a limited-depth search
-fn find_java_executable(dir: &PathBuf) -> Option<PathBuf> {
- let bin_name = if cfg!(windows) { "java.exe" } else { "java" };
-
- // Directly look in the bin directory
- let direct_bin = dir.join("bin").join(bin_name);
- if direct_bin.exists() {
- let resolved = std::fs::canonicalize(&direct_bin).unwrap_or(direct_bin);
- return Some(strip_unc_prefix(resolved));
- }
-
- // macOS: Contents/Home/bin/java
- #[cfg(target_os = "macos")]
- {
- let macos_bin = dir.join("Contents").join("Home").join("bin").join(bin_name);
- if macos_bin.exists() {
- return Some(macos_bin);
- }
- }
-
- // Look in subdirectories (handle nested directories after Adoptium extraction)
- if let Ok(entries) = std::fs::read_dir(dir) {
- for entry in entries.flatten() {
- let path = entry.path();
- if path.is_dir() {
- // Try direct bin path
- let nested_bin = path.join("bin").join(bin_name);
- if nested_bin.exists() {
- let resolved = std::fs::canonicalize(&nested_bin).unwrap_or(nested_bin);
- return Some(strip_unc_prefix(resolved));
- }
-
- // macOS: nested/Contents/Home/bin/java
- #[cfg(target_os = "macos")]
- {
- let macos_nested = path
- .join("Contents")
- .join("Home")
- .join("bin")
- .join(bin_name);
- if macos_nested.exists() {
- return Some(macos_nested);
- }
- }
- }
- }
- }
-
- None
-}
-
-/// Resume pending Java downloads from queue
-pub async fn resume_pending_downloads(
- app_handle: &AppHandle,
-) -> Result<Vec<JavaInstallation>, String> {
- let queue = DownloadQueue::load(app_handle);
- let mut installed = Vec::new();
-
- for pending in queue.pending_downloads.iter() {
- let image_type = if pending.image_type == "jdk" {
- ImageType::Jdk
- } else {
- ImageType::Jre
- };
-
- // Try to resume the download
- match download_and_install_java(
- app_handle,
- pending.major_version,
- image_type,
- Some(PathBuf::from(&pending.install_path)),
- )
- .await
- {
- Ok(installation) => {
- installed.push(installation);
- }
- Err(e) => {
- eprintln!(
- "Failed to resume Java {} {} download: {}",
- pending.major_version, pending.image_type, e
- );
- }
- }
- }
-
- Ok(installed)
-}
-
-/// Cancel current Java download
-pub fn cancel_current_download() {
- downloader::cancel_java_download();
-}
-
-/// Get pending downloads from queue
-pub fn get_pending_downloads(app_handle: &AppHandle) -> Vec<PendingJavaDownload> {
- let queue = DownloadQueue::load(app_handle);
- queue.pending_downloads
-}
-
-/// Clear a specific pending download
-#[allow(dead_code)]
-pub fn clear_pending_download(
- app_handle: &AppHandle,
- major_version: u32,
- image_type: &str,
-) -> Result<(), String> {
- let mut queue = DownloadQueue::load(app_handle);
- queue.remove(major_version, image_type);
- queue.save(app_handle)
-}
diff --git a/src-tauri/src/core/java/detection.rs b/src-tauri/src/core/java/detection.rs
new file mode 100644
index 0000000..95e7803
--- /dev/null
+++ b/src-tauri/src/core/java/detection.rs
@@ -0,0 +1,247 @@
+use std::io::Read;
+use std::path::PathBuf;
+use std::process::{Command, Stdio};
+use std::time::Duration;
+
+#[cfg(target_os = "windows")]
+use std::os::windows::process::CommandExt;
+
+use super::strip_unc_prefix;
+
+const WHICH_TIMEOUT: Duration = Duration::from_secs(2);
+
+/// Finds Java installation from SDKMAN! if available
+///
+/// Checks the standard SDKMAN! installation path:
+/// `~/.sdkman/candidates/java/current/bin/java`
+///
+/// # Returns
+/// `Some(PathBuf)` if SDKMAN! Java is found and exists, `None` otherwise
+pub fn find_sdkman_java() -> Option<PathBuf> {
+ let home = std::env::var("HOME").ok()?;
+ let sdkman_path = PathBuf::from(&home).join(".sdkman/candidates/java/current/bin/java");
+ if sdkman_path.exists() {
+ Some(sdkman_path)
+ } else {
+ None
+ }
+}
+
+/// Runs `which` (Unix) or `where` (Windows) command to find Java in PATH with timeout
+///
+/// This function spawns a subprocess to locate the `java` executable in the system PATH.
+/// It enforces a 2-second timeout to prevent hanging if the command takes too long.
+///
+/// # Returns
+/// `Some(String)` containing the output (paths separated by newlines) if successful,
+/// `None` if the command fails, times out, or returns non-zero exit code
+///
+/// # Platform-specific behavior
+/// - Unix/Linux/macOS: Uses `which java`
+/// - Windows: Uses `where java` and hides the console window
+///
+/// # Timeout Behavior
+/// If the command does not complete within 2 seconds, the process is killed
+/// and `None` is returned. This prevents the launcher from hanging on systems
+/// where `which`/`where` may be slow or unresponsive.
+fn run_which_command_with_timeout() -> Option<String> {
+ let mut cmd = Command::new(if cfg!(windows) { "where" } else { "which" });
+ cmd.arg("java");
+ // Hide console window on Windows
+ #[cfg(target_os = "windows")]
+ cmd.creation_flags(0x08000000);
+ cmd.stdout(Stdio::piped());
+
+ let mut child = cmd.spawn().ok()?;
+ let start = std::time::Instant::now();
+
+ loop {
+ // Check if timeout has been exceeded
+ if start.elapsed() > WHICH_TIMEOUT {
+ let _ = child.kill();
+ let _ = child.wait();
+ return None;
+ }
+
+ match child.try_wait() {
+ Ok(Some(status)) => {
+ if status.success() {
+ let mut output = String::new();
+ if let Some(mut stdout) = child.stdout.take() {
+ let _ = stdout.read_to_string(&mut output);
+ }
+ return Some(output);
+ } else {
+ let _ = child.wait();
+ return None;
+ }
+ }
+ Ok(None) => {
+ // Command still running, sleep briefly before checking again
+ std::thread::sleep(Duration::from_millis(50));
+ }
+ Err(_) => {
+ let _ = child.kill();
+ let _ = child.wait();
+ return None;
+ }
+ }
+ }
+}
+
+/// Detects all available Java installations on the system
+///
+/// This function searches for Java installations in multiple locations:
+/// - **All platforms**: `JAVA_HOME` environment variable, `java` in PATH
+/// - **Linux**: `/usr/lib/jvm`, `/usr/java`, `/opt/java`, `/opt/jdk`, `/opt/openjdk`, SDKMAN!
+/// - **macOS**: `/Library/Java/JavaVirtualMachines`, `/System/Library/Java/JavaVirtualMachines`,
+/// Homebrew paths (`/usr/local/opt/openjdk`, `/opt/homebrew/opt/openjdk`), SDKMAN!
+/// - **Windows**: `Program Files`, `Program Files (x86)`, `LOCALAPPDATA` for various JDK distributions
+///
+/// # Returns
+/// A vector of `PathBuf` pointing to Java executables found on the system.
+/// Note: Paths may include symlinks and duplicates; callers should canonicalize and deduplicate as needed.
+///
+/// # Examples
+/// ```ignore
+/// let candidates = get_java_candidates();
+/// for java_path in candidates {
+/// println!("Found Java at: {}", java_path.display());
+/// }
+/// ```
+pub fn get_java_candidates() -> Vec<PathBuf> {
+ let mut candidates = Vec::new();
+
+ // Try to find Java in PATH using 'which' or 'where' command with timeout
+ // CAUTION: linux 'which' may return symlinks, so we need to canonicalize later
+ if let Some(paths_str) = run_which_command_with_timeout() {
+ for line in paths_str.lines() {
+ let path = PathBuf::from(line.trim());
+ if path.exists() {
+ let resolved = std::fs::canonicalize(&path).unwrap_or(path);
+ let final_path = strip_unc_prefix(resolved);
+ candidates.push(final_path);
+ }
+ }
+ }
+
+ #[cfg(target_os = "linux")]
+ {
+ let linux_paths = [
+ "/usr/lib/jvm",
+ "/usr/java",
+ "/opt/java",
+ "/opt/jdk",
+ "/opt/openjdk",
+ ];
+
+ for base in &linux_paths {
+ if let Ok(entries) = std::fs::read_dir(base) {
+ for entry in entries.flatten() {
+ let java_path = entry.path().join("bin/java");
+ if java_path.exists() {
+ candidates.push(java_path);
+ }
+ }
+ }
+ }
+
+ // Check common SDKMAN! java candidates
+ if let Some(sdkman_java) = find_sdkman_java() {
+ candidates.push(sdkman_java);
+ }
+ }
+
+ #[cfg(target_os = "macos")]
+ {
+ let mac_paths = [
+ "/Library/Java/JavaVirtualMachines",
+ "/System/Library/Java/JavaVirtualMachines",
+ "/usr/local/opt/openjdk/bin/java",
+ "/opt/homebrew/opt/openjdk/bin/java",
+ ];
+
+ for path in &mac_paths {
+ let p = PathBuf::from(path);
+ if p.is_dir() {
+ if let Ok(entries) = std::fs::read_dir(&p) {
+ for entry in entries.flatten() {
+ let java_path = entry.path().join("Contents/Home/bin/java");
+ if java_path.exists() {
+ candidates.push(java_path);
+ }
+ }
+ }
+ } else if p.exists() {
+ candidates.push(p);
+ }
+ }
+
+ // Check common Homebrew java candidates for aarch64 macs
+ let homebrew_arm = PathBuf::from("/opt/homebrew/Cellar/openjdk");
+ if homebrew_arm.exists() {
+ if let Ok(entries) = std::fs::read_dir(&homebrew_arm) {
+ for entry in entries.flatten() {
+ let java_path = entry
+ .path()
+ .join("libexec/openjdk.jdk/Contents/Home/bin/java");
+ if java_path.exists() {
+ candidates.push(java_path);
+ }
+ }
+ }
+ }
+
+ // Check common SDKMAN! java candidates
+ if let Some(sdkman_java) = find_sdkman_java() {
+ candidates.push(sdkman_java);
+ }
+ }
+
+ #[cfg(target_os = "windows")]
+ {
+ let program_files =
+ std::env::var("ProgramFiles").unwrap_or_else(|_| "C:\\Program Files".to_string());
+ let program_files_x86 = std::env::var("ProgramFiles(x86)")
+ .unwrap_or_else(|_| "C:\\Program Files (x86)".to_string());
+ let local_app_data = std::env::var("LOCALAPPDATA").unwrap_or_default();
+
+ // Common installation paths for various JDK distributions
+ let mut win_paths = vec![];
+ for base in &[&program_files, &program_files_x86, &local_app_data] {
+ win_paths.push(format!("{}\\Java", base));
+ win_paths.push(format!("{}\\Eclipse Adoptium", base));
+ win_paths.push(format!("{}\\AdoptOpenJDK", base));
+ win_paths.push(format!("{}\\Microsoft\\jdk", base));
+ win_paths.push(format!("{}\\Zulu", base));
+ win_paths.push(format!("{}\\Amazon Corretto", base));
+ win_paths.push(format!("{}\\BellSoft\\LibericaJDK", base));
+ win_paths.push(format!("{}\\Programs\\Eclipse Adoptium", base));
+ }
+
+ for base in &win_paths {
+ let base_path = PathBuf::from(base);
+ if base_path.exists() {
+ if let Ok(entries) = std::fs::read_dir(&base_path) {
+ for entry in entries.flatten() {
+ let java_path = entry.path().join("bin\\java.exe");
+ if java_path.exists() {
+ candidates.push(java_path);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Check JAVA_HOME environment variable
+ if let Ok(java_home) = std::env::var("JAVA_HOME") {
+ let bin_name = if cfg!(windows) { "java.exe" } else { "java" };
+ let java_path = PathBuf::from(&java_home).join("bin").join(bin_name);
+ if java_path.exists() {
+ candidates.push(java_path);
+ }
+ }
+
+ candidates
+}
diff --git a/src-tauri/src/core/java/error.rs b/src-tauri/src/core/java/error.rs
new file mode 100644
index 0000000..bf78d3b
--- /dev/null
+++ b/src-tauri/src/core/java/error.rs
@@ -0,0 +1,95 @@
+use std::fmt;
+
+/// Unified error type for Java component operations
+///
+/// This enum represents all possible errors that can occur in the Java component,
+/// providing a consistent error handling interface across all modules.
+#[derive(Debug, Clone)]
+pub enum JavaError {
+ // Java installation not found at the specified path
+ NotFound,
+ // Invalid Java version format or unable to parse version
+ InvalidVersion(String),
+ // Java installation verification failed (e.g., -version command failed)
+ VerificationFailed(String),
+ // Network error during API calls or downloads
+ NetworkError(String),
+ // File I/O error (reading, writing, or accessing files)
+ IoError(String),
+ // Timeout occurred during operation
+ Timeout(String),
+ // Serialization/deserialization error
+ SerializationError(String),
+ // Invalid configuration or parameters
+ InvalidConfig(String),
+ // Download or installation failed
+ DownloadFailed(String),
+ // Extraction or decompression failed
+ ExtractionFailed(String),
+ // Checksum verification failed
+ ChecksumMismatch(String),
+ // Other unspecified errors
+ Other(String),
+}
+
+impl fmt::Display for JavaError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ JavaError::NotFound => write!(f, "Java installation not found"),
+ JavaError::InvalidVersion(msg) => write!(f, "Invalid Java version: {}", msg),
+ JavaError::VerificationFailed(msg) => write!(f, "Java verification failed: {}", msg),
+ JavaError::NetworkError(msg) => write!(f, "Network error: {}", msg),
+ JavaError::IoError(msg) => write!(f, "I/O error: {}", msg),
+ JavaError::Timeout(msg) => write!(f, "Operation timeout: {}", msg),
+ JavaError::SerializationError(msg) => write!(f, "Serialization error: {}", msg),
+ JavaError::InvalidConfig(msg) => write!(f, "Invalid configuration: {}", msg),
+ JavaError::DownloadFailed(msg) => write!(f, "Download failed: {}", msg),
+ JavaError::ExtractionFailed(msg) => write!(f, "Extraction failed: {}", msg),
+ JavaError::ChecksumMismatch(msg) => write!(f, "Checksum mismatch: {}", msg),
+ JavaError::Other(msg) => write!(f, "{}", msg),
+ }
+ }
+}
+
+impl std::error::Error for JavaError {}
+
+/// Convert JavaError to String for Tauri command results
+impl From<JavaError> for String {
+ fn from(err: JavaError) -> Self {
+ err.to_string()
+ }
+}
+
+/// Convert std::io::Error to JavaError
+impl From<std::io::Error> for JavaError {
+ fn from(err: std::io::Error) -> Self {
+ JavaError::IoError(err.to_string())
+ }
+}
+
+/// Convert serde_json::Error to JavaError
+impl From<serde_json::Error> for JavaError {
+ fn from(err: serde_json::Error) -> Self {
+ JavaError::SerializationError(err.to_string())
+ }
+}
+
+/// Convert reqwest::Error to JavaError
+impl From<reqwest::Error> for JavaError {
+ fn from(err: reqwest::Error) -> Self {
+ if err.is_timeout() {
+ JavaError::Timeout(err.to_string())
+ } else if err.is_connect() || err.is_request() {
+ JavaError::NetworkError(err.to_string())
+ } else {
+ JavaError::NetworkError(err.to_string())
+ }
+ }
+}
+
+/// Convert String to JavaError
+impl From<String> for JavaError {
+ fn from(err: String) -> Self {
+ JavaError::Other(err)
+ }
+}
diff --git a/src-tauri/src/core/java/mod.rs b/src-tauri/src/core/java/mod.rs
new file mode 100644
index 0000000..770ba08
--- /dev/null
+++ b/src-tauri/src/core/java/mod.rs
@@ -0,0 +1,530 @@
+use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
+use tauri::{AppHandle, Emitter, Manager};
+
+pub mod detection;
+pub mod error;
+pub mod persistence;
+pub mod priority;
+pub mod provider;
+pub mod providers;
+pub mod validation;
+
+pub use error::JavaError;
+
+/// Remove the UNC prefix (\\?\) from Windows paths
+pub fn strip_unc_prefix(path: PathBuf) -> PathBuf {
+ #[cfg(target_os = "windows")]
+ {
+ let s = path.to_string_lossy().to_string();
+ if s.starts_with(r"\\?\\") {
+ return PathBuf::from(&s[4..]);
+ }
+ }
+ path
+}
+
+use crate::core::downloader::{DownloadQueue, JavaDownloadProgress, PendingJavaDownload};
+use crate::utils::zip;
+use provider::JavaProvider;
+use providers::AdoptiumProvider;
+
+const CACHE_DURATION_SECS: u64 = 24 * 60 * 60;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct JavaInstallation {
+ pub path: String,
+ pub version: String,
+ pub arch: String,
+ pub vendor: String,
+ pub source: String,
+ pub is_64bit: bool,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum ImageType {
+ Jre,
+ Jdk,
+}
+
+impl Default for ImageType {
+ fn default() -> Self {
+ Self::Jre
+ }
+}
+
+impl std::fmt::Display for ImageType {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Jre => write!(f, "jre"),
+ Self::Jdk => write!(f, "jdk"),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct JavaReleaseInfo {
+ pub major_version: u32,
+ pub image_type: String,
+ pub version: String,
+ pub release_name: String,
+ pub release_date: Option<String>,
+ pub file_size: u64,
+ pub checksum: Option<String>,
+ pub download_url: String,
+ pub is_lts: bool,
+ pub is_available: bool,
+ pub architecture: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct JavaCatalog {
+ pub releases: Vec<JavaReleaseInfo>,
+ pub available_major_versions: Vec<u32>,
+ pub lts_versions: Vec<u32>,
+ pub cached_at: u64,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct JavaDownloadInfo {
+ pub version: String, // e.g., "17.0.2+8"
+ pub release_name: String, // e.g., "jdk-17.0.2+8"
+ pub download_url: String, // Direct download URL
+ pub file_name: String, // e.g., "OpenJDK17U-jre_x64_linux_hotspot_17.0.2_8.tar.gz"
+ pub file_size: u64, // in bytes
+ pub checksum: Option<String>, // SHA256 checksum
+ pub image_type: String, // "jre" or "jdk"
+}
+
+pub fn get_java_install_dir(app_handle: &AppHandle) -> PathBuf {
+ app_handle.path().app_data_dir().unwrap().join("java")
+}
+
+fn get_catalog_cache_path(app_handle: &AppHandle) -> PathBuf {
+ app_handle
+ .path()
+ .app_data_dir()
+ .unwrap()
+ .join("java_catalog_cache.json")
+}
+
+pub fn load_cached_catalog(app_handle: &AppHandle) -> Option<JavaCatalog> {
+ let cache_path = get_catalog_cache_path(app_handle);
+ if !cache_path.exists() {
+ return None;
+ }
+
+ // Read cache file
+ let content = std::fs::read_to_string(&cache_path).ok()?;
+ let catalog: JavaCatalog = serde_json::from_str(&content).ok()?;
+
+ // Get current time in seconds since UNIX_EPOCH
+ let now = std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
+
+ // Check if cache is still valid
+ if now - catalog.cached_at < CACHE_DURATION_SECS {
+ Some(catalog)
+ } else {
+ None
+ }
+}
+
+pub fn save_catalog_cache(app_handle: &AppHandle, catalog: &JavaCatalog) -> Result<(), String> {
+ let cache_path = get_catalog_cache_path(app_handle);
+ let content = serde_json::to_string_pretty(catalog).map_err(|e| e.to_string())?;
+ std::fs::write(&cache_path, content).map_err(|e| e.to_string())?;
+ Ok(())
+}
+
+#[allow(dead_code)]
+pub fn clear_catalog_cache(app_handle: &AppHandle) -> Result<(), String> {
+ let cache_path = get_catalog_cache_path(app_handle);
+ if cache_path.exists() {
+ std::fs::remove_file(&cache_path).map_err(|e| e.to_string())?;
+ }
+ Ok(())
+}
+
+pub async fn fetch_java_catalog(
+ app_handle: &AppHandle,
+ force_refresh: bool,
+) -> Result<JavaCatalog, String> {
+ let provider = AdoptiumProvider::new();
+ provider
+ .fetch_catalog(app_handle, force_refresh)
+ .await
+ .map_err(|e| e.to_string())
+}
+
+pub async fn fetch_java_release(
+ major_version: u32,
+ image_type: ImageType,
+) -> Result<JavaDownloadInfo, String> {
+ let provider = AdoptiumProvider::new();
+ provider
+ .fetch_release(major_version, image_type)
+ .await
+ .map_err(|e| e.to_string())
+}
+
+pub async fn fetch_available_versions() -> Result<Vec<u32>, String> {
+ let provider = AdoptiumProvider::new();
+ provider
+ .available_versions()
+ .await
+ .map_err(|e| e.to_string())
+}
+
+pub async fn download_and_install_java(
+ app_handle: &AppHandle,
+ major_version: u32,
+ image_type: ImageType,
+ custom_path: Option<PathBuf>,
+) -> Result<JavaInstallation, String> {
+ let provider = AdoptiumProvider::new();
+ let info = provider.fetch_release(major_version, image_type).await?;
+ let file_name = info.file_name.clone();
+
+ let install_base = custom_path.unwrap_or_else(|| get_java_install_dir(app_handle));
+ let version_dir = install_base.join(format!(
+ "{}-{}-{}",
+ provider.install_prefix(),
+ major_version,
+ image_type
+ ));
+
+ std::fs::create_dir_all(&install_base)
+ .map_err(|e| format!("Failed to create installation directory: {}", e))?;
+
+ let mut queue = DownloadQueue::load(app_handle);
+ queue.add(PendingJavaDownload {
+ major_version,
+ image_type: image_type.to_string(),
+ download_url: info.download_url.clone(),
+ file_name: info.file_name.clone(),
+ file_size: info.file_size,
+ checksum: info.checksum.clone(),
+ install_path: install_base.to_string_lossy().to_string(),
+ created_at: std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs(),
+ });
+ queue.save(app_handle)?;
+
+ let archive_path = install_base.join(&info.file_name);
+
+ let need_download = if archive_path.exists() {
+ if let Some(expected_checksum) = &info.checksum {
+ let data = std::fs::read(&archive_path)
+ .map_err(|e| format!("Failed to read downloaded file: {}", e))?;
+ !crate::core::downloader::verify_checksum(&data, Some(expected_checksum), None)
+ } else {
+ false
+ }
+ } else {
+ true
+ };
+
+ if need_download {
+ crate::core::downloader::download_with_resume(
+ app_handle,
+ &info.download_url,
+ &archive_path,
+ info.checksum.as_deref(),
+ info.file_size,
+ )
+ .await?;
+ }
+
+ let _ = app_handle.emit(
+ "java-download-progress",
+ JavaDownloadProgress {
+ file_name: file_name.clone(),
+ downloaded_bytes: info.file_size,
+ total_bytes: info.file_size,
+ speed_bytes_per_sec: 0,
+ eta_seconds: 0,
+ status: "Extracting".to_string(),
+ percentage: 100.0,
+ },
+ );
+
+ if version_dir.exists() {
+ std::fs::remove_dir_all(&version_dir)
+ .map_err(|e| format!("Failed to remove old version directory: {}", e))?;
+ }
+
+ std::fs::create_dir_all(&version_dir)
+ .map_err(|e| format!("Failed to create version directory: {}", e))?;
+
+ let top_level_dir = if info.file_name.ends_with(".tar.gz") || info.file_name.ends_with(".tgz") {
+ zip::extract_tar_gz(&archive_path, &version_dir)?
+ } else if info.file_name.ends_with(".zip") {
+ zip::extract_zip(&archive_path, &version_dir)?;
+ find_top_level_dir(&version_dir)?
+ } else {
+ return Err(format!("Unsupported archive format: {}", info.file_name));
+ };
+
+ let _ = std::fs::remove_file(&archive_path);
+
+ let java_home = version_dir.join(&top_level_dir);
+ let java_bin = if cfg!(target_os = "macos") {
+ java_home
+ .join("Contents")
+ .join("Home")
+ .join("bin")
+ .join("java")
+ } else if cfg!(windows) {
+ java_home.join("bin").join("java.exe")
+ } else {
+ java_home.join("bin").join("java")
+ };
+
+ if !java_bin.exists() {
+ return Err(format!(
+ "Installation completed but Java executable not found: {}",
+ java_bin.display()
+ ));
+ }
+
+ let java_bin = std::fs::canonicalize(&java_bin).map_err(|e| e.to_string())?;
+ let java_bin = strip_unc_prefix(java_bin);
+
+ let installation = validation::check_java_installation(&java_bin)
+ .await
+ .ok_or_else(|| "Failed to verify Java installation".to_string())?;
+
+ queue.remove(major_version, &image_type.to_string());
+ queue.save(app_handle)?;
+
+ let _ = app_handle.emit(
+ "java-download-progress",
+ JavaDownloadProgress {
+ file_name,
+ downloaded_bytes: info.file_size,
+ total_bytes: info.file_size,
+ speed_bytes_per_sec: 0,
+ eta_seconds: 0,
+ status: "Completed".to_string(),
+ percentage: 100.0,
+ },
+ );
+
+ Ok(installation)
+}
+
+fn find_top_level_dir(extract_dir: &PathBuf) -> Result<String, String> {
+ let entries: Vec<_> = std::fs::read_dir(extract_dir)
+ .map_err(|e| format!("Failed to read directory: {}", e))?
+ .filter_map(|e| e.ok())
+ .filter(|e| e.path().is_dir())
+ .collect();
+
+ if entries.len() == 1 {
+ Ok(entries[0].file_name().to_string_lossy().to_string())
+ } else {
+ Ok(String::new())
+ }
+}
+
+pub async fn detect_java_installations() -> Vec<JavaInstallation> {
+ let mut installations = Vec::new();
+ let candidates = detection::get_java_candidates();
+
+ for candidate in candidates {
+ if let Some(java) = validation::check_java_installation(&candidate).await {
+ if !installations
+ .iter()
+ .any(|j: &JavaInstallation| j.path == java.path)
+ {
+ installations.push(java);
+ }
+ }
+ }
+
+ installations.sort_by(|a, b| {
+ let v_a = validation::parse_java_version(&a.version);
+ let v_b = validation::parse_java_version(&b.version);
+ v_b.cmp(&v_a)
+ });
+
+ installations
+}
+
+pub async fn get_recommended_java(required_major_version: Option<u64>) -> Option<JavaInstallation> {
+ let installations = detect_java_installations().await;
+
+ if let Some(required) = required_major_version {
+ installations.into_iter().find(|java| {
+ let major = validation::parse_java_version(&java.version);
+ major >= required as u32
+ })
+ } else {
+ installations.into_iter().next()
+ }
+}
+
+pub async fn get_compatible_java(
+ app_handle: &AppHandle,
+ required_major_version: Option<u64>,
+ max_major_version: Option<u32>,
+) -> Option<JavaInstallation> {
+ let installations = detect_all_java_installations(app_handle).await;
+
+ installations.into_iter().find(|java| {
+ let major = validation::parse_java_version(&java.version);
+ validation::is_version_compatible(major, required_major_version, max_major_version)
+ })
+}
+
+pub async fn is_java_compatible(
+ java_path: &str,
+ required_major_version: Option<u64>,
+ max_major_version: Option<u32>,
+) -> bool {
+ let java_path_buf = PathBuf::from(java_path);
+ if let Some(java) = validation::check_java_installation(&java_path_buf).await {
+ let major = validation::parse_java_version(&java.version);
+ validation::is_version_compatible(major, required_major_version, max_major_version)
+ } else {
+ false
+ }
+}
+
+pub async fn detect_all_java_installations(app_handle: &AppHandle) -> Vec<JavaInstallation> {
+ let mut installations = detect_java_installations().await;
+
+ let dropout_java_dir = get_java_install_dir(app_handle);
+ if dropout_java_dir.exists() {
+ if let Ok(entries) = std::fs::read_dir(&dropout_java_dir) {
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if path.is_dir() {
+ let java_bin = find_java_executable(&path);
+ if let Some(java_path) = java_bin {
+ if let Some(java) = validation::check_java_installation(&java_path).await {
+ if !installations.iter().any(|j| j.path == java.path) {
+ installations.push(java);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ installations.sort_by(|a, b| {
+ let v_a = validation::parse_java_version(&a.version);
+ let v_b = validation::parse_java_version(&b.version);
+ v_b.cmp(&v_a)
+ });
+
+ installations
+}
+
+fn find_java_executable(dir: &PathBuf) -> Option<PathBuf> {
+ let bin_name = if cfg!(windows) { "java.exe" } else { "java" };
+
+ let direct_bin = dir.join("bin").join(bin_name);
+ if direct_bin.exists() {
+ let resolved = std::fs::canonicalize(&direct_bin).unwrap_or(direct_bin);
+ return Some(strip_unc_prefix(resolved));
+ }
+
+ #[cfg(target_os = "macos")]
+ {
+ let macos_bin = dir.join("Contents").join("Home").join("bin").join(bin_name);
+ if macos_bin.exists() {
+ return Some(macos_bin);
+ }
+ }
+
+ if let Ok(entries) = std::fs::read_dir(dir) {
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if path.is_dir() {
+ let nested_bin = path.join("bin").join(bin_name);
+ if nested_bin.exists() {
+ let resolved = std::fs::canonicalize(&nested_bin).unwrap_or(nested_bin);
+ return Some(strip_unc_prefix(resolved));
+ }
+
+ #[cfg(target_os = "macos")]
+ {
+ let macos_nested = path
+ .join("Contents")
+ .join("Home")
+ .join("bin")
+ .join(bin_name);
+ if macos_nested.exists() {
+ return Some(macos_nested);
+ }
+ }
+ }
+ }
+ }
+
+ None
+}
+
+pub async fn resume_pending_downloads(
+ app_handle: &AppHandle,
+) -> Result<Vec<JavaInstallation>, String> {
+ let queue = DownloadQueue::load(app_handle);
+ let mut installed = Vec::new();
+
+ for pending in queue.pending_downloads.iter() {
+ let image_type = if pending.image_type == "jdk" {
+ ImageType::Jdk
+ } else {
+ ImageType::Jre
+ };
+
+ match download_and_install_java(
+ app_handle,
+ pending.major_version,
+ image_type,
+ Some(PathBuf::from(&pending.install_path)),
+ )
+ .await
+ {
+ Ok(installation) => {
+ installed.push(installation);
+ }
+ Err(e) => {
+ eprintln!(
+ "Failed to resume Java {} {} download: {}",
+ pending.major_version, pending.image_type, e
+ );
+ }
+ }
+ }
+
+ Ok(installed)
+}
+
+pub fn cancel_current_download() {
+ crate::core::downloader::cancel_java_download();
+}
+
+pub fn get_pending_downloads(app_handle: &AppHandle) -> Vec<PendingJavaDownload> {
+ let queue = DownloadQueue::load(app_handle);
+ queue.pending_downloads
+}
+
+#[allow(dead_code)]
+pub fn clear_pending_download(
+ app_handle: &AppHandle,
+ major_version: u32,
+ image_type: &str,
+) -> Result<(), String> {
+ let mut queue = DownloadQueue::load(app_handle);
+ queue.remove(major_version, image_type);
+ queue.save(app_handle)
+}
diff --git a/src-tauri/src/core/java/persistence.rs b/src-tauri/src/core/java/persistence.rs
new file mode 100644
index 0000000..fd81394
--- /dev/null
+++ b/src-tauri/src/core/java/persistence.rs
@@ -0,0 +1,114 @@
+use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
+use tauri::{AppHandle, Manager};
+
+use super::error::JavaError;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct JavaConfig {
+ pub user_defined_paths: Vec<String>,
+ pub preferred_java_path: Option<String>,
+ pub last_detection_time: u64,
+}
+
+impl Default for JavaConfig {
+ fn default() -> Self {
+ Self {
+ user_defined_paths: Vec::new(),
+ preferred_java_path: None,
+ last_detection_time: 0,
+ }
+ }
+}
+
+fn get_java_config_path(app_handle: &AppHandle) -> PathBuf {
+ app_handle
+ .path()
+ .app_data_dir()
+ .unwrap()
+ .join("java_config.json")
+}
+
+pub fn load_java_config(app_handle: &AppHandle) -> JavaConfig {
+ let config_path = get_java_config_path(app_handle);
+ if !config_path.exists() {
+ return JavaConfig::default();
+ }
+
+ match std::fs::read_to_string(&config_path) {
+ Ok(content) => match serde_json::from_str(&content) {
+ Ok(config) => config,
+ Err(err) => {
+ // Log the error but don't panic - return default config
+ log::warn!(
+ "Failed to parse Java config at {}: {}. Using default configuration.",
+ config_path.display(),
+ err
+ );
+ JavaConfig::default()
+ }
+ },
+ Err(err) => {
+ log::warn!(
+ "Failed to read Java config at {}: {}. Using default configuration.",
+ config_path.display(),
+ err
+ );
+ JavaConfig::default()
+ }
+ }
+}
+
+pub fn save_java_config(app_handle: &AppHandle, config: &JavaConfig) -> Result<(), JavaError> {
+ let config_path = get_java_config_path(app_handle);
+ let content = serde_json::to_string_pretty(config)?;
+
+ std::fs::create_dir_all(config_path.parent().ok_or_else(|| {
+ JavaError::InvalidConfig("Java config path has no parent directory".to_string())
+ })?)?;
+
+ std::fs::write(&config_path, content)?;
+ Ok(())
+}
+
+#[allow(dead_code)]
+pub fn add_user_defined_path(app_handle: &AppHandle, path: String) -> Result<(), JavaError> {
+ let mut config = load_java_config(app_handle);
+ if !config.user_defined_paths.contains(&path) {
+ config.user_defined_paths.push(path);
+ }
+ save_java_config(app_handle, &config)
+}
+
+#[allow(dead_code)]
+pub fn remove_user_defined_path(app_handle: &AppHandle, path: &str) -> Result<(), JavaError> {
+ let mut config = load_java_config(app_handle);
+ config.user_defined_paths.retain(|p| p != path);
+ save_java_config(app_handle, &config)
+}
+
+#[allow(dead_code)]
+pub fn set_preferred_java_path(
+ app_handle: &AppHandle,
+ path: Option<String>,
+) -> Result<(), JavaError> {
+ let mut config = load_java_config(app_handle);
+ config.preferred_java_path = path;
+ save_java_config(app_handle, &config)
+}
+
+#[allow(dead_code)]
+pub fn get_preferred_java_path(app_handle: &AppHandle) -> Option<String> {
+ let config = load_java_config(app_handle);
+ config.preferred_java_path
+}
+
+#[allow(dead_code)]
+pub fn update_last_detection_time(app_handle: &AppHandle) -> Result<(), JavaError> {
+ let mut config = load_java_config(app_handle);
+ config.last_detection_time = std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .map_err(|e| JavaError::Other(format!("System time error: {}", e)))?
+ .as_secs();
+ save_java_config(app_handle, &config)
+}
diff --git a/src-tauri/src/core/java/priority.rs b/src-tauri/src/core/java/priority.rs
new file mode 100644
index 0000000..e456680
--- /dev/null
+++ b/src-tauri/src/core/java/priority.rs
@@ -0,0 +1,59 @@
+use tauri::AppHandle;
+
+use super::JavaInstallation;
+use crate::core::java::persistence;
+use crate::core::java::validation;
+
+pub async fn resolve_java_for_launch(
+ app_handle: &AppHandle,
+ instance_java_override: Option<&str>,
+ global_java_path: Option<&str>,
+ required_major_version: Option<u64>,
+ max_major_version: Option<u32>,
+) -> Option<JavaInstallation> {
+ if let Some(override_path) = instance_java_override {
+ if !override_path.is_empty() {
+ let path_buf = std::path::PathBuf::from(override_path);
+ if let Some(java) = validation::check_java_installation(&path_buf).await {
+ if is_version_compatible(&java, required_major_version, max_major_version) {
+ return Some(java);
+ }
+ }
+ }
+ }
+
+ if let Some(global_path) = global_java_path {
+ if !global_path.is_empty() {
+ let path_buf = std::path::PathBuf::from(global_path);
+ if let Some(java) = validation::check_java_installation(&path_buf).await {
+ if is_version_compatible(&java, required_major_version, max_major_version) {
+ return Some(java);
+ }
+ }
+ }
+ }
+
+ let preferred = persistence::get_preferred_java_path(app_handle);
+ if let Some(pref_path) = preferred {
+ let path_buf = std::path::PathBuf::from(&pref_path);
+ if let Some(java) = validation::check_java_installation(&path_buf).await {
+ if is_version_compatible(&java, required_major_version, max_major_version) {
+ return Some(java);
+ }
+ }
+ }
+
+ let installations = super::detect_all_java_installations(app_handle).await;
+ installations
+ .into_iter()
+ .find(|java| is_version_compatible(java, required_major_version, max_major_version))
+}
+
+fn is_version_compatible(
+ java: &JavaInstallation,
+ required_major_version: Option<u64>,
+ max_major_version: Option<u32>,
+) -> bool {
+ let major = validation::parse_java_version(&java.version);
+ validation::is_version_compatible(major, required_major_version, max_major_version)
+}
diff --git a/src-tauri/src/core/java/provider.rs b/src-tauri/src/core/java/provider.rs
new file mode 100644
index 0000000..8aa0a0d
--- /dev/null
+++ b/src-tauri/src/core/java/provider.rs
@@ -0,0 +1,58 @@
+use crate::core::java::{ImageType, JavaCatalog, JavaDownloadInfo, JavaError};
+use tauri::AppHandle;
+
+/// Trait for Java distribution providers (e.g., Adoptium, Corretto)
+///
+/// Implementations handle fetching Java catalogs and release information
+/// from different distribution providers.
+pub trait JavaProvider: Send + Sync {
+ /// Fetch the Java catalog (all available versions for this provider)
+ ///
+ /// # Arguments
+ /// * `app_handle` - The Tauri app handle for cache access
+ /// * `force_refresh` - If true, bypass cache and fetch fresh data
+ ///
+ /// # Returns
+ /// * `Ok(JavaCatalog)` with available versions
+ /// * `Err(JavaError)` if fetch or parsing fails
+ async fn fetch_catalog(
+ &self,
+ app_handle: &AppHandle,
+ force_refresh: bool,
+ ) -> Result<JavaCatalog, JavaError>;
+
+ /// Fetch a specific Java release
+ ///
+ /// # Arguments
+ /// * `major_version` - The major version number (e.g., 17, 21)
+ /// * `image_type` - Whether to fetch JRE or JDK
+ ///
+ /// # Returns
+ /// * `Ok(JavaDownloadInfo)` with download details
+ /// * `Err(JavaError)` if fetch or parsing fails
+ async fn fetch_release(
+ &self,
+ major_version: u32,
+ image_type: ImageType,
+ ) -> Result<JavaDownloadInfo, JavaError>;
+
+ /// Get list of available major versions
+ ///
+ /// # Returns
+ /// * `Ok(Vec<u32>)` with available major versions
+ /// * `Err(JavaError)` if fetch fails
+ async fn available_versions(&self) -> Result<Vec<u32>, JavaError>;
+
+ /// Get provider name (e.g., "adoptium", "corretto")
+ #[allow(dead_code)]
+ fn provider_name(&self) -> &'static str;
+
+ /// Get OS name for this provider's API
+ fn os_name(&self) -> &'static str;
+
+ /// Get architecture name for this provider's API
+ fn arch_name(&self) -> &'static str;
+
+ /// Get installation directory prefix (e.g., "temurin", "corretto")
+ fn install_prefix(&self) -> &'static str;
+}
diff --git a/src-tauri/src/core/java/providers/adoptium.rs b/src-tauri/src/core/java/providers/adoptium.rs
new file mode 100644
index 0000000..a73a0f6
--- /dev/null
+++ b/src-tauri/src/core/java/providers/adoptium.rs
@@ -0,0 +1,334 @@
+use crate::core::java::error::JavaError;
+use crate::core::java::provider::JavaProvider;
+use crate::core::java::save_catalog_cache;
+use crate::core::java::{ImageType, JavaCatalog, JavaDownloadInfo, JavaReleaseInfo};
+use serde::Deserialize;
+use tauri::AppHandle;
+
+const ADOPTIUM_API_BASE: &str = "https://api.adoptium.net/v3";
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct AdoptiumAsset {
+ pub binary: AdoptiumBinary,
+ pub release_name: String,
+ pub version: AdoptiumVersionData,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[allow(dead_code)]
+pub struct AdoptiumBinary {
+ pub os: String,
+ pub architecture: String,
+ pub image_type: String,
+ pub package: AdoptiumPackage,
+ #[serde(default)]
+ pub updated_at: Option<String>,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct AdoptiumPackage {
+ pub name: String,
+ pub link: String,
+ pub size: u64,
+ pub checksum: Option<String>,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[allow(dead_code)]
+pub struct AdoptiumVersionData {
+ pub major: u32,
+ pub minor: u32,
+ pub security: u32,
+ pub semver: String,
+ pub openjdk_version: String,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[allow(dead_code)]
+pub struct AvailableReleases {
+ pub available_releases: Vec<u32>,
+ pub available_lts_releases: Vec<u32>,
+ pub most_recent_lts: Option<u32>,
+ pub most_recent_feature_release: Option<u32>,
+}
+
+pub struct AdoptiumProvider;
+
+impl AdoptiumProvider {
+ pub fn new() -> Self {
+ Self
+ }
+}
+
+impl Default for AdoptiumProvider {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl JavaProvider for AdoptiumProvider {
+ async fn fetch_catalog(
+ &self,
+ app_handle: &AppHandle,
+ force_refresh: bool,
+ ) -> Result<JavaCatalog, JavaError> {
+ if !force_refresh {
+ if let Some(cached) = crate::core::java::load_cached_catalog(app_handle) {
+ return Ok(cached);
+ }
+ }
+
+ let os = self.os_name();
+ let arch = self.arch_name();
+ let client = reqwest::Client::new();
+
+ let releases_url = format!("{}/info/available_releases", ADOPTIUM_API_BASE);
+ let available: AvailableReleases = client
+ .get(&releases_url)
+ .header("Accept", "application/json")
+ .send()
+ .await
+ .map_err(|e| {
+ JavaError::NetworkError(format!("Failed to fetch available releases: {}", e))
+ })?
+ .json::<AvailableReleases>()
+ .await
+ .map_err(|e| {
+ JavaError::SerializationError(format!("Failed to parse available releases: {}", e))
+ })?;
+
+ // Parallelize HTTP requests for better performance
+ let mut fetch_tasks = Vec::new();
+
+ for major_version in &available.available_releases {
+ for image_type in &["jre", "jdk"] {
+ let major_version = *major_version;
+ let image_type = image_type.to_string();
+ let url = format!(
+ "{}/assets/latest/{}/hotspot?os={}&architecture={}&image_type={}",
+ ADOPTIUM_API_BASE, major_version, os, arch, image_type
+ );
+ let client = client.clone();
+ let is_lts = available.available_lts_releases.contains(&major_version);
+ let arch = arch.to_string();
+
+ let task = tokio::spawn(async move {
+ match client
+ .get(&url)
+ .header("Accept", "application/json")
+ .send()
+ .await
+ {
+ Ok(response) => {
+ if response.status().is_success() {
+ if let Ok(assets) = response.json::<Vec<AdoptiumAsset>>().await {
+ if let Some(asset) = assets.into_iter().next() {
+ let release_date = asset.binary.updated_at.clone();
+ return Some(JavaReleaseInfo {
+ major_version,
+ image_type,
+ version: asset.version.semver.clone(),
+ release_name: asset.release_name.clone(),
+ release_date,
+ file_size: asset.binary.package.size,
+ checksum: asset.binary.package.checksum,
+ download_url: asset.binary.package.link,
+ is_lts,
+ is_available: true,
+ architecture: asset.binary.architecture.clone(),
+ });
+ }
+ }
+ }
+ // Fallback for unsuccessful response
+ Some(JavaReleaseInfo {
+ major_version,
+ image_type,
+ version: format!("{}.x", major_version),
+ release_name: format!("jdk-{}", major_version),
+ release_date: None,
+ file_size: 0,
+ checksum: None,
+ download_url: String::new(),
+ is_lts,
+ is_available: false,
+ architecture: arch,
+ })
+ }
+ Err(_) => Some(JavaReleaseInfo {
+ major_version,
+ image_type,
+ version: format!("{}.x", major_version),
+ release_name: format!("jdk-{}", major_version),
+ release_date: None,
+ file_size: 0,
+ checksum: None,
+ download_url: String::new(),
+ is_lts,
+ is_available: false,
+ architecture: arch,
+ }),
+ }
+ });
+ fetch_tasks.push(task);
+ }
+ }
+
+ // Collect all results concurrently
+ let mut releases = Vec::new();
+ for task in fetch_tasks {
+ match task.await {
+ Ok(Some(release)) => {
+ releases.push(release);
+ }
+ Ok(None) => {
+ // Task completed but returned None, should not happen in current implementation
+ }
+ Err(e) => {
+ return Err(JavaError::NetworkError(format!(
+ "Failed to join Adoptium catalog fetch task: {}",
+ e
+ )));
+ }
+ }
+ }
+
+ let now = std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
+
+ let catalog = JavaCatalog {
+ releases,
+ available_major_versions: available.available_releases,
+ lts_versions: available.available_lts_releases,
+ cached_at: now,
+ };
+
+ let _ = save_catalog_cache(app_handle, &catalog);
+
+ Ok(catalog)
+ }
+
+ async fn fetch_release(
+ &self,
+ major_version: u32,
+ image_type: ImageType,
+ ) -> Result<JavaDownloadInfo, JavaError> {
+ let os = self.os_name();
+ let arch = self.arch_name();
+
+ let url = format!(
+ "{}/assets/latest/{}/hotspot?os={}&architecture={}&image_type={}",
+ ADOPTIUM_API_BASE, major_version, os, arch, image_type
+ );
+
+ let client = reqwest::Client::new();
+ let response = client
+ .get(&url)
+ .header("Accept", "application/json")
+ .send()
+ .await
+ .map_err(|e| JavaError::NetworkError(format!("Network request failed: {}", e)))?;
+
+ if !response.status().is_success() {
+ return Err(JavaError::NetworkError(format!(
+ "Adoptium API returned error: {} - The version/platform might be unavailable",
+ response.status()
+ )));
+ }
+
+ let assets: Vec<AdoptiumAsset> =
+ response.json::<Vec<AdoptiumAsset>>().await.map_err(|e| {
+ JavaError::SerializationError(format!("Failed to parse API response: {}", e))
+ })?;
+
+ let asset = assets
+ .into_iter()
+ .next()
+ .ok_or_else(|| JavaError::NotFound)?;
+
+ Ok(JavaDownloadInfo {
+ version: asset.version.semver.clone(),
+ release_name: asset.release_name,
+ download_url: asset.binary.package.link,
+ file_name: asset.binary.package.name,
+ file_size: asset.binary.package.size,
+ checksum: asset.binary.package.checksum,
+ image_type: asset.binary.image_type,
+ })
+ }
+
+ async fn available_versions(&self) -> Result<Vec<u32>, JavaError> {
+ let url = format!("{}/info/available_releases", ADOPTIUM_API_BASE);
+
+ let response = reqwest::get(url)
+ .await
+ .map_err(|e| JavaError::NetworkError(format!("Network request failed: {}", e)))?;
+
+ let releases: AvailableReleases =
+ response.json::<AvailableReleases>().await.map_err(|e| {
+ JavaError::SerializationError(format!("Failed to parse response: {}", e))
+ })?;
+
+ Ok(releases.available_releases)
+ }
+
+ fn provider_name(&self) -> &'static str {
+ "adoptium"
+ }
+
+ fn os_name(&self) -> &'static str {
+ #[cfg(target_os = "linux")]
+ {
+ if std::path::Path::new("/etc/alpine-release").exists() {
+ return "alpine-linux";
+ }
+ "linux"
+ }
+ #[cfg(target_os = "macos")]
+ {
+ "mac"
+ }
+ #[cfg(target_os = "windows")]
+ {
+ "windows"
+ }
+ #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
+ {
+ "linux"
+ }
+ }
+
+ fn arch_name(&self) -> &'static str {
+ #[cfg(target_arch = "x86_64")]
+ {
+ "x64"
+ }
+ #[cfg(target_arch = "aarch64")]
+ {
+ "aarch64"
+ }
+ #[cfg(target_arch = "x86")]
+ {
+ "x86"
+ }
+ #[cfg(target_arch = "arm")]
+ {
+ "arm"
+ }
+ #[cfg(not(any(
+ target_arch = "x86_64",
+ target_arch = "aarch64",
+ target_arch = "x86",
+ target_arch = "arm"
+ )))]
+ {
+ "x64"
+ }
+ }
+
+ fn install_prefix(&self) -> &'static str {
+ "temurin"
+ }
+}
diff --git a/src-tauri/src/core/java/providers/mod.rs b/src-tauri/src/core/java/providers/mod.rs
new file mode 100644
index 0000000..16eb5c7
--- /dev/null
+++ b/src-tauri/src/core/java/providers/mod.rs
@@ -0,0 +1,3 @@
+pub mod adoptium;
+
+pub use adoptium::AdoptiumProvider;
diff --git a/src-tauri/src/core/java/validation.rs b/src-tauri/src/core/java/validation.rs
new file mode 100644
index 0000000..48782f6
--- /dev/null
+++ b/src-tauri/src/core/java/validation.rs
@@ -0,0 +1,146 @@
+use std::collections::HashMap;
+use std::path::PathBuf;
+use std::process::Command;
+
+#[cfg(target_os = "windows")]
+use std::os::windows::process::CommandExt;
+
+use super::JavaInstallation;
+
+pub async fn check_java_installation(path: &PathBuf) -> Option<JavaInstallation> {
+ let path = path.clone();
+ tokio::task::spawn_blocking(move || check_java_installation_blocking(&path))
+ .await
+ .ok()?
+}
+
+fn check_java_installation_blocking(path: &PathBuf) -> Option<JavaInstallation> {
+ let mut cmd = Command::new(path);
+ cmd.arg("-version");
+
+ // Hide console window
+ #[cfg(target_os = "windows")]
+ cmd.creation_flags(0x08000000);
+
+ let output = cmd.output().ok()?;
+
+ let version_output = String::from_utf8_lossy(&output.stderr);
+
+ let version = parse_version_string(&version_output)?;
+ let arch = extract_architecture(&version_output);
+ let vendor = extract_vendor(&version_output);
+ let is_64bit = version_output.to_lowercase().contains("64-bit") || arch == "aarch64";
+
+ Some(JavaInstallation {
+ path: path.to_string_lossy().to_string(),
+ version,
+ arch,
+ vendor,
+ source: "system".to_string(),
+ is_64bit,
+ })
+}
+
+pub fn parse_version_string(output: &str) -> Option<String> {
+ for line in output.lines() {
+ if line.contains("version") {
+ if let Some(start) = line.find('"') {
+ if let Some(end) = line[start + 1..].find('"') {
+ return Some(line[start + 1..start + 1 + end].to_string());
+ }
+ }
+ }
+ }
+ None
+}
+
+pub fn parse_java_version(version: &str) -> u32 {
+ let parts: Vec<&str> = version.split('.').collect();
+ if let Some(first) = parts.first() {
+ // Handle both legacy (1.x) and modern (x) versioning
+ if *first == "1" {
+ // Legacy versioning
+ parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0)
+ } else {
+ // Modern versioning
+ first.parse().unwrap_or(0)
+ }
+ } else {
+ 0
+ }
+}
+
+pub fn extract_architecture(version_output: &str) -> String {
+ if version_output.contains("64-Bit") {
+ "x64".to_string()
+ } else if version_output.contains("32-Bit") {
+ "x86".to_string()
+ } else if version_output.contains("aarch64") || version_output.contains("ARM64") {
+ "aarch64".to_string()
+ } else {
+ "x64".to_string()
+ }
+}
+
+pub fn extract_vendor(version_output: &str) -> String {
+ let lower = version_output.to_lowercase();
+
+ let vendor_name: HashMap<&str, &str> = [
+ // Eclipse/Adoptium
+ ("temurin", "Temurin (Eclipse)"),
+ ("adoptium", "Eclipse Adoptium"),
+ // Amazon
+ ("corretto", "Corretto (Amazon)"),
+ ("amzn", "Corretto (Amazon)"),
+ // Alibaba
+ ("dragonwell", "Dragonwell (Alibaba)"),
+ ("albba", "Dragonwell (Alibaba)"),
+ // GraalVM
+ ("graalvm", "GraalVM"),
+ // Oracle
+ ("oracle", "Java SE Development Kit (Oracle)"),
+ // Tencent
+ ("kona", "Kona (Tencent)"),
+ // BellSoft
+ ("liberica", "Liberica (Bellsoft)"),
+ ("mandrel", "Mandrel (Red Hat)"),
+ // Microsoft
+ ("microsoft", "OpenJDK (Microsoft)"),
+ // SAP
+ ("sapmachine", "SapMachine (SAP)"),
+ // IBM
+ ("semeru", "Semeru (IBM)"),
+ ("sem", "Semeru (IBM)"),
+ // Azul
+ ("zulu", "Zulu (Azul Systems)"),
+ // Trava
+ ("trava", "Trava (Trava)"),
+ // Huawei
+ ("bisheng", "BiSheng (Huawei)"),
+ // Generic OpenJDK
+ ("openjdk", "OpenJDK"),
+ ]
+ .iter()
+ .cloned()
+ .collect();
+
+ for (key, name) in vendor_name {
+ if lower.contains(key) {
+ return name.to_string();
+ }
+ }
+
+ "Unknown".to_string()
+}
+
+pub fn is_version_compatible(
+ major: u32,
+ required_major_version: Option<u64>,
+ max_major_version: Option<u32>,
+) -> bool {
+ let meets_min = required_major_version
+ .map(|r| major >= r as u32)
+ .unwrap_or(true);
+ let meets_max = max_major_version.map(|m| major <= m).unwrap_or(true);
+ meets_min && meets_max
+}
diff --git a/src-tauri/src/core/manifest.rs b/src-tauri/src/core/manifest.rs
index e792071..9e4cb4e 100644
--- a/src-tauri/src/core/manifest.rs
+++ b/src-tauri/src/core/manifest.rs
@@ -3,20 +3,36 @@ use std::error::Error;
use std::path::PathBuf;
use crate::core::game_version::GameVersion;
-
-#[derive(Debug, Deserialize, Serialize)]
+use ts_rs::TS;
+
+#[derive(Debug, Deserialize, Serialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/manifest.ts"
+)]
pub struct VersionManifest {
pub latest: Latest,
pub versions: Vec<Version>,
}
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Deserialize, Serialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/manifest.ts"
+)]
pub struct Latest {
pub release: String,
pub snapshot: String,
}
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Deserialize, Serialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(
+ export,
+ export_to = "../../packages/ui-new/src/types/bindings/manifest.ts"
+)]
pub struct Version {
pub id: String,
#[serde(rename = "type")]
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index 0f1d7a1..b74c746 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -198,92 +198,54 @@ async fn start_game(
None
};
- // Check if configured Java is compatible
+ // Resolve Java using priority-based resolution
+ // Priority: instance override > global config > user preference > auto-detect
+ // TODO: refactor into a separate function
let app_handle = window.app_handle();
- let mut java_path_to_use = config.java_path.clone();
- if !java_path_to_use.is_empty() && java_path_to_use != "java" {
- let is_compatible =
- core::java::is_java_compatible(&java_path_to_use, required_java_major, max_java_major);
-
- if !is_compatible {
- emit_log!(
- window,
- format!(
- "Configured Java version may not be compatible. Looking for compatible Java..."
- )
- );
-
- // Try to find a compatible Java version
- if let Some(compatible_java) =
- core::java::get_compatible_java(app_handle, required_java_major, max_java_major)
- {
- emit_log!(
- window,
- format!(
- "Found compatible Java {} at: {}",
- compatible_java.version, compatible_java.path
- )
- );
- java_path_to_use = compatible_java.path;
- } else {
- let version_constraint = if let Some(max) = max_java_major {
- if let Some(min) = required_java_major {
- if min == max as u64 {
- format!("Java {}", min)
- } else {
- format!("Java {} to {}", min, max)
- }
- } else {
- format!("Java {} (or lower)", max)
- }
- } else if let Some(min) = required_java_major {
- format!("Java {} or higher", min)
- } else {
- "any Java version".to_string()
- };
+ let instance = instance_state
+ .get_instance(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
- return Err(format!(
- "No compatible Java installation found. This version requires {}. Please install a compatible Java version in settings.",
- version_constraint
- ));
- }
- }
- } else {
- // No Java configured, try to find a compatible one
- if let Some(compatible_java) =
- core::java::get_compatible_java(app_handle, required_java_major, max_java_major)
- {
- emit_log!(
- window,
- format!(
- "Using Java {} at: {}",
- compatible_java.version, compatible_java.path
- )
- );
- java_path_to_use = compatible_java.path;
- } else {
- let version_constraint = if let Some(max) = max_java_major {
- if let Some(min) = required_java_major {
- if min == max as u64 {
- format!("Java {}", min)
- } else {
- format!("Java {} to {}", min, max)
- }
+ let java_installation = core::java::priority::resolve_java_for_launch(
+ app_handle,
+ instance.java_path_override.as_deref(),
+ Some(&config.java_path),
+ required_java_major,
+ max_java_major,
+ )
+ .await
+ .ok_or_else(|| {
+ let version_constraint = if let Some(max) = max_java_major {
+ if let Some(min) = required_java_major {
+ if min == max as u64 {
+ format!("Java {}", min)
} else {
- format!("Java {} (or lower)", max)
+ format!("Java {} to {}", min, max)
}
- } else if let Some(min) = required_java_major {
- format!("Java {} or higher", min)
} else {
- "any Java version".to_string()
- };
+ format!("Java {} (or lower)", max)
+ }
+ } else if let Some(min) = required_java_major {
+ format!("Java {} or higher", min)
+ } else {
+ "any Java version".to_string()
+ };
- return Err(format!(
- "No compatible Java installation found. This version requires {}. Please install a compatible Java version in settings.",
- version_constraint
- ));
- }
- }
+ format!(
+ "No compatible Java installation found. This version requires {}. Please install a compatible Java version in settings.",
+ version_constraint
+ )
+ })?;
+
+ emit_log!(
+ window,
+ format!(
+ "Using Java {} at: {}",
+ java_installation.version, java_installation.path
+ )
+ );
+
+ let java_path_to_use = java_installation.path;
// 2. Prepare download tasks
emit_log!(window, "Preparing download tasks...".to_string());
@@ -1555,10 +1517,18 @@ async fn refresh_account(
/// Detect Java installations on the system
#[tauri::command]
+async fn detect_all_java_installations(
+ app_handle: tauri::AppHandle,
+) -> Result<Vec<core::java::JavaInstallation>, String> {
+ Ok(core::java::detect_all_java_installations(&app_handle).await)
+}
+
+/// Alias for detect_all_java_installations (for backward compatibility)
+#[tauri::command]
async fn detect_java(
app_handle: tauri::AppHandle,
) -> Result<Vec<core::java::JavaInstallation>, String> {
- Ok(core::java::detect_all_java_installations(&app_handle))
+ Ok(core::java::detect_all_java_installations(&app_handle).await)
}
/// Get recommended Java for a specific Minecraft version
@@ -1566,7 +1536,7 @@ async fn detect_java(
async fn get_recommended_java(
required_major_version: Option<u64>,
) -> Result<Option<core::java::JavaInstallation>, String> {
- Ok(core::java::get_recommended_java(required_major_version))
+ Ok(core::java::get_recommended_java(required_major_version).await)
}
/// Get Adoptium Java download info
@@ -1579,7 +1549,9 @@ async fn fetch_adoptium_java(
"jdk" => core::java::ImageType::Jdk,
_ => core::java::ImageType::Jre,
};
- core::java::fetch_java_release(major_version, img_type).await
+ core::java::fetch_java_release(major_version, img_type)
+ .await
+ .map_err(|e| e.to_string())
}
/// Download and install Adoptium Java
@@ -1595,13 +1567,17 @@ async fn download_adoptium_java(
_ => core::java::ImageType::Jre,
};
let path = custom_path.map(std::path::PathBuf::from);
- core::java::download_and_install_java(&app_handle, major_version, img_type, path).await
+ core::java::download_and_install_java(&app_handle, major_version, img_type, path)
+ .await
+ .map_err(|e| e.to_string())
}
/// Get available Adoptium Java versions
#[tauri::command]
async fn fetch_available_java_versions() -> Result<Vec<u32>, String> {
- core::java::fetch_available_versions().await
+ core::java::fetch_available_versions()
+ .await
+ .map_err(|e| e.to_string())
}
/// Fetch Java catalog with platform availability (uses cache)
@@ -1609,7 +1585,9 @@ async fn fetch_available_java_versions() -> Result<Vec<u32>, String> {
async fn fetch_java_catalog(
app_handle: tauri::AppHandle,
) -> Result<core::java::JavaCatalog, String> {
- core::java::fetch_java_catalog(&app_handle, false).await
+ core::java::fetch_java_catalog(&app_handle, false)
+ .await
+ .map_err(|e| e.to_string())
}
/// Refresh Java catalog (bypass cache)
@@ -1617,7 +1595,9 @@ async fn fetch_java_catalog(
async fn refresh_java_catalog(
app_handle: tauri::AppHandle,
) -> Result<core::java::JavaCatalog, String> {
- core::java::fetch_java_catalog(&app_handle, true).await
+ core::java::fetch_java_catalog(&app_handle, true)
+ .await
+ .map_err(|e| e.to_string())
}
/// Cancel current Java download
@@ -2060,7 +2040,7 @@ async fn install_forge(
config.java_path.clone()
} else {
// Try to find a suitable Java installation
- let javas = core::java::detect_all_java_installations(app_handle);
+ let javas = core::java::detect_all_java_installations(app_handle).await;
if let Some(java) = javas.first() {
java.path.clone()
} else {