Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 73 additions & 9 deletions src-tauri/src/core/java/detection.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,84 @@
use std::io::Read;
use std::path::Path;
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;
use crate::core::java::strip_unc_prefix;

const WHICH_TIMEOUT: Duration = Duration::from_secs(2);

/// Scans a directory for Java installations, filtering out symlinks
///
/// # Arguments
/// * `base_dir` - Base directory to scan (e.g., mise or SDKMAN java dir)
/// * `should_skip` - Predicate to determine if an entry should be skipped
///
/// # Returns
/// First valid Java installation found, or `None`
fn scan_java_dir<F>(base_dir: &Path, should_skip: F) -> Option<PathBuf>
where
F: Fn(&std::fs::DirEntry) -> bool,
{
std::fs::read_dir(base_dir)
.ok()?
.flatten()
.filter(|entry| {
let path = entry.path();
// Only consider real directories, not symlinks
path.is_dir() && !path.is_symlink() && !should_skip(entry)
})
.find_map(|entry| {
let java_path = entry.path().join("bin/java");
if java_path.exists() && java_path.is_file() {
Some(java_path)
} else {
None
}
})
}
Comment on lines +20 to +42
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scan_java_dir() returns only the first matching Java (find_map), and read_dir order is filesystem-dependent. This makes SDKMAN/mise detection nondeterministic and also prevents detecting multiple installed versions under those managers. Consider returning a Vec<PathBuf> (collect all valid bin/java paths), sorting deterministically, and letting the compatibility filter pick the best match.

Copilot uses AI. Check for mistakes.

/// Finds Java installation from SDKMAN! if available
///
/// Checks the standard SDKMAN! installation path:
/// `~/.sdkman/candidates/java/current/bin/java`
/// Scans the SDKMAN! candidates directory and returns the first valid Java installation found.
/// Skips the 'current' symlink to avoid duplicates.
///
/// Path: `~/.sdkman/candidates/java/`
///
/// # Returns
/// `Some(PathBuf)` if SDKMAN! Java is found and exists, `None` otherwise
/// `Some(PathBuf)` pointing to `bin/java` if found, `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
let sdkman_base = PathBuf::from(&home).join(".sdkman/candidates/java/");

if !sdkman_base.exists() {
return None;
}

scan_java_dir(&sdkman_base, |entry| entry.file_name() == "current")
}

/// Finds Java installation from mise if available
///
/// Scans the mise Java installation directory and returns the first valid installation found.
/// Skips version alias symlinks (e.g., `21`, `21.0`, `latest`, `lts`) to avoid duplicates.
///
/// Path: `~/.local/share/mise/installs/java/`
///
/// # Returns
/// `Some(PathBuf)` pointing to `bin/java` if found, `None` otherwise
pub fn find_mise_java() -> Option<PathBuf> {
let home = std::env::var("HOME").ok()?;
let mise_base = PathBuf::from(&home).join(".local/share/mise/installs/java/");

if !mise_base.exists() {
return None;
}

scan_java_dir(&mise_base, |_| false) // mise: no additional filtering needed
}

/// Runs `which` (Unix) or `where` (Windows) command to find Java in PATH with timeout
Expand Down Expand Up @@ -150,6 +204,11 @@ pub fn get_java_candidates() -> Vec<PathBuf> {
if let Some(sdkman_java) = find_sdkman_java() {
candidates.push(sdkman_java);
}

// Check common mise java candidates
if let Some(mise_java) = find_mise_java() {
candidates.push(mise_java);
}
}

#[cfg(target_os = "macos")]
Expand Down Expand Up @@ -196,6 +255,11 @@ pub fn get_java_candidates() -> Vec<PathBuf> {
if let Some(sdkman_java) = find_sdkman_java() {
candidates.push(sdkman_java);
}

// Check common mise java candidates
if let Some(mise_java) = find_mise_java() {
candidates.push(mise_java);
}
}

#[cfg(target_os = "windows")]
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/core/java/persistence.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::error::JavaError;
use crate::core::java::error::JavaError;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tauri::{AppHandle, Manager};
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/core/java/priority.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use tauri::AppHandle;

use super::JavaInstallation;
use crate::core::java::persistence;
use crate::core::java::validation;
use crate::core::java::JavaInstallation;

pub async fn resolve_java_for_launch(
app_handle: &AppHandle,
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/core/java/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::process::Command;
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;

use super::JavaInstallation;
use crate::core::java::JavaInstallation;

pub async fn check_java_installation(path: &PathBuf) -> Option<JavaInstallation> {
let path = path.clone();
Expand Down