Skip to main content

jdkman/
commands.rs

1use crate::{
2    prelude::{candidates_api, candidates_dir, platform, Candidate},
3    sdkman::{archives_dir, temp_dir},
4};
5use std::{ffi::OsStr, io};
6
7pub(crate) mod results {
8    pub use super::default_command::{ChangedDefault, DefaultResult};
9    pub use super::install_command::InstallError;
10    pub use super::install_command::InstallResult;
11    pub use super::use_command::{UseResult, Using};
12}
13
14pub struct JdkCurrent;
15pub struct JdkDefault;
16pub struct JdkInstall;
17pub struct JdkList;
18pub struct JdkUse;
19
20impl JdkCurrent {
21    pub fn run() -> Option<Candidate> {
22        current_command::run(candidates_dir())
23    }
24}
25
26impl JdkDefault {
27    pub fn run(
28        query: Option<impl AsRef<OsStr>>,
29        show_suggested_default: bool,
30        verbose: bool,
31    ) -> io::Result<results::DefaultResult> {
32        if show_suggested_default {
33            Ok(results::DefaultResult::Suggest(default_command::suggested(
34                verbose,
35                candidates_api(),
36            )?))
37        } else {
38            default_command::run(candidates_dir(), verbose, query.as_ref().map(OsStr::new))
39        }
40    }
41
42    pub fn set(candidate: Candidate) -> io::Result<results::ChangedDefault> {
43        default_command::set_default(candidates_dir(), candidate)
44    }
45}
46
47impl JdkList {
48    pub fn run(verbose: bool, all: bool) -> io::Result<String> {
49        let current = JdkCurrent::run();
50        if all {
51            list_command::run_online(
52                current.as_ref().map(Candidate::name),
53                candidates_dir(),
54                candidates_api(),
55                platform(),
56                verbose,
57            )
58        } else {
59            list_command::run(current.as_ref().map(Candidate::name), candidates_dir())
60        }
61    }
62}
63
64impl JdkInstall {
65    pub fn run(
66        verbose: bool,
67        install_suggested_default: bool,
68        validate_version: bool,
69        version: &str,
70    ) -> results::InstallResult {
71        install_command::run(
72            candidates_dir(),
73            archives_dir(),
74            temp_dir(),
75            candidates_api(),
76            platform(),
77            verbose,
78            install_suggested_default,
79            validate_version,
80            version,
81        )
82    }
83}
84
85impl JdkUse {
86    pub fn run(query: Option<impl AsRef<OsStr>>, verbose: bool) -> io::Result<results::UseResult> {
87        let current = JdkCurrent::run();
88        use_command::run(
89            candidates_dir(),
90            current.as_ref().map(Candidate::name),
91            verbose,
92            query.as_ref().map(OsStr::new),
93        )
94    }
95
96    pub fn set(candidate: Candidate) -> results::Using {
97        use_command::set_use(candidates_dir(), candidate)
98    }
99}
100
101mod current_command {
102    use crate::candidate::Candidate;
103    use std::{env, fs, path::Path};
104
105    pub(super) fn run(candidates_dir: &Path) -> Option<Candidate> {
106        // try to find the currently enabled candidate in the PATH
107        // If path is empty None is returned
108        // If the candidate is not found, None is returned
109        // If the candidate appears multiple times, the first match is returned
110        //   as this is consistent with how the PATH would be searched
111        let path = env::var_os("PATH").unwrap_or_default();
112        env::split_paths(&path)
113            .find_map(|path| {
114                path.starts_with(candidates_dir)
115                    .then(|| {
116                        // we have a component that starts with the candidate but
117                        // we may have additional components in the path (such as /bin)
118                        // so we navigate the ancestors until the parent matches the candidates_dir
119                        // then we take the current path as the one that has the correct version
120
121                        path.ancestors()
122                            // ancestors always starts with the current path,
123                            //  but we're only interested in the actual parents
124                            .skip(1)
125                            // once a parent no longer starts with the candidate dir, we can stop searching
126                            .take_while(|path| path.starts_with(candidates_dir))
127                            .try_fold(&*path, |parent_parent, parent| {
128                                if parent == candidates_dir {
129                                    // try_fold short circuits when an Err is returned
130                                    Err(parent_parent)
131                                } else {
132                                    // if not found, we traverse up the ancestors chain
133                                    Ok(parent)
134                                }
135                            })
136                            // we're only interested in the found case, where we returned an error
137                            .err()
138                            .map(ToOwned::to_owned)
139                    })
140                    .flatten()
141            })
142            // Resolve any links, most likely
143            // resolve 'current' to where it actually points at.
144            // This already returns the correct absolute path that we want
145            // SDKman sets the links to relativ paths, so just using `std::fs::read_link`
146            // would only yield the version as the full path, not the full absolute path.
147            .map(fs::canonicalize)
148            .and_then(Result::ok)
149            .map(Candidate::new)
150    }
151}
152
153mod use_command {
154    use super::shared::select_candidates;
155    use crate::{prelude::Candidate, select::Selection};
156    use std::{
157        env,
158        ffi::{OsStr, OsString},
159        io,
160        path::{Path, PathBuf},
161    };
162
163    pub enum UseResult {
164        KeepCurrent,
165        Invalid(Option<String>),
166        Use(Using),
167    }
168
169    pub struct Using {
170        pub name: String,
171        pub java_home: PathBuf,
172        pub path: Option<OsString>,
173    }
174
175    pub(super) fn run(
176        candidates_dir: &Path,
177        current: Option<&str>,
178        verbose: bool,
179        query: Option<&OsStr>,
180    ) -> io::Result<UseResult> {
181        let selected = select_candidates(query, current, candidates_dir, verbose)?;
182
183        let use_result = match selected {
184            Selection::Cancelled => UseResult::KeepCurrent,
185            Selection::NoMatch => {
186                UseResult::Invalid(query.and_then(|q| q.to_str()).map(String::from))
187            }
188            Selection::Selected(selection) => {
189                let using = set_use(candidates_dir, selection);
190                UseResult::Use(using)
191            }
192        };
193
194        Ok(use_result)
195    }
196
197    pub(super) fn set_use(candidates_dir: &Path, candidate: Candidate) -> Using {
198        let name = String::from(candidate.name());
199        let candidate_path = candidate.into_path();
200
201        let old_path = env::var_os("PATH");
202        let new_path = old_path.map(|path| {
203            // remove references to the previous candidate from the path
204            let mut candidate_path = Some(candidate_path.clone());
205            let new_path = env::split_paths(&path).filter_map(|path| {
206                if path.starts_with(candidates_dir) {
207                    candidate_path.take().map(|mut new_candidate_path| {
208                        let previous_path = path
209                            .strip_prefix(candidates_dir)
210                            .expect("starts_with returned true");
211                        // skip old candidate name and add all entries that came after it
212                        // this actual path usually contains an addition /bin
213                        for path_segment in previous_path.iter().skip(1) {
214                            new_candidate_path.push(path_segment);
215                        }
216                        new_candidate_path
217                    })
218                } else {
219                    Some(path)
220                }
221            });
222            env::join_paths(new_path).expect("input is from existing PATH")
223        });
224
225        Using {
226            name,
227            java_home: candidate_path,
228            path: new_path,
229        }
230    }
231}
232
233mod default_command {
234    use super::shared::{request, select_candidates};
235    use crate::{prelude::Candidate, select::Selection};
236    use std::{
237        ffi::OsStr,
238        fs,
239        io::{self, ErrorKind},
240        path::Path,
241    };
242
243    #[cfg(unix)]
244    use std::os::unix::fs::symlink;
245    #[cfg(windows)]
246    use std::os::windows::fs::symlink_dir as symlink;
247
248    pub enum DefaultResult {
249        KeepCurrent,
250        CandidateNotFound { query: Option<String> },
251        Selected(ChangedDefault),
252        Suggest(String),
253    }
254
255    pub struct ChangedDefault {
256        pub name: String,
257        pub before: Option<String>,
258    }
259
260    pub(super) fn suggested(verbose: bool, candidates_api: &str) -> io::Result<String> {
261        request(format!("{candidates_api}/candidates/default/java"), verbose)
262    }
263
264    pub(super) fn run(
265        candidates_dir: &Path,
266        verbose: bool,
267        query: Option<&OsStr>,
268    ) -> io::Result<DefaultResult> {
269        let selected = select_candidates(query, None, candidates_dir, verbose)?;
270
271        let default_result = match selected {
272            Selection::Cancelled => DefaultResult::KeepCurrent,
273            Selection::NoMatch => {
274                let query = query.and_then(|q| q.to_str()).map(String::from);
275                DefaultResult::CandidateNotFound { query }
276            }
277            Selection::Selected(selection) => {
278                let selected = set_default(candidates_dir, selection)?;
279                DefaultResult::Selected(selected)
280            }
281        };
282
283        Ok(default_result)
284    }
285
286    pub(super) fn set_default(
287        candidates_dir: &Path,
288        candidate: Candidate,
289    ) -> io::Result<ChangedDefault> {
290        let name = String::from(candidate.name());
291        let candidate_path = candidate.into_path();
292        let current_path = candidates_dir.join("current");
293
294        // We need to unlink any existing link and bail if current seems to not be a link at all
295        let previous = match fs::symlink_metadata(&current_path) {
296            Ok(meta) => {
297                if meta.is_file() || meta.is_dir() {
298                    return Err(io::Error::new(
299                        ErrorKind::AlreadyExists,
300                        format!(
301                            "The 'current' link [{}] cannot be changed since it is not a link.",
302                            current_path.display()
303                        ),
304                    ));
305                }
306                let previous = fs::read_link(&current_path)?;
307                let previous = previous.strip_prefix(candidates_dir).unwrap_or(&previous);
308                let previous = previous.to_str().map(String::from);
309
310                fs::remove_file(&current_path)?;
311
312                previous
313            }
314            Err(err) if err.kind() == ErrorKind::NotFound => None,
315            Err(e) => return Err(e),
316        };
317
318        // emulate sdkman by linking the relative path
319        let candidate_path = candidate_path
320            .strip_prefix(candidates_dir)
321            .unwrap_or(&candidate_path);
322
323        symlink(candidate_path, &current_path)?;
324
325        Ok(ChangedDefault {
326            name,
327            before: previous,
328        })
329    }
330}
331
332mod validate_command {
333    use super::shared::request;
334    use std::io;
335
336    pub(super) fn run(
337        candidates_api: &str,
338        platform: &str,
339        verbose: bool,
340        version: &str,
341    ) -> io::Result<bool> {
342        let url = format!("{candidates_api}/candidates/validate/java/{version}/{platform}",);
343        let valid = request(url, verbose)?;
344        match valid.trim() {
345            "valid" => Ok(true),
346            "invalid" => Ok(false),
347            otherwise => {
348                let err_msg = format!("Unexpected validation response, expected either 'valid' or 'invalid', but got {otherwise}");
349                Err(crate::io_err(&err_msg))
350            }
351        }
352    }
353}
354
355mod install_command {
356    use super::{default_command, validate_command};
357    use crate::{eprintln_color, prelude::Candidate};
358    use bstr::ByteSlice;
359    use console::Style;
360    use std::{
361        fs::{self, OpenOptions},
362        io::{self, BufWriter, ErrorKind},
363        path::{Path, PathBuf},
364        process::{Command, Stdio},
365        time::Instant,
366    };
367
368    pub type InstallResult = Result<Candidate, InstallError>;
369
370    pub enum InstallError {
371        AlreadyInstalled(String),
372        InvalidVersion(String),
373        ArchiveCorrupt(String, PathBuf),
374        DownloadError(reqwest::Error),
375        Other(io::Error),
376    }
377
378    impl From<io::Error> for InstallError {
379        fn from(val: io::Error) -> Self {
380            Self::Other(val)
381        }
382    }
383    impl From<reqwest::Error> for InstallError {
384        fn from(val: reqwest::Error) -> Self {
385            Self::DownloadError(val)
386        }
387    }
388
389    #[allow(clippy::too_many_arguments)]
390    pub(super) fn run(
391        candidates_dir: &Path,
392        archives_dir: &Path,
393        temp_dir: &Path,
394        candidates_api: &str,
395        platform: &str,
396        verbose: bool,
397        install_suggested_default: bool,
398        validate_version: bool,
399        version: &str,
400    ) -> InstallResult {
401        // determine version
402        let default_version;
403        let version = if install_suggested_default {
404            default_version = default_command::suggested(verbose, candidates_api)?;
405            &default_version
406        } else {
407            version
408        };
409
410        let candidates_path = validate_candidate(
411            candidates_dir,
412            candidates_api,
413            platform,
414            verbose,
415            validate_version,
416            version,
417        )?;
418
419        let zip_archive_target = archives_dir.join(format!("java-{version}.zip"));
420
421        // download unless file already exists
422        if !matches!(fs::symlink_metadata(&zip_archive_target), Ok(meta) if meta.is_file()) {
423            download(
424                temp_dir,
425                candidates_api,
426                platform,
427                verbose,
428                version,
429                &zip_archive_target,
430            )?;
431        } else {
432            eprintln!(
433                "Found a previously downloaded java {version} archive. Not downloading it again..."
434            );
435        }
436
437        let zip_archive_target = validate_archive(version, zip_archive_target)?;
438
439        install_archive(temp_dir, version, candidates_path, zip_archive_target)
440    }
441
442    pub(super) fn validate_candidate(
443        candidates_dir: &Path,
444        candidates_api: &str,
445        platform: &str,
446        verbose: bool,
447        validate_version: bool,
448        version: &str,
449    ) -> Result<PathBuf, InstallError> {
450        let candidates_path = candidates_dir.join(version);
451        if matches!(fs::symlink_metadata(&candidates_path), Ok(existing) if !existing.is_file()) {
452            return Err(InstallError::AlreadyInstalled(version.into()));
453        }
454
455        if validate_version && !validate_command::run(candidates_api, platform, verbose, version)? {
456            return Err(InstallError::InvalidVersion(version.into()));
457        };
458
459        Ok(candidates_path)
460    }
461
462    pub(super) fn download(
463        temp_dir: &Path,
464        candidates_api: &str,
465        platform: &str,
466        verbose: bool,
467        version: &str,
468        zip_archive_target: &Path,
469    ) -> Result<(), InstallError> {
470        pre_install(temp_dir, candidates_api, platform, verbose, version)?;
471
472        let url = format!("{candidates_api}/broker/download/java/{version}/{platform}");
473
474        let binary_input = temp_dir.join(format!("java-{version}.bin"));
475
476        let file = OpenOptions::new()
477            .write(true)
478            .create_new(true)
479            .open(&binary_input)?;
480
481        // TODO: indicatif progressbar every 100K every 50ms/20fps
482        // TODO: .verbose_fn_if(verbose, super::shared::print_verbose)
483
484        let start = Instant::now();
485        let mut response = reqwest::blocking::get(&url)?;
486
487        if let Some(size) = response.content_length() {
488            file.set_len(size)?;
489        }
490
491        let mut wtr = BufWriter::new(file);
492        let bytes = response.copy_to(&mut wtr)?;
493
494        if verbose {
495            let took = start.elapsed();
496            eprintln_color!(@Style::new().for_stderr().dim(), "Downloaded {} bytes of binaryto {} in {:?}", bytes, binary_input.display(), took);
497        }
498
499        let zip_output = post_install(
500            temp_dir,
501            candidates_api,
502            platform,
503            verbose,
504            version,
505            binary_input,
506        )?;
507
508        dbg!(fs::rename(dbg!(zip_output), dbg!(&zip_archive_target)))?;
509
510        Ok(())
511    }
512
513    pub(super) fn validate_archive(
514        version: &str,
515        zip_archive_target: PathBuf,
516    ) -> Result<PathBuf, InstallError> {
517        let output = Command::new("unzip")
518            .arg("-t")
519            .arg(&zip_archive_target)
520            .stdin(Stdio::null())
521            .stdout(Stdio::piped())
522            .stderr(Stdio::inherit())
523            .output()?;
524
525        if !output.status.success()
526            || !output
527                .stdout
528                .contains_str("No errors detected in compressed data")
529        {
530            try_fs(|| fs::remove_file(&zip_archive_target))?;
531
532            return Err(InstallError::ArchiveCorrupt(
533                version.into(),
534                zip_archive_target,
535            ));
536        }
537
538        Ok(zip_archive_target)
539    }
540
541    pub(super) fn install_archive(
542        temp_dir: &Path,
543        version: &str,
544        candidates_path: PathBuf,
545        zip_archive_target: PathBuf,
546    ) -> InstallResult {
547        let out = temp_dir.join("out");
548        try_fs(|| fs::remove_dir_all(&out))?;
549
550        if !Command::new("unzip")
551            .arg("-oq")
552            .arg(&zip_archive_target)
553            .arg("-d")
554            .arg(&out)
555            .status()?
556            .success()
557        {
558            return Err(InstallError::ArchiveCorrupt(
559                version.into(),
560                zip_archive_target,
561            ));
562        }
563
564        dbg!(fs::rename(dbg!(out), dbg!(&candidates_path)))?;
565
566        Ok(Candidate::new(candidates_path))
567    }
568
569    fn pre_install(
570        _temp_dir: &Path,
571        _candidates_api: &str,
572        _platform: &str,
573        _verbose: bool,
574        _version: &str,
575    ) -> io::Result<()> {
576        // TODO: implement
577        // # pre-installation hook: implements function __sdkman_pre_installation_hook
578        // local pre_installation_hook="${SDKMAN_DIR}/tmp/hook_pre_${candidate}_${version}.sh"
579        // __sdkman_echo_debug "Get pre-installation hook: ${SDKMAN_CANDIDATES_API}/hooks/pre/${candidate}/${version}/${platform_parameter}"
580        // __sdkman_secure_curl "${SDKMAN_CANDIDATES_API}/hooks/pre/${candidate}/${version}/${platform_parameter}" >| "$pre_installation_hook"
581        // __sdkman_echo_debug "Copy remote pre-installation hook: $pre_installation_hook"
582        // source "$pre_installation_hook"
583        // __sdkman_pre_installation_hook || return 1
584        // __sdkman_echo_debug "Completed pre-installation hook..."
585
586        Ok(())
587    }
588
589    fn post_install(
590        temp_dir: &Path,
591        _candidates_api: &str,
592        _platform: &str,
593        _verbose: bool,
594        version: &str,
595        binary_input: PathBuf,
596    ) -> Result<PathBuf, InstallError> {
597        // TODO: implement
598        // # post-installation hook: implements function __sdkman_post_installation_hook
599        // # responsible for taking `binary_input` and producing `zip_output`
600        // local post_installation_hook="${SDKMAN_DIR}/tmp/hook_post_${candidate}_${version}.sh"
601        // __sdkman_echo_debug "Get post-installation hook: ${SDKMAN_CANDIDATES_API}/hooks/post/${candidate}/${version}/${platform_parameter}"
602        // __sdkman_secure_curl "${SDKMAN_CANDIDATES_API}/hooks/post/${candidate}/${version}/${platform_parameter}" >| "$post_installation_hook"
603        // __sdkman_echo_debug "Copy remote post-installation hook: ${post_installation_hook}"
604        // source "$post_installation_hook"
605        // __sdkman_post_installation_hook || return 1
606        // __sdkman_echo_debug "Processed binary as: $zip_output"
607        // __sdkman_echo_debug "Completed post-installation hook..."
608
609        eprintln!("Running tar test");
610
611        let binary_validation = Command::new("tar")
612            .arg("tzf")
613            .arg(&binary_input)
614            .stdout(Stdio::null())
615            .status()?;
616        if !binary_validation.success() {
617            return Err(InstallError::ArchiveCorrupt(version.into(), binary_input));
618        }
619
620        let work_dir = temp_dir.join("out");
621        fs::create_dir_all(&work_dir)?;
622
623        eprintln!("Running tar extract");
624
625        if !Command::new("tar")
626            .arg("zxf")
627            .arg(&binary_input)
628            .arg("-C")
629            .arg(&work_dir)
630            .status()?
631            .success()
632        {
633            return Err(InstallError::ArchiveCorrupt(version.into(), binary_input));
634        }
635
636        let zip_output = binary_input.with_extension("zip");
637
638        eprintln!("Running zip");
639
640        if !Command::new("zip")
641            .current_dir(&work_dir)
642            .arg("-qyr")
643            .arg(&zip_output)
644            .arg(".")
645            .status()?
646            .success()
647        {
648            return Err(InstallError::ArchiveCorrupt(version.into(), zip_output));
649        }
650
651        fs::remove_file(binary_input)?;
652        fs::remove_dir_all(work_dir)?;
653
654        Ok(zip_output)
655    }
656
657    fn try_fs(f: impl FnOnce() -> io::Result<()>) -> io::Result<()> {
658        match f() {
659            Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
660            otherwise => otherwise,
661        }
662    }
663}
664
665mod list_command {
666    use super::shared::list_candidates;
667    use crate::commands::shared::request;
668    use std::{fmt::Write, io, path::Path};
669
670    pub(super) fn run(current: Option<&str>, candidates_dir: &Path) -> io::Result<String> {
671        let candidates = list_candidates(candidates_dir)?;
672
673        let mut output = String::with_capacity(1024);
674        output.push_str(
675            "--------------------------------------------------------------------------------\n",
676        );
677        if candidates.is_empty() && crate::use_color() {
678            let _ = writeln!(
679                &mut output,
680                "   {}",
681                console::style("None installed!").yellow()
682            );
683        }
684
685        for candidate in candidates {
686            let candidate = candidate.into_name();
687
688            if matches!(current, Some(c) if c == candidate) {
689                let _ = writeln!(&mut output, " > {candidate}");
690            } else {
691                let _ = writeln!(&mut output, " * {candidate}");
692            }
693        }
694
695        output.push_str(
696            "--------------------------------------------------------------------------------\n",
697        );
698        output.push_str(
699            "* - installed                                                                   \n",
700        );
701        output.push_str(
702            "> - currently in use                                                            \n",
703        );
704        output.push_str(
705            "--------------------------------------------------------------------------------\n",
706        );
707
708        Ok(output)
709    }
710
711    pub(super) fn run_online(
712        current: Option<&str>,
713        candidates_dir: &Path,
714        candidates_api: &str,
715        platform: &str,
716        verbose: bool,
717    ) -> io::Result<String> {
718        let installed = list_candidates(candidates_dir)?;
719
720        let url = format!(
721            "{api}/candidates/java/{platform}/versions/list?current={current}&installed=${installed}",
722            api = candidates_api,
723            platform = platform,
724            current = current.unwrap_or_default(),
725            installed = installed.join(",")
726        );
727
728        request(url, verbose)
729    }
730}
731
732mod shared {
733
734    use crate::io_err;
735    use crate::{
736        prelude::Candidate,
737        select::{SelectOptions, Selection},
738    };
739    use std::{
740        ffi::OsStr,
741        fs::{self, DirEntry},
742        io,
743        path::Path,
744    };
745
746    pub(super) fn request(url: String, _verbose: bool) -> io::Result<String> {
747        static APP_USER_AGENT: &str =
748            concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
749        // TODO: .verbose_fn_if(verbose, print_verbose)
750
751        let response = reqwest::blocking::Client::builder()
752            .user_agent(APP_USER_AGENT)
753            .connection_verbose(_verbose)
754            .build()
755            .map_err(io_err)?
756            .get(&url)
757            .send()
758            .map_err(io_err)?
759            .error_for_status()
760            .map_err(io_err)?
761            .text()
762            .map_err(io_err)?;
763
764        // TODO:
765        // if verbose {
766        //     eprintln_color!(@Style::new().for_stderr().dim(), "{:#?}", response.timings);
767        // }
768
769        Ok(response)
770    }
771
772    // pub(super) fn print_verbose(msg: VerboseMessage, data: &[u8]) {
773    //     let style = match msg {
774    //         VerboseMessage::Text => Style::new().for_stderr().dim(),
775    //         VerboseMessage::OutgoingHeader => Style::new().for_stderr().dim().magenta(),
776    //         VerboseMessage::FirstIncomingHeader => Style::new().for_stderr().green(),
777    //         VerboseMessage::IncomingHeader => Style::new().for_stderr().cyan(),
778    //     };
779    //     match std::str::from_utf8(data) {
780    //         Ok(s) => eprint_color!(@style, "{}", s),
781    //         Err(_) => eprint_color!(@style.red(), "({} bytes of data)", data.len()),
782    //     }
783    // }
784
785    pub(super) fn list_candidates(candidates_dir: &Path) -> io::Result<Vec<Candidate>> {
786        let mut candidates = fs::read_dir(candidates_dir)?
787            .filter_map(read_entry)
788            .collect::<Vec<_>>();
789
790        // sort by version
791        candidates.sort();
792
793        Ok(candidates)
794    }
795
796    pub(super) fn select_candidates(
797        query: Option<&OsStr>,
798        current: Option<&str>,
799        candidates_dir: &Path,
800        verbose: bool,
801    ) -> io::Result<Selection<Candidate>> {
802        let candidates = list_candidates(candidates_dir)?;
803
804        // pre select whatever current points to
805        let pre_select = current
806            .and_then(|current| candidates.iter().position(|c| c.name() == current))
807            .map(|p| p as u32);
808
809        // Note that `java -version` prints to stderr and
810        // backends might only show stderr when the command fails.
811        // So we use `--version` instead, which is also more idiomatic.
812        let preview_command = format!(
813            "{0}/{{2}}/bin/java --version; {0}/{{2}}/bin/java -Xinternalversion",
814            candidates_dir.display()
815        );
816
817        let selected = SelectOptions::new(candidates)
818            .pre_select(pre_select)
819            .query(query)
820            .preview_command(preview_command.as_str())
821            .verbose(verbose)
822            .select()?;
823
824        Ok(selected)
825    }
826
827    fn read_entry(entry: io::Result<DirEntry>) -> Option<Candidate> {
828        let entry = entry.ok()?;
829
830        // We are only interested in directories,
831        // the alternatives being files or symlinks.
832        // We only want to list actual files,
833        // not duplicate current or other symlinks
834        entry
835            .metadata()
836            .ok()?
837            .is_dir()
838            .then(|| Candidate::new(entry.path()))
839    }
840}