1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
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
}
|