1use core::fmt;
2use std::borrow::Cow;
3use std::cmp::Reverse;
4use std::ffi::OsStr;
5use std::io::{self, Write};
6#[cfg(windows)]
7use std::os::windows::fs::MetadataExt;
8use std::path::{Path, PathBuf};
9use std::str::FromStr;
10
11use fs_err as fs;
12use itertools::Itertools;
13use thiserror::Error;
14use tracing::{debug, warn};
15use uv_preview::{Preview, PreviewFeatures};
16#[cfg(windows)]
17use windows::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
18
19use uv_fs::{LockedFile, Simplified, replace_symlink, symlink_or_copy_file};
20use uv_platform::{Error as PlatformError, Os};
21use uv_platform::{LibcDetectionError, Platform};
22use uv_state::{StateBucket, StateStore};
23use uv_static::EnvVars;
24use uv_trampoline_builder::{Launcher, LauncherKind};
25
26use crate::downloads::{Error as DownloadError, ManagedPythonDownload};
27use crate::implementation::{
28 Error as ImplementationError, ImplementationName, LenientImplementationName,
29};
30use crate::installation::{self, PythonInstallationKey};
31use crate::python_version::PythonVersion;
32use crate::{
33 PythonInstallationMinorVersionKey, PythonRequest, PythonVariant, macos_dylib, sysconfig,
34};
35
36#[derive(Error, Debug)]
37pub enum Error {
38 #[error(transparent)]
39 Io(#[from] io::Error),
40 #[error(transparent)]
41 Download(#[from] DownloadError),
42 #[error(transparent)]
43 PlatformError(#[from] PlatformError),
44 #[error(transparent)]
45 ImplementationError(#[from] ImplementationError),
46 #[error("Invalid python version: {0}")]
47 InvalidPythonVersion(String),
48 #[error(transparent)]
49 ExtractError(#[from] uv_extract::Error),
50 #[error(transparent)]
51 SysconfigError(#[from] sysconfig::Error),
52 #[error("Failed to copy to: {0}", to.user_display())]
53 CopyError {
54 to: PathBuf,
55 #[source]
56 err: io::Error,
57 },
58 #[error("Missing expected Python executable at {}", _0.user_display())]
59 MissingExecutable(PathBuf),
60 #[error("Missing expected target directory for Python minor version link at {}", _0.user_display())]
61 MissingPythonMinorVersionLinkTargetDirectory(PathBuf),
62 #[error("Failed to create canonical Python executable at {} from {}", to.user_display(), from.user_display())]
63 CanonicalizeExecutable {
64 from: PathBuf,
65 to: PathBuf,
66 #[source]
67 err: io::Error,
68 },
69 #[error("Failed to create Python executable link at {} from {}", to.user_display(), from.user_display())]
70 LinkExecutable {
71 from: PathBuf,
72 to: PathBuf,
73 #[source]
74 err: io::Error,
75 },
76 #[error("Failed to create Python minor version link directory at {} from {}", to.user_display(), from.user_display())]
77 PythonMinorVersionLinkDirectory {
78 from: PathBuf,
79 to: PathBuf,
80 #[source]
81 err: io::Error,
82 },
83 #[error("Failed to create directory for Python executable link at {}", to.user_display())]
84 ExecutableDirectory {
85 to: PathBuf,
86 #[source]
87 err: io::Error,
88 },
89 #[error("Failed to read Python installation directory: {0}", dir.user_display())]
90 ReadError {
91 dir: PathBuf,
92 #[source]
93 err: io::Error,
94 },
95 #[error("Failed to find a directory to install executables into")]
96 NoExecutableDirectory,
97 #[error(transparent)]
98 LauncherError(#[from] uv_trampoline_builder::Error),
99 #[error("Failed to read managed Python directory name: {0}")]
100 NameError(String),
101 #[error("Failed to construct absolute path to managed Python directory: {}", _0.user_display())]
102 AbsolutePath(PathBuf, #[source] io::Error),
103 #[error(transparent)]
104 NameParseError(#[from] installation::PythonInstallationKeyError),
105 #[error("Failed to determine the libc used on the current platform")]
106 LibcDetection(#[from] LibcDetectionError),
107 #[error(transparent)]
108 MacOsDylib(#[from] macos_dylib::Error),
109}
110#[derive(Debug, Clone, Eq, PartialEq)]
112pub struct ManagedPythonInstallations {
113 root: PathBuf,
115}
116
117impl ManagedPythonInstallations {
118 fn from_path(root: impl Into<PathBuf>) -> Self {
120 Self { root: root.into() }
121 }
122
123 pub async fn lock(&self) -> Result<LockedFile, Error> {
126 Ok(LockedFile::acquire(self.root.join(".lock"), self.root.user_display()).await?)
127 }
128
129 pub fn from_settings(install_dir: Option<PathBuf>) -> Result<Self, Error> {
136 if let Some(install_dir) = install_dir {
137 Ok(Self::from_path(install_dir))
138 } else if let Some(install_dir) =
139 std::env::var_os(EnvVars::UV_PYTHON_INSTALL_DIR).filter(|s| !s.is_empty())
140 {
141 Ok(Self::from_path(install_dir))
142 } else {
143 Ok(Self::from_path(
144 StateStore::from_settings(None)?.bucket(StateBucket::ManagedPython),
145 ))
146 }
147 }
148
149 pub fn temp() -> Result<Self, Error> {
151 Ok(Self::from_path(
152 StateStore::temp()?.bucket(StateBucket::ManagedPython),
153 ))
154 }
155
156 pub fn scratch(&self) -> PathBuf {
158 self.root.join(".temp")
159 }
160
161 pub fn init(self) -> Result<Self, Error> {
165 let root = &self.root;
166
167 if !root.exists()
169 && root
170 .parent()
171 .is_some_and(|parent| parent.join("toolchains").exists())
172 {
173 let deprecated = root.parent().unwrap().join("toolchains");
174 fs::rename(&deprecated, root)?;
176 uv_fs::replace_symlink(root, &deprecated)?;
178 } else {
179 fs::create_dir_all(root)?;
180 }
181
182 fs::create_dir_all(root)?;
184
185 let scratch = self.scratch();
187 fs::create_dir_all(&scratch)?;
188
189 match fs::OpenOptions::new()
191 .write(true)
192 .create_new(true)
193 .open(root.join(".gitignore"))
194 {
195 Ok(mut file) => file.write_all(b"*")?,
196 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
197 Err(err) => return Err(err.into()),
198 }
199
200 Ok(self)
201 }
202
203 pub fn find_all(
208 &self,
209 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + use<>, Error> {
210 let dirs = match fs_err::read_dir(&self.root) {
211 Ok(installation_dirs) => {
212 let directories: Vec<_> = installation_dirs
214 .filter_map(|read_dir| match read_dir {
215 Ok(entry) => match entry.file_type() {
216 Ok(file_type) => file_type.is_dir().then_some(Ok(entry.path())),
217 Err(err) => Some(Err(err)),
218 },
219 Err(err) => Some(Err(err)),
220 })
221 .collect::<Result<_, io::Error>>()
222 .map_err(|err| Error::ReadError {
223 dir: self.root.clone(),
224 err,
225 })?;
226 directories
227 }
228 Err(err) if err.kind() == io::ErrorKind::NotFound => vec![],
229 Err(err) => {
230 return Err(Error::ReadError {
231 dir: self.root.clone(),
232 err,
233 });
234 }
235 };
236 let scratch = self.scratch();
237 Ok(dirs
238 .into_iter()
239 .filter(|path| *path != scratch)
241 .filter(|path| {
243 path.file_name()
244 .and_then(OsStr::to_str)
245 .map(|name| !name.starts_with('.'))
246 .unwrap_or(true)
247 })
248 .filter_map(|path| {
249 ManagedPythonInstallation::from_path(path)
250 .inspect_err(|err| {
251 warn!("Ignoring malformed managed Python entry:\n {err}");
252 })
253 .ok()
254 })
255 .sorted_unstable_by_key(|installation| Reverse(installation.key().clone())))
256 }
257
258 pub fn find_matching_current_platform(
260 &self,
261 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + use<>, Error> {
262 let platform = Platform::from_env()?;
263
264 let iter = Self::from_settings(None)?
265 .find_all()?
266 .filter(move |installation| {
267 if !platform.supports(installation.platform()) {
268 debug!("Skipping managed installation `{installation}`: not supported by current platform `{platform}`");
269 return false;
270 }
271 true
272 });
273
274 Ok(iter)
275 }
276
277 pub fn find_version<'a>(
284 &'a self,
285 version: &'a PythonVersion,
286 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + 'a, Error> {
287 Ok(self
288 .find_matching_current_platform()?
289 .filter(move |installation| {
290 installation
291 .path
292 .file_name()
293 .map(OsStr::to_string_lossy)
294 .is_some_and(|filename| filename.starts_with(&format!("cpython-{version}")))
295 }))
296 }
297
298 pub fn root(&self) -> &Path {
299 &self.root
300 }
301}
302
303static EXTERNALLY_MANAGED: &str = "[externally-managed]
304Error=This Python installation is managed by uv and should not be modified.
305";
306
307#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
309pub struct ManagedPythonInstallation {
310 path: PathBuf,
312 key: PythonInstallationKey,
314 url: Option<Cow<'static, str>>,
318 sha256: Option<Cow<'static, str>>,
322 build: Option<Cow<'static, str>>,
326}
327
328impl ManagedPythonInstallation {
329 pub fn new(path: PathBuf, download: &ManagedPythonDownload) -> Self {
330 Self {
331 path,
332 key: download.key().clone(),
333 url: Some(download.url().clone()),
334 sha256: download.sha256().cloned(),
335 build: download.build().map(Cow::Borrowed),
336 }
337 }
338
339 pub(crate) fn from_path(path: PathBuf) -> Result<Self, Error> {
340 let key = PythonInstallationKey::from_str(
341 path.file_name()
342 .ok_or(Error::NameError("name is empty".to_string()))?
343 .to_str()
344 .ok_or(Error::NameError("not a valid string".to_string()))?,
345 )?;
346
347 let path = std::path::absolute(&path).map_err(|err| Error::AbsolutePath(path, err))?;
348
349 let build = match fs::read_to_string(path.join("BUILD")) {
351 Ok(content) => Some(Cow::Owned(content.trim().to_string())),
352 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
353 Err(err) => return Err(err.into()),
354 };
355
356 Ok(Self {
357 path,
358 key,
359 url: None,
360 sha256: None,
361 build,
362 })
363 }
364
365 pub fn executable(&self, windowed: bool) -> PathBuf {
374 let version = match self.implementation() {
375 ImplementationName::CPython => {
376 if cfg!(unix) {
377 format!("{}.{}", self.key.major, self.key.minor)
378 } else {
379 String::new()
380 }
381 }
382 ImplementationName::PyPy => format!("{}.{}", self.key.major, self.key.minor),
384 ImplementationName::Pyodide => String::new(),
386 ImplementationName::GraalPy => String::new(),
387 };
388
389 let variant = if self.implementation() == ImplementationName::GraalPy {
392 ""
393 } else if cfg!(unix) {
394 self.key.variant.executable_suffix()
395 } else if cfg!(windows) && windowed {
396 "w"
398 } else {
399 ""
400 };
401
402 let name = format!(
403 "{implementation}{version}{variant}{exe}",
404 implementation = self.implementation().executable_name(),
405 exe = std::env::consts::EXE_SUFFIX
406 );
407
408 let executable = executable_path_from_base(
409 self.python_dir().as_path(),
410 &name,
411 &LenientImplementationName::from(self.implementation()),
412 *self.key.os(),
413 );
414
415 if cfg!(windows)
420 && matches!(self.key.variant, PythonVariant::Freethreaded)
421 && !executable.exists()
422 {
423 return self.python_dir().join(format!(
425 "python{}.{}t{}",
426 self.key.major,
427 self.key.minor,
428 std::env::consts::EXE_SUFFIX
429 ));
430 }
431
432 executable
433 }
434
435 fn python_dir(&self) -> PathBuf {
436 let install = self.path.join("install");
437 if install.is_dir() {
438 install
439 } else {
440 self.path.clone()
441 }
442 }
443
444 pub fn version(&self) -> PythonVersion {
446 self.key.version()
447 }
448
449 pub fn implementation(&self) -> ImplementationName {
450 match self.key.implementation().into_owned() {
451 LenientImplementationName::Known(implementation) => implementation,
452 LenientImplementationName::Unknown(_) => {
453 panic!("Managed Python installations should have a known implementation")
454 }
455 }
456 }
457
458 pub fn path(&self) -> &Path {
459 &self.path
460 }
461
462 pub fn key(&self) -> &PythonInstallationKey {
463 &self.key
464 }
465
466 pub fn platform(&self) -> &Platform {
467 self.key.platform()
468 }
469
470 pub fn build(&self) -> Option<&str> {
472 self.build.as_deref()
473 }
474
475 pub fn minor_version_key(&self) -> &PythonInstallationMinorVersionKey {
476 PythonInstallationMinorVersionKey::ref_cast(&self.key)
477 }
478
479 pub fn satisfies(&self, request: &PythonRequest) -> bool {
480 match request {
481 PythonRequest::File(path) => self.executable(false) == *path,
482 PythonRequest::Default | PythonRequest::Any => true,
483 PythonRequest::Directory(path) => self.path() == *path,
484 PythonRequest::ExecutableName(name) => self
485 .executable(false)
486 .file_name()
487 .is_some_and(|filename| filename.to_string_lossy() == *name),
488 PythonRequest::Implementation(implementation) => {
489 *implementation == self.implementation()
490 }
491 PythonRequest::ImplementationVersion(implementation, version) => {
492 *implementation == self.implementation() && version.matches_version(&self.version())
493 }
494 PythonRequest::Version(version) => version.matches_version(&self.version()),
495 PythonRequest::Key(request) => request.satisfied_by_key(self.key()),
496 }
497 }
498
499 pub fn ensure_canonical_executables(&self) -> Result<(), Error> {
501 let python = self.executable(false);
502
503 let canonical_names = &["python"];
504
505 for name in canonical_names {
506 let executable =
507 python.with_file_name(format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX));
508
509 if executable == python {
512 continue;
513 }
514
515 match symlink_or_copy_file(&python, &executable) {
516 Ok(()) => {
517 debug!(
518 "Created link {} -> {}",
519 executable.user_display(),
520 python.user_display(),
521 );
522 }
523 Err(err) if err.kind() == io::ErrorKind::NotFound => {
524 return Err(Error::MissingExecutable(python.clone()));
525 }
526 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
527 Err(err) => {
528 return Err(Error::CanonicalizeExecutable {
529 from: executable,
530 to: python,
531 err,
532 });
533 }
534 }
535 }
536
537 Ok(())
538 }
539
540 pub fn ensure_minor_version_link(&self, preview: Preview) -> Result<(), Error> {
543 if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self, preview) {
544 minor_version_link.create_directory()?;
545 }
546 Ok(())
547 }
548
549 pub fn update_minor_version_link(&self, preview: Preview) -> Result<(), Error> {
555 if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self, preview) {
556 if !minor_version_link.exists() {
557 return Ok(());
558 }
559 minor_version_link.create_directory()?;
560 }
561 Ok(())
562 }
563
564 pub fn ensure_externally_managed(&self) -> Result<(), Error> {
567 if self.key.os().is_emscripten() {
568 return Ok(());
571 }
572 let stdlib = if self.key.os().is_windows() {
574 self.python_dir().join("Lib")
575 } else {
576 let lib_suffix = self.key.variant.lib_suffix();
577 let python = if matches!(
578 self.key.implementation,
579 LenientImplementationName::Known(ImplementationName::PyPy)
580 ) {
581 format!("pypy{}", self.key.version().python_version())
582 } else {
583 format!("python{}{lib_suffix}", self.key.version().python_version())
584 };
585 self.python_dir().join("lib").join(python)
586 };
587
588 let file = stdlib.join("EXTERNALLY-MANAGED");
589 fs_err::write(file, EXTERNALLY_MANAGED)?;
590
591 Ok(())
592 }
593
594 pub fn ensure_sysconfig_patched(&self) -> Result<(), Error> {
596 if cfg!(unix) {
597 if self.key.os().is_emscripten() {
598 return Ok(());
601 }
602 if self.implementation() == ImplementationName::CPython {
603 sysconfig::update_sysconfig(
604 self.path(),
605 self.key.major,
606 self.key.minor,
607 self.key.variant.lib_suffix(),
608 )?;
609 }
610 }
611 Ok(())
612 }
613
614 pub fn ensure_dylib_patched(&self) -> Result<(), macos_dylib::Error> {
621 if cfg!(target_os = "macos") {
622 if self.key().os().is_like_darwin() {
623 if self.implementation() == ImplementationName::CPython {
624 let dylib_path = self.python_dir().join("lib").join(format!(
625 "{}python{}{}{}",
626 std::env::consts::DLL_PREFIX,
627 self.key.version().python_version(),
628 self.key.variant().executable_suffix(),
629 std::env::consts::DLL_SUFFIX
630 ));
631 macos_dylib::patch_dylib_install_name(dylib_path)?;
632 }
633 }
634 }
635 Ok(())
636 }
637
638 pub fn ensure_build_file(&self) -> Result<(), Error> {
640 if let Some(ref build) = self.build {
641 let build_file = self.path.join("BUILD");
642 fs::write(&build_file, build.as_ref())?;
643 }
644 Ok(())
645 }
646
647 pub fn is_bin_link(&self, path: &Path) -> bool {
650 if cfg!(unix) {
651 same_file::is_same_file(path, self.executable(false)).unwrap_or_default()
652 } else if cfg!(windows) {
653 let Some(launcher) = Launcher::try_from_path(path).unwrap_or_default() else {
654 return false;
655 };
656 if !matches!(launcher.kind, LauncherKind::Python) {
657 return false;
658 }
659 dunce::canonicalize(&launcher.python_path).unwrap_or(launcher.python_path)
663 == self.executable(false)
664 } else {
665 unreachable!("Only Windows and Unix are supported")
666 }
667 }
668
669 pub fn is_upgrade_of(&self, other: &Self) -> bool {
671 if self.key.implementation != other.key.implementation {
673 return false;
674 }
675 if self.key.variant != other.key.variant {
677 return false;
678 }
679 if (self.key.major, self.key.minor) != (other.key.major, other.key.minor) {
681 return false;
682 }
683 if self.key.patch == other.key.patch {
685 return match (self.key.prerelease, other.key.prerelease) {
686 (Some(self_pre), Some(other_pre)) => self_pre > other_pre,
688 (None, Some(_)) => true,
690 (_, None) => false,
692 };
693 }
694 if self.key.patch < other.key.patch {
696 return false;
697 }
698 true
699 }
700
701 pub fn url(&self) -> Option<&str> {
702 self.url.as_deref()
703 }
704
705 pub fn sha256(&self) -> Option<&str> {
706 self.sha256.as_deref()
707 }
708}
709
710#[derive(Clone, Debug)]
713pub struct PythonMinorVersionLink {
714 pub symlink_directory: PathBuf,
716 pub symlink_executable: PathBuf,
719 pub target_directory: PathBuf,
722}
723
724impl PythonMinorVersionLink {
725 pub fn from_executable(
745 executable: &Path,
746 key: &PythonInstallationKey,
747 preview: Preview,
748 ) -> Option<Self> {
749 let implementation = key.implementation();
750 if !matches!(
751 implementation.as_ref(),
752 LenientImplementationName::Known(ImplementationName::CPython)
753 ) {
754 return None;
756 }
757 let executable_name = executable
758 .file_name()
759 .expect("Executable file name should exist");
760 let symlink_directory_name = PythonInstallationMinorVersionKey::ref_cast(key).to_string();
761 let parent = executable
762 .parent()
763 .expect("Executable should have parent directory");
764
765 let target_directory = if cfg!(unix) {
767 if parent
768 .components()
769 .next_back()
770 .is_some_and(|c| c.as_os_str() == "bin")
771 {
772 parent.parent()?.to_path_buf()
773 } else {
774 return None;
775 }
776 } else if cfg!(windows) {
777 parent.to_path_buf()
778 } else {
779 unimplemented!("Only Windows and Unix systems are supported.")
780 };
781 let symlink_directory = target_directory.with_file_name(symlink_directory_name);
782 if target_directory == symlink_directory {
784 return None;
785 }
786 let symlink_executable = executable_path_from_base(
788 symlink_directory.as_path(),
789 &executable_name.to_string_lossy(),
790 &implementation,
791 *key.os(),
792 );
793 let minor_version_link = Self {
794 symlink_directory,
795 symlink_executable,
796 target_directory,
797 };
798 if !preview.is_enabled(PreviewFeatures::PYTHON_UPGRADE) && !minor_version_link.exists() {
802 return None;
803 }
804 Some(minor_version_link)
805 }
806
807 pub fn from_installation(
808 installation: &ManagedPythonInstallation,
809 preview: Preview,
810 ) -> Option<Self> {
811 Self::from_executable(
812 installation.executable(false).as_path(),
813 installation.key(),
814 preview,
815 )
816 }
817
818 pub fn create_directory(&self) -> Result<(), Error> {
819 match replace_symlink(
820 self.target_directory.as_path(),
821 self.symlink_directory.as_path(),
822 ) {
823 Ok(()) => {
824 debug!(
825 "Created link {} -> {}",
826 &self.symlink_directory.user_display(),
827 &self.target_directory.user_display(),
828 );
829 }
830 Err(err) if err.kind() == io::ErrorKind::NotFound => {
831 return Err(Error::MissingPythonMinorVersionLinkTargetDirectory(
832 self.target_directory.clone(),
833 ));
834 }
835 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
836 Err(err) => {
837 return Err(Error::PythonMinorVersionLinkDirectory {
838 from: self.symlink_directory.clone(),
839 to: self.target_directory.clone(),
840 err,
841 });
842 }
843 }
844 Ok(())
845 }
846
847 pub fn exists(&self) -> bool {
848 #[cfg(unix)]
849 {
850 self.symlink_directory
851 .symlink_metadata()
852 .map(|metadata| metadata.file_type().is_symlink())
853 .unwrap_or(false)
854 }
855 #[cfg(windows)]
856 {
857 self.symlink_directory
858 .symlink_metadata()
859 .is_ok_and(|metadata| {
860 (metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0) != 0
863 })
864 }
865 }
866}
867
868fn executable_path_from_base(
872 base: &Path,
873 executable_name: &str,
874 implementation: &LenientImplementationName,
875 os: Os,
876) -> PathBuf {
877 if matches!(
878 implementation,
879 &LenientImplementationName::Known(ImplementationName::GraalPy)
880 ) {
881 base.join("bin").join(executable_name)
883 } else if os.is_emscripten()
884 || matches!(
885 implementation,
886 &LenientImplementationName::Known(ImplementationName::Pyodide)
887 )
888 {
889 base.join(executable_name)
891 } else if os.is_windows() {
892 base.join(executable_name)
894 } else {
895 base.join("bin").join(executable_name)
897 }
898}
899
900pub fn create_link_to_executable(link: &Path, executable: &Path) -> Result<(), Error> {
904 let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?;
905 fs_err::create_dir_all(link_parent).map_err(|err| Error::ExecutableDirectory {
906 to: link_parent.to_path_buf(),
907 err,
908 })?;
909
910 if cfg!(unix) {
911 match symlink_or_copy_file(executable, link) {
913 Ok(()) => Ok(()),
914 Err(err) if err.kind() == io::ErrorKind::NotFound => {
915 Err(Error::MissingExecutable(executable.to_path_buf()))
916 }
917 Err(err) => Err(Error::LinkExecutable {
918 from: executable.to_path_buf(),
919 to: link.to_path_buf(),
920 err,
921 }),
922 }
923 } else if cfg!(windows) {
924 use uv_trampoline_builder::windows_python_launcher;
925
926 let launcher = windows_python_launcher(executable, false)?;
928
929 #[allow(clippy::disallowed_types)]
932 {
933 std::fs::File::create_new(link)
934 .and_then(|mut file| file.write_all(launcher.as_ref()))
935 .map_err(|err| Error::LinkExecutable {
936 from: executable.to_path_buf(),
937 to: link.to_path_buf(),
938 err,
939 })
940 }
941 } else {
942 unimplemented!("Only Windows and Unix are supported.")
943 }
944}
945
946pub fn platform_key_from_env() -> Result<String, Error> {
949 Ok(Platform::from_env()?.to_string().to_lowercase())
950}
951
952impl fmt::Display for ManagedPythonInstallation {
953 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
954 write!(
955 f,
956 "{}",
957 self.path
958 .file_name()
959 .unwrap_or(self.path.as_os_str())
960 .to_string_lossy()
961 )
962 }
963}
964
965pub fn python_executable_dir() -> Result<PathBuf, Error> {
967 uv_dirs::user_executable_directory(Some(EnvVars::UV_PYTHON_BIN_DIR))
968 .ok_or(Error::NoExecutableDirectory)
969}
970
971#[cfg(test)]
972mod tests {
973 use super::*;
974 use crate::implementation::LenientImplementationName;
975 use crate::installation::PythonInstallationKey;
976 use crate::{ImplementationName, PythonVariant};
977 use std::path::PathBuf;
978 use std::str::FromStr;
979 use uv_pep440::{Prerelease, PrereleaseKind};
980 use uv_platform::Platform;
981
982 fn create_test_installation(
983 implementation: ImplementationName,
984 major: u8,
985 minor: u8,
986 patch: u8,
987 prerelease: Option<Prerelease>,
988 variant: PythonVariant,
989 ) -> ManagedPythonInstallation {
990 let platform = Platform::from_str("linux-x86_64-gnu").unwrap();
991 let key = PythonInstallationKey::new(
992 LenientImplementationName::Known(implementation),
993 major,
994 minor,
995 patch,
996 prerelease,
997 platform,
998 variant,
999 );
1000 ManagedPythonInstallation {
1001 path: PathBuf::from("/test/path"),
1002 key,
1003 url: None,
1004 sha256: None,
1005 build: None,
1006 }
1007 }
1008
1009 #[test]
1010 fn test_is_upgrade_of_same_version() {
1011 let installation = create_test_installation(
1012 ImplementationName::CPython,
1013 3,
1014 10,
1015 8,
1016 None,
1017 PythonVariant::Default,
1018 );
1019
1020 assert!(!installation.is_upgrade_of(&installation));
1022 }
1023
1024 #[test]
1025 fn test_is_upgrade_of_patch_version() {
1026 let older = create_test_installation(
1027 ImplementationName::CPython,
1028 3,
1029 10,
1030 8,
1031 None,
1032 PythonVariant::Default,
1033 );
1034 let newer = create_test_installation(
1035 ImplementationName::CPython,
1036 3,
1037 10,
1038 9,
1039 None,
1040 PythonVariant::Default,
1041 );
1042
1043 assert!(newer.is_upgrade_of(&older));
1045 assert!(!older.is_upgrade_of(&newer));
1047 }
1048
1049 #[test]
1050 fn test_is_upgrade_of_different_minor_version() {
1051 let py310 = create_test_installation(
1052 ImplementationName::CPython,
1053 3,
1054 10,
1055 8,
1056 None,
1057 PythonVariant::Default,
1058 );
1059 let py311 = create_test_installation(
1060 ImplementationName::CPython,
1061 3,
1062 11,
1063 0,
1064 None,
1065 PythonVariant::Default,
1066 );
1067
1068 assert!(!py311.is_upgrade_of(&py310));
1070 assert!(!py310.is_upgrade_of(&py311));
1071 }
1072
1073 #[test]
1074 fn test_is_upgrade_of_different_implementation() {
1075 let cpython = create_test_installation(
1076 ImplementationName::CPython,
1077 3,
1078 10,
1079 8,
1080 None,
1081 PythonVariant::Default,
1082 );
1083 let pypy = create_test_installation(
1084 ImplementationName::PyPy,
1085 3,
1086 10,
1087 9,
1088 None,
1089 PythonVariant::Default,
1090 );
1091
1092 assert!(!pypy.is_upgrade_of(&cpython));
1094 assert!(!cpython.is_upgrade_of(&pypy));
1095 }
1096
1097 #[test]
1098 fn test_is_upgrade_of_different_variant() {
1099 let default = create_test_installation(
1100 ImplementationName::CPython,
1101 3,
1102 10,
1103 8,
1104 None,
1105 PythonVariant::Default,
1106 );
1107 let freethreaded = create_test_installation(
1108 ImplementationName::CPython,
1109 3,
1110 10,
1111 9,
1112 None,
1113 PythonVariant::Freethreaded,
1114 );
1115
1116 assert!(!freethreaded.is_upgrade_of(&default));
1118 assert!(!default.is_upgrade_of(&freethreaded));
1119 }
1120
1121 #[test]
1122 fn test_is_upgrade_of_prerelease() {
1123 let stable = create_test_installation(
1124 ImplementationName::CPython,
1125 3,
1126 10,
1127 8,
1128 None,
1129 PythonVariant::Default,
1130 );
1131 let prerelease = create_test_installation(
1132 ImplementationName::CPython,
1133 3,
1134 10,
1135 8,
1136 Some(Prerelease {
1137 kind: PrereleaseKind::Alpha,
1138 number: 1,
1139 }),
1140 PythonVariant::Default,
1141 );
1142
1143 assert!(stable.is_upgrade_of(&prerelease));
1145
1146 assert!(!prerelease.is_upgrade_of(&stable));
1148 }
1149
1150 #[test]
1151 fn test_is_upgrade_of_prerelease_to_prerelease() {
1152 let alpha1 = create_test_installation(
1153 ImplementationName::CPython,
1154 3,
1155 10,
1156 8,
1157 Some(Prerelease {
1158 kind: PrereleaseKind::Alpha,
1159 number: 1,
1160 }),
1161 PythonVariant::Default,
1162 );
1163 let alpha2 = create_test_installation(
1164 ImplementationName::CPython,
1165 3,
1166 10,
1167 8,
1168 Some(Prerelease {
1169 kind: PrereleaseKind::Alpha,
1170 number: 2,
1171 }),
1172 PythonVariant::Default,
1173 );
1174
1175 assert!(alpha2.is_upgrade_of(&alpha1));
1177 assert!(!alpha1.is_upgrade_of(&alpha2));
1179 }
1180
1181 #[test]
1182 fn test_is_upgrade_of_prerelease_same_patch() {
1183 let prerelease = create_test_installation(
1184 ImplementationName::CPython,
1185 3,
1186 10,
1187 8,
1188 Some(Prerelease {
1189 kind: PrereleaseKind::Alpha,
1190 number: 1,
1191 }),
1192 PythonVariant::Default,
1193 );
1194
1195 assert!(!prerelease.is_upgrade_of(&prerelease));
1197 }
1198}