scl_core/
java.rs

1//! Java 的搜索,版本检测
2use std::path::{Path, PathBuf};
3
4use inner_future::stream::StreamExt;
5
6use crate::{prelude::*, utils::Arch};
7
8/// 一个 Java 运行时类型
9pub struct JavaRuntime {
10    java_path: String,
11    java_version: String,
12    java_main_version: u8,
13    java_64bit: bool,
14    java_arch: Arch,
15}
16
17impl JavaRuntime {
18    /// 通过一个指向 Java 可执行文件的路径来创建 [`JavaRuntime`]
19    ///
20    /// 在此会尝试运行这个文件并获取相关的版本信息,确认无误后返回
21    pub async fn from_java_path(java_path: impl AsRef<std::ffi::OsStr>) -> DynResult<Self> {
22        let output = query_java_version_output(&java_path).await?;
23        let version = query_java_version(&output);
24        let java_main_version = get_java_version(version);
25        let java_64bit = query_java_is_64bit(&output);
26        let java_arch = {
27            #[cfg(target_os = "macos")]
28            {
29                crate::utils::get_exec_arch(std::path::PathBuf::from(java_path.as_ref())).await?
30            }
31            #[cfg(target_os = "windows")]
32            {
33                // TODO: 这里要给 ARM 架构做支持
34                if java_64bit {
35                    Arch::X64
36                } else {
37                    Arch::X86
38                }
39            }
40            #[cfg(target_os = "linux")]
41            {
42                #[cfg(target_arch = "i686")]
43                {
44                    Arch::X86
45                }
46                #[cfg(target_arch = "x86_64")]
47                {
48                    Arch::X64
49                }
50                #[cfg(target_arch = "aarch64")]
51                {
52                    Arch::ARM64
53                }
54            }
55        };
56        Ok(Self {
57            java_path: java_path.as_ref().to_string_lossy().to_string(),
58            java_64bit,
59            java_version: version.to_owned(),
60            java_main_version,
61            java_arch,
62        })
63    }
64
65    /// 获取此 Java 运行时的可执行文件路径
66    #[inline]
67    pub fn path(&self) -> &str {
68        &self.java_path
69    }
70
71    /// 获取此 Java 运行时的版本号
72    #[inline]
73    pub fn version(&self) -> &str {
74        &self.java_version
75    }
76
77    /// 获取此 Java 运行时的运行架构是否是针对 64 位平台的
78    #[inline]
79    pub fn is_64bit(&self) -> bool {
80        self.java_64bit
81    }
82
83    /// 获取此 Java 运行时的主 Java 版本号
84    #[inline]
85    pub fn main_version(&self) -> u8 {
86        self.java_main_version
87    }
88
89    /// 获取此 Java 运行时的运行架构
90    #[inline]
91    pub fn arch(&self) -> Arch {
92        self.java_arch
93    }
94}
95
96/// 执行 Java 并使用 -version 获取其版本输出
97async fn query_java_version_output(java_path: impl AsRef<std::ffi::OsStr>) -> DynResult<String> {
98    let c = {
99        #[cfg(windows)]
100        {
101            use inner_future::process::windows::CommandExt;
102            inner_future::process::Command::new(java_path)
103                .arg("-version")
104                .stdout(std::process::Stdio::piped())
105                .stderr(std::process::Stdio::piped())
106                .creation_flags(0x00000200 | 0x08000000)
107                .spawn()?
108        }
109        #[cfg(not(windows))]
110        {
111            inner_future::process::Command::new(java_path)
112                .arg("-version")
113                .stdout(std::process::Stdio::piped())
114                .stderr(std::process::Stdio::piped())
115                .spawn()?
116        }
117    };
118    let output = c.output().await?;
119    Ok(String::from_utf8(output.stderr)?)
120}
121
122/// 根据 Java 的版本输出确认是否为 64 位版本
123fn query_java_is_64bit(java_output: &str) -> bool {
124    java_output.contains("64-Bit")
125}
126
127/// 根据 Java 裁剪的版本号文本确认主版本号
128/// - `1.8.x` 将返回 `8`
129/// - `1.7.x` 将返回 `7`
130/// - `10+` 将返回其主版本数字
131/// - 其余返回 `0` 表示未知版本
132fn get_java_version(java_version_string: &str) -> u8 {
133    fn parser(input: &str) -> nom::IResult<&str, &str> {
134        nom::character::complete::digit1(input)
135    }
136    if let Ok((_, r)) = parser(java_version_string) {
137        let r = r.parse().unwrap(); // 应当不会出错的
138        if r > 1 {
139            r
140        } else if java_version_string.contains("1.8") {
141            8
142        } else if java_version_string.contains("1.7") {
143            7
144        } else {
145            0 // 未知版本
146        }
147    } else if java_version_string.contains("1.8") {
148        8
149    } else if java_version_string.contains("1.7") {
150        7
151    } else {
152        0 // 未知版本
153    }
154}
155
156/// 搜索可能存在 Java 的地方找到可用的 Java 运行时
157/// 返回的结果列表项均为 java.exe/javaw.exe 或 java 执行文件的目录
158pub async fn search_for_java() -> Vec<String> {
159    // 从安装目录中搜索 Java
160    async fn check_bin_java_directory(path: impl AsRef<Path>, result: &mut Vec<String>) {
161        println!("Searching {}", path.as_ref().to_string_lossy());
162        if let Ok(mut d) = inner_future::fs::read_dir(path).await {
163            while let Ok(Some(d)) = d.try_next().await {
164                let mut path = d.path();
165                #[cfg(target_os = "macos")]
166                path.push("Contents/Home");
167                path.push("bin");
168                #[cfg(windows)]
169                {
170                    path.push("java.exe");
171                    if path.is_file() {
172                        result.push(d.path().to_string_lossy().to_string());
173                    }
174                    {
175                        path.pop();
176                        path.push("javaw.exe");
177                        if path.is_file() {
178                            result.push(path.to_string_lossy().to_string());
179                        }
180                    }
181                }
182                #[cfg(not(windows))]
183                {
184                    path.push("java");
185                    if path.is_file() {
186                        result.push(path.to_string_lossy().to_string());
187                    }
188                }
189            }
190        }
191    }
192
193    fn check_java(path: impl Into<PathBuf>, result: &mut Vec<String>) {
194        let mut path: PathBuf = path.into();
195        #[cfg(windows)]
196        {
197            path.push("java.exe");
198            if path.is_file() {
199                result.push(path.to_string_lossy().to_string());
200            }
201            path.pop();
202            path.push("javaw.exe");
203            if path.is_file() {
204                result.push(path.to_string_lossy().to_string());
205            }
206        }
207        #[cfg(not(windows))]
208        {
209            path.push("java");
210            if path.is_file() {
211                result.push(path.to_string_lossy().to_string());
212            }
213        }
214    }
215    #[cfg(windows)]
216    {
217        let mut result = Vec::with_capacity(16);
218        // 从注册表中搜索 JavaHome
219        use winreg::{enums::*, *};
220        fn search_local_machine_reg_value(key: &str, result: &mut Vec<String>) {
221            let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
222            if let Ok(subkey) = hklm
223                .open_subkey("SOFTWARE\\JavaSoft")
224                .and_then(|subkey| subkey.open_subkey(key))
225            {
226                for s in subkey.enum_keys().flatten() {
227                    let subkey = subkey.open_subkey(s);
228                    if let Ok(subkey) = subkey {
229                        if let Ok(v) = subkey.get_value("JavaHome") {
230                            result.push(v);
231                        }
232                    }
233                }
234            }
235        }
236        search_local_machine_reg_value("Java Runtime Environment", &mut result);
237        search_local_machine_reg_value("Java Development Kit", &mut result);
238        search_local_machine_reg_value("JRE", &mut result);
239        search_local_machine_reg_value("JDK", &mut result);
240        async fn list_java_directory(path: &str, result: &mut Vec<String>) {
241            let mut path = std::path::PathBuf::from(path);
242            path.push("Java");
243            check_bin_java_directory(&path, result).await;
244            path.pop();
245            path.push("BellSoft");
246            check_bin_java_directory(&path, result).await;
247            path.pop();
248            path.push("AdoptOpenJDK");
249            check_bin_java_directory(&path, result).await;
250            path.pop();
251            path.push("Zulu");
252            check_bin_java_directory(&path, result).await;
253            path.pop();
254            path.push("Microsoft");
255            check_bin_java_directory(&path, result).await;
256            path.pop();
257            path.push("Eclipse Foundation");
258            check_bin_java_directory(&path, result).await;
259            path.pop();
260            path.push("Semeru");
261            check_bin_java_directory(&path, result).await;
262            path.pop();
263        }
264        if let Ok(program_files) = std::env::var("ProgramFiles")
265            .or_else::<std::env::VarError, _>(|_| Ok("C:\\Program Files".into()))
266        {
267            list_java_directory(&program_files, &mut result).await;
268        }
269        if let Ok(program_files) = std::env::var("ProgramFiles(x86)")
270            .or_else::<std::env::VarError, _>(|_| Ok("C:\\Program Files (x86)".into()))
271        {
272            list_java_directory(&program_files, &mut result).await;
273            let mut minecraft_launcher_dir = PathBuf::from(program_files);
274            minecraft_launcher_dir
275                .push("Minecraft Launcher\\runtime\\jre-legacy\\windows-x64\\jre-legacy\\bin");
276            check_java(&minecraft_launcher_dir, &mut result);
277            minecraft_launcher_dir.pop();
278            minecraft_launcher_dir.pop();
279            minecraft_launcher_dir.pop();
280            minecraft_launcher_dir.push("windows-x86\\jre-legacy\\bin");
281            check_java(&minecraft_launcher_dir, &mut result);
282            minecraft_launcher_dir.pop();
283            minecraft_launcher_dir.pop();
284            minecraft_launcher_dir.pop();
285            minecraft_launcher_dir.pop();
286            minecraft_launcher_dir.push("java-runtime-alpha\\windows-x64\\java-runtime-alpha\\bin");
287            check_java(&minecraft_launcher_dir, &mut result);
288            minecraft_launcher_dir.pop();
289            minecraft_launcher_dir.pop();
290            minecraft_launcher_dir.pop();
291            minecraft_launcher_dir.push("windows-x86\\java-runtime-alpha\\bin");
292            check_java(&minecraft_launcher_dir, &mut result);
293        }
294        if let Ok(program_files) = std::env::var("ProgramFiles(ARM)")
295            .or_else::<std::env::VarError, _>(|_| Ok("C:\\Program Files (ARM)".into()))
296        {
297            list_java_directory(&program_files, &mut result).await;
298        }
299        // 从环境变量中查询 java.exe
300        if let Ok(paths) = std::env::var("PATH") {
301            for path in paths.split(';') {
302                check_java(path, &mut result);
303            }
304        }
305        // 从 JABBA 中查询 java.exe
306        if let Ok(path) = std::env::var("JABBA_HOME") {
307            let path = PathBuf::from(path).join("jdk");
308            check_bin_java_directory(path.as_path(), &mut result).await;
309        }
310        let mut result: Vec<_> = result
311            .into_iter()
312            .map(|mut a| {
313                #[cfg(windows)]
314                {
315                    if !a.ends_with("java.exe")
316                        && !a.ends_with("javaw.exe")
317                        && !a.ends_with("java")
318                        && !a.ends_with("javaw")
319                    {
320                        if !a.ends_with('\\') && !a.ends_with('/') {
321                            a.push('\\');
322                        }
323                        a.push_str("bin\\java.exe");
324                    }
325                }
326                #[cfg(not(windows))]
327                {
328                    if !a.ends_with("java") {
329                        if !a.ends_with('/') {
330                            a.push('/');
331                        }
332                        a.push_str("bin/java");
333                    }
334                }
335                a
336            })
337            .collect();
338        result.sort();
339        result.dedup();
340        result
341    }
342    #[cfg(target_os = "linux")]
343    {
344        let mut result = Vec::with_capacity(16);
345
346        // 一些常见目录
347        check_bin_java_directory("/usr/java/bin", &mut result).await;
348        check_bin_java_directory("/usr/lib/jvm/bin", &mut result).await;
349        check_bin_java_directory("/usr/lib32/jvm/bin", &mut result).await;
350
351        // 从环境变量中查询 java.exe
352        if let Ok(paths) = std::env::var("PATH") {
353            for path in paths.split(';') {
354                check_java(path, &mut result);
355            }
356        }
357        // 从 JABBA 中查询 java.exe
358        if let Ok(path) = std::env::var("JABBA_HOME") {
359            let path = PathBuf::from(path).join("jdk");
360            check_bin_java_directory(path.as_path(), &mut result).await;
361        }
362        // 主目录里的 .minecraft/runtime 文件夹
363        if let Some(mut home_path) = dirs::home_dir() {
364            home_path.push(".minecraft/runtime/jre-legacy/linux/jre-legacy/bin");
365            check_java(&home_path, &mut result);
366            home_path.push("../../../../java-runtime-alpha/linux/java-runtime-alpha/bin");
367            check_java(&home_path, &mut result);
368        }
369
370        result
371    }
372    #[cfg(target_os = "macos")]
373    {
374        let mut result = Vec::with_capacity(16);
375
376        check_bin_java_directory("/Library/Java/JavaVirtualMachines", &mut result).await;
377        check_bin_java_directory("/System/Library/Java/JavaVirtualMachines", &mut result).await;
378        check_java(
379            "/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java",
380            &mut result,
381        );
382        check_java("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java", &mut result);
383
384        result
385    }
386}
387
388/// 根据 Java 的输出裁剪出版本号文本
389fn query_java_version(java_output: &str) -> &str {
390    fn parser(input: &str) -> nom::IResult<&str, &str> {
391        nom::bytes::complete::take_until("\"")(input)
392    }
393    let pat = "version \"";
394    if let Some(p) = java_output.find(pat) {
395        if let Ok((_, r)) = parser(&java_output[p + pat.len()..]) {
396            r
397        } else {
398            ""
399        }
400    } else {
401        ""
402    }
403}