hexomc_lib/java/
detector.rs1use std::path::{Path, PathBuf};
2use std::process::Command;
3
4#[derive(Debug, Clone)]
5pub struct JavaInfo {
6 pub path: PathBuf,
7 pub version: u32,
8}
9
10pub fn find_java(required_version: u32) -> Option<JavaInfo> {
11 let candidates = collect_java_candidates();
12
13 for path in &candidates {
14 if let Some(info) = probe_java(path) {
15 if info.version == required_version {
16 return Some(info);
17 }
18 }
19 }
20
21 let mut best: Option<JavaInfo> = None;
22 for path in &candidates {
23 if let Some(info) = probe_java(path) {
24 if info.version >= required_version {
25 if best.as_ref().map_or(true, |b| info.version < b.version) {
26 best = Some(info);
27 }
28 }
29 }
30 }
31 best
32}
33
34fn collect_java_candidates() -> Vec<PathBuf> {
36 let mut candidates: Vec<PathBuf> = Vec::new();
37
38 if let Ok(java_home) = std::env::var("JAVA_HOME") {
39 candidates.push(PathBuf::from(&java_home).join("bin").join(java_exe()));
40 }
41
42 if let Ok(path) = which::which("java") {
43 candidates.push(path);
44 }
45
46 candidates.extend(scan_well_known_dirs());
47
48 candidates
49}
50
51fn scan_well_known_dirs() -> Vec<PathBuf> {
52 let mut found = Vec::new();
53
54 #[cfg(target_os = "windows")]
55 {
56 let roots = [
57 r"C:\Program Files\Java",
58 r"C:\Program Files\Eclipse Adoptium",
59 r"C:\Program Files\Microsoft",
60 r"C:\Program Files\Zulu",
61 ];
62 for root in &roots {
63 found.extend(scan_dir_for_java(Path::new(root)));
64 }
65 }
66
67 #[cfg(target_os = "macos")]
68 {
69 let root = Path::new("/Library/Java/JavaVirtualMachines");
70 if root.exists() {
71 if let Ok(entries) = std::fs::read_dir(root) {
72 for e in entries.flatten() {
73 let java = e
74 .path()
75 .join("Contents")
76 .join("Home")
77 .join("bin")
78 .join("java");
79 if java.exists() {
80 found.push(java);
81 }
82 }
83 }
84 }
85 }
86
87 #[cfg(target_os = "linux")]
88 {
89 let roots = ["/usr/lib/jvm", "/usr/local/lib/jvm"];
90 for root in &roots {
91 found.extend(scan_dir_for_java(Path::new(root)));
92 }
93 }
94
95 found
96}
97
98fn scan_dir_for_java(root: &Path) -> Vec<PathBuf> {
99 let mut found = Vec::new();
100 if !root.exists() {
101 return found;
102 }
103 if let Ok(entries) = std::fs::read_dir(root) {
104 for e in entries.flatten() {
105 let java = e.path().join("bin").join(java_exe());
106 if java.exists() {
107 found.push(java);
108 }
109 }
110 }
111 found
112}
113
114pub fn probe_java(path: &Path) -> Option<JavaInfo> {
115 let output = Command::new(path).arg("-version").output().ok()?;
116
117 let text = String::from_utf8_lossy(&output.stderr);
118 let version = parse_java_version(&text)?;
119 Some(JavaInfo {
120 path: path.to_path_buf(),
121 version,
122 })
123}
124
125fn parse_java_version(output: &str) -> Option<u32> {
126 for line in output.lines() {
127 if line.contains("version") {
128 let start = line.find('"')? + 1;
129 let end = line.rfind('"')?;
130 let ver_str = &line[start..end];
131
132 let first = ver_str.split('.').next()?;
133
134 if first == "1" {
135 let major: u32 = ver_str.split('.').nth(1)?.parse().ok()?;
136 return Some(major);
137 } else {
138 let major: u32 = first.parse().ok()?;
139 return Some(major);
140 }
141 }
142 }
143 None
144}
145
146fn java_exe() -> &'static str {
147 if cfg!(target_os = "windows") {
148 "java.exe"
149 } else {
150 "java"
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::parse_java_version;
157
158 #[test]
159 fn parse_java8() {
160 let output = r#"java version "1.8.0_392"
161Java(TM) SE Runtime Environment (build 1.8.0_392-b08)
162Java HotSpot(TM) 64-Bit Server VM (build 25.392-b08, mixed mode)"#;
163 assert_eq!(parse_java_version(output), Some(8));
164 }
165
166 #[test]
167 fn parse_java11() {
168 let output = r#"openjdk version "11.0.21" 2023-10-17
169OpenJDK Runtime Environment Temurin-11.0.21+9 (build 11.0.21+9)
170OpenJDK 64-Bit Server VM Temurin-11.0.21+9 (build 11.0.21+9, mixed mode)"#;
171 assert_eq!(parse_java_version(output), Some(11));
172 }
173
174 #[test]
175 fn parse_java17() {
176 let output = r#"openjdk version "17.0.9" 2023-10-17
177OpenJDK Runtime Environment Temurin-17.0.9+9 (build 17.0.9+9)
178OpenJDK 64-Bit Server VM Temurin-17.0.9+9 (build 17.0.9+9, mixed mode)"#;
179 assert_eq!(parse_java_version(output), Some(17));
180 }
181
182 #[test]
183 fn parse_java21() {
184 let output = r#"openjdk version "21" 2023-09-19
185OpenJDK Runtime Environment (build 21+35)
186OpenJDK 64-Bit Server VM (build 21+35, mixed mode, sharing)"#;
187 assert_eq!(parse_java_version(output), Some(21));
188 }
189
190 #[test]
191 fn parse_invalid() {
192 assert_eq!(parse_java_version("no version here"), None);
193 }
194}