aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src-tauri/src/core/java.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src-tauri/src/core/java.rs')
-rw-r--r--src-tauri/src/core/java.rs1119
1 files changed, 0 insertions, 1119 deletions
diff --git a/src-tauri/src/core/java.rs b/src-tauri/src/core/java.rs
deleted file mode 100644
index 459ab15..0000000
--- a/src-tauri/src/core/java.rs
+++ /dev/null
@@ -1,1119 +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 ts_rs::TS;
-
-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, TS)]
-#[serde(rename_all = "camelCase")]
-#[ts(export, export_to = "../../packages/ui-new/src/types/bindings/java.ts")]
-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, TS)]
-#[ts(
- export,
- rename_all = "lowercase",
- export_to = "../../packages/ui-new/src/types/bindings/java.ts"
-)]
-#[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, TS)]
-#[serde(rename_all = "camelCase")]
-#[ts(export, export_to = "../../packages/ui-new/src/types/bindings/java.ts")]
-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, TS)]
-#[serde(rename_all = "camelCase")]
-#[ts(export, export_to = "../../packages/ui-new/src/types/bindings/java.ts")]
-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, TS)]
-#[serde(rename_all = "camelCase")]
-#[ts(export, export_to = "../../packages/ui-new/src/types/bindings/java.ts")]
-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
-}
-
-/// TODO: Refactor to use a more robust method
-/// 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")]
- // hide console window
- 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)
-}