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 implementation: ImplementationName,
306 url: Option<Cow<'static, str>>,
310 sha256: Option<Digest<32>>,
314 build: Option<Cow<'static, str>>,
318}
319
320impl ManagedPythonInstallation {
321 pub fn new(path: PathBuf, download: &ManagedPythonDownload) -> Result<Self, Error> {
322 let implementation = ImplementationName::try_from(&download.key().implementation)?;
323 Ok(Self {
324 path,
325 key: download.key().clone(),
326 implementation,
327 url: Some(download.url().clone()),
328 sha256: download.sha256().cloned(),
329 build: download.build().map(Cow::Borrowed),
330 })
331 }
332
333 fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
334 let path = path.as_ref();
335
336 let key = PythonInstallationKey::from_str(
337 path.file_name()
338 .ok_or(Error::NameError("name is empty".to_string()))?
339 .to_str()
340 .ok_or(Error::NameError("not a valid string".to_string()))?,
341 )?;
342
343 let implementation = ImplementationName::try_from(&key.implementation)?;
344
345 let path = std::path::absolute(path)
346 .map_err(|err| Error::AbsolutePath(path.to_path_buf(), err))?;
347
348 let build = match fs::read_to_string(path.join("BUILD")) {
350 Ok(content) => Some(Cow::Owned(content.trim().to_string())),
351 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
352 Err(err) => return Err(err.into()),
353 };
354
355 Ok(Self {
356 path,
357 key,
358 implementation,
359 url: None,
360 sha256: None,
361 build,
362 })
363 }
364
365 pub fn try_from_interpreter(interpreter: &Interpreter) -> Option<Self> {
369 let managed_root = ManagedPythonInstallations::from_settings(None).ok()?;
370 let root = managed_root.absolute_root().ok()?;
371
372 let sys_base_prefix = dunce::canonicalize(interpreter.sys_base_prefix())
376 .unwrap_or_else(|_| interpreter.sys_base_prefix().to_path_buf());
377 let root = dunce::canonicalize(&root).unwrap_or(root);
378
379 let suffix = sys_base_prefix.strip_prefix(&root).ok()?;
381
382 let first_component = suffix.components().next()?;
383 let name = first_component.as_os_str().to_str()?;
384
385 PythonInstallationKey::from_str(name).ok()?;
387
388 let path = root.join(name);
390 Self::from_path(path).ok()
391 }
392
393 pub fn executable(&self, windowed: bool) -> PathBuf {
402 let version = match self.implementation() {
403 ImplementationName::CPython => {
404 if cfg!(unix) {
405 format!("{}.{}", self.key.major, self.key.minor)
406 } else {
407 String::new()
408 }
409 }
410 ImplementationName::PyPy => format!("{}.{}", self.key.major, self.key.minor),
412 ImplementationName::Pyodide => String::new(),
414 ImplementationName::GraalPy => String::new(),
415 };
416
417 let variant = if self.implementation() == ImplementationName::GraalPy {
420 ""
421 } else if cfg!(unix) {
422 self.key.variant.executable_suffix()
423 } else if cfg!(windows) && windowed {
424 "w"
426 } else {
427 ""
428 };
429
430 let name = format!(
431 "{implementation}{version}{variant}{exe}",
432 implementation = self.implementation().executable_name(),
433 exe = std::env::consts::EXE_SUFFIX
434 );
435
436 let executable = executable_path_from_base(
437 self.python_dir().as_path(),
438 &name,
439 &LenientImplementationName::from(self.implementation()),
440 *self.key.os(),
441 );
442
443 if cfg!(windows)
448 && matches!(self.key.variant, PythonVariant::Freethreaded)
449 && !executable.exists()
450 {
451 return self.python_dir().join(format!(
453 "python{}.{}t{}",
454 self.key.major,
455 self.key.minor,
456 std::env::consts::EXE_SUFFIX
457 ));
458 }
459
460 executable
461 }
462
463 fn python_dir(&self) -> PathBuf {
464 let install = self.path.join("install");
465 if install.is_dir() {
466 install
467 } else {
468 self.path.clone()
469 }
470 }
471
472 pub(crate) fn version(&self) -> PythonVersion {
474 self.key.version()
475 }
476
477 pub(crate) fn key_implementation(&self) -> ImplementationName {
479 self.implementation
480 }
481
482 pub fn implementation(&self) -> ImplementationName {
483 if self.key.os().is_emscripten() {
484 ImplementationName::Pyodide
485 } else {
486 self.implementation
487 }
488 }
489
490 pub fn path(&self) -> &Path {
491 &self.path
492 }
493
494 pub fn key(&self) -> &PythonInstallationKey {
495 &self.key
496 }
497
498 pub(crate) fn platform(&self) -> &Platform {
499 self.key.platform()
500 }
501
502 pub fn build(&self) -> Option<&str> {
504 self.build.as_deref()
505 }
506
507 pub fn minor_version_key(&self) -> &PythonInstallationMinorVersionKey {
508 PythonInstallationMinorVersionKey::ref_cast(&self.key)
509 }
510
511 pub fn ensure_canonical_executables(&self) -> Result<(), Error> {
513 let python = self.executable(false);
514
515 let canonical_names = &["python"];
516
517 for name in canonical_names {
518 let executable =
519 python.with_file_name(format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX));
520
521 if executable == python {
524 continue;
525 }
526
527 match symlink_or_copy_file(&python, &executable) {
528 Ok(()) => {
529 debug!(
530 "Created link `{}` -> `{}`",
531 executable.user_display(),
532 python.user_display(),
533 );
534 }
535 Err(err) if err.kind() == io::ErrorKind::NotFound => {
536 return Err(Error::MissingExecutable(python.clone()));
537 }
538 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
539 Err(err) => {
540 return Err(Error::CanonicalizeExecutable(err));
541 }
542 }
543 }
544
545 Ok(())
546 }
547
548 pub fn ensure_minor_version_link(&self) -> Result<(), Error> {
551 if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self) {
552 minor_version_link.create_directory()?;
553 }
554 Ok(())
555 }
556
557 pub fn ensure_externally_managed(&self) -> Result<(), Error> {
560 if self.key.os().is_emscripten() {
561 return Ok(());
564 }
565 let stdlib = if self.key.os().is_windows() {
567 self.python_dir().join("Lib")
568 } else {
569 let lib_suffix = self.key.variant.lib_suffix();
570 let python = if matches!(
571 self.key.implementation,
572 LenientImplementationName::Known(ImplementationName::PyPy)
573 ) {
574 format!("pypy{}", self.key.version().python_version())
575 } else {
576 format!("python{}{lib_suffix}", self.key.version().python_version())
577 };
578 self.python_dir().join("lib").join(python)
579 };
580
581 let file = stdlib.join("EXTERNALLY-MANAGED");
582 fs_err::write(file, EXTERNALLY_MANAGED)?;
583
584 Ok(())
585 }
586
587 pub fn ensure_sysconfig_patched(&self) -> Result<(), Error> {
589 if cfg!(unix) && !self.key.os().is_windows() {
590 if self.key.os().is_emscripten() {
591 return Ok(());
594 }
595 if self.implementation() == ImplementationName::CPython {
596 sysconfig::update_sysconfig(
597 self.path(),
598 self.key.major,
599 self.key.minor,
600 self.key.variant.lib_suffix(),
601 )?;
602 }
603 }
604 Ok(())
605 }
606
607 pub fn ensure_dylib_patched(&self) -> Result<(), macos_dylib::Error> {
614 if cfg!(target_os = "macos") {
615 if self.key().os().is_like_darwin() {
616 if self.implementation() == ImplementationName::CPython {
617 let dylib_path = self.python_dir().join("lib").join(format!(
618 "{}python{}{}{}",
619 std::env::consts::DLL_PREFIX,
620 self.key.version().python_version(),
621 self.key.variant().executable_suffix(),
622 std::env::consts::DLL_SUFFIX
623 ));
624 macos_dylib::patch_dylib_install_name(dylib_path)?;
625 }
626 }
627 }
628 Ok(())
629 }
630
631 pub fn ensure_build_file(&self) -> Result<(), Error> {
633 if let Some(ref build) = self.build {
634 let build_file = self.path.join("BUILD");
635 fs::write(&build_file, build.as_ref())?;
636 }
637 Ok(())
638 }
639
640 pub fn is_bin_link(&self, path: &Path) -> bool {
643 if cfg!(unix) {
644 same_file::is_same_file(path, self.executable(false)).unwrap_or_default()
645 } else if cfg!(windows) {
646 let Some(launcher) = Launcher::try_from_path(path).unwrap_or_default() else {
647 return false;
648 };
649 if !matches!(launcher.kind, LauncherKind::Python) {
650 return false;
651 }
652 dunce::canonicalize(&launcher.python_path).unwrap_or(launcher.python_path)
656 == self.executable(false)
657 } else {
658 unreachable!("Only Windows and Unix are supported")
659 }
660 }
661
662 pub fn is_upgrade_of(&self, other: &Self) -> bool {
664 if self.key.implementation != other.key.implementation {
666 return false;
667 }
668 if self.key.variant != other.key.variant {
670 return false;
671 }
672 if (self.key.major, self.key.minor) != (other.key.major, other.key.minor) {
674 return false;
675 }
676 if self.key.patch == other.key.patch {
679 return match (self.key.prerelease, other.key.prerelease) {
680 (Some(self_pre), Some(other_pre)) => self_pre > other_pre,
682 (None, Some(_)) => true,
684 (Some(_), None) => false,
686 (None, None) => match (self.build.as_deref(), other.build.as_deref()) {
688 (Some(_), None) => true,
690 (Some(self_build), Some(other_build)) => {
692 compare_build_versions(self_build, other_build) == Ordering::Greater
693 }
694 (None, _) => false,
696 },
697 };
698 }
699 if self.key.patch < other.key.patch {
701 return false;
702 }
703 true
704 }
705
706 #[cfg(windows)]
707 pub(crate) fn url(&self) -> Option<&str> {
708 self.url.as_deref()
709 }
710
711 #[cfg(windows)]
712 pub(crate) fn sha256(&self) -> Option<&str> {
713 self.sha256.as_ref().map(Digest::as_str)
714 }
715}
716
717#[derive(Clone, Debug)]
720pub struct PythonMinorVersionLink {
721 pub symlink_directory: PathBuf,
723 pub symlink_executable: PathBuf,
726 pub target_directory: PathBuf,
729}
730
731impl PythonMinorVersionLink {
732 fn from_executable(executable: &Path, key: &PythonInstallationKey) -> Option<Self> {
752 let implementation = key.implementation();
753 if !matches!(
754 implementation.as_ref(),
755 LenientImplementationName::Known(ImplementationName::CPython)
756 ) {
757 return None;
759 }
760 let executable_name = executable
761 .file_name()
762 .expect("Executable file name should exist");
763 let symlink_directory_name = PythonInstallationMinorVersionKey::ref_cast(key).to_string();
764 let parent = executable
765 .parent()
766 .expect("Executable should have parent directory");
767
768 let target_directory = if cfg!(unix) {
770 if parent
771 .components()
772 .next_back()
773 .is_some_and(|c| c.as_os_str() == "bin")
774 {
775 parent.parent()?.to_path_buf()
776 } else {
777 return None;
778 }
779 } else if cfg!(windows) {
780 parent.to_path_buf()
781 } else {
782 unimplemented!("Only Windows and Unix systems are supported.")
783 };
784 let symlink_directory = target_directory.with_file_name(symlink_directory_name);
785 if target_directory == symlink_directory {
787 return None;
788 }
789 let symlink_executable = executable_path_from_base(
791 symlink_directory.as_path(),
792 &executable_name.to_string_lossy(),
793 &implementation,
794 *key.os(),
795 );
796 let minor_version_link = Self {
797 symlink_directory,
798 symlink_executable,
799 target_directory,
800 };
801 Some(minor_version_link)
802 }
803
804 pub fn from_installation(installation: &ManagedPythonInstallation) -> Option<Self> {
805 Self::from_executable(installation.executable(false).as_path(), installation.key())
806 }
807
808 fn create_directory(&self) -> Result<(), Error> {
809 match replace_symlink(
810 self.target_directory.as_path(),
811 self.symlink_directory.as_path(),
812 ) {
813 Ok(()) => {
814 debug!(
815 "Created link `{}` -> `{}`",
816 &self.symlink_directory.user_display(),
817 &self.target_directory.user_display(),
818 );
819 }
820 Err(err) if err.kind() == io::ErrorKind::NotFound => {
821 return Err(Error::MissingPythonMinorVersionLinkTargetDirectory(
822 self.target_directory.clone(),
823 ));
824 }
825 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
826 Err(err) => {
827 return Err(Error::PythonMinorVersionLinkDirectory(err));
828 }
829 }
830 Ok(())
831 }
832
833 pub fn exists(&self) -> bool {
840 let points_to_target = || {
841 fs_err::read_link(&self.symlink_directory)
842 .is_ok_and(|target| verbatim_path(&target) == verbatim_path(&self.target_directory))
843 };
844
845 cfg_select! {
846 unix => {
847 self.symlink_directory
848 .symlink_metadata()
849 .is_ok_and(|metadata| metadata.file_type().is_symlink())
850 && points_to_target()
851 },
852 windows => {
853 self.symlink_directory
854 .symlink_metadata()
855 .is_ok_and(|metadata| {
856 (metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0) != 0
859 })
860 && points_to_target()
861 },
862 }
863 }
864}
865
866fn executable_path_from_base(
870 base: &Path,
871 executable_name: &str,
872 implementation: &LenientImplementationName,
873 os: Os,
874) -> PathBuf {
875 if matches!(
876 implementation,
877 &LenientImplementationName::Known(ImplementationName::GraalPy)
878 ) {
879 base.join("bin").join(executable_name)
881 } else if os.is_emscripten()
882 || matches!(
883 implementation,
884 &LenientImplementationName::Known(ImplementationName::Pyodide)
885 )
886 {
887 base.join(executable_name)
889 } else if os.is_windows() {
890 base.join(executable_name)
892 } else {
893 base.join("bin").join(executable_name)
895 }
896}
897
898#[derive(Debug, Clone, Copy)]
902pub struct PythonExecutable<'a> {
903 path: &'a Path,
904 window_mode: WindowMode,
905}
906
907impl<'a> PythonExecutable<'a> {
908 pub fn console(path: &'a Path) -> Self {
910 Self {
911 path,
912 window_mode: WindowMode::Console,
913 }
914 }
915
916 pub fn windowed(path: &'a Path) -> Self {
918 Self {
919 path,
920 window_mode: WindowMode::Windowed,
921 }
922 }
923}
924
925pub fn create_link_to_executable(
929 link: &Path,
930 executable: PythonExecutable<'_>,
931) -> Result<(), Error> {
932 let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?;
933 fs_err::create_dir_all(link_parent).map_err(Error::ExecutableDirectory)?;
934
935 if cfg!(unix) {
936 match symlink_or_copy_file(executable.path, link) {
938 Ok(()) => Ok(()),
939 Err(err) if err.kind() == io::ErrorKind::NotFound => {
940 Err(Error::MissingExecutable(executable.path.to_path_buf()))
941 }
942 Err(err) => Err(Error::LinkExecutable(err)),
943 }
944 } else if cfg!(windows) {
945 let launcher = windows_python_launcher(executable.path, executable.window_mode)?;
946
947 #[expect(clippy::disallowed_types)]
950 {
951 std::fs::File::create_new(link)
952 .and_then(|mut file| file.write_all(launcher.as_ref()))
953 .map_err(Error::LinkExecutable)
954 }
955 } else {
956 unimplemented!("Only Windows and Unix are supported.")
957 }
958}
959
960pub fn replace_link_to_executable(
966 link: &Path,
967 executable: PythonExecutable<'_>,
968) -> Result<(), Error> {
969 let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?;
970 fs_err::create_dir_all(link_parent).map_err(Error::ExecutableDirectory)?;
971
972 if cfg!(unix) {
973 replace_symlink(executable.path, link).map_err(Error::LinkExecutable)
974 } else if cfg!(windows) {
975 let launcher = windows_python_launcher(executable.path, executable.window_mode)?;
976
977 uv_fs::write_atomic_sync(link, &*launcher).map_err(Error::LinkExecutable)
978 } else {
979 unimplemented!("Only Windows and Unix are supported.")
980 }
981}
982
983pub fn platform_key_from_env() -> Result<String, Error> {
986 Ok(Platform::from_env()?.to_string().to_lowercase())
987}
988
989impl fmt::Display for ManagedPythonInstallation {
990 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
991 write!(
992 f,
993 "{}",
994 self.path
995 .file_name()
996 .unwrap_or(self.path.as_os_str())
997 .to_string_lossy()
998 )
999 }
1000}
1001
1002pub fn python_executable_dir() -> Result<PathBuf, Error> {
1004 uv_dirs::user_executable_directory(Some(EnvVars::UV_PYTHON_BIN_DIR))
1005 .ok_or(Error::NoExecutableDirectory)
1006}
1007
1008#[cfg(test)]
1009mod tests {
1010 use super::*;
1011 use crate::implementation::LenientImplementationName;
1012 use crate::installation::PythonInstallationKey;
1013 use crate::{ImplementationName, PythonVariant};
1014 use std::path::PathBuf;
1015 use std::str::FromStr;
1016 use uv_pep440::{Prerelease, PrereleaseKind};
1017 use uv_platform::Platform;
1018
1019 fn create_test_installation(
1020 implementation: ImplementationName,
1021 major: u8,
1022 minor: u8,
1023 patch: u8,
1024 prerelease: Option<Prerelease>,
1025 variant: PythonVariant,
1026 build: Option<&str>,
1027 ) -> ManagedPythonInstallation {
1028 let platform = Platform::from_str("linux-x86_64-gnu").unwrap();
1029 let key = PythonInstallationKey::new(
1030 LenientImplementationName::Known(implementation),
1031 major,
1032 minor,
1033 patch,
1034 prerelease,
1035 platform,
1036 variant,
1037 );
1038 ManagedPythonInstallation {
1039 path: PathBuf::from("/test/path"),
1040 key,
1041 implementation,
1042 url: None,
1043 sha256: None,
1044 build: build.map(|s| Cow::Owned(s.to_owned())),
1045 }
1046 }
1047
1048 #[test]
1049 fn test_is_upgrade_of_same_version() {
1050 let installation = create_test_installation(
1051 ImplementationName::CPython,
1052 3,
1053 10,
1054 8,
1055 None,
1056 PythonVariant::Default,
1057 None,
1058 );
1059
1060 assert!(!installation.is_upgrade_of(&installation));
1062 }
1063
1064 #[test]
1065 fn test_is_upgrade_of_patch_version() {
1066 let older = create_test_installation(
1067 ImplementationName::CPython,
1068 3,
1069 10,
1070 8,
1071 None,
1072 PythonVariant::Default,
1073 None,
1074 );
1075 let newer = create_test_installation(
1076 ImplementationName::CPython,
1077 3,
1078 10,
1079 9,
1080 None,
1081 PythonVariant::Default,
1082 None,
1083 );
1084
1085 assert!(newer.is_upgrade_of(&older));
1087 assert!(!older.is_upgrade_of(&newer));
1089 }
1090
1091 #[test]
1092 fn test_is_upgrade_of_different_minor_version() {
1093 let py310 = create_test_installation(
1094 ImplementationName::CPython,
1095 3,
1096 10,
1097 8,
1098 None,
1099 PythonVariant::Default,
1100 None,
1101 );
1102 let py311 = create_test_installation(
1103 ImplementationName::CPython,
1104 3,
1105 11,
1106 0,
1107 None,
1108 PythonVariant::Default,
1109 None,
1110 );
1111
1112 assert!(!py311.is_upgrade_of(&py310));
1114 assert!(!py310.is_upgrade_of(&py311));
1115 }
1116
1117 #[test]
1118 fn test_is_upgrade_of_different_implementation() {
1119 let cpython = create_test_installation(
1120 ImplementationName::CPython,
1121 3,
1122 10,
1123 8,
1124 None,
1125 PythonVariant::Default,
1126 None,
1127 );
1128 let pypy = create_test_installation(
1129 ImplementationName::PyPy,
1130 3,
1131 10,
1132 9,
1133 None,
1134 PythonVariant::Default,
1135 None,
1136 );
1137
1138 assert!(!pypy.is_upgrade_of(&cpython));
1140 assert!(!cpython.is_upgrade_of(&pypy));
1141 }
1142
1143 #[test]
1144 fn test_is_upgrade_of_different_variant() {
1145 let default = create_test_installation(
1146 ImplementationName::CPython,
1147 3,
1148 10,
1149 8,
1150 None,
1151 PythonVariant::Default,
1152 None,
1153 );
1154 let freethreaded = create_test_installation(
1155 ImplementationName::CPython,
1156 3,
1157 10,
1158 9,
1159 None,
1160 PythonVariant::Freethreaded,
1161 None,
1162 );
1163
1164 assert!(!freethreaded.is_upgrade_of(&default));
1166 assert!(!default.is_upgrade_of(&freethreaded));
1167 }
1168
1169 #[test]
1170 fn test_is_upgrade_of_prerelease() {
1171 let stable = create_test_installation(
1172 ImplementationName::CPython,
1173 3,
1174 10,
1175 8,
1176 None,
1177 PythonVariant::Default,
1178 None,
1179 );
1180 let prerelease = create_test_installation(
1181 ImplementationName::CPython,
1182 3,
1183 10,
1184 8,
1185 Some(Prerelease {
1186 kind: PrereleaseKind::Alpha,
1187 number: 1,
1188 }),
1189 PythonVariant::Default,
1190 None,
1191 );
1192
1193 assert!(stable.is_upgrade_of(&prerelease));
1195
1196 assert!(!prerelease.is_upgrade_of(&stable));
1198 }
1199
1200 #[test]
1201 fn test_is_upgrade_of_prerelease_to_prerelease() {
1202 let alpha1 = create_test_installation(
1203 ImplementationName::CPython,
1204 3,
1205 10,
1206 8,
1207 Some(Prerelease {
1208 kind: PrereleaseKind::Alpha,
1209 number: 1,
1210 }),
1211 PythonVariant::Default,
1212 None,
1213 );
1214 let alpha2 = create_test_installation(
1215 ImplementationName::CPython,
1216 3,
1217 10,
1218 8,
1219 Some(Prerelease {
1220 kind: PrereleaseKind::Alpha,
1221 number: 2,
1222 }),
1223 PythonVariant::Default,
1224 None,
1225 );
1226
1227 assert!(alpha2.is_upgrade_of(&alpha1));
1229 assert!(!alpha1.is_upgrade_of(&alpha2));
1231 }
1232
1233 #[test]
1234 fn test_is_upgrade_of_prerelease_same_patch() {
1235 let prerelease = create_test_installation(
1236 ImplementationName::CPython,
1237 3,
1238 10,
1239 8,
1240 Some(Prerelease {
1241 kind: PrereleaseKind::Alpha,
1242 number: 1,
1243 }),
1244 PythonVariant::Default,
1245 None,
1246 );
1247
1248 assert!(!prerelease.is_upgrade_of(&prerelease));
1250 }
1251
1252 #[test]
1253 fn test_is_upgrade_of_build_version() {
1254 let older_build = create_test_installation(
1255 ImplementationName::CPython,
1256 3,
1257 10,
1258 8,
1259 None,
1260 PythonVariant::Default,
1261 Some("20240101"),
1262 );
1263 let newer_build = create_test_installation(
1264 ImplementationName::CPython,
1265 3,
1266 10,
1267 8,
1268 None,
1269 PythonVariant::Default,
1270 Some("20240201"),
1271 );
1272
1273 assert!(newer_build.is_upgrade_of(&older_build));
1275 assert!(!older_build.is_upgrade_of(&newer_build));
1277 }
1278
1279 #[test]
1280 fn test_is_upgrade_of_build_version_same() {
1281 let installation = create_test_installation(
1282 ImplementationName::CPython,
1283 3,
1284 10,
1285 8,
1286 None,
1287 PythonVariant::Default,
1288 Some("20240101"),
1289 );
1290
1291 assert!(!installation.is_upgrade_of(&installation));
1293 }
1294
1295 #[test]
1296 fn test_is_upgrade_of_build_with_legacy_installation() {
1297 let legacy = create_test_installation(
1298 ImplementationName::CPython,
1299 3,
1300 10,
1301 8,
1302 None,
1303 PythonVariant::Default,
1304 None,
1305 );
1306 let with_build = create_test_installation(
1307 ImplementationName::CPython,
1308 3,
1309 10,
1310 8,
1311 None,
1312 PythonVariant::Default,
1313 Some("20240101"),
1314 );
1315
1316 assert!(with_build.is_upgrade_of(&legacy));
1318 assert!(!legacy.is_upgrade_of(&with_build));
1320 }
1321
1322 #[test]
1323 fn test_is_upgrade_of_patch_takes_precedence_over_build() {
1324 let older_patch_newer_build = create_test_installation(
1325 ImplementationName::CPython,
1326 3,
1327 10,
1328 8,
1329 None,
1330 PythonVariant::Default,
1331 Some("20240201"),
1332 );
1333 let newer_patch_older_build = create_test_installation(
1334 ImplementationName::CPython,
1335 3,
1336 10,
1337 9,
1338 None,
1339 PythonVariant::Default,
1340 Some("20240101"),
1341 );
1342
1343 assert!(newer_patch_older_build.is_upgrade_of(&older_patch_newer_build));
1345 assert!(!older_patch_newer_build.is_upgrade_of(&newer_patch_older_build));
1347 }
1348
1349 #[test]
1350 fn test_find_version_matching() {
1351 use crate::PythonVersion;
1352
1353 let platform = Platform::from_env().unwrap();
1354 let temp_dir = tempfile::tempdir().unwrap();
1355
1356 fs::create_dir(temp_dir.path().join(format!("cpython-3.10.0-{platform}"))).unwrap();
1358
1359 temp_env::with_var(
1360 uv_static::EnvVars::UV_PYTHON_INSTALL_DIR,
1361 Some(temp_dir.path()),
1362 || {
1363 let installations = ManagedPythonInstallations::from_settings(None).unwrap();
1364
1365 let v3_1 = PythonVersion::from_str("3.1").unwrap();
1367 let matched: Vec<_> = installations.find_version(&v3_1).unwrap().collect();
1368 assert_eq!(matched.len(), 0);
1369
1370 let v3_10 = PythonVersion::from_str("3.10").unwrap();
1372 let matched: Vec<_> = installations.find_version(&v3_10).unwrap().collect();
1373 assert_eq!(matched.len(), 1);
1374 },
1375 );
1376 }
1377
1378 #[test]
1379 fn test_relative_install_dir_resolves_against_pwd() {
1380 let temp_dir = tempfile::tempdir().unwrap();
1381 let workdir = temp_dir.path().join("workdir");
1382 fs::create_dir(&workdir).unwrap();
1383
1384 temp_env::with_vars(
1385 [
1386 (
1387 uv_static::EnvVars::UV_PYTHON_INSTALL_DIR,
1388 Some(std::ffi::OsStr::new(".python-installs")),
1389 ),
1390 (uv_static::EnvVars::PWD, Some(workdir.as_os_str())),
1391 ],
1392 || {
1393 let installations = ManagedPythonInstallations::from_settings(None).unwrap();
1394 assert_eq!(
1395 installations.absolute_root().unwrap(),
1396 workdir.join(".python-installs")
1397 );
1398 },
1399 );
1400 }
1401}