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