Skip to main content

eska_tool_runtime/
resolver.rs

1//! Deterministic component selection without executable probes or installation.
2
3use crate::{Component, Error};
4use std::{
5    env, fs, io,
6    path::{Path, PathBuf},
7};
8
9/// Validate an explicit executable path without trying another installed component.
10///
11/// # Errors
12/// Returns the explicit invalid path or filesystem failure.
13pub fn resolve_at(component: Component, path: &Path) -> Result<PathBuf, Error> {
14    let invalid = |source| Error::Invalid {
15        component,
16        path: path.to_owned(),
17        source,
18    };
19    let canonical = fs::canonicalize(path).map_err(invalid)?;
20    if !canonical.is_file() || env::current_exe().is_ok_and(|shell| shell == canonical) {
21        return Err(Error::Selection {
22            component,
23            path: path.to_owned(),
24        });
25    }
26    crate::installation::verify(&canonical, component.name())?;
27    Ok(canonical)
28}
29
30/// Prefer an explicit path, then sibling release tools, PATH and the managed user bin directory.
31///
32/// # Errors
33/// Reports a missing component or the first invalid selected executable without installing it.
34pub fn resolve(component: Component) -> Result<PathBuf, Error> {
35    if let Some(path) = env::var_os(component.variable()) {
36        return resolve_at(component, Path::new(&path));
37    }
38    let name = format!("{}{}", component.name(), env::consts::EXE_SUFFIX);
39    let mut candidates = Vec::new();
40    if let Ok(shell) = env::current_exe()
41        && let Some(directory) = shell.parent()
42    {
43        candidates.push(directory.join(&name));
44    }
45    if let Some(path) = env::var_os("PATH") {
46        candidates.extend(env::split_paths(&path).map(|directory| directory.join(&name)));
47    }
48    if let Some(home) = env::var_os(if cfg!(windows) { "USERPROFILE" } else { "HOME" }) {
49        candidates.push(PathBuf::from(home).join(".eska/bin").join(name));
50    }
51    for candidate in candidates {
52        match fs::symlink_metadata(&candidate) {
53            Ok(_) => return resolve_at(component, &candidate),
54            Err(error) if error.kind() == io::ErrorKind::NotFound => {}
55            Err(source) => {
56                return Err(Error::Invalid {
57                    component,
58                    path: candidate,
59                    source,
60                });
61            }
62        }
63    }
64    Err(Error::Missing { component })
65}