Skip to main content

hexomc_lib/java/
detector.rs

1use 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
34/// All candidate java executable paths (existence not checked).
35fn 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}