1use core::fmt;
2use std::borrow::Cow;
3use std::cmp::{Ordering, 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};
15#[cfg(windows)]
16use windows::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
17
18use uv_fs::{
19 LockedFile, LockedFileError, LockedFileMode, Simplified, normalize_absolute_path,
20 replace_symlink, symlink_or_copy_file, verbatim_path,
21};
22use uv_platform::{Error as PlatformError, Os};
23use uv_platform::{LibcDetectionError, Platform};
24use uv_pypi_types::Digest;
25use uv_state::{StateBucket, StateStore};
26use uv_static::EnvVars;
27use uv_trampoline_builder::{Launcher, LauncherKind, WindowMode, windows_python_launcher};
28
29use crate::discovery::VersionRequest;
30use crate::downloads::{Error as DownloadError, ManagedPythonDownload};
31use crate::implementation::{
32 Error as ImplementationError, ImplementationName, LenientImplementationName,
33};
34use crate::installation::{self, PythonInstallationKey};
35use crate::interpreter::Interpreter;
36use crate::python_version::PythonVersion;
37use crate::{PythonInstallationMinorVersionKey, PythonVariant, macos_dylib, sysconfig};
38
39#[derive(Error, Debug)]
40pub enum Error {
41 #[error(transparent)]
42 Io(#[from] io::Error),
43 #[error(transparent)]
44 LockedFile(#[from] LockedFileError),
45 #[error(transparent)]
46 Download(#[from] DownloadError),
47 #[error(transparent)]
48 PlatformError(#[from] PlatformError),
49 #[error(transparent)]
50 ImplementationError(#[from] ImplementationError),
51 #[error("Invalid python version: {0}")]
52 InvalidPythonVersion(String),
53 #[error(transparent)]
54 ExtractError(#[from] uv_extract::Error),
55 #[error(transparent)]
56 SysconfigError(#[from] sysconfig::Error),
57 #[error("Missing expected Python executable at {}", _0.user_display())]
58 MissingExecutable(PathBuf),
59 #[error("Missing expected target directory for Python minor version link at {}", _0.user_display())]
60 MissingPythonMinorVersionLinkTargetDirectory(PathBuf),
61 #[error("Failed to create canonical Python executable")]
62 CanonicalizeExecutable(#[source] io::Error),
63 #[error("Failed to create Python executable link")]
64 LinkExecutable(#[source] io::Error),
65 #[error("Failed to create Python minor version link directory")]
66 PythonMinorVersionLinkDirectory(#[source] io::Error),
67 #[error("Failed to create directory for Python executable link")]
68 ExecutableDirectory(#[source] io::Error),
69 #[error("Failed to read Python installation directory")]
70 ReadError(#[source] io::Error),
71 #[error("Failed to find a directory to install executables into")]
72 NoExecutableDirectory,
73 #[error(transparent)]
74 LauncherError(#[from] uv_trampoline_builder::Error),
75 #[error("Failed to read managed Python directory name: {0}")]
76 NameError(String),
77 #[error("Failed to construct absolute path to managed Python directory: {}", _0.user_display())]
78 AbsolutePath(PathBuf, #[source] io::Error),
79 #[error(transparent)]
80 NameParseError(#[from] installation::PythonInstallationKeyError),
81 #[error("Failed to determine the libc used on the current platform")]
82 LibcDetection(#[from] LibcDetectionError),
83 #[error(transparent)]
84 MacOsDylib(#[from] macos_dylib::Error),
85}
86
87pub fn compare_build_versions(a: &str, b: &str) -> Ordering {
92 match (a.parse::<u64>(), b.parse::<u64>()) {
93 (Ok(a_num), Ok(b_num)) => a_num.cmp(&b_num),
94 _ => a.cmp(b),
95 }
96}
97
98#[derive(Debug, Clone, Eq, PartialEq)]
100pub struct ManagedPythonInstallations {
101 root: PathBuf,
103}
104
105impl ManagedPythonInstallations {
106 fn from_path(root: impl Into<PathBuf>) -> Self {
108 Self { root: root.into() }
109 }
110
111 pub async fn lock(&self) -> Result<LockedFile, Error> {
114 Ok(LockedFile::acquire(
115 self.root.join(".lock"),
116 LockedFileMode::Exclusive,
117 self.root.user_display(),
118 )
119 .await?)
120 }
121
122 pub fn from_settings(install_dir: Option<PathBuf>) -> Result<Self, Error> {
129 if let Some(install_dir) = install_dir {
130 Ok(Self::from_path(install_dir))
131 } else if let Some(install_dir) =
132 std::env::var_os(EnvVars::UV_PYTHON_INSTALL_DIR).filter(|s| !s.is_empty())
133 {
134 Ok(Self::from_path(install_dir))
135 } else {
136 Ok(Self::from_path(
137 StateStore::from_settings(None)?.bucket(StateBucket::ManagedPython),
138 ))
139 }
140 }
141
142 #[cfg(all(test, unix))]
144 pub(crate) fn temp() -> Result<Self, Error> {
145 Ok(Self::from_path(
146 StateStore::temp()?.bucket(StateBucket::ManagedPython),
147 ))
148 }
149
150 pub fn scratch(&self) -> PathBuf {
152 self.root.join(".temp")
153 }
154
155 pub fn init(self) -> Result<Self, Error> {
159 let root = &self.root;
160
161 if !root.exists()
163 && root
164 .parent()
165 .is_some_and(|parent| parent.join("toolchains").exists())
166 {
167 let deprecated = root.parent().unwrap().join("toolchains");
168 fs::rename(&deprecated, root)?;
170 uv_fs::replace_symlink(root, &deprecated)?;
172 } else {
173 fs::create_dir_all(root)?;
174 }
175
176 fs::create_dir_all(root)?;
178
179 let scratch = self.scratch();
181 fs::create_dir_all(&scratch)?;
182
183 match fs::OpenOptions::new()
185 .write(true)
186 .create_new(true)
187 .open(root.join(".gitignore"))
188 {
189 Ok(mut file) => file.write_all(b"*")?,
190 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
191 Err(err) => return Err(err.into()),
192 }
193
194 Ok(self)
195 }
196
197 pub fn find_all(
202 &self,
203 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + use<>, Error> {
204 let dirs = match fs_err::read_dir(&self.root) {
205 Ok(installation_dirs) => {
206 let directories: Vec<_> = installation_dirs
208 .filter_map(|read_dir| match read_dir {
209 Ok(entry) => match entry.file_type() {
210 Ok(file_type) => file_type.is_dir().then_some(Ok(entry.path())),
211 Err(err) => Some(Err(err)),
212 },
213 Err(err) => Some(Err(err)),
214 })
215 .collect::<Result<_, io::Error>>()
216 .map_err(Error::ReadError)?;
217 directories
218 }
219 Err(err) if err.kind() == io::ErrorKind::NotFound => vec![],
220 Err(err) => {
221 return Err(Error::ReadError(err));
222 }
223 };
224 let scratch = self.scratch();
225 Ok(dirs
226 .into_iter()
227 .filter(|path| *path != scratch)
229 .filter(|path| {
231 path.file_name()
232 .and_then(OsStr::to_str)
233 .is_none_or(|name| !name.starts_with('.'))
234 })
235 .filter_map(|path| {
236 ManagedPythonInstallation::from_path(path)
237 .inspect_err(|err| {
238 warn!("Ignoring malformed managed Python entry:\n {err}");
239 })
240 .ok()
241 })
242 .sorted_unstable_by_key(|installation| Reverse(installation.key().clone())))
243 }
244
245 pub(crate) fn find_matching_current_platform()
247 -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + use<>, Error> {
248 let platform = Platform::from_env()?;
249
250 let iter = Self::from_settings(None)?
251 .find_all()?
252 .filter(move |installation| {
253 if !platform.supports(installation.platform()) {
254 debug!("Skipping managed installation `{installation}`: not supported by current platform `{platform}`");
255 return false;
256 }
257 true
258 });
259
260 Ok(iter)
261 }
262
263 pub fn find_version<'a>(
270 &'a self,
271 version: &'a PythonVersion,
272 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + 'a, Error> {
273 let request = VersionRequest::from(version);
274 Ok(Self::find_matching_current_platform()?
275 .filter(move |installation| request.matches_installation_key(installation.key())))
276 }
277
278 pub fn root(&self) -> &Path {
279 &self.root
280 }
281
282 pub(crate) fn absolute_root(&self) -> Result<PathBuf, Error> {
283 let root = if self.root.is_absolute() {
284 self.root.clone()
285 } else {
286 crate::current_dir()?.join(&self.root)
287 };
288
289 normalize_absolute_path(&root).map_err(|err| Error::AbsolutePath(self.root.clone(), err))
290 }
291}
292
293static EXTERNALLY_MANAGED: &str = "[externally-managed]
294Error=This Python installation is managed by uv and should not be modified.
295";
296
297#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
299pub struct ManagedPythonInstallation {
300 path: PathBuf,
302 key: PythonInstallationKey,
304 url: Option<Cow<'static, str>>,
308 sha256: Option<Digest<32>>,
312 build: Option<Cow<'static, str>>,
316}
317
318impl ManagedPythonInstallation {
319 pub fn new(path: PathBuf, download: &ManagedPythonDownload) -> Self {
320 Self {
321 path,
322 key: download.key().clone(),
323 url: Some(download.url().clone()),
324 sha256: download.sha256().cloned(),
325 build: download.build().map(Cow::Borrowed),
326 }
327 }
328
329 fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
330 let path = path.as_ref();
331
332 let key = PythonInstallationKey::from_str(
333 path.file_name()
334 .ok_or(Error::NameError("name is empty".to_string()))?
335 .to_str()
336 .ok_or(Error::NameError("not a valid string".to_string()))?,
337 )?;
338
339 let path = std::path::absolute(path)
340 .map_err(|err| Error::AbsolutePath(path.to_path_buf(), err))?;
341
342 let build = match fs::read_to_string(path.join("BUILD")) {
344 Ok(content) => Some(Cow::Owned(content.trim().to_string())),
345 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
346 Err(err) => return Err(err.into()),
347 };
348
349 Ok(Self {
350 path,
351 key,
352 url: None,
353 sha256: None,
354 build,
355 })
356 }
357
358 pub fn try_from_interpreter(interpreter: &Interpreter) -> Option<Self> {
362 let managed_root = ManagedPythonInstallations::from_settings(None).ok()?;
363 let root = managed_root.absolute_root().ok()?;
364
365 let sys_base_prefix = dunce::canonicalize(interpreter.sys_base_prefix())
369 .unwrap_or_else(|_| interpreter.sys_base_prefix().to_path_buf());
370 let root = dunce::canonicalize(&root).unwrap_or(root);
371
372 let suffix = sys_base_prefix.strip_prefix(&root).ok()?;
374
375 let first_component = suffix.components().next()?;
376 let name = first_component.as_os_str().to_str()?;
377
378 PythonInstallationKey::from_str(name).ok()?;
380
381 let path = root.join(name);
383 Self::from_path(path).ok()
384 }
385
386 pub fn executable(&self, windowed: bool) -> PathBuf {
395 let version = match self.implementation() {
396 ImplementationName::CPython => {
397 if cfg!(unix) {
398 format!("{}.{}", self.key.major, self.key.minor)
399 } else {
400 String::new()
401 }
402 }
403 ImplementationName::PyPy => format!("{}.{}", self.key.major, self.key.minor),
405 ImplementationName::Pyodide => String::new(),
407 ImplementationName::GraalPy => String::new(),
408 };
409
410 let variant = if self.implementation() == ImplementationName::GraalPy {
413 ""
414 } else if cfg!(unix) {
415 self.key.variant.executable_suffix()
416 } else if cfg!(windows) && windowed {
417 "w"
419 } else {
420 ""
421 };
422
423 let name = format!(
424 "{implementation}{version}{variant}{exe}",
425 implementation = self.implementation().executable_name(),
426 exe = std::env::consts::EXE_SUFFIX
427 );
428
429 let executable = executable_path_from_base(
430 self.python_dir().as_path(),
431 &name,
432 &LenientImplementationName::from(self.implementation()),
433 *self.key.os(),
434 );
435
436 if cfg!(windows)
441 && matches!(self.key.variant, PythonVariant::Freethreaded)
442 && !executable.exists()
443 {
444 return self.python_dir().join(format!(
446 "python{}.{}t{}",
447 self.key.major,
448 self.key.minor,
449 std::env::consts::EXE_SUFFIX
450 ));
451 }
452
453 executable
454 }
455
456 fn python_dir(&self) -> PathBuf {
457 let install = self.path.join("install");
458 if install.is_dir() {
459 install
460 } else {
461 self.path.clone()
462 }
463 }
464
465 pub(crate) fn version(&self) -> PythonVersion {
467 self.key.version()
468 }
469
470 pub fn implementation(&self) -> ImplementationName {
471 match self.key.implementation().into_owned() {
472 LenientImplementationName::Known(implementation) => implementation,
473 LenientImplementationName::Unknown(_) => {
474 panic!("Managed Python installations should have a known implementation")
475 }
476 }
477 }
478
479 pub fn path(&self) -> &Path {
480 &self.path
481 }
482
483 pub fn key(&self) -> &PythonInstallationKey {
484 &self.key
485 }
486
487 pub(crate) fn platform(&self) -> &Platform {
488 self.key.platform()
489 }
490
491 pub fn build(&self) -> Option<&str> {
493 self.build.as_deref()
494 }
495
496 pub fn minor_version_key(&self) -> &PythonInstallationMinorVersionKey {
497 PythonInstallationMinorVersionKey::ref_cast(&self.key)
498 }
499
500 pub fn ensure_canonical_executables(&self) -> Result<(), Error> {
502 let python = self.executable(false);
503
504 let canonical_names = &["python"];
505
506 for name in canonical_names {
507 let executable =
508 python.with_file_name(format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX));
509
510 if executable == python {
513 continue;
514 }
515
516 match symlink_or_copy_file(&python, &executable) {
517 Ok(()) => {
518 debug!(
519 "Created link {} -> {}",
520 executable.user_display(),
521 python.user_display(),
522 );
523 }
524 Err(err) if err.kind() == io::ErrorKind::NotFound => {
525 return Err(Error::MissingExecutable(python.clone()));
526 }
527 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
528 Err(err) => {
529 return Err(Error::CanonicalizeExecutable(err));
530 }
531 }
532 }
533
534 Ok(())
535 }
536
537 pub fn ensure_minor_version_link(&self) -> Result<(), Error> {
540 if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self) {
541 minor_version_link.create_directory()?;
542 }
543 Ok(())
544 }
545
546 pub fn ensure_externally_managed(&self) -> Result<(), Error> {
549 if self.key.os().is_emscripten() {
550 return Ok(());
553 }
554 let stdlib = if self.key.os().is_windows() {
556 self.python_dir().join("Lib")
557 } else {
558 let lib_suffix = self.key.variant.lib_suffix();
559 let python = if matches!(
560 self.key.implementation,
561 LenientImplementationName::Known(ImplementationName::PyPy)
562 ) {
563 format!("pypy{}", self.key.version().python_version())
564 } else {
565 format!("python{}{lib_suffix}", self.key.version().python_version())
566 };
567 self.python_dir().join("lib").join(python)
568 };
569
570 let file = stdlib.join("EXTERNALLY-MANAGED");
571 fs_err::write(file, EXTERNALLY_MANAGED)?;
572
573 Ok(())
574 }
575
576 pub fn ensure_sysconfig_patched(&self) -> Result<(), Error> {
578 if cfg!(unix) && !self.key.os().is_windows() {
579 if self.key.os().is_emscripten() {
580 return Ok(());
583 }
584 if self.implementation() == ImplementationName::CPython {
585 sysconfig::update_sysconfig(
586 self.path(),
587 self.key.major,
588 self.key.minor,
589 self.key.variant.lib_suffix(),
590 )?;
591 }
592 }
593 Ok(())
594 }
595
596 pub fn ensure_dylib_patched(&self) -> Result<(), macos_dylib::Error> {
603 if cfg!(target_os = "macos") {
604 if self.key().os().is_like_darwin() {
605 if self.implementation() == ImplementationName::CPython {
606 let dylib_path = self.python_dir().join("lib").join(format!(
607 "{}python{}{}{}",
608 std::env::consts::DLL_PREFIX,
609 self.key.version().python_version(),
610 self.key.variant().executable_suffix(),
611 std::env::consts::DLL_SUFFIX
612 ));
613 macos_dylib::patch_dylib_install_name(dylib_path)?;
614 }
615 }
616 }
617 Ok(())
618 }
619
620 pub fn ensure_build_file(&self) -> Result<(), Error> {
622 if let Some(ref build) = self.build {
623 let build_file = self.path.join("BUILD");
624 fs::write(&build_file, build.as_ref())?;
625 }
626 Ok(())
627 }
628
629 pub fn is_bin_link(&self, path: &Path) -> bool {
632 if cfg!(unix) {
633 same_file::is_same_file(path, self.executable(false)).unwrap_or_default()
634 } else if cfg!(windows) {
635 let Some(launcher) = Launcher::try_from_path(path).unwrap_or_default() else {
636 return false;
637 };
638 if !matches!(launcher.kind, LauncherKind::Python) {
639 return false;
640 }
641 dunce::canonicalize(&launcher.python_path).unwrap_or(launcher.python_path)
645 == self.executable(false)
646 } else {
647 unreachable!("Only Windows and Unix are supported")
648 }
649 }
650
651 pub fn is_upgrade_of(&self, other: &Self) -> bool {
653 if self.key.implementation != other.key.implementation {
655 return false;
656 }
657 if self.key.variant != other.key.variant {
659 return false;
660 }
661 if (self.key.major, self.key.minor) != (other.key.major, other.key.minor) {
663 return false;
664 }
665 if self.key.patch == other.key.patch {
668 return match (self.key.prerelease, other.key.prerelease) {
669 (Some(self_pre), Some(other_pre)) => self_pre > other_pre,
671 (None, Some(_)) => true,
673 (Some(_), None) => false,
675 (None, None) => match (self.build.as_deref(), other.build.as_deref()) {
677 (Some(_), None) => true,
679 (Some(self_build), Some(other_build)) => {
681 compare_build_versions(self_build, other_build) == Ordering::Greater
682 }
683 (None, _) => false,
685 },
686 };
687 }
688 if self.key.patch < other.key.patch {
690 return false;
691 }
692 true
693 }
694
695 #[cfg(windows)]
696 pub(crate) fn url(&self) -> Option<&str> {
697 self.url.as_deref()
698 }
699
700 #[cfg(windows)]
701 pub(crate) fn sha256(&self) -> Option<&str> {
702 self.sha256.as_ref().map(Digest::as_str)
703 }
704}
705
706#[derive(Clone, Debug)]
709pub struct PythonMinorVersionLink {
710 pub symlink_directory: PathBuf,
712 pub symlink_executable: PathBuf,
715 pub target_directory: PathBuf,
718}
719
720impl PythonMinorVersionLink {
721 fn from_executable(executable: &Path, key: &PythonInstallationKey) -> Option<Self> {
741 let implementation = key.implementation();
742 if !matches!(
743 implementation.as_ref(),
744 LenientImplementationName::Known(ImplementationName::CPython)
745 ) {
746 return None;
748 }
749 let executable_name = executable
750 .file_name()
751 .expect("Executable file name should exist");
752 let symlink_directory_name = PythonInstallationMinorVersionKey::ref_cast(key).to_string();
753 let parent = executable
754 .parent()
755 .expect("Executable should have parent directory");
756
757 let target_directory = if cfg!(unix) {
759 if parent
760 .components()
761 .next_back()
762 .is_some_and(|c| c.as_os_str() == "bin")
763 {
764 parent.parent()?.to_path_buf()
765 } else {
766 return None;
767 }
768 } else if cfg!(windows) {
769 parent.to_path_buf()
770 } else {
771 unimplemented!("Only Windows and Unix systems are supported.")
772 };
773 let symlink_directory = target_directory.with_file_name(symlink_directory_name);
774 if target_directory == symlink_directory {
776 return None;
777 }
778 let symlink_executable = executable_path_from_base(
780 symlink_directory.as_path(),
781 &executable_name.to_string_lossy(),
782 &implementation,
783 *key.os(),
784 );
785 let minor_version_link = Self {
786 symlink_directory,
787 symlink_executable,
788 target_directory,
789 };
790 Some(minor_version_link)
791 }
792
793 pub fn from_installation(installation: &ManagedPythonInstallation) -> Option<Self> {
794 Self::from_executable(installation.executable(false).as_path(), installation.key())
795 }
796
797 fn create_directory(&self) -> Result<(), Error> {
798 match replace_symlink(
799 self.target_directory.as_path(),
800 self.symlink_directory.as_path(),
801 ) {
802 Ok(()) => {
803 debug!(
804 "Created link {} -> {}",
805 &self.symlink_directory.user_display(),
806 &self.target_directory.user_display(),
807 );
808 }
809 Err(err) if err.kind() == io::ErrorKind::NotFound => {
810 return Err(Error::MissingPythonMinorVersionLinkTargetDirectory(
811 self.target_directory.clone(),
812 ));
813 }
814 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
815 Err(err) => {
816 return Err(Error::PythonMinorVersionLinkDirectory(err));
817 }
818 }
819 Ok(())
820 }
821
822 pub fn exists(&self) -> bool {
829 let points_to_target = || {
830 fs_err::read_link(&self.symlink_directory)
831 .is_ok_and(|target| verbatim_path(&target) == verbatim_path(&self.target_directory))
832 };
833
834 cfg_select! {
835 unix => {
836 self.symlink_directory
837 .symlink_metadata()
838 .is_ok_and(|metadata| metadata.file_type().is_symlink())
839 && points_to_target()
840 },
841 windows => {
842 self.symlink_directory
843 .symlink_metadata()
844 .is_ok_and(|metadata| {
845 (metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0) != 0
848 })
849 && points_to_target()
850 },
851 }
852 }
853}
854
855fn executable_path_from_base(
859 base: &Path,
860 executable_name: &str,
861 implementation: &LenientImplementationName,
862 os: Os,
863) -> PathBuf {
864 if matches!(
865 implementation,
866 &LenientImplementationName::Known(ImplementationName::GraalPy)
867 ) {
868 base.join("bin").join(executable_name)
870 } else if os.is_emscripten()
871 || matches!(
872 implementation,
873 &LenientImplementationName::Known(ImplementationName::Pyodide)
874 )
875 {
876 base.join(executable_name)
878 } else if os.is_windows() {
879 base.join(executable_name)
881 } else {
882 base.join("bin").join(executable_name)
884 }
885}
886
887#[derive(Debug, Clone, Copy)]
891pub struct PythonExecutable<'a> {
892 path: &'a Path,
893 window_mode: WindowMode,
894}
895
896impl<'a> PythonExecutable<'a> {
897 pub fn console(path: &'a Path) -> Self {
899 Self {
900 path,
901 window_mode: WindowMode::Console,
902 }
903 }
904
905 pub fn windowed(path: &'a Path) -> Self {
907 Self {
908 path,
909 window_mode: WindowMode::Windowed,
910 }
911 }
912}
913
914pub fn create_link_to_executable(
918 link: &Path,
919 executable: PythonExecutable<'_>,
920) -> Result<(), Error> {
921 let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?;
922 fs_err::create_dir_all(link_parent).map_err(Error::ExecutableDirectory)?;
923
924 if cfg!(unix) {
925 match symlink_or_copy_file(executable.path, link) {
927 Ok(()) => Ok(()),
928 Err(err) if err.kind() == io::ErrorKind::NotFound => {
929 Err(Error::MissingExecutable(executable.path.to_path_buf()))
930 }
931 Err(err) => Err(Error::LinkExecutable(err)),
932 }
933 } else if cfg!(windows) {
934 let launcher = windows_python_launcher(executable.path, executable.window_mode)?;
935
936 #[expect(clippy::disallowed_types)]
939 {
940 std::fs::File::create_new(link)
941 .and_then(|mut file| file.write_all(launcher.as_ref()))
942 .map_err(Error::LinkExecutable)
943 }
944 } else {
945 unimplemented!("Only Windows and Unix are supported.")
946 }
947}
948
949pub fn replace_link_to_executable(
955 link: &Path,
956 executable: PythonExecutable<'_>,
957) -> Result<(), Error> {
958 let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?;
959 fs_err::create_dir_all(link_parent).map_err(Error::ExecutableDirectory)?;
960
961 if cfg!(unix) {
962 replace_symlink(executable.path, link).map_err(Error::LinkExecutable)
963 } else if cfg!(windows) {
964 let launcher = windows_python_launcher(executable.path, executable.window_mode)?;
965
966 uv_fs::write_atomic_sync(link, &*launcher).map_err(Error::LinkExecutable)
967 } else {
968 unimplemented!("Only Windows and Unix are supported.")
969 }
970}
971
972pub fn platform_key_from_env() -> Result<String, Error> {
975 Ok(Platform::from_env()?.to_string().to_lowercase())
976}
977
978impl fmt::Display for ManagedPythonInstallation {
979 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
980 write!(
981 f,
982 "{}",
983 self.path
984 .file_name()
985 .unwrap_or(self.path.as_os_str())
986 .to_string_lossy()
987 )
988 }
989}
990
991pub fn python_executable_dir() -> Result<PathBuf, Error> {
993 uv_dirs::user_executable_directory(Some(EnvVars::UV_PYTHON_BIN_DIR))
994 .ok_or(Error::NoExecutableDirectory)
995}
996
997#[cfg(test)]
998mod tests {
999 use super::*;
1000 use crate::implementation::LenientImplementationName;
1001 use crate::installation::PythonInstallationKey;
1002 use crate::{ImplementationName, PythonVariant};
1003 use std::path::PathBuf;
1004 use std::str::FromStr;
1005 use uv_pep440::{Prerelease, PrereleaseKind};
1006 use uv_platform::Platform;
1007
1008 fn create_test_installation(
1009 implementation: ImplementationName,
1010 major: u8,
1011 minor: u8,
1012 patch: u8,
1013 prerelease: Option<Prerelease>,
1014 variant: PythonVariant,
1015 build: Option<&str>,
1016 ) -> ManagedPythonInstallation {
1017 let platform = Platform::from_str("linux-x86_64-gnu").unwrap();
1018 let key = PythonInstallationKey::new(
1019 LenientImplementationName::Known(implementation),
1020 major,
1021 minor,
1022 patch,
1023 prerelease,
1024 platform,
1025 variant,
1026 );
1027 ManagedPythonInstallation {
1028 path: PathBuf::from("/test/path"),
1029 key,
1030 url: None,
1031 sha256: None,
1032 build: build.map(|s| Cow::Owned(s.to_owned())),
1033 }
1034 }
1035
1036 #[test]
1037 fn test_is_upgrade_of_same_version() {
1038 let installation = create_test_installation(
1039 ImplementationName::CPython,
1040 3,
1041 10,
1042 8,
1043 None,
1044 PythonVariant::Default,
1045 None,
1046 );
1047
1048 assert!(!installation.is_upgrade_of(&installation));
1050 }
1051
1052 #[test]
1053 fn test_is_upgrade_of_patch_version() {
1054 let older = create_test_installation(
1055 ImplementationName::CPython,
1056 3,
1057 10,
1058 8,
1059 None,
1060 PythonVariant::Default,
1061 None,
1062 );
1063 let newer = create_test_installation(
1064 ImplementationName::CPython,
1065 3,
1066 10,
1067 9,
1068 None,
1069 PythonVariant::Default,
1070 None,
1071 );
1072
1073 assert!(newer.is_upgrade_of(&older));
1075 assert!(!older.is_upgrade_of(&newer));
1077 }
1078
1079 #[test]
1080 fn test_is_upgrade_of_different_minor_version() {
1081 let py310 = create_test_installation(
1082 ImplementationName::CPython,
1083 3,
1084 10,
1085 8,
1086 None,
1087 PythonVariant::Default,
1088 None,
1089 );
1090 let py311 = create_test_installation(
1091 ImplementationName::CPython,
1092 3,
1093 11,
1094 0,
1095 None,
1096 PythonVariant::Default,
1097 None,
1098 );
1099
1100 assert!(!py311.is_upgrade_of(&py310));
1102 assert!(!py310.is_upgrade_of(&py311));
1103 }
1104
1105 #[test]
1106 fn test_is_upgrade_of_different_implementation() {
1107 let cpython = create_test_installation(
1108 ImplementationName::CPython,
1109 3,
1110 10,
1111 8,
1112 None,
1113 PythonVariant::Default,
1114 None,
1115 );
1116 let pypy = create_test_installation(
1117 ImplementationName::PyPy,
1118 3,
1119 10,
1120 9,
1121 None,
1122 PythonVariant::Default,
1123 None,
1124 );
1125
1126 assert!(!pypy.is_upgrade_of(&cpython));
1128 assert!(!cpython.is_upgrade_of(&pypy));
1129 }
1130
1131 #[test]
1132 fn test_is_upgrade_of_different_variant() {
1133 let default = create_test_installation(
1134 ImplementationName::CPython,
1135 3,
1136 10,
1137 8,
1138 None,
1139 PythonVariant::Default,
1140 None,
1141 );
1142 let freethreaded = create_test_installation(
1143 ImplementationName::CPython,
1144 3,
1145 10,
1146 9,
1147 None,
1148 PythonVariant::Freethreaded,
1149 None,
1150 );
1151
1152 assert!(!freethreaded.is_upgrade_of(&default));
1154 assert!(!default.is_upgrade_of(&freethreaded));
1155 }
1156
1157 #[test]
1158 fn test_is_upgrade_of_prerelease() {
1159 let stable = create_test_installation(
1160 ImplementationName::CPython,
1161 3,
1162 10,
1163 8,
1164 None,
1165 PythonVariant::Default,
1166 None,
1167 );
1168 let prerelease = create_test_installation(
1169 ImplementationName::CPython,
1170 3,
1171 10,
1172 8,
1173 Some(Prerelease {
1174 kind: PrereleaseKind::Alpha,
1175 number: 1,
1176 }),
1177 PythonVariant::Default,
1178 None,
1179 );
1180
1181 assert!(stable.is_upgrade_of(&prerelease));
1183
1184 assert!(!prerelease.is_upgrade_of(&stable));
1186 }
1187
1188 #[test]
1189 fn test_is_upgrade_of_prerelease_to_prerelease() {
1190 let alpha1 = create_test_installation(
1191 ImplementationName::CPython,
1192 3,
1193 10,
1194 8,
1195 Some(Prerelease {
1196 kind: PrereleaseKind::Alpha,
1197 number: 1,
1198 }),
1199 PythonVariant::Default,
1200 None,
1201 );
1202 let alpha2 = create_test_installation(
1203 ImplementationName::CPython,
1204 3,
1205 10,
1206 8,
1207 Some(Prerelease {
1208 kind: PrereleaseKind::Alpha,
1209 number: 2,
1210 }),
1211 PythonVariant::Default,
1212 None,
1213 );
1214
1215 assert!(alpha2.is_upgrade_of(&alpha1));
1217 assert!(!alpha1.is_upgrade_of(&alpha2));
1219 }
1220
1221 #[test]
1222 fn test_is_upgrade_of_prerelease_same_patch() {
1223 let prerelease = create_test_installation(
1224 ImplementationName::CPython,
1225 3,
1226 10,
1227 8,
1228 Some(Prerelease {
1229 kind: PrereleaseKind::Alpha,
1230 number: 1,
1231 }),
1232 PythonVariant::Default,
1233 None,
1234 );
1235
1236 assert!(!prerelease.is_upgrade_of(&prerelease));
1238 }
1239
1240 #[test]
1241 fn test_is_upgrade_of_build_version() {
1242 let older_build = create_test_installation(
1243 ImplementationName::CPython,
1244 3,
1245 10,
1246 8,
1247 None,
1248 PythonVariant::Default,
1249 Some("20240101"),
1250 );
1251 let newer_build = create_test_installation(
1252 ImplementationName::CPython,
1253 3,
1254 10,
1255 8,
1256 None,
1257 PythonVariant::Default,
1258 Some("20240201"),
1259 );
1260
1261 assert!(newer_build.is_upgrade_of(&older_build));
1263 assert!(!older_build.is_upgrade_of(&newer_build));
1265 }
1266
1267 #[test]
1268 fn test_is_upgrade_of_build_version_same() {
1269 let installation = create_test_installation(
1270 ImplementationName::CPython,
1271 3,
1272 10,
1273 8,
1274 None,
1275 PythonVariant::Default,
1276 Some("20240101"),
1277 );
1278
1279 assert!(!installation.is_upgrade_of(&installation));
1281 }
1282
1283 #[test]
1284 fn test_is_upgrade_of_build_with_legacy_installation() {
1285 let legacy = create_test_installation(
1286 ImplementationName::CPython,
1287 3,
1288 10,
1289 8,
1290 None,
1291 PythonVariant::Default,
1292 None,
1293 );
1294 let with_build = create_test_installation(
1295 ImplementationName::CPython,
1296 3,
1297 10,
1298 8,
1299 None,
1300 PythonVariant::Default,
1301 Some("20240101"),
1302 );
1303
1304 assert!(with_build.is_upgrade_of(&legacy));
1306 assert!(!legacy.is_upgrade_of(&with_build));
1308 }
1309
1310 #[test]
1311 fn test_is_upgrade_of_patch_takes_precedence_over_build() {
1312 let older_patch_newer_build = create_test_installation(
1313 ImplementationName::CPython,
1314 3,
1315 10,
1316 8,
1317 None,
1318 PythonVariant::Default,
1319 Some("20240201"),
1320 );
1321 let newer_patch_older_build = create_test_installation(
1322 ImplementationName::CPython,
1323 3,
1324 10,
1325 9,
1326 None,
1327 PythonVariant::Default,
1328 Some("20240101"),
1329 );
1330
1331 assert!(newer_patch_older_build.is_upgrade_of(&older_patch_newer_build));
1333 assert!(!older_patch_newer_build.is_upgrade_of(&newer_patch_older_build));
1335 }
1336
1337 #[test]
1338 fn test_find_version_matching() {
1339 use crate::PythonVersion;
1340
1341 let platform = Platform::from_env().unwrap();
1342 let temp_dir = tempfile::tempdir().unwrap();
1343
1344 fs::create_dir(temp_dir.path().join(format!("cpython-3.10.0-{platform}"))).unwrap();
1346
1347 temp_env::with_var(
1348 uv_static::EnvVars::UV_PYTHON_INSTALL_DIR,
1349 Some(temp_dir.path()),
1350 || {
1351 let installations = ManagedPythonInstallations::from_settings(None).unwrap();
1352
1353 let v3_1 = PythonVersion::from_str("3.1").unwrap();
1355 let matched: Vec<_> = installations.find_version(&v3_1).unwrap().collect();
1356 assert_eq!(matched.len(), 0);
1357
1358 let v3_10 = PythonVersion::from_str("3.10").unwrap();
1360 let matched: Vec<_> = installations.find_version(&v3_10).unwrap().collect();
1361 assert_eq!(matched.len(), 1);
1362 },
1363 );
1364 }
1365
1366 #[test]
1367 fn test_relative_install_dir_resolves_against_pwd() {
1368 let temp_dir = tempfile::tempdir().unwrap();
1369 let workdir = temp_dir.path().join("workdir");
1370 fs::create_dir(&workdir).unwrap();
1371
1372 temp_env::with_vars(
1373 [
1374 (
1375 uv_static::EnvVars::UV_PYTHON_INSTALL_DIR,
1376 Some(std::ffi::OsStr::new(".python-installs")),
1377 ),
1378 (uv_static::EnvVars::PWD, Some(workdir.as_os_str())),
1379 ],
1380 || {
1381 let installations = ManagedPythonInstallations::from_settings(None).unwrap();
1382 assert_eq!(
1383 installations.absolute_root().unwrap(),
1384 workdir.join(".python-installs")
1385 );
1386 },
1387 );
1388 }
1389}