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