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, WindowMode, windows_python_launcher};
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 #[cfg(all(test, unix))]
143 pub(crate) fn temp() -> Result<Self, Error> {
144 Ok(Self::from_path(
145 StateStore::temp()?.bucket(StateBucket::ManagedPython),
146 ))
147 }
148
149 pub fn scratch(&self) -> PathBuf {
151 self.root.join(".temp")
152 }
153
154 pub fn init(self) -> Result<Self, Error> {
158 let root = &self.root;
159
160 if !root.exists()
162 && root
163 .parent()
164 .is_some_and(|parent| parent.join("toolchains").exists())
165 {
166 let deprecated = root.parent().unwrap().join("toolchains");
167 fs::rename(&deprecated, root)?;
169 uv_fs::replace_symlink(root, &deprecated)?;
171 } else {
172 fs::create_dir_all(root)?;
173 }
174
175 fs::create_dir_all(root)?;
177
178 let scratch = self.scratch();
180 fs::create_dir_all(&scratch)?;
181
182 match fs::OpenOptions::new()
184 .write(true)
185 .create_new(true)
186 .open(root.join(".gitignore"))
187 {
188 Ok(mut file) => file.write_all(b"*")?,
189 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
190 Err(err) => return Err(err.into()),
191 }
192
193 Ok(self)
194 }
195
196 pub fn find_all(
201 &self,
202 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + use<>, Error> {
203 let dirs = match fs_err::read_dir(&self.root) {
204 Ok(installation_dirs) => {
205 let directories: Vec<_> = installation_dirs
207 .filter_map(|read_dir| match read_dir {
208 Ok(entry) => match entry.file_type() {
209 Ok(file_type) => file_type.is_dir().then_some(Ok(entry.path())),
210 Err(err) => Some(Err(err)),
211 },
212 Err(err) => Some(Err(err)),
213 })
214 .collect::<Result<_, io::Error>>()
215 .map_err(Error::ReadError)?;
216 directories
217 }
218 Err(err) if err.kind() == io::ErrorKind::NotFound => vec![],
219 Err(err) => {
220 return Err(Error::ReadError(err));
221 }
222 };
223 let scratch = self.scratch();
224 Ok(dirs
225 .into_iter()
226 .filter(|path| *path != scratch)
228 .filter(|path| {
230 path.file_name()
231 .and_then(OsStr::to_str)
232 .is_none_or(|name| !name.starts_with('.'))
233 })
234 .filter_map(|path| {
235 ManagedPythonInstallation::from_path(path)
236 .inspect_err(|err| {
237 warn!("Ignoring malformed managed Python entry:\n {err}");
238 })
239 .ok()
240 })
241 .sorted_unstable_by_key(|installation| Reverse(installation.key().clone())))
242 }
243
244 pub(crate) fn find_matching_current_platform()
246 -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + use<>, Error> {
247 let platform = Platform::from_env()?;
248
249 let iter = Self::from_settings(None)?
250 .find_all()?
251 .filter(move |installation| {
252 if !platform.supports(installation.platform()) {
253 debug!("Skipping managed installation `{installation}`: not supported by current platform `{platform}`");
254 return false;
255 }
256 true
257 });
258
259 Ok(iter)
260 }
261
262 pub fn find_version<'a>(
269 &'a self,
270 version: &'a PythonVersion,
271 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + 'a, Error> {
272 let request = VersionRequest::from(version);
273 Ok(Self::find_matching_current_platform()?
274 .filter(move |installation| request.matches_installation_key(installation.key())))
275 }
276
277 pub fn root(&self) -> &Path {
278 &self.root
279 }
280
281 pub(crate) fn absolute_root(&self) -> Result<PathBuf, Error> {
282 let root = if self.root.is_absolute() {
283 self.root.clone()
284 } else {
285 crate::current_dir()?.join(&self.root)
286 };
287
288 normalize_absolute_path(&root).map_err(|err| Error::AbsolutePath(self.root.clone(), err))
289 }
290}
291
292static EXTERNALLY_MANAGED: &str = "[externally-managed]
293Error=This Python installation is managed by uv and should not be modified.
294";
295
296#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
298pub struct ManagedPythonInstallation {
299 path: PathBuf,
301 key: PythonInstallationKey,
303 url: Option<Cow<'static, str>>,
307 sha256: Option<Cow<'static, str>>,
311 build: Option<Cow<'static, str>>,
315}
316
317impl ManagedPythonInstallation {
318 pub fn new(path: PathBuf, download: &ManagedPythonDownload) -> Self {
319 Self {
320 path,
321 key: download.key().clone(),
322 url: Some(download.url().clone()),
323 sha256: download.sha256().cloned(),
324 build: download.build().map(Cow::Borrowed),
325 }
326 }
327
328 fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
329 let path = path.as_ref();
330
331 let key = PythonInstallationKey::from_str(
332 path.file_name()
333 .ok_or(Error::NameError("name is empty".to_string()))?
334 .to_str()
335 .ok_or(Error::NameError("not a valid string".to_string()))?,
336 )?;
337
338 let path = std::path::absolute(path)
339 .map_err(|err| Error::AbsolutePath(path.to_path_buf(), err))?;
340
341 let build = match fs::read_to_string(path.join("BUILD")) {
343 Ok(content) => Some(Cow::Owned(content.trim().to_string())),
344 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
345 Err(err) => return Err(err.into()),
346 };
347
348 Ok(Self {
349 path,
350 key,
351 url: None,
352 sha256: None,
353 build,
354 })
355 }
356
357 pub fn try_from_interpreter(interpreter: &Interpreter) -> Option<Self> {
361 let managed_root = ManagedPythonInstallations::from_settings(None).ok()?;
362 let root = managed_root.absolute_root().ok()?;
363
364 let sys_base_prefix = dunce::canonicalize(interpreter.sys_base_prefix())
368 .unwrap_or_else(|_| interpreter.sys_base_prefix().to_path_buf());
369 let root = dunce::canonicalize(&root).unwrap_or(root);
370
371 let suffix = sys_base_prefix.strip_prefix(&root).ok()?;
373
374 let first_component = suffix.components().next()?;
375 let name = first_component.as_os_str().to_str()?;
376
377 PythonInstallationKey::from_str(name).ok()?;
379
380 let path = root.join(name);
382 Self::from_path(path).ok()
383 }
384
385 pub fn executable(&self, windowed: bool) -> PathBuf {
394 let version = match self.implementation() {
395 ImplementationName::CPython => {
396 if cfg!(unix) {
397 format!("{}.{}", self.key.major, self.key.minor)
398 } else {
399 String::new()
400 }
401 }
402 ImplementationName::PyPy => format!("{}.{}", self.key.major, self.key.minor),
404 ImplementationName::Pyodide => String::new(),
406 ImplementationName::GraalPy => String::new(),
407 };
408
409 let variant = if self.implementation() == ImplementationName::GraalPy {
412 ""
413 } else if cfg!(unix) {
414 self.key.variant.executable_suffix()
415 } else if cfg!(windows) && windowed {
416 "w"
418 } else {
419 ""
420 };
421
422 let name = format!(
423 "{implementation}{version}{variant}{exe}",
424 implementation = self.implementation().executable_name(),
425 exe = std::env::consts::EXE_SUFFIX
426 );
427
428 let executable = executable_path_from_base(
429 self.python_dir().as_path(),
430 &name,
431 &LenientImplementationName::from(self.implementation()),
432 *self.key.os(),
433 );
434
435 if cfg!(windows)
440 && matches!(self.key.variant, PythonVariant::Freethreaded)
441 && !executable.exists()
442 {
443 return self.python_dir().join(format!(
445 "python{}.{}t{}",
446 self.key.major,
447 self.key.minor,
448 std::env::consts::EXE_SUFFIX
449 ));
450 }
451
452 executable
453 }
454
455 fn python_dir(&self) -> PathBuf {
456 let install = self.path.join("install");
457 if install.is_dir() {
458 install
459 } else {
460 self.path.clone()
461 }
462 }
463
464 pub(crate) fn version(&self) -> PythonVersion {
466 self.key.version()
467 }
468
469 pub fn implementation(&self) -> ImplementationName {
470 match self.key.implementation().into_owned() {
471 LenientImplementationName::Known(implementation) => implementation,
472 LenientImplementationName::Unknown(_) => {
473 panic!("Managed Python installations should have a known implementation")
474 }
475 }
476 }
477
478 pub fn path(&self) -> &Path {
479 &self.path
480 }
481
482 pub fn key(&self) -> &PythonInstallationKey {
483 &self.key
484 }
485
486 pub(crate) fn platform(&self) -> &Platform {
487 self.key.platform()
488 }
489
490 pub fn build(&self) -> Option<&str> {
492 self.build.as_deref()
493 }
494
495 pub fn minor_version_key(&self) -> &PythonInstallationMinorVersionKey {
496 PythonInstallationMinorVersionKey::ref_cast(&self.key)
497 }
498
499 pub fn ensure_canonical_executables(&self) -> Result<(), Error> {
501 let python = self.executable(false);
502
503 let canonical_names = &["python"];
504
505 for name in canonical_names {
506 let executable =
507 python.with_file_name(format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX));
508
509 if executable == python {
512 continue;
513 }
514
515 match symlink_or_copy_file(&python, &executable) {
516 Ok(()) => {
517 debug!(
518 "Created link {} -> {}",
519 executable.user_display(),
520 python.user_display(),
521 );
522 }
523 Err(err) if err.kind() == io::ErrorKind::NotFound => {
524 return Err(Error::MissingExecutable(python.clone()));
525 }
526 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
527 Err(err) => {
528 return Err(Error::CanonicalizeExecutable(err));
529 }
530 }
531 }
532
533 Ok(())
534 }
535
536 pub fn ensure_minor_version_link(&self) -> Result<(), Error> {
539 if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self) {
540 minor_version_link.create_directory()?;
541 }
542 Ok(())
543 }
544
545 pub fn ensure_externally_managed(&self) -> Result<(), Error> {
548 if self.key.os().is_emscripten() {
549 return Ok(());
552 }
553 let stdlib = if self.key.os().is_windows() {
555 self.python_dir().join("Lib")
556 } else {
557 let lib_suffix = self.key.variant.lib_suffix();
558 let python = if matches!(
559 self.key.implementation,
560 LenientImplementationName::Known(ImplementationName::PyPy)
561 ) {
562 format!("pypy{}", self.key.version().python_version())
563 } else {
564 format!("python{}{lib_suffix}", self.key.version().python_version())
565 };
566 self.python_dir().join("lib").join(python)
567 };
568
569 let file = stdlib.join("EXTERNALLY-MANAGED");
570 fs_err::write(file, EXTERNALLY_MANAGED)?;
571
572 Ok(())
573 }
574
575 pub fn ensure_sysconfig_patched(&self) -> Result<(), Error> {
577 if cfg!(unix) && !self.key.os().is_windows() {
578 if self.key.os().is_emscripten() {
579 return Ok(());
582 }
583 if self.implementation() == ImplementationName::CPython {
584 sysconfig::update_sysconfig(
585 self.path(),
586 self.key.major,
587 self.key.minor,
588 self.key.variant.lib_suffix(),
589 )?;
590 }
591 }
592 Ok(())
593 }
594
595 pub fn ensure_dylib_patched(&self) -> Result<(), macos_dylib::Error> {
602 if cfg!(target_os = "macos") {
603 if self.key().os().is_like_darwin() {
604 if self.implementation() == ImplementationName::CPython {
605 let dylib_path = self.python_dir().join("lib").join(format!(
606 "{}python{}{}{}",
607 std::env::consts::DLL_PREFIX,
608 self.key.version().python_version(),
609 self.key.variant().executable_suffix(),
610 std::env::consts::DLL_SUFFIX
611 ));
612 macos_dylib::patch_dylib_install_name(dylib_path)?;
613 }
614 }
615 }
616 Ok(())
617 }
618
619 pub fn ensure_build_file(&self) -> Result<(), Error> {
621 if let Some(ref build) = self.build {
622 let build_file = self.path.join("BUILD");
623 fs::write(&build_file, build.as_ref())?;
624 }
625 Ok(())
626 }
627
628 pub fn is_bin_link(&self, path: &Path) -> bool {
631 if cfg!(unix) {
632 same_file::is_same_file(path, self.executable(false)).unwrap_or_default()
633 } else if cfg!(windows) {
634 let Some(launcher) = Launcher::try_from_path(path).unwrap_or_default() else {
635 return false;
636 };
637 if !matches!(launcher.kind, LauncherKind::Python) {
638 return false;
639 }
640 dunce::canonicalize(&launcher.python_path).unwrap_or(launcher.python_path)
644 == self.executable(false)
645 } else {
646 unreachable!("Only Windows and Unix are supported")
647 }
648 }
649
650 pub fn is_upgrade_of(&self, other: &Self) -> bool {
652 if self.key.implementation != other.key.implementation {
654 return false;
655 }
656 if self.key.variant != other.key.variant {
658 return false;
659 }
660 if (self.key.major, self.key.minor) != (other.key.major, other.key.minor) {
662 return false;
663 }
664 if self.key.patch == other.key.patch {
667 return match (self.key.prerelease, other.key.prerelease) {
668 (Some(self_pre), Some(other_pre)) => self_pre > other_pre,
670 (None, Some(_)) => true,
672 (Some(_), None) => false,
674 (None, None) => match (self.build.as_deref(), other.build.as_deref()) {
676 (Some(_), None) => true,
678 (Some(self_build), Some(other_build)) => {
680 compare_build_versions(self_build, other_build) == Ordering::Greater
681 }
682 (None, _) => false,
684 },
685 };
686 }
687 if self.key.patch < other.key.patch {
689 return false;
690 }
691 true
692 }
693
694 #[cfg(windows)]
695 pub(crate) fn url(&self) -> Option<&str> {
696 self.url.as_deref()
697 }
698
699 #[cfg(windows)]
700 pub(crate) fn sha256(&self) -> Option<&str> {
701 self.sha256.as_deref()
702 }
703}
704
705#[derive(Clone, Debug)]
708pub struct PythonMinorVersionLink {
709 pub symlink_directory: PathBuf,
711 pub symlink_executable: PathBuf,
714 pub target_directory: PathBuf,
717}
718
719impl PythonMinorVersionLink {
720 fn from_executable(executable: &Path, key: &PythonInstallationKey) -> Option<Self> {
740 let implementation = key.implementation();
741 if !matches!(
742 implementation.as_ref(),
743 LenientImplementationName::Known(ImplementationName::CPython)
744 ) {
745 return None;
747 }
748 let executable_name = executable
749 .file_name()
750 .expect("Executable file name should exist");
751 let symlink_directory_name = PythonInstallationMinorVersionKey::ref_cast(key).to_string();
752 let parent = executable
753 .parent()
754 .expect("Executable should have parent directory");
755
756 let target_directory = if cfg!(unix) {
758 if parent
759 .components()
760 .next_back()
761 .is_some_and(|c| c.as_os_str() == "bin")
762 {
763 parent.parent()?.to_path_buf()
764 } else {
765 return None;
766 }
767 } else if cfg!(windows) {
768 parent.to_path_buf()
769 } else {
770 unimplemented!("Only Windows and Unix systems are supported.")
771 };
772 let symlink_directory = target_directory.with_file_name(symlink_directory_name);
773 if target_directory == symlink_directory {
775 return None;
776 }
777 let symlink_executable = executable_path_from_base(
779 symlink_directory.as_path(),
780 &executable_name.to_string_lossy(),
781 &implementation,
782 *key.os(),
783 );
784 let minor_version_link = Self {
785 symlink_directory,
786 symlink_executable,
787 target_directory,
788 };
789 Some(minor_version_link)
790 }
791
792 pub fn from_installation(installation: &ManagedPythonInstallation) -> Option<Self> {
793 Self::from_executable(installation.executable(false).as_path(), installation.key())
794 }
795
796 fn create_directory(&self) -> Result<(), Error> {
797 match replace_symlink(
798 self.target_directory.as_path(),
799 self.symlink_directory.as_path(),
800 ) {
801 Ok(()) => {
802 debug!(
803 "Created link {} -> {}",
804 &self.symlink_directory.user_display(),
805 &self.target_directory.user_display(),
806 );
807 }
808 Err(err) if err.kind() == io::ErrorKind::NotFound => {
809 return Err(Error::MissingPythonMinorVersionLinkTargetDirectory(
810 self.target_directory.clone(),
811 ));
812 }
813 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
814 Err(err) => {
815 return Err(Error::PythonMinorVersionLinkDirectory(err));
816 }
817 }
818 Ok(())
819 }
820
821 pub fn exists(&self) -> bool {
828 let points_to_target = || {
829 fs_err::read_link(&self.symlink_directory)
830 .is_ok_and(|target| verbatim_path(&target) == verbatim_path(&self.target_directory))
831 };
832
833 cfg_select! {
834 unix => {
835 self.symlink_directory
836 .symlink_metadata()
837 .is_ok_and(|metadata| metadata.file_type().is_symlink())
838 && points_to_target()
839 },
840 windows => {
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}
853
854fn executable_path_from_base(
858 base: &Path,
859 executable_name: &str,
860 implementation: &LenientImplementationName,
861 os: Os,
862) -> PathBuf {
863 if matches!(
864 implementation,
865 &LenientImplementationName::Known(ImplementationName::GraalPy)
866 ) {
867 base.join("bin").join(executable_name)
869 } else if os.is_emscripten()
870 || matches!(
871 implementation,
872 &LenientImplementationName::Known(ImplementationName::Pyodide)
873 )
874 {
875 base.join(executable_name)
877 } else if os.is_windows() {
878 base.join(executable_name)
880 } else {
881 base.join("bin").join(executable_name)
883 }
884}
885
886#[derive(Debug, Clone, Copy)]
890pub struct PythonExecutable<'a> {
891 path: &'a Path,
892 window_mode: WindowMode,
893}
894
895impl<'a> PythonExecutable<'a> {
896 pub fn console(path: &'a Path) -> Self {
898 Self {
899 path,
900 window_mode: WindowMode::Console,
901 }
902 }
903
904 pub fn windowed(path: &'a Path) -> Self {
906 Self {
907 path,
908 window_mode: WindowMode::Windowed,
909 }
910 }
911}
912
913pub fn create_link_to_executable(
917 link: &Path,
918 executable: PythonExecutable<'_>,
919) -> Result<(), Error> {
920 let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?;
921 fs_err::create_dir_all(link_parent).map_err(Error::ExecutableDirectory)?;
922
923 if cfg!(unix) {
924 match symlink_or_copy_file(executable.path, link) {
926 Ok(()) => Ok(()),
927 Err(err) if err.kind() == io::ErrorKind::NotFound => {
928 Err(Error::MissingExecutable(executable.path.to_path_buf()))
929 }
930 Err(err) => Err(Error::LinkExecutable(err)),
931 }
932 } else if cfg!(windows) {
933 let launcher = windows_python_launcher(executable.path, executable.window_mode)?;
934
935 #[expect(clippy::disallowed_types)]
938 {
939 std::fs::File::create_new(link)
940 .and_then(|mut file| file.write_all(launcher.as_ref()))
941 .map_err(Error::LinkExecutable)
942 }
943 } else {
944 unimplemented!("Only Windows and Unix are supported.")
945 }
946}
947
948pub fn replace_link_to_executable(
954 link: &Path,
955 executable: PythonExecutable<'_>,
956) -> Result<(), Error> {
957 let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?;
958 fs_err::create_dir_all(link_parent).map_err(Error::ExecutableDirectory)?;
959
960 if cfg!(unix) {
961 replace_symlink(executable.path, link).map_err(Error::LinkExecutable)
962 } else if cfg!(windows) {
963 let launcher = windows_python_launcher(executable.path, executable.window_mode)?;
964
965 uv_fs::write_atomic_sync(link, &*launcher).map_err(Error::LinkExecutable)
966 } else {
967 unimplemented!("Only Windows and Unix are supported.")
968 }
969}
970
971pub fn platform_key_from_env() -> Result<String, Error> {
974 Ok(Platform::from_env()?.to_string().to_lowercase())
975}
976
977impl fmt::Display for ManagedPythonInstallation {
978 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
979 write!(
980 f,
981 "{}",
982 self.path
983 .file_name()
984 .unwrap_or(self.path.as_os_str())
985 .to_string_lossy()
986 )
987 }
988}
989
990pub fn python_executable_dir() -> Result<PathBuf, Error> {
992 uv_dirs::user_executable_directory(Some(EnvVars::UV_PYTHON_BIN_DIR))
993 .ok_or(Error::NoExecutableDirectory)
994}
995
996#[cfg(test)]
997mod tests {
998 use super::*;
999 use crate::implementation::LenientImplementationName;
1000 use crate::installation::PythonInstallationKey;
1001 use crate::{ImplementationName, PythonVariant};
1002 use std::path::PathBuf;
1003 use std::str::FromStr;
1004 use uv_pep440::{Prerelease, PrereleaseKind};
1005 use uv_platform::Platform;
1006
1007 fn create_test_installation(
1008 implementation: ImplementationName,
1009 major: u8,
1010 minor: u8,
1011 patch: u8,
1012 prerelease: Option<Prerelease>,
1013 variant: PythonVariant,
1014 build: Option<&str>,
1015 ) -> ManagedPythonInstallation {
1016 let platform = Platform::from_str("linux-x86_64-gnu").unwrap();
1017 let key = PythonInstallationKey::new(
1018 LenientImplementationName::Known(implementation),
1019 major,
1020 minor,
1021 patch,
1022 prerelease,
1023 platform,
1024 variant,
1025 );
1026 ManagedPythonInstallation {
1027 path: PathBuf::from("/test/path"),
1028 key,
1029 url: None,
1030 sha256: None,
1031 build: build.map(|s| Cow::Owned(s.to_owned())),
1032 }
1033 }
1034
1035 #[test]
1036 fn test_is_upgrade_of_same_version() {
1037 let installation = create_test_installation(
1038 ImplementationName::CPython,
1039 3,
1040 10,
1041 8,
1042 None,
1043 PythonVariant::Default,
1044 None,
1045 );
1046
1047 assert!(!installation.is_upgrade_of(&installation));
1049 }
1050
1051 #[test]
1052 fn test_is_upgrade_of_patch_version() {
1053 let older = create_test_installation(
1054 ImplementationName::CPython,
1055 3,
1056 10,
1057 8,
1058 None,
1059 PythonVariant::Default,
1060 None,
1061 );
1062 let newer = create_test_installation(
1063 ImplementationName::CPython,
1064 3,
1065 10,
1066 9,
1067 None,
1068 PythonVariant::Default,
1069 None,
1070 );
1071
1072 assert!(newer.is_upgrade_of(&older));
1074 assert!(!older.is_upgrade_of(&newer));
1076 }
1077
1078 #[test]
1079 fn test_is_upgrade_of_different_minor_version() {
1080 let py310 = create_test_installation(
1081 ImplementationName::CPython,
1082 3,
1083 10,
1084 8,
1085 None,
1086 PythonVariant::Default,
1087 None,
1088 );
1089 let py311 = create_test_installation(
1090 ImplementationName::CPython,
1091 3,
1092 11,
1093 0,
1094 None,
1095 PythonVariant::Default,
1096 None,
1097 );
1098
1099 assert!(!py311.is_upgrade_of(&py310));
1101 assert!(!py310.is_upgrade_of(&py311));
1102 }
1103
1104 #[test]
1105 fn test_is_upgrade_of_different_implementation() {
1106 let cpython = create_test_installation(
1107 ImplementationName::CPython,
1108 3,
1109 10,
1110 8,
1111 None,
1112 PythonVariant::Default,
1113 None,
1114 );
1115 let pypy = create_test_installation(
1116 ImplementationName::PyPy,
1117 3,
1118 10,
1119 9,
1120 None,
1121 PythonVariant::Default,
1122 None,
1123 );
1124
1125 assert!(!pypy.is_upgrade_of(&cpython));
1127 assert!(!cpython.is_upgrade_of(&pypy));
1128 }
1129
1130 #[test]
1131 fn test_is_upgrade_of_different_variant() {
1132 let default = create_test_installation(
1133 ImplementationName::CPython,
1134 3,
1135 10,
1136 8,
1137 None,
1138 PythonVariant::Default,
1139 None,
1140 );
1141 let freethreaded = create_test_installation(
1142 ImplementationName::CPython,
1143 3,
1144 10,
1145 9,
1146 None,
1147 PythonVariant::Freethreaded,
1148 None,
1149 );
1150
1151 assert!(!freethreaded.is_upgrade_of(&default));
1153 assert!(!default.is_upgrade_of(&freethreaded));
1154 }
1155
1156 #[test]
1157 fn test_is_upgrade_of_prerelease() {
1158 let stable = create_test_installation(
1159 ImplementationName::CPython,
1160 3,
1161 10,
1162 8,
1163 None,
1164 PythonVariant::Default,
1165 None,
1166 );
1167 let prerelease = create_test_installation(
1168 ImplementationName::CPython,
1169 3,
1170 10,
1171 8,
1172 Some(Prerelease {
1173 kind: PrereleaseKind::Alpha,
1174 number: 1,
1175 }),
1176 PythonVariant::Default,
1177 None,
1178 );
1179
1180 assert!(stable.is_upgrade_of(&prerelease));
1182
1183 assert!(!prerelease.is_upgrade_of(&stable));
1185 }
1186
1187 #[test]
1188 fn test_is_upgrade_of_prerelease_to_prerelease() {
1189 let alpha1 = create_test_installation(
1190 ImplementationName::CPython,
1191 3,
1192 10,
1193 8,
1194 Some(Prerelease {
1195 kind: PrereleaseKind::Alpha,
1196 number: 1,
1197 }),
1198 PythonVariant::Default,
1199 None,
1200 );
1201 let alpha2 = create_test_installation(
1202 ImplementationName::CPython,
1203 3,
1204 10,
1205 8,
1206 Some(Prerelease {
1207 kind: PrereleaseKind::Alpha,
1208 number: 2,
1209 }),
1210 PythonVariant::Default,
1211 None,
1212 );
1213
1214 assert!(alpha2.is_upgrade_of(&alpha1));
1216 assert!(!alpha1.is_upgrade_of(&alpha2));
1218 }
1219
1220 #[test]
1221 fn test_is_upgrade_of_prerelease_same_patch() {
1222 let prerelease = create_test_installation(
1223 ImplementationName::CPython,
1224 3,
1225 10,
1226 8,
1227 Some(Prerelease {
1228 kind: PrereleaseKind::Alpha,
1229 number: 1,
1230 }),
1231 PythonVariant::Default,
1232 None,
1233 );
1234
1235 assert!(!prerelease.is_upgrade_of(&prerelease));
1237 }
1238
1239 #[test]
1240 fn test_is_upgrade_of_build_version() {
1241 let older_build = create_test_installation(
1242 ImplementationName::CPython,
1243 3,
1244 10,
1245 8,
1246 None,
1247 PythonVariant::Default,
1248 Some("20240101"),
1249 );
1250 let newer_build = create_test_installation(
1251 ImplementationName::CPython,
1252 3,
1253 10,
1254 8,
1255 None,
1256 PythonVariant::Default,
1257 Some("20240201"),
1258 );
1259
1260 assert!(newer_build.is_upgrade_of(&older_build));
1262 assert!(!older_build.is_upgrade_of(&newer_build));
1264 }
1265
1266 #[test]
1267 fn test_is_upgrade_of_build_version_same() {
1268 let installation = create_test_installation(
1269 ImplementationName::CPython,
1270 3,
1271 10,
1272 8,
1273 None,
1274 PythonVariant::Default,
1275 Some("20240101"),
1276 );
1277
1278 assert!(!installation.is_upgrade_of(&installation));
1280 }
1281
1282 #[test]
1283 fn test_is_upgrade_of_build_with_legacy_installation() {
1284 let legacy = create_test_installation(
1285 ImplementationName::CPython,
1286 3,
1287 10,
1288 8,
1289 None,
1290 PythonVariant::Default,
1291 None,
1292 );
1293 let with_build = create_test_installation(
1294 ImplementationName::CPython,
1295 3,
1296 10,
1297 8,
1298 None,
1299 PythonVariant::Default,
1300 Some("20240101"),
1301 );
1302
1303 assert!(with_build.is_upgrade_of(&legacy));
1305 assert!(!legacy.is_upgrade_of(&with_build));
1307 }
1308
1309 #[test]
1310 fn test_is_upgrade_of_patch_takes_precedence_over_build() {
1311 let older_patch_newer_build = create_test_installation(
1312 ImplementationName::CPython,
1313 3,
1314 10,
1315 8,
1316 None,
1317 PythonVariant::Default,
1318 Some("20240201"),
1319 );
1320 let newer_patch_older_build = create_test_installation(
1321 ImplementationName::CPython,
1322 3,
1323 10,
1324 9,
1325 None,
1326 PythonVariant::Default,
1327 Some("20240101"),
1328 );
1329
1330 assert!(newer_patch_older_build.is_upgrade_of(&older_patch_newer_build));
1332 assert!(!older_patch_newer_build.is_upgrade_of(&newer_patch_older_build));
1334 }
1335
1336 #[test]
1337 fn test_find_version_matching() {
1338 use crate::PythonVersion;
1339
1340 let platform = Platform::from_env().unwrap();
1341 let temp_dir = tempfile::tempdir().unwrap();
1342
1343 fs::create_dir(temp_dir.path().join(format!("cpython-3.10.0-{platform}"))).unwrap();
1345
1346 temp_env::with_var(
1347 uv_static::EnvVars::UV_PYTHON_INSTALL_DIR,
1348 Some(temp_dir.path()),
1349 || {
1350 let installations = ManagedPythonInstallations::from_settings(None).unwrap();
1351
1352 let v3_1 = PythonVersion::from_str("3.1").unwrap();
1354 let matched: Vec<_> = installations.find_version(&v3_1).unwrap().collect();
1355 assert_eq!(matched.len(), 0);
1356
1357 let v3_10 = PythonVersion::from_str("3.10").unwrap();
1359 let matched: Vec<_> = installations.find_version(&v3_10).unwrap().collect();
1360 assert_eq!(matched.len(), 1);
1361 },
1362 );
1363 }
1364
1365 #[test]
1366 fn test_relative_install_dir_resolves_against_pwd() {
1367 let temp_dir = tempfile::tempdir().unwrap();
1368 let workdir = temp_dir.path().join("workdir");
1369 fs::create_dir(&workdir).unwrap();
1370
1371 temp_env::with_vars(
1372 [
1373 (
1374 uv_static::EnvVars::UV_PYTHON_INSTALL_DIR,
1375 Some(std::ffi::OsStr::new(".python-installs")),
1376 ),
1377 (uv_static::EnvVars::PWD, Some(workdir.as_os_str())),
1378 ],
1379 || {
1380 let installations = ManagedPythonInstallations::from_settings(None).unwrap();
1381 assert_eq!(
1382 installations.absolute_root().unwrap(),
1383 workdir.join(".python-installs")
1384 );
1385 },
1386 );
1387 }
1388}