pyenv_python/
lib.rs

1#![forbid(unsafe_code)]
2
3use std::{env, fmt, io};
4use std::ffi::{OsStr, OsString};
5use std::fmt::{Debug, Display, Formatter};
6use std::fs::File;
7use std::path::{Path, PathBuf};
8
9use apply::Apply;
10use is_executable::IsExecutable;
11use same_file::Handle;
12use thiserror::Error;
13
14mod version;
15
16/// A root `pyenv` directory.
17#[derive(Debug)]
18pub struct PyenvRoot {
19    root: PathBuf,
20}
21
22impl Display for PyenvRoot {
23    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
24        write!(f, "{}", self.root.display())
25    }
26}
27
28/// Why the pyenv root, i.e. what `$(pyenv root)` returns, could not be found.
29///
30/// See [`PyenvRoot::new`].
31#[derive(Debug, Error)]
32pub enum PyenvRootError {
33    /// Either the environment variable `$PYENV_ROOT` does not exist,
34    /// or the home directory does not exist
35    /// (very unlikely, unless in an OS like wasm).
36    #[error("the environment variable $PYENV_ROOT does not exist")]
37    NoEnvVarOrHomeDir,
38    /// The pyenv root is not a directory.
39    #[error("pyenv root is not a directory: {root}")]
40    NotADir { root: PathBuf },
41    /// The pyenv root could not be accessed (usually it doesn't exist).
42    #[error("could not find pyenv root: {root}")]
43    IOError { root: PathBuf, source: io::Error },
44}
45
46impl PyenvRoot {
47    /// Returns what `$(pyenv root)` would return.
48    /// That is, `$PYENV_ROOT` or `$HOME/.pyenv` if they exist.
49    ///
50    /// See [`PyenvRootError`] for possible errors.
51    pub fn new() -> Result<Self, PyenvRootError> {
52        use PyenvRootError::*;
53        let root = env::var_os("PYENV_ROOT")
54            .map(|root| root.into())
55            .or_else(|| dirs_next::home_dir().map(|home| home.join(".pyenv")))
56            .ok_or(NoEnvVarOrHomeDir)?;
57        match root.metadata() {
58            Ok(metadata) => if metadata.is_dir() {
59                Ok(Self { root })
60            } else {
61                Err(NotADir { root })
62            },
63            Err(source) => Err(IOError { root, source }),
64        }
65    }
66}
67
68/// Where the given [`PyenvVersion`] was found from.
69#[derive(Debug, Copy, Clone)]
70pub enum PyenvVersionFrom {
71    Shell,
72    Local,
73    Global,
74}
75
76impl Display for PyenvVersionFrom {
77    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
78        let name = match self {
79            Self::Shell => "shell",
80            Self::Local => "local",
81            Self::Global => "global",
82        };
83        write!(f, "{}", name)
84    }
85}
86
87/// A `pyenv` version, either a `python` version or a virtualenv name,
88/// and where it was looked-up from.
89#[derive(Debug)]
90pub struct PyenvVersion {
91    version: String,
92    from: PyenvVersionFrom,
93}
94
95impl Display for PyenvVersion {
96    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
97        write!(f, "python {} from {}", self.version, self.from)
98    }
99}
100
101impl PyenvRoot {
102    /// Returns the current pyenv version as determined by
103    /// <https://github.com/pyenv/pyenv#choosing-the-python-version>.
104    fn version(&self) -> Result<PyenvVersion, ()> {
105        self
106            .root
107            .as_path()
108            .apply(version::pyenv_version)
109            .ok_or(())
110    }
111    
112    fn python_path(&self, path_components: &[&str]) -> UncheckedPythonPath {
113        let mut path = self.root.clone();
114        for path_component in path_components {
115            path.push(path_component)
116        }
117        path.push("python");
118        UncheckedPythonPath::from_existing(path)
119    }
120    
121    fn python_version_path(&self, version: &PyenvVersion) -> UncheckedPythonPath {
122        self.python_path(&[
123            "versions",
124            version.version.as_str(),
125            "bin",
126        ])
127    }
128    
129    fn python_shim_path(&self) -> UncheckedPythonPath {
130        self.python_path(&[
131            "shims",
132        ])
133    }
134}
135
136/// A path that might be a `python` executable.
137#[derive(Debug)]
138pub struct UncheckedPythonPath {
139    path: PathBuf,
140}
141
142impl Display for UncheckedPythonPath {
143    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
144        write!(f, "unchecked({})", self.path.display())
145    }
146}
147
148/// The path to an existing (likely) `python` executable.
149#[derive(Debug, Eq)]
150pub struct PythonExecutable {
151    /// The name to execute this python executable as (arg0).
152    /// If [`None`], then the file name of the [`PythonExecutable::path`] is used instead.
153    name: Option<OsString>,
154    /// The path to the python executable.
155    path: PathBuf,
156    /// An open handle to the python executable for file equality.
157    handle: Handle,
158}
159
160impl PartialEq for PythonExecutable {
161    fn eq(&self, other: &Self) -> bool {
162        self.handle == other.handle
163    }
164}
165
166impl Display for PythonExecutable {
167    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
168        write!(f, "{}", self.path.display())
169    }
170}
171
172impl PythonExecutable {
173    pub fn path(&self) -> &Path {
174        self.path.as_path()
175    }
176    
177    pub fn into_path(self) -> PathBuf {
178        self.path
179    }
180    
181    pub fn name(&self) -> &OsStr {
182        self.name.as_deref()
183            .unwrap_or_else(|| self.path.file_name()
184                .expect("python executable should always have a file name (i.e. not root)")
185            )
186    }
187    
188    pub fn handle(&self) -> &Handle {
189        &self.handle
190    }
191    
192    pub fn file(&self) -> &File {
193        self.handle.as_file()
194    }
195}
196
197#[derive(Error, Debug)]
198pub enum PyenvPythonExecutableError {
199    #[error("python not found: {0}")]
200    NotFound(#[from] io::Error),
201    #[error("python must be executable")]
202    NotExecutable,
203}
204
205impl PythonExecutable {
206    /// Check that the `python` path actually points to an executable
207    /// (can't verify that it's actually Python, but it's at least an executable named `python`).
208    ///
209    /// See [`PyenvPythonExecutableError`] for possible errors.
210    pub fn new(path: PathBuf) -> Result<Self, (PyenvPythonExecutableError, PathBuf)> {
211        use PyenvPythonExecutableError::*;
212        match (|path: &Path| {
213            let handle = Handle::from_path(path)?;
214            if !path.is_executable() {
215                // Means path must have a file name.
216                return Err(NotExecutable);
217            }
218            Ok(handle)
219        })(path.as_path()) {
220            Ok(handle) => Ok(Self {
221                name: None,
222                path,
223                handle,
224            }),
225            Err(e) => Err((e, path))
226        }
227    }
228    
229    pub fn current() -> io::Result<Self> {
230        let name = env::args_os()
231            .next()
232            .map(PathBuf::from)
233            .and_then(|path| path.file_name().map(|name| name.to_os_string()));
234        // TODO What to do if arg0 doesn't exist or is `/` (no file name)?
235        // TODO Do I just silently default to the current executable's name?
236        // TODO Though in all normal invocations (like as a symlink), this won't happen.
237        let path = env::current_exe()?;
238        let handle = Handle::from_path(path.as_path())?;
239        Ok(Self {
240            name,
241            path,
242            handle,
243        })
244    }
245}
246
247impl UncheckedPythonPath {
248    pub fn from_existing(path: PathBuf) -> Self {
249        Self { path }
250    }
251    
252    pub fn check(self) -> Result<PythonExecutable, (PyenvPythonExecutableError, PathBuf)> {
253        PythonExecutable::new(self.path)
254    }
255}
256
257pub trait HasPython {
258    fn python(&self) -> &PythonExecutable;
259    
260    fn into_python(self) -> PythonExecutable;
261}
262
263impl HasPython for PythonExecutable {
264    fn python(&self) -> &PythonExecutable {
265        self
266    }
267    
268    fn into_python(self) -> PythonExecutable {
269        self
270    }
271}
272
273/// An existing `pyenv` `python` executable.
274#[derive(Debug)]
275pub struct Pyenv {
276    root: PyenvRoot,
277    version: PyenvVersion,
278    python_path: PythonExecutable,
279}
280
281impl Display for Pyenv {
282    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
283        write!(f, "pyenv {} at {}", self.version, self.python_path)
284    }
285}
286
287impl HasPython for Pyenv {
288    fn python(&self) -> &PythonExecutable {
289        &self.python_path
290    }
291    
292    fn into_python(self) -> PythonExecutable {
293        self.python_path
294    }
295}
296
297/// Possible errors in looking up the current `pyenv` `python` executable.
298#[derive(Error, Debug)]
299pub enum PyenvError {
300    // The `pyenv` root couldn't be found, so the `pyenv` version or `python` executable couldn't.
301    #[error("pyenv python can't be found because no root was found: {error}")]
302    NoRoot {
303        #[from] error: PyenvRootError,
304    },
305    /// The `pyenv` version can't be found anywhere,
306    /// neither the shell, local, or global versions.
307    ///
308    /// See <https://github.com/pyenv/pyenv#choosing-the-python-version> for the algorithm.
309    #[error("pyenv python can't be found because no version was found in shell, local, or global using root {root}")]
310    NoVersion {
311        root: PyenvRoot,
312    },
313    /// The `pyenv` `python` executable can't be found or is not an executable.
314    #[error("pyenv {version} can't be found at {python_path}")]
315    NoExecutable {
316        #[source] error: PyenvPythonExecutableError,
317        root: PyenvRoot,
318        version: PyenvVersion,
319        python_path: PathBuf,
320    },
321}
322
323impl Pyenv {
324    /// Looks up the current `pyenv` `python` executable and version,
325    /// or returns which part could not be found.
326    ///
327    /// See [`PyenvError`] for possible errors.
328    pub fn new() -> Result<Self, PyenvError> {
329        use PyenvError::*;
330        let root = PyenvRoot::new()?;
331        // Have to use `match` here instead of `map_err()?` so rustc can see the moves are disjoint.
332        let version = match root.version() {
333            Err(()) => return Err(NoVersion { root }),
334            Ok(version) => version,
335        };
336        let python_path = match root.python_version_path(&version).check() {
337            Err((error, python_path)) => return Err(NoExecutable {
338                error,
339                root,
340                version,
341                python_path,
342            }),
343            Ok(path) => path,
344        };
345        Ok(Self {
346            root,
347            version,
348            python_path,
349        })
350    }
351}
352
353/// A `python` executable, either a `pyenv` one or the system `python`
354/// (i.e. whatever else is in `$PATH`).
355#[derive(Debug)]
356pub enum Python {
357    Pyenv(Pyenv),
358    System(PythonExecutable),
359}
360
361impl Display for Python {
362    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
363        match self {
364            Self::Pyenv(pyenv) =>
365                write!(f, "{}", pyenv),
366            Self::System(python_executable) =>
367                write!(f, "system python on $PATH at {}", python_executable),
368        }
369    }
370}
371
372impl HasPython for Python {
373    fn python(&self) -> &PythonExecutable {
374        match self {
375            Self::Pyenv(pyenv) => pyenv.python(),
376            Self::System(python) => python.python(),
377        }
378    }
379    
380    fn into_python(self) -> PythonExecutable {
381        match self {
382            Self::Pyenv(pyenv) => pyenv.into_python(),
383            Self::System(python) => python.into_python(),
384        }
385    }
386}
387
388#[derive(Error, Debug)]
389pub enum SystemPythonError {
390    #[error("failed to get current executable: {0}")]
391    NoCurrentExe(#[from] io::Error),
392    #[error("no $PATH to search")]
393    NoPath,
394    #[error("no other python in $PATH")]
395    NotInPath,
396}
397
398impl Python {
399    /// Lookup the current system `python`, i.e., whatever next is in `$PATH`
400    /// that's not the current executable or a `pyenv` shim.
401    ///
402    /// Pass a [`PyenvRoot`] to avoid `pyenv` shims.
403    /// If there is no `pyenv` root than [`None`] will work.
404    ///
405    /// Specifically, this returns the next `python` on `$PATH`,
406    /// excluding the current executable and `$PYENV_ROOT/shims/python`.
407    /// Otherwise, an infinite loop would be formed between ourselves and `$PYENV_ROOT/shims/python`.
408    ///
409    /// See [`SystemPythonError`] for possible errors.
410    pub fn system(pyenv_root: Option<PyenvRoot>) -> Result<PythonExecutable, SystemPythonError> {
411        use SystemPythonError::*;
412        let current_python = PythonExecutable::current()?;
413        let pyenv_shim_python = pyenv_root
414            .map(|root| root.python_shim_path())
415            .and_then(|path| path.check().ok());
416        let path_var = env::var_os("PATH").ok_or(NoPath)?;
417        env::split_paths(&path_var)
418            .map(|mut path| {
419                path.push(current_python.name());
420                path
421            })
422            .map(UncheckedPythonPath::from_existing)
423            .filter_map(|python| python.check().ok())
424            .find(|python| python != &current_python && Some(python) != pyenv_shim_python.as_ref())
425            .ok_or(NotInPath)
426    }
427}
428
429#[derive(Error, Debug)]
430#[error("couldn't find pyenv and system python: {pyenv}, {system}")]
431pub struct PythonError {
432    pub pyenv: PyenvError,
433    pub system: SystemPythonError,
434}
435
436impl Python {
437    /// Lookup a `python` executable.
438    ///
439    /// If a `pyenv` `python` cannot be found (see [`Pyenv::new`]),
440    /// try finding the system `python` (see [`Python::system`]).
441    /// If neither can be found, return the errors for both in [`PythonError`].
442    pub fn new() -> Result<Self, PythonError> {
443        match Pyenv::new() {
444            Ok(pyenv) => Ok(Self::Pyenv(pyenv)),
445            Err(pyenv_error) => match Self::system(None) {
446                Ok(system_python) => Ok(Self::System(system_python)),
447                Err(system_python_error) => Err(PythonError {
448                    pyenv: pyenv_error,
449                    system: system_python_error,
450                }),
451            },
452        }
453    }
454}