1use crate::{JavaError, JavaInfo};
15use log::debug;
16use std::collections::VecDeque;
17use std::env;
18use std::fs;
19use std::path::{Path, PathBuf};
20
21const MAX_SEARCH_DEPTH: usize = 6;
22
23const JAVA_KEYWORDS: &[&str] = &["java", "jdk", "jre", "jvm", "openjdk", "graalvm"];
24
25const EXCLUDE_FOLDERS: &[&str] = &[
26 "node_modules",
27 ".git",
28 "__pycache__",
29 "vendor",
30 "cache",
31 "temp",
32 "tmp",
33 "logs",
34 "log",
35];
36
37pub fn quick_search() -> Result<Vec<JavaInfo>, JavaError> {
65 let mut results: Vec<JavaInfo> = Vec::new();
66
67 if let Ok(paths) = env::var("PATH") {
68 for path in env::split_paths(&paths) {
69 let java_exe = if cfg!(windows) { "java.exe" } else { "java" };
70 let java_path = path.join(java_exe);
71
72 debug!("quick_search: checking PATH entry {:?}", java_path);
73 if java_path.is_file()
74 && let Ok(info) = JavaInfo::new(java_path.to_string_lossy().to_string())
75 {
76 debug!(
77 "quick_search: found {} at {}",
78 info.version,
79 java_path.display()
80 );
81 results.push(info);
82 }
83 }
84 }
85
86 debug!("quick_search: found {} Java installation(s)", results.len());
87 Ok(results)
88}
89
90fn dedup_nested(javas: Vec<JavaInfo>) -> Vec<JavaInfo> {
96 let mut keep = vec![true; javas.len()];
97 for i in 0..javas.len() {
98 for j in 0..javas.len() {
99 if i != j && javas[i].java_home.starts_with(&javas[j].java_home) {
100 keep[i] = false;
101 }
102 }
103 }
104 javas
105 .into_iter()
106 .enumerate()
107 .filter(|(i, _)| keep[*i])
108 .map(|(_, j)| j)
109 .collect()
110}
111
112pub fn deep_search() -> Result<Vec<JavaInfo>, JavaError> {
142 #[cfg(target_os = "windows")]
143 {
144 match deep_search_everything() {
145 Ok(results) => return Ok(results),
146 Err(e) => {
147 debug!(
148 "deep_search: Everything SDK unavailable ({}), falling back to full_search",
149 e
150 );
151 }
152 }
153 full_search()
154 }
155
156 #[cfg(target_os = "linux")]
157 {
158 full_search()
159 }
160
161 #[cfg(target_os = "macos")]
162 {
163 full_search()
164 }
165
166 #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
167 {
168 Ok(Vec::new())
169 }
170}
171
172pub fn full_search() -> Result<Vec<JavaInfo>, JavaError> {
212 let mut paths = Vec::new();
213
214 if let Some(jh) = env::var_os("JAVA_HOME") {
215 let java_exe =
216 Path::new(&jh)
217 .join("bin")
218 .join(if cfg!(windows) { "java.exe" } else { "java" });
219 let jh_str = java_exe.to_string_lossy();
220 debug!("full_search: checking JAVA_HOME at {jh_str}");
221 if java_exe.exists() {
222 debug!("full_search: JAVA_HOME -> {jh_str}");
223 paths.push(java_exe);
224 }
225 }
226
227 #[cfg(target_os = "windows")]
228 {
229 paths.extend(scan_registry());
230 paths.extend(scan_default_paths());
231 paths.extend(scan_chocolatey());
232 paths.extend(scan_scoop());
233 paths.extend(scan_jetbrains_windows());
234 paths.extend(scan_microsoft_store());
235 paths.extend(scan_where_command());
236 }
237
238 #[cfg(target_os = "linux")]
239 {
240 paths.extend(scan_linux());
241 paths.extend(scan_sdkman_linux());
242 paths.extend(scan_jbang_linux());
243 paths.extend(scan_asdf_linux());
244 paths.extend(scan_jetbrains_linux());
245 }
246
247 #[cfg(target_os = "macos")]
248 {
249 paths.extend(scan_macos());
250 paths.extend(scan_sdkman_macos());
251 paths.extend(scan_jbang_macos());
252 paths.extend(scan_asdf_macos());
253 paths.extend(scan_homebrew_macos());
254 paths.extend(scan_jetbrains_macos());
255 }
256
257 paths.sort();
258 paths.dedup();
259
260 debug!(
261 "full_search: {} candidate path(s) before JavaInfo resolution",
262 paths.len()
263 );
264
265 let javas: Vec<JavaInfo> = paths
266 .into_iter()
267 .filter_map(|p| {
268 let result = JavaInfo::new(p.to_string_lossy().to_string());
269 if result.is_err() {
270 debug!("full_search: skipping invalid path {}", p.display());
271 }
272 result.ok()
273 })
274 .collect();
275
276 let count_before = javas.len();
277 let javas = dedup_nested(javas);
278 let removed = count_before - javas.len();
279 if removed > 0 {
280 debug!("full_search: removed {removed} nested JRE entr(ies)");
281 }
282 Ok(javas)
283}
284
285#[cfg(feature = "parallel")]
315pub fn parallel_full_search() -> Result<Vec<JavaInfo>, JavaError> {
316 use rayon::prelude::*;
317
318 let mut paths = Vec::new();
319
320 if let Some(jh) = env::var_os("JAVA_HOME") {
321 let java_exe =
322 Path::new(&jh)
323 .join("bin")
324 .join(if cfg!(windows) { "java.exe" } else { "java" });
325 let jh_str = java_exe.to_string_lossy();
326 debug!("full_search: checking JAVA_HOME at {jh_str}");
327 if java_exe.exists() {
328 debug!("full_search: JAVA_HOME -> {jh_str}");
329 paths.push(java_exe);
330 }
331 }
332
333 #[cfg(target_os = "windows")]
334 {
335 paths.par_extend(scan_registry());
336 paths.par_extend(scan_default_paths());
337 paths.par_extend(scan_chocolatey());
338 paths.par_extend(scan_scoop());
339 paths.par_extend(scan_jetbrains_windows());
340 paths.par_extend(scan_microsoft_store());
341 paths.par_extend(scan_where_command());
342 }
343
344 #[cfg(target_os = "linux")]
345 {
346 paths.par_extend(scan_linux());
347 paths.par_extend(scan_sdkman_linux());
348 paths.par_extend(scan_jbang_linux());
349 paths.par_extend(scan_asdf_linux());
350 paths.par_extend(scan_jetbrains_linux());
351 }
352
353 #[cfg(target_os = "macos")]
354 {
355 paths.par_extend(scan_macos());
356 paths.par_extend(scan_sdkman_macos());
357 paths.par_extend(scan_jbang_macos());
358 paths.par_extend(scan_asdf_macos());
359 paths.par_extend(scan_homebrew_macos());
360 paths.par_extend(scan_jetbrains_macos());
361 }
362
363 paths.par_sort_unstable();
364 paths.dedup();
365
366 debug!(
367 "parallel_full_search: {} candidate path(s) before JavaInfo resolution",
368 paths.len()
369 );
370
371 let javas: Vec<JavaInfo> = paths
372 .into_par_iter()
373 .filter_map(|p| {
374 let result = JavaInfo::new(p.to_string_lossy().to_string());
375 if result.is_err() {
376 debug!(
377 "parallel_full_search: skipping invalid path {}",
378 p.display()
379 );
380 }
381 result.ok()
382 })
383 .collect();
384
385 let count_before = javas.len();
386 let javas = dedup_nested(javas);
387 let removed = count_before - javas.len();
388 if removed > 0 {
389 debug!("parallel_full_search: removed {removed} nested JRE entr(ies)");
390 }
391 Ok(javas)
392}
393
394#[cfg(target_os = "windows")]
395fn deep_search_everything() -> Result<Vec<JavaInfo>, JavaError> {
396 use everything_sdk::*;
397
398 let mut results: Vec<JavaInfo> = Vec::new();
399 let mut everything = global().try_lock().map_err(|_| {
400 JavaError::RuntimeError("Failed to lock Everything global state".to_string())
401 })?;
402
403 match everything.is_db_loaded() {
404 Ok(false) => {
405 debug!("deep_search_everything: database not fully loaded");
406 return Err(JavaError::ExecuteError(
407 "Everything database is not fully loaded".to_string(),
408 ));
409 }
410 Err(EverythingError::Ipc) => {
411 debug!("deep_search_everything: Everything IPC unavailable (not running)");
412 return Err(JavaError::ExecuteError(
413 "Everything is not running in the background. Please start Everything.exe"
414 .to_string(),
415 ));
416 }
417 _ => {}
418 }
419
420 debug!("deep_search_everything: querying Everything SDK");
421 let mut searcher = everything.searcher();
422 searcher.set_search("\"java.exe\" !C:\\Windows\\");
423 searcher.set_request_flags(
424 RequestFlags::EVERYTHING_REQUEST_FILE_NAME
425 | RequestFlags::EVERYTHING_REQUEST_PATH
426 | RequestFlags::EVERYTHING_REQUEST_SIZE,
427 );
428 searcher.set_sort(SortType::EVERYTHING_SORT_NAME_ASCENDING);
429
430 assert!(!searcher.get_match_case());
431
432 let query_results = searcher.query();
433
434 for item in query_results.iter() {
435 if let Ok(path) = item.filepath()
436 && let Ok(info) = JavaInfo::new(path.to_string_lossy().to_string())
437 {
438 debug!(
439 "deep_search_everything: found {} at {}",
440 info.version,
441 path.display()
442 );
443 results.push(info);
444 }
445 }
446
447 debug!(
448 "deep_search_everything: found {} Java installation(s)",
449 results.len()
450 );
451 Ok(results)
452}
453
454#[cfg(target_os = "windows")]
455fn scan_registry() -> Vec<PathBuf> {
456 use winreg::RegKey;
457 use winreg::enums::*;
458
459 let mut results = Vec::new();
460
461 let entries: &[(*mut std::ffi::c_void, &str, &str)] = &[
462 (
464 HKEY_LOCAL_MACHINE,
465 r"SOFTWARE\JavaSoft\Java Development Kit",
466 "JavaHome",
467 ),
468 (
469 HKEY_LOCAL_MACHINE,
470 r"SOFTWARE\JavaSoft\Java Runtime Environment",
471 "JavaHome",
472 ),
473 (
474 HKEY_LOCAL_MACHINE,
475 r"SOFTWARE\WOW6432Node\JavaSoft\Java Development Kit",
476 "JavaHome",
477 ),
478 (
479 HKEY_LOCAL_MACHINE,
480 r"SOFTWARE\WOW6432Node\JavaSoft\Java Runtime Environment",
481 "JavaHome",
482 ),
483 (
485 HKEY_CURRENT_USER,
486 r"SOFTWARE\JavaSoft\Java Development Kit",
487 "JavaHome",
488 ),
489 (
490 HKEY_CURRENT_USER,
491 r"SOFTWARE\JavaSoft\Java Runtime Environment",
492 "JavaHome",
493 ),
494 (
495 HKEY_CURRENT_USER,
496 r"SOFTWARE\WOW6432Node\JavaSoft\Java Development Kit",
497 "JavaHome",
498 ),
499 (
500 HKEY_CURRENT_USER,
501 r"SOFTWARE\WOW6432Node\JavaSoft\Java Runtime Environment",
502 "JavaHome",
503 ),
504 (
506 HKEY_LOCAL_MACHINE,
507 r"SOFTWARE\Azul Systems\Zulu",
508 "InstallationPath",
509 ),
510 (
511 HKEY_LOCAL_MACHINE,
512 r"SOFTWARE\BellSoft\Liberica",
513 "InstallationPath",
514 ),
515 (
516 HKEY_LOCAL_MACHINE,
517 r"SOFTWARE\Eclipse Foundation\Temurin",
518 "InstallationPath",
519 ),
520 (
521 HKEY_CURRENT_USER,
522 r"SOFTWARE\Eclipse Foundation\Temurin",
523 "InstallationPath",
524 ),
525 (
526 HKEY_LOCAL_MACHINE,
527 r"SOFTWARE\Amazon Corretto",
528 "InstallationPath",
529 ),
530 (
531 HKEY_CURRENT_USER,
532 r"SOFTWARE\Amazon Corretto",
533 "InstallationPath",
534 ),
535 (HKEY_LOCAL_MACHINE, r"SOFTWARE\GraalVM", "InstallationPath"),
536 (HKEY_CURRENT_USER, r"SOFTWARE\GraalVM", "InstallationPath"),
537 ];
538
539 for &(root, subpath, value_name) in entries {
540 debug!("scan_registry: checking {subpath}");
541 let key = match RegKey::predef(root).open_subkey(subpath) {
542 Ok(k) => k,
543 Err(_) => continue,
544 };
545 for subkey_name in key.enum_keys().filter_map(|k| k.ok()) {
546 let subkey = match key.open_subkey(subkey_name) {
547 Ok(sk) => sk,
548 Err(_) => continue,
549 };
550 let mut install_path: String = match subkey.get_value(value_name) {
551 Ok(v) => v,
552 Err(_) => continue,
553 };
554 install_path = install_path.trim_end_matches('\\').to_string();
555 let java_exe = Path::new(&install_path).join("bin").join("java.exe");
556 if java_exe.exists() {
557 debug!("scan_registry: found valid Java at {install_path}");
558 results.push(java_exe);
559 } else {
560 debug!("scan_registry: stale registry entry at {install_path}");
561 }
562 }
563 }
564
565 results
566}
567
568#[cfg(target_os = "windows")]
572fn scan_chocolatey() -> Vec<PathBuf> {
573 let prog_data = match env::var_os("ProgramData") {
574 Some(v) => Path::new(&v).join("chocolatey").join("lib"),
575 None => return Vec::new(),
576 };
577
578 debug!("scan_chocolatey: checking {}", prog_data.display());
579 if !prog_data.exists() {
580 return Vec::new();
581 }
582
583 let mut results = Vec::new();
584 if let Ok(entries) = fs::read_dir(&prog_data) {
585 for entry in entries.flatten() {
586 let dir = entry.path();
587 if !dir.is_dir() {
588 continue;
589 }
590 let java_exe = dir.join("bin").join("java.exe");
591 if java_exe.exists() {
592 debug!("scan_chocolatey: found Java at {}", java_exe.display());
593 results.push(java_exe);
594 }
595 }
596 }
597 results
598}
599
600#[cfg(target_os = "windows")]
604fn scan_scoop() -> Vec<PathBuf> {
605 let scoop_home = if let Some(v) = env::var_os("SCOOP") {
606 Path::new(&v).to_path_buf()
607 } else if let Some(v) = env::var_os("USERPROFILE") {
608 Path::new(&v).join("scoop").join("apps")
609 } else {
610 return Vec::new();
611 };
612
613 debug!("scan_scoop: checking {}", scoop_home.display());
614 if !scoop_home.exists() {
615 return Vec::new();
616 }
617
618 let mut results = Vec::new();
619 if let Ok(entries) = fs::read_dir(&scoop_home) {
620 for entry in entries.flatten() {
621 let dir_name = entry.file_name().to_string_lossy().to_lowercase();
622 if !JAVA_KEYWORDS.iter().any(|kw| dir_name.contains(kw)) {
623 continue;
624 }
625 let current_java = entry.path().join("current").join("bin").join("java.exe");
626 if current_java.exists() {
627 debug!("scan_scoop: found Java at {}", current_java.display());
628 results.push(current_java);
629 }
630 }
631 }
632 results
633}
634
635#[cfg(target_os = "windows")]
639fn scan_jetbrains_windows() -> Vec<PathBuf> {
640 let mut results = Vec::new();
641
642 let search_roots: Vec<PathBuf> = {
643 let mut roots = Vec::new();
644 if let Some(localappdata) = env::var_os("LOCALAPPDATA") {
645 roots.push(Path::new(&localappdata).join("JetBrains"));
646 }
647 if let Some(prog_files) = env::var_os("ProgramFiles") {
648 roots.push(Path::new(&prog_files).join("JetBrains"));
649 }
650 if let Some(prog_files_x86) = env::var_os("ProgramFiles(x86)") {
651 roots.push(Path::new(&prog_files_x86).join("JetBrains"));
652 }
653 roots
654 };
655
656 for root in search_roots {
657 if !root.exists() {
658 continue;
659 }
660 debug!("scan_jetbrains_windows: checking {}", root.display());
661 if let Ok(entries) = fs::read_dir(&root) {
662 for entry in entries.flatten() {
663 let jbr = entry.path().join("jbr").join("bin").join("java.exe");
664 if jbr.exists() {
665 debug!("scan_jetbrains_windows: found Java at {}", jbr.display());
666 results.push(jbr);
667 }
668 }
669 }
670 }
671 results
672}
673
674#[cfg(target_os = "linux")]
678fn scan_sdkman_linux() -> Vec<PathBuf> {
679 let home = match env::var("HOME") {
680 Ok(h) => h,
681 Err(_) => return Vec::new(),
682 };
683 let sdkman_candidates = Path::new(&home)
684 .join(".sdkman")
685 .join("candidates")
686 .join("java");
687 if !sdkman_candidates.exists() {
688 return Vec::new();
689 }
690
691 debug!(
692 "scan_sdkman_linux: checking {}",
693 sdkman_candidates.display()
694 );
695 let mut results = Vec::new();
696
697 if let Ok(entries) = fs::read_dir(&sdkman_candidates) {
698 for entry in entries.flatten() {
699 let java_exe = entry.path().join("bin").join("java");
700 if java_exe.exists() {
701 #[cfg(unix)]
702 {
703 use std::os::unix::fs::PermissionsExt;
704 if let Ok(metadata) = java_exe.metadata() {
705 if metadata.permissions().mode() & 0o111 != 0 {
706 debug!("scan_sdkman_linux: found Java at {}", java_exe.display());
707 results.push(java_exe);
708 }
709 }
710 }
711 #[cfg(not(unix))]
712 {
713 results.push(java_exe);
714 }
715 }
716 }
717 }
718 results
719}
720
721#[cfg(target_os = "linux")]
725fn scan_jetbrains_linux() -> Vec<PathBuf> {
726 let mut results = Vec::new();
727
728 let search_roots: Vec<PathBuf> = {
729 let mut roots: Vec<PathBuf> = vec![Path::new("/opt").join("JetBrains")];
730 if let Ok(home) = env::var("HOME") {
731 roots.push(
732 Path::new(&home)
733 .join(".local")
734 .join("share")
735 .join("JetBrains"),
736 );
737 }
738 roots
739 };
740
741 for root in &search_roots {
742 if !root.exists() {
743 continue;
744 }
745 debug!("scan_jetbrains_linux: checking {}", root.display());
746 if let Ok(entries) = fs::read_dir(root) {
747 for entry in entries.flatten() {
748 let jbr = entry.path().join("jbr").join("bin").join("java");
749 if jbr.exists() {
750 #[cfg(unix)]
751 {
752 use std::os::unix::fs::PermissionsExt;
753 if let Ok(metadata) = jbr.metadata() {
754 if metadata.permissions().mode() & 0o111 != 0 {
755 debug!("scan_jetbrains_linux: found Java at {}", jbr.display());
756 results.push(jbr);
757 }
758 }
759 }
760 #[cfg(not(unix))]
761 {
762 results.push(jbr);
763 }
764 }
765 }
766 }
767 }
768
769 results
770}
771
772#[cfg(target_os = "macos")]
776fn scan_sdkman_macos() -> Vec<PathBuf> {
777 let home = match env::var("HOME") {
778 Ok(h) => h,
779 Err(_) => return Vec::new(),
780 };
781 let sdkman_candidates = Path::new(&home)
782 .join(".sdkman")
783 .join("candidates")
784 .join("java");
785 if !sdkman_candidates.exists() {
786 return Vec::new();
787 }
788
789 debug!(
790 "scan_sdkman_macos: checking {}",
791 sdkman_candidates.display()
792 );
793 let mut results = Vec::new();
794
795 if let Ok(entries) = fs::read_dir(&sdkman_candidates) {
796 for entry in entries.flatten() {
797 let java_exe = entry.path().join("bin").join("java");
798 if java_exe.exists() {
799 #[cfg(unix)]
800 {
801 use std::os::unix::fs::PermissionsExt;
802 if let Ok(metadata) = java_exe.metadata() {
803 if metadata.permissions().mode() & 0o111 != 0 {
804 debug!("scan_sdkman_macos: found Java at {}", java_exe.display());
805 results.push(java_exe);
806 }
807 }
808 }
809 #[cfg(not(unix))]
810 {
811 results.push(java_exe);
812 }
813 }
814 }
815 }
816 results
817}
818
819#[cfg(target_os = "macos")]
823fn scan_homebrew_macos() -> Vec<PathBuf> {
824 let mut results = Vec::new();
825
826 let brew_roots: &[&str] = &["/opt/homebrew/opt", "/usr/local/opt"];
827
828 for root in brew_roots {
829 let root_path = Path::new(root);
830 if !root_path.exists() {
831 continue;
832 }
833 debug!("scan_homebrew_macos: checking {}", root);
834 if let Ok(entries) = fs::read_dir(root_path) {
835 for entry in entries.flatten() {
836 let dir_name = entry.file_name().to_string_lossy().to_lowercase();
837 if dir_name.starts_with("openjdk") || dir_name.starts_with("java") {
838 let java_exe = entry.path().join("bin").join("java");
839 if java_exe.exists() {
840 #[cfg(unix)]
841 {
842 use std::os::unix::fs::PermissionsExt;
843 if let Ok(metadata) = java_exe.metadata() {
844 if metadata.permissions().mode() & 0o111 != 0 {
845 debug!(
846 "scan_homebrew_macos: found Java at {}",
847 java_exe.display()
848 );
849 results.push(java_exe);
850 }
851 }
852 }
853 #[cfg(not(unix))]
854 {
855 results.push(java_exe);
856 }
857 }
858 }
859 }
860 }
861 }
862 results
863}
864
865#[cfg(target_os = "macos")]
869fn scan_jetbrains_macos() -> Vec<PathBuf> {
870 let mut results = Vec::new();
871
872 debug!("scan_jetbrains_macos: checking /Applications");
874 if let Ok(entries) = fs::read_dir("/Applications") {
875 for entry in entries.flatten() {
876 let app_name = entry.file_name().to_string_lossy().to_lowercase();
877 if !app_name.contains("jetbrains")
878 && !app_name.contains("intellij")
879 && !app_name.contains("android studio")
880 {
881 continue;
882 }
883 let jbr_home = entry
884 .path()
885 .join("Contents")
886 .join("jbr")
887 .join("Contents")
888 .join("Home");
889 let java_exe = jbr_home.join("bin").join("java");
890 if java_exe.exists() {
891 #[cfg(unix)]
892 {
893 use std::os::unix::fs::PermissionsExt;
894 if let Ok(metadata) = java_exe.metadata() {
895 if metadata.permissions().mode() & 0o111 != 0 {
896 debug!("scan_jetbrains_macos: found Java at {}", java_exe.display());
897 results.push(java_exe);
898 }
899 }
900 }
901 #[cfg(not(unix))]
902 {
903 results.push(java_exe);
904 }
905 }
906 }
907 }
908
909 if let Ok(home) = env::var("HOME") {
911 let jetbrains_support = Path::new(&home)
912 .join("Library")
913 .join("Application Support")
914 .join("JetBrains");
915 if jetbrains_support.exists() {
916 debug!(
917 "scan_jetbrains_macos: checking {}",
918 jetbrains_support.display()
919 );
920 if let Ok(entries) = fs::read_dir(&jetbrains_support) {
921 for entry in entries.flatten() {
922 let jbr_home = entry.path().join("jbr").join("Contents").join("Home");
923 let java_exe = jbr_home.join("bin").join("java");
924 if java_exe.exists() {
925 #[cfg(unix)]
926 {
927 use std::os::unix::fs::PermissionsExt;
928 if let Ok(metadata) = java_exe.metadata() {
929 if metadata.permissions().mode() & 0o111 != 0 {
930 debug!(
931 "scan_jetbrains_macos: found Java at {}",
932 java_exe.display()
933 );
934 results.push(java_exe);
935 }
936 }
937 }
938 #[cfg(not(unix))]
939 {
940 results.push(java_exe);
941 }
942 }
943 }
944 }
945 }
946 }
947
948 results
949}
950
951#[cfg(target_os = "linux")]
955fn scan_jbang_linux() -> Vec<PathBuf> {
956 let home = match env::var("HOME") {
957 Ok(h) => h,
958 Err(_) => return Vec::new(),
959 };
960 let jbang_jdks = Path::new(&home).join(".jbang").join("cache").join("jdks");
961 if !jbang_jdks.exists() {
962 return Vec::new();
963 }
964
965 debug!("scan_jbang_linux: checking {}", jbang_jdks.display());
966 let mut results = Vec::new();
967
968 if let Ok(entries) = fs::read_dir(&jbang_jdks) {
969 for entry in entries.flatten() {
970 let java_exe = entry.path().join("bin").join("java");
971 if java_exe.exists() {
972 #[cfg(unix)]
973 {
974 use std::os::unix::fs::PermissionsExt;
975 if let Ok(metadata) = java_exe.metadata() {
976 if metadata.permissions().mode() & 0o111 != 0 {
977 debug!("scan_jbang_linux: found Java at {}", java_exe.display());
978 results.push(java_exe);
979 }
980 }
981 }
982 #[cfg(not(unix))]
983 {
984 results.push(java_exe);
985 }
986 }
987 }
988 }
989 results
990}
991
992#[cfg(target_os = "linux")]
996fn scan_asdf_linux() -> Vec<PathBuf> {
997 let home = match env::var("HOME") {
998 Ok(h) => h,
999 Err(_) => return Vec::new(),
1000 };
1001 let asdf_java = Path::new(&home).join(".asdf").join("installs").join("java");
1002 if !asdf_java.exists() {
1003 return Vec::new();
1004 }
1005
1006 debug!("scan_asdf_linux: checking {}", asdf_java.display());
1007 let mut results = Vec::new();
1008
1009 if let Ok(entries) = fs::read_dir(&asdf_java) {
1010 for entry in entries.flatten() {
1011 let java_exe = entry.path().join("bin").join("java");
1012 if java_exe.exists() {
1013 #[cfg(unix)]
1014 {
1015 use std::os::unix::fs::PermissionsExt;
1016 if let Ok(metadata) = java_exe.metadata() {
1017 if metadata.permissions().mode() & 0o111 != 0 {
1018 debug!("scan_asdf_linux: found Java at {}", java_exe.display());
1019 results.push(java_exe);
1020 }
1021 }
1022 }
1023 #[cfg(not(unix))]
1024 {
1025 results.push(java_exe);
1026 }
1027 }
1028 }
1029 }
1030 results
1031}
1032
1033#[cfg(target_os = "macos")]
1037fn scan_jbang_macos() -> Vec<PathBuf> {
1038 let home = match env::var("HOME") {
1039 Ok(h) => h,
1040 Err(_) => return Vec::new(),
1041 };
1042 let jbang_jdks = Path::new(&home).join(".jbang").join("cache").join("jdks");
1043 if !jbang_jdks.exists() {
1044 return Vec::new();
1045 }
1046
1047 debug!("scan_jbang_macos: checking {}", jbang_jdks.display());
1048 let mut results = Vec::new();
1049
1050 if let Ok(entries) = fs::read_dir(&jbang_jdks) {
1051 for entry in entries.flatten() {
1052 let java_exe = entry.path().join("bin").join("java");
1053 if java_exe.exists() {
1054 #[cfg(unix)]
1055 {
1056 use std::os::unix::fs::PermissionsExt;
1057 if let Ok(metadata) = java_exe.metadata() {
1058 if metadata.permissions().mode() & 0o111 != 0 {
1059 debug!("scan_jbang_macos: found Java at {}", java_exe.display());
1060 results.push(java_exe);
1061 }
1062 }
1063 }
1064 #[cfg(not(unix))]
1065 {
1066 results.push(java_exe);
1067 }
1068 }
1069 }
1070 }
1071 results
1072}
1073
1074#[cfg(target_os = "macos")]
1078fn scan_asdf_macos() -> Vec<PathBuf> {
1079 let home = match env::var("HOME") {
1080 Ok(h) => h,
1081 Err(_) => return Vec::new(),
1082 };
1083 let asdf_java = Path::new(&home).join(".asdf").join("installs").join("java");
1084 if !asdf_java.exists() {
1085 return Vec::new();
1086 }
1087
1088 debug!("scan_asdf_macos: checking {}", asdf_java.display());
1089 let mut results = Vec::new();
1090
1091 if let Ok(entries) = fs::read_dir(&asdf_java) {
1092 for entry in entries.flatten() {
1093 let java_exe = entry.path().join("bin").join("java");
1094 if java_exe.exists() {
1095 #[cfg(unix)]
1096 {
1097 use std::os::unix::fs::PermissionsExt;
1098 if let Ok(metadata) = java_exe.metadata() {
1099 if metadata.permissions().mode() & 0o111 != 0 {
1100 debug!("scan_asdf_macos: found Java at {}", java_exe.display());
1101 results.push(java_exe);
1102 }
1103 }
1104 }
1105 #[cfg(not(unix))]
1106 {
1107 results.push(java_exe);
1108 }
1109 }
1110 }
1111 }
1112 results
1113}
1114
1115#[cfg(test)]
1116mod tests {
1117 use super::*;
1118
1119 #[test]
1120 fn test_dedup_nested_no_duplicates() {
1121 let javas = vec![
1122 JavaInfo {
1123 java_home: PathBuf::from("/usr/lib/jvm/java-11"),
1124 ..Default::default()
1125 },
1126 JavaInfo {
1127 java_home: PathBuf::from("/usr/lib/jvm/java-17"),
1128 ..Default::default()
1129 },
1130 ];
1131 let result = dedup_nested(javas);
1132 assert_eq!(result.len(), 2);
1133 }
1134
1135 #[test]
1136 fn test_dedup_nested_removes_jre() {
1137 let javas = vec![
1138 JavaInfo {
1139 java_home: PathBuf::from("/usr/lib/jvm/java-11"),
1140 ..Default::default()
1141 },
1142 JavaInfo {
1143 java_home: PathBuf::from("/usr/lib/jvm/java-11/jre"),
1144 ..Default::default()
1145 },
1146 ];
1147 let result = dedup_nested(javas);
1148 assert_eq!(result.len(), 1);
1149 assert_eq!(result[0].java_home, PathBuf::from("/usr/lib/jvm/java-11"));
1150 }
1151
1152 #[test]
1153 fn test_dedup_nested_same_path() {
1154 let javas = vec![
1155 JavaInfo {
1156 java_home: PathBuf::from("/usr/lib/jvm/java-11"),
1157 ..Default::default()
1158 },
1159 JavaInfo {
1160 java_home: PathBuf::from("/usr/lib/jvm/java-11"),
1161 ..Default::default()
1162 },
1163 ];
1164 let result = dedup_nested(javas);
1165 assert_eq!(result.len(), 0);
1166 }
1167
1168 #[test]
1169 fn test_dedup_nested_standalone_jre() {
1170 let javas = vec![
1171 JavaInfo {
1172 java_home: PathBuf::from("/usr/lib/jvm/java-11"),
1173 ..Default::default()
1174 },
1175 JavaInfo {
1176 java_home: PathBuf::from("/usr/lib/jvm/jre-8"),
1177 ..Default::default()
1178 },
1179 ];
1180 let result = dedup_nested(javas);
1181 assert_eq!(result.len(), 2);
1182 }
1183
1184 #[test]
1185 fn test_is_version_like_valid() {
1186 assert!(is_version_like("11.0.2"));
1187 assert!(is_version_like("1.8.0_202"));
1188 assert!(is_version_like("17_3"));
1189 }
1190
1191 #[test]
1192 fn test_is_version_like_invalid() {
1193 assert!(!is_version_like(""));
1194 assert!(!is_version_like(&"a".repeat(21)));
1195 assert!(!is_version_like("no_digits"));
1196 }
1197
1198 #[test]
1199 fn test_is_version_like_more() {
1200 assert!(is_version_like("8"));
1201 assert!(is_version_like("11.0"));
1202 assert!(is_version_like("1.8"));
1203 assert!(is_version_like("20.0.0.0"));
1204 assert!(is_version_like(".1"));
1205 assert!(!is_version_like(".."));
1206 assert!(!is_version_like("a-b-c"));
1207 }
1208
1209 #[test]
1210 fn test_dedup_nested_deeply_nested() {
1211 let javas = vec![
1212 JavaInfo {
1213 java_home: PathBuf::from("/usr/lib/jvm/java-11"),
1214 ..Default::default()
1215 },
1216 JavaInfo {
1217 java_home: PathBuf::from("/usr/lib/jvm/java-11/jre"),
1218 ..Default::default()
1219 },
1220 JavaInfo {
1221 java_home: PathBuf::from("/usr/lib/jvm/java-11/jre/lib"),
1222 ..Default::default()
1223 },
1224 ];
1225 let result = dedup_nested(javas);
1226 assert_eq!(result.len(), 1);
1227 assert_eq!(result[0].java_home, PathBuf::from("/usr/lib/jvm/java-11"));
1228 }
1229
1230 #[test]
1231 fn test_filter_by_version() {
1232 let javas = vec![
1233 JavaInfo {
1234 version: "11.0.2".into(),
1235 parsed_version: crate::JavaVersion::parse("11.0.2"),
1236 ..Default::default()
1237 },
1238 JavaInfo {
1239 version: "17.0.1".into(),
1240 parsed_version: crate::JavaVersion::parse("17.0.1"),
1241 ..Default::default()
1242 },
1243 ];
1244 let filtered = crate::filter_by_version(javas, "17");
1245 assert_eq!(filtered.len(), 1);
1246 assert_eq!(filtered[0].version, "17.0.1");
1247 }
1248
1249 #[test]
1250 fn test_best_match() {
1251 let javas = vec![
1252 JavaInfo {
1253 version: "11.0.2".into(),
1254 parsed_version: crate::JavaVersion::parse("11.0.2"),
1255 ..Default::default()
1256 },
1257 JavaInfo {
1258 version: "17.0.1".into(),
1259 parsed_version: crate::JavaVersion::parse("17.0.1"),
1260 ..Default::default()
1261 },
1262 JavaInfo {
1263 version: "8.0_202".into(),
1264 parsed_version: crate::JavaVersion::parse("1.8.0_202"),
1265 ..Default::default()
1266 },
1267 ];
1268 let best = crate::best_match(javas, "8");
1269 assert!(best.is_some());
1270 assert_eq!(best.unwrap().version, "8.0_202");
1271 }
1272
1273 #[test]
1274 fn test_filter_by_version_no_match() {
1275 let javas = vec![JavaInfo {
1276 version: "11.0.2".into(),
1277 parsed_version: crate::JavaVersion::parse("11.0.2"),
1278 ..Default::default()
1279 }];
1280 let filtered = crate::filter_by_version(javas, "17");
1281 assert!(filtered.is_empty());
1282 }
1283
1284 #[test]
1285 fn test_best_match_no_match() {
1286 let javas = vec![JavaInfo {
1287 version: "11.0.2".into(),
1288 parsed_version: crate::JavaVersion::parse("11.0.2"),
1289 ..Default::default()
1290 }];
1291 let best = crate::best_match(javas, "17");
1292 assert!(best.is_none());
1293 }
1294}
1295
1296#[cfg(target_os = "windows")]
1300fn scan_default_paths() -> Vec<PathBuf> {
1301 let mut results = Vec::new();
1302 let roots = default_path_roots();
1303
1304 for root in roots {
1305 if !root.exists() {
1306 debug!(
1307 "scan_default_paths: root does not exist: {}",
1308 root.display()
1309 );
1310 continue;
1311 }
1312
1313 debug!("scan_default_paths: starting BFS from {}", root.display());
1314 let mut queue = VecDeque::new();
1315 queue.push_back((root, 0));
1316
1317 while let Some((current, depth)) = queue.pop_front() {
1318 if depth > MAX_SEARCH_DEPTH {
1319 continue;
1320 }
1321
1322 if let Ok(entries) = fs::read_dir(¤t) {
1323 for entry in entries.flatten() {
1324 let path = entry.path();
1325 if !path.is_dir() {
1326 continue;
1327 }
1328
1329 let java_exe = path.join("java.exe");
1330 if java_exe.exists() {
1331 debug!("scan_default_paths: found Java at {}", path.display());
1332 results.push(java_exe);
1333 continue;
1334 }
1335
1336 if should_explore_deeper(&path) {
1337 debug!(
1338 "scan_default_paths: exploring {} (depth {})",
1339 path.display(),
1340 depth + 1
1341 );
1342 queue.push_back((path, depth + 1));
1343 }
1344 }
1345 }
1346 }
1347 }
1348
1349 results
1350}
1351
1352#[cfg(target_os = "windows")]
1353fn default_path_roots() -> Vec<PathBuf> {
1354 let mut roots = Vec::new();
1355
1356 if let Some(appdata) = env::var_os("APPDATA") {
1357 roots.push(Path::new(&appdata).join(".minecraft").join("runtime"));
1358 }
1359
1360 if let Some(localappdata) = env::var_os("LOCALAPPDATA") {
1361 roots.push(Path::new(&localappdata).to_path_buf());
1362 }
1363
1364 if let Some(profile) = env::var_os("USERPROFILE") {
1365 roots.push(Path::new(&profile).to_path_buf());
1366 }
1367
1368 for drive in fixed_drives() {
1369 let prog_files = Path::new(&drive).join("Program Files");
1370 if prog_files.exists() {
1371 roots.push(prog_files);
1372 }
1373
1374 let prog_files_x86 = Path::new(&drive).join("Program Files (x86)");
1375 if prog_files_x86.exists() {
1376 roots.push(prog_files_x86);
1377 }
1378
1379 if let Ok(entries) = fs::read_dir(&drive) {
1380 for entry in entries.flatten() {
1381 let dir_name = entry.file_name().to_string_lossy().to_lowercase();
1382 if JAVA_KEYWORDS.iter().any(|kw| dir_name.contains(kw)) {
1383 roots.push(entry.path());
1384 }
1385 }
1386 }
1387 }
1388
1389 roots
1390}
1391
1392#[cfg(target_os = "windows")]
1393fn fixed_drives() -> Vec<String> {
1394 let mut drives = Vec::new();
1395 for letter in 'A'..='Z' {
1396 let drive = format!("{letter}:\\");
1397 let p = Path::new(&drive);
1398 if p.exists() && fs::read_dir(p).is_ok() {
1399 drives.push(drive);
1400 }
1401 }
1402 drives
1403}
1404
1405#[cfg(target_os = "windows")]
1406fn should_explore_deeper(path: &Path) -> bool {
1407 let name = match path.file_name() {
1408 Some(n) => n.to_string_lossy(),
1409 None => return false,
1410 };
1411
1412 let lower = name.to_lowercase();
1413
1414 for ex in EXCLUDE_FOLDERS {
1415 if lower.contains(ex) {
1416 return false;
1417 }
1418 }
1419
1420 for kw in JAVA_KEYWORDS {
1421 if lower.contains(kw) {
1422 return true;
1423 }
1424 }
1425
1426 is_version_like(&name)
1427}
1428
1429fn is_version_like(name: &str) -> bool {
1430 if name.is_empty() || name.len() > 20 {
1431 return false;
1432 }
1433
1434 let has_digit = name.chars().any(|c| c.is_ascii_digit());
1435 if !has_digit {
1436 return false;
1437 }
1438
1439 name.chars()
1440 .all(|c| c.is_ascii_digit() || c == '.' || c == '_' || c == '-')
1441}
1442
1443#[cfg(target_os = "windows")]
1444fn scan_microsoft_store() -> Vec<PathBuf> {
1445 let localappdata = match env::var_os("LOCALAPPDATA") {
1446 Some(v) => v,
1447 None => return Vec::new(),
1448 };
1449
1450 let base = Path::new(&localappdata)
1451 .join(r"Packages\Microsoft.4297127D64EC6_8wekyb3d8bbwe\LocalCache\Local\runtime");
1452
1453 if !base.exists() {
1454 debug!("scan_microsoft_store: base path does not exist");
1455 return Vec::new();
1456 }
1457
1458 debug!("scan_microsoft_store: scanning {}", base.display());
1459 let mut results = Vec::new();
1460
1461 if let Ok(runtimes) = fs::read_dir(&base) {
1462 for runtime in runtimes.flatten() {
1463 let runtime_path = runtime.path();
1464 let name = runtime_path.file_name().map(|n| n.to_string_lossy());
1465 if !name
1466 .as_deref()
1467 .is_some_and(|n| n.starts_with("java-runtime"))
1468 {
1469 continue;
1470 }
1471
1472 if let Ok(archs) = fs::read_dir(&runtime_path) {
1473 for arch in archs.flatten() {
1474 let arch_path = arch.path();
1475 if !arch_path.is_dir() {
1476 continue;
1477 }
1478
1479 if let Ok(versions) = fs::read_dir(&arch_path) {
1480 for version in versions.flatten() {
1481 let java_exe = version.path().join("bin").join("java.exe");
1482 if java_exe.exists() {
1483 results.push(java_exe);
1484 }
1485 }
1486 }
1487 }
1488 }
1489 }
1490 }
1491
1492 results
1493}
1494
1495#[cfg(target_os = "windows")]
1496fn scan_where_command() -> Vec<PathBuf> {
1497 let mut results = Vec::new();
1498
1499 debug!("scan_where_command: running `where java`");
1500 if let Ok(output) = std::process::Command::new("where").arg("java").output()
1501 && output.status.success()
1502 {
1503 let stdout = String::from_utf8_lossy(&output.stdout);
1504 for line in stdout.lines() {
1505 let trimmed = line.trim();
1506 if !trimmed.is_empty() {
1507 let p = Path::new(trimmed);
1508 if p.exists() {
1509 results.push(p.to_path_buf());
1510 }
1511 }
1512 }
1513 }
1514
1515 results
1516}
1517
1518#[cfg(target_os = "linux")]
1519fn scan_linux() -> Vec<PathBuf> {
1520 use walkdir::WalkDir;
1521
1522 let mut results = Vec::new();
1523
1524 let mut search_dirs: Vec<PathBuf> = vec![
1525 "/usr/lib/jvm".into(),
1526 "/usr/java".into(),
1527 "/opt".into(),
1528 "/usr/local".into(),
1529 ];
1530
1531 if let Ok(home) = env::var("HOME") {
1532 let mc_runtime = Path::new(&home).join(".minecraft").join("runtime");
1533 if mc_runtime.exists() {
1534 search_dirs.push(mc_runtime);
1535 }
1536 }
1537
1538 for dir in search_dirs {
1539 if !dir.exists() {
1540 debug!("scan_linux: directory does not exist: {}", dir.display());
1541 continue;
1542 }
1543
1544 debug!("scan_linux: walking {}", dir.display());
1545 for entry in WalkDir::new(&dir)
1546 .follow_links(true)
1547 .into_iter()
1548 .filter_map(|e| e.ok())
1549 {
1550 let entry_path = entry.path();
1551
1552 if entry_path.file_name() != Some(std::ffi::OsStr::new("java")) {
1553 continue;
1554 }
1555 if !entry_path.is_file() {
1556 continue;
1557 }
1558
1559 if let Some(parent) = entry_path.parent() {
1560 if !should_explore_linux(parent) {
1561 continue;
1562 }
1563 }
1564
1565 #[cfg(unix)]
1566 {
1567 use std::os::unix::fs::PermissionsExt;
1568 if let Ok(metadata) = entry_path.metadata() {
1569 let permissions = metadata.permissions();
1570 if permissions.mode() & 0o111 != 0 {
1571 debug!("scan_linux: found Java at {}", entry_path.display());
1572 results.push(entry_path.to_path_buf());
1573 }
1574 }
1575 }
1576 #[cfg(not(unix))]
1577 {
1578 results.push(entry_path.to_path_buf());
1579 }
1580 }
1581 }
1582
1583 results
1584}
1585
1586#[cfg(target_os = "linux")]
1587fn should_explore_linux(path: &Path) -> bool {
1588 let name = match path.file_name() {
1589 Some(n) => n.to_string_lossy(),
1590 None => return true,
1591 };
1592
1593 let lower = name.to_lowercase();
1594
1595 for ex in EXCLUDE_FOLDERS {
1596 if lower.contains(ex) {
1597 return false;
1598 }
1599 }
1600
1601 for kw in JAVA_KEYWORDS {
1602 if lower.contains(kw) {
1603 return true;
1604 }
1605 }
1606
1607 is_version_like(&name)
1608}
1609
1610#[cfg(target_os = "macos")]
1611fn scan_macos() -> Vec<PathBuf> {
1612 use walkdir::WalkDir;
1613
1614 let mut results = Vec::new();
1615
1616 let mut search_dirs: Vec<PathBuf> = vec!["/Library/Java/JavaVirtualMachines".into()];
1617
1618 if let Ok(home) = env::var("HOME") {
1619 let user_jvm = Path::new(&home).join("Library/Java/JavaVirtualMachines");
1620 if user_jvm.exists() {
1621 search_dirs.push(user_jvm);
1622 }
1623
1624 let mc_runtime = Path::new(&home).join(".minecraft").join("runtime");
1625 if mc_runtime.exists() {
1626 search_dirs.push(mc_runtime);
1627 }
1628 }
1629
1630 debug!("scan_macos: running /usr/libexec/java_home");
1632 if let Ok(output) = std::process::Command::new("/usr/libexec/java_home").output() {
1633 if output.status.success() {
1634 let path_str = String::from_utf8_lossy(&output.stdout);
1635 let jh_path = Path::new(path_str.trim());
1636 if jh_path.exists() {
1637 let java_exe = jh_path.join("bin").join("java");
1638 if java_exe.exists() {
1639 debug!(
1640 "scan_macos: /usr/libexec/java_home -> {}",
1641 java_exe.display()
1642 );
1643 results.push(java_exe);
1644 }
1645 }
1646 }
1647 }
1648
1649 for dir in search_dirs {
1650 if !dir.exists() {
1651 debug!("scan_macos: directory does not exist: {}", dir.display());
1652 continue;
1653 }
1654
1655 debug!("scan_macos: walking {}", dir.display());
1656 for entry in WalkDir::new(&dir)
1657 .max_depth(5)
1658 .follow_links(true)
1659 .into_iter()
1660 .filter_map(|e| e.ok())
1661 {
1662 let entry_path = entry.path();
1663
1664 if entry_path.file_name() != Some(std::ffi::OsStr::new("java")) {
1665 continue;
1666 }
1667 if !entry_path.is_file() {
1668 continue;
1669 }
1670
1671 #[cfg(unix)]
1672 {
1673 use std::os::unix::fs::PermissionsExt;
1674 if let Ok(metadata) = entry_path.metadata() {
1675 let permissions = metadata.permissions();
1676 if permissions.mode() & 0o111 != 0 {
1677 debug!("scan_macos: found Java at {}", entry_path.display());
1678 results.push(entry_path.to_path_buf());
1679 }
1680 }
1681 }
1682 #[cfg(not(unix))]
1683 {
1684 results.push(entry_path.to_path_buf());
1685 }
1686 }
1687 }
1688
1689 results
1690}