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#[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#[derive(Debug, Error)]
32pub enum PyenvRootError {
33 #[error("the environment variable $PYENV_ROOT does not exist")]
37 NoEnvVarOrHomeDir,
38 #[error("pyenv root is not a directory: {root}")]
40 NotADir { root: PathBuf },
41 #[error("could not find pyenv root: {root}")]
43 IOError { root: PathBuf, source: io::Error },
44}
45
46impl PyenvRoot {
47 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#[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#[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 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#[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#[derive(Debug, Eq)]
150pub struct PythonExecutable {
151 name: Option<OsString>,
154 path: PathBuf,
156 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 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 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 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#[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#[derive(Error, Debug)]
299pub enum PyenvError {
300 #[error("pyenv python can't be found because no root was found: {error}")]
302 NoRoot {
303 #[from] error: PyenvRootError,
304 },
305 #[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 #[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 pub fn new() -> Result<Self, PyenvError> {
329 use PyenvError::*;
330 let root = PyenvRoot::new()?;
331 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#[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 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 != ¤t_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 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}