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, replace_symlink, symlink_or_copy_file,
20};
21use uv_platform::{Error as PlatformError, Os};
22use uv_platform::{LibcDetectionError, Platform};
23use uv_state::{StateBucket, StateStore};
24use uv_static::EnvVars;
25use uv_trampoline_builder::{Launcher, LauncherKind};
26
27use crate::discovery::VersionRequest;
28use crate::downloads::{Error as DownloadError, ManagedPythonDownload};
29use crate::implementation::{
30 Error as ImplementationError, ImplementationName, LenientImplementationName,
31};
32use crate::installation::{self, PythonInstallationKey};
33use crate::interpreter::Interpreter;
34use crate::python_version::PythonVersion;
35use crate::{
36 PythonInstallationMinorVersionKey, PythonRequest, PythonVariant, macos_dylib, sysconfig,
37};
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 pub 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 .map(|name| !name.starts_with('.'))
233 .unwrap_or(true)
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 fn find_matching_current_platform(
247 &self,
248 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + use<>, Error> {
249 let platform = Platform::from_env()?;
250
251 let iter = Self::from_settings(None)?
252 .find_all()?
253 .filter(move |installation| {
254 if !platform.supports(installation.platform()) {
255 debug!("Skipping managed installation `{installation}`: not supported by current platform `{platform}`");
256 return false;
257 }
258 true
259 });
260
261 Ok(iter)
262 }
263
264 pub fn find_version<'a>(
271 &'a self,
272 version: &'a PythonVersion,
273 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + 'a, Error> {
274 let request = VersionRequest::from(version);
275 Ok(self
276 .find_matching_current_platform()?
277 .filter(move |installation| request.matches_installation_key(installation.key())))
278 }
279
280 pub fn root(&self) -> &Path {
281 &self.root
282 }
283}
284
285static EXTERNALLY_MANAGED: &str = "[externally-managed]
286Error=This Python installation is managed by uv and should not be modified.
287";
288
289#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
291pub struct ManagedPythonInstallation {
292 path: PathBuf,
294 key: PythonInstallationKey,
296 url: Option<Cow<'static, str>>,
300 sha256: Option<Cow<'static, str>>,
304 build: Option<Cow<'static, str>>,
308}
309
310impl ManagedPythonInstallation {
311 pub fn new(path: PathBuf, download: &ManagedPythonDownload) -> Self {
312 Self {
313 path,
314 key: download.key().clone(),
315 url: Some(download.url().clone()),
316 sha256: download.sha256().cloned(),
317 build: download.build().map(Cow::Borrowed),
318 }
319 }
320
321 pub(crate) fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
322 let path = path.as_ref();
323
324 let key = PythonInstallationKey::from_str(
325 path.file_name()
326 .ok_or(Error::NameError("name is empty".to_string()))?
327 .to_str()
328 .ok_or(Error::NameError("not a valid string".to_string()))?,
329 )?;
330
331 let path = std::path::absolute(path)
332 .map_err(|err| Error::AbsolutePath(path.to_path_buf(), err))?;
333
334 let build = match fs::read_to_string(path.join("BUILD")) {
336 Ok(content) => Some(Cow::Owned(content.trim().to_string())),
337 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
338 Err(err) => return Err(err.into()),
339 };
340
341 Ok(Self {
342 path,
343 key,
344 url: None,
345 sha256: None,
346 build,
347 })
348 }
349
350 pub fn try_from_interpreter(interpreter: &Interpreter) -> Option<Self> {
354 let managed_root = ManagedPythonInstallations::from_settings(None).ok()?;
355
356 let sys_base_prefix = dunce::canonicalize(interpreter.sys_base_prefix())
360 .unwrap_or_else(|_| interpreter.sys_base_prefix().to_path_buf());
361 let root = dunce::canonicalize(managed_root.root())
362 .unwrap_or_else(|_| managed_root.root().to_path_buf());
363
364 let suffix = sys_base_prefix.strip_prefix(&root).ok()?;
366
367 let first_component = suffix.components().next()?;
368 let name = first_component.as_os_str().to_str()?;
369
370 PythonInstallationKey::from_str(name).ok()?;
372
373 let path = managed_root.root().join(name);
375 Self::from_path(path).ok()
376 }
377
378 pub fn executable(&self, windowed: bool) -> PathBuf {
387 let version = match self.implementation() {
388 ImplementationName::CPython => {
389 if cfg!(unix) {
390 format!("{}.{}", self.key.major, self.key.minor)
391 } else {
392 String::new()
393 }
394 }
395 ImplementationName::PyPy => format!("{}.{}", self.key.major, self.key.minor),
397 ImplementationName::Pyodide => String::new(),
399 ImplementationName::GraalPy => String::new(),
400 };
401
402 let variant = if self.implementation() == ImplementationName::GraalPy {
405 ""
406 } else if cfg!(unix) {
407 self.key.variant.executable_suffix()
408 } else if cfg!(windows) && windowed {
409 "w"
411 } else {
412 ""
413 };
414
415 let name = format!(
416 "{implementation}{version}{variant}{exe}",
417 implementation = self.implementation().executable_name(),
418 exe = std::env::consts::EXE_SUFFIX
419 );
420
421 let executable = executable_path_from_base(
422 self.python_dir().as_path(),
423 &name,
424 &LenientImplementationName::from(self.implementation()),
425 *self.key.os(),
426 );
427
428 if cfg!(windows)
433 && matches!(self.key.variant, PythonVariant::Freethreaded)
434 && !executable.exists()
435 {
436 return self.python_dir().join(format!(
438 "python{}.{}t{}",
439 self.key.major,
440 self.key.minor,
441 std::env::consts::EXE_SUFFIX
442 ));
443 }
444
445 executable
446 }
447
448 fn python_dir(&self) -> PathBuf {
449 let install = self.path.join("install");
450 if install.is_dir() {
451 install
452 } else {
453 self.path.clone()
454 }
455 }
456
457 pub fn version(&self) -> PythonVersion {
459 self.key.version()
460 }
461
462 pub fn implementation(&self) -> ImplementationName {
463 match self.key.implementation().into_owned() {
464 LenientImplementationName::Known(implementation) => implementation,
465 LenientImplementationName::Unknown(_) => {
466 panic!("Managed Python installations should have a known implementation")
467 }
468 }
469 }
470
471 pub fn path(&self) -> &Path {
472 &self.path
473 }
474
475 pub fn key(&self) -> &PythonInstallationKey {
476 &self.key
477 }
478
479 pub fn platform(&self) -> &Platform {
480 self.key.platform()
481 }
482
483 pub fn build(&self) -> Option<&str> {
485 self.build.as_deref()
486 }
487
488 pub fn minor_version_key(&self) -> &PythonInstallationMinorVersionKey {
489 PythonInstallationMinorVersionKey::ref_cast(&self.key)
490 }
491
492 pub fn satisfies(&self, request: &PythonRequest) -> bool {
493 match request {
494 PythonRequest::File(path) => self.executable(false) == *path,
495 PythonRequest::Default | PythonRequest::Any => true,
496 PythonRequest::Directory(path) => self.path() == *path,
497 PythonRequest::ExecutableName(name) => self
498 .executable(false)
499 .file_name()
500 .is_some_and(|filename| filename.to_string_lossy() == *name),
501 PythonRequest::Implementation(implementation) => {
502 *implementation == self.implementation()
503 }
504 PythonRequest::ImplementationVersion(implementation, version) => {
505 *implementation == self.implementation() && version.matches_version(&self.version())
506 }
507 PythonRequest::Version(version) => version.matches_version(&self.version()),
508 PythonRequest::Key(request) => request.satisfied_by_key(self.key()),
509 }
510 }
511
512 pub fn ensure_canonical_executables(&self) -> Result<(), Error> {
514 let python = self.executable(false);
515
516 let canonical_names = &["python"];
517
518 for name in canonical_names {
519 let executable =
520 python.with_file_name(format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX));
521
522 if executable == python {
525 continue;
526 }
527
528 match symlink_or_copy_file(&python, &executable) {
529 Ok(()) => {
530 debug!(
531 "Created link {} -> {}",
532 executable.user_display(),
533 python.user_display(),
534 );
535 }
536 Err(err) if err.kind() == io::ErrorKind::NotFound => {
537 return Err(Error::MissingExecutable(python.clone()));
538 }
539 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
540 Err(err) => {
541 return Err(Error::CanonicalizeExecutable(err));
542 }
543 }
544 }
545
546 Ok(())
547 }
548
549 pub fn ensure_minor_version_link(&self) -> Result<(), Error> {
552 if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self) {
553 minor_version_link.create_directory()?;
554 }
555 Ok(())
556 }
557
558 pub fn ensure_externally_managed(&self) -> Result<(), Error> {
561 if self.key.os().is_emscripten() {
562 return Ok(());
565 }
566 let stdlib = if self.key.os().is_windows() {
568 self.python_dir().join("Lib")
569 } else {
570 let lib_suffix = self.key.variant.lib_suffix();
571 let python = if matches!(
572 self.key.implementation,
573 LenientImplementationName::Known(ImplementationName::PyPy)
574 ) {
575 format!("pypy{}", self.key.version().python_version())
576 } else {
577 format!("python{}{lib_suffix}", self.key.version().python_version())
578 };
579 self.python_dir().join("lib").join(python)
580 };
581
582 let file = stdlib.join("EXTERNALLY-MANAGED");
583 fs_err::write(file, EXTERNALLY_MANAGED)?;
584
585 Ok(())
586 }
587
588 pub fn ensure_sysconfig_patched(&self) -> Result<(), Error> {
590 if cfg!(unix) {
591 if self.key.os().is_emscripten() {
592 return Ok(());
595 }
596 if self.implementation() == ImplementationName::CPython {
597 sysconfig::update_sysconfig(
598 self.path(),
599 self.key.major,
600 self.key.minor,
601 self.key.variant.lib_suffix(),
602 )?;
603 }
604 }
605 Ok(())
606 }
607
608 pub fn ensure_dylib_patched(&self) -> Result<(), macos_dylib::Error> {
615 if cfg!(target_os = "macos") {
616 if self.key().os().is_like_darwin() {
617 if self.implementation() == ImplementationName::CPython {
618 let dylib_path = self.python_dir().join("lib").join(format!(
619 "{}python{}{}{}",
620 std::env::consts::DLL_PREFIX,
621 self.key.version().python_version(),
622 self.key.variant().executable_suffix(),
623 std::env::consts::DLL_SUFFIX
624 ));
625 macos_dylib::patch_dylib_install_name(dylib_path)?;
626 }
627 }
628 }
629 Ok(())
630 }
631
632 pub fn ensure_build_file(&self) -> Result<(), Error> {
634 if let Some(ref build) = self.build {
635 let build_file = self.path.join("BUILD");
636 fs::write(&build_file, build.as_ref())?;
637 }
638 Ok(())
639 }
640
641 pub fn is_bin_link(&self, path: &Path) -> bool {
644 if cfg!(unix) {
645 same_file::is_same_file(path, self.executable(false)).unwrap_or_default()
646 } else if cfg!(windows) {
647 let Some(launcher) = Launcher::try_from_path(path).unwrap_or_default() else {
648 return false;
649 };
650 if !matches!(launcher.kind, LauncherKind::Python) {
651 return false;
652 }
653 dunce::canonicalize(&launcher.python_path).unwrap_or(launcher.python_path)
657 == self.executable(false)
658 } else {
659 unreachable!("Only Windows and Unix are supported")
660 }
661 }
662
663 pub fn is_upgrade_of(&self, other: &Self) -> bool {
665 if self.key.implementation != other.key.implementation {
667 return false;
668 }
669 if self.key.variant != other.key.variant {
671 return false;
672 }
673 if (self.key.major, self.key.minor) != (other.key.major, other.key.minor) {
675 return false;
676 }
677 if self.key.patch == other.key.patch {
680 return match (self.key.prerelease, other.key.prerelease) {
681 (Some(self_pre), Some(other_pre)) => self_pre > other_pre,
683 (None, Some(_)) => true,
685 (Some(_), None) => false,
687 (None, None) => match (self.build.as_deref(), other.build.as_deref()) {
689 (Some(_), None) => true,
691 (Some(self_build), Some(other_build)) => {
693 compare_build_versions(self_build, other_build) == Ordering::Greater
694 }
695 (None, _) => false,
697 },
698 };
699 }
700 if self.key.patch < other.key.patch {
702 return false;
703 }
704 true
705 }
706
707 pub fn url(&self) -> Option<&str> {
708 self.url.as_deref()
709 }
710
711 pub fn sha256(&self) -> Option<&str> {
712 self.sha256.as_deref()
713 }
714}
715
716#[derive(Clone, Debug)]
719pub struct PythonMinorVersionLink {
720 pub symlink_directory: PathBuf,
722 pub symlink_executable: PathBuf,
725 pub target_directory: PathBuf,
728}
729
730impl PythonMinorVersionLink {
731 fn from_executable(executable: &Path, key: &PythonInstallationKey) -> Option<Self> {
751 let implementation = key.implementation();
752 if !matches!(
753 implementation.as_ref(),
754 LenientImplementationName::Known(ImplementationName::CPython)
755 ) {
756 return None;
758 }
759 let executable_name = executable
760 .file_name()
761 .expect("Executable file name should exist");
762 let symlink_directory_name = PythonInstallationMinorVersionKey::ref_cast(key).to_string();
763 let parent = executable
764 .parent()
765 .expect("Executable should have parent directory");
766
767 let target_directory = if cfg!(unix) {
769 if parent
770 .components()
771 .next_back()
772 .is_some_and(|c| c.as_os_str() == "bin")
773 {
774 parent.parent()?.to_path_buf()
775 } else {
776 return None;
777 }
778 } else if cfg!(windows) {
779 parent.to_path_buf()
780 } else {
781 unimplemented!("Only Windows and Unix systems are supported.")
782 };
783 let symlink_directory = target_directory.with_file_name(symlink_directory_name);
784 if target_directory == symlink_directory {
786 return None;
787 }
788 let symlink_executable = executable_path_from_base(
790 symlink_directory.as_path(),
791 &executable_name.to_string_lossy(),
792 &implementation,
793 *key.os(),
794 );
795 let minor_version_link = Self {
796 symlink_directory,
797 symlink_executable,
798 target_directory,
799 };
800 Some(minor_version_link)
801 }
802
803 pub fn from_installation(installation: &ManagedPythonInstallation) -> Option<Self> {
804 Self::from_executable(installation.executable(false).as_path(), installation.key())
805 }
806
807 pub fn create_directory(&self) -> Result<(), Error> {
808 match replace_symlink(
809 self.target_directory.as_path(),
810 self.symlink_directory.as_path(),
811 ) {
812 Ok(()) => {
813 debug!(
814 "Created link {} -> {}",
815 &self.symlink_directory.user_display(),
816 &self.target_directory.user_display(),
817 );
818 }
819 Err(err) if err.kind() == io::ErrorKind::NotFound => {
820 return Err(Error::MissingPythonMinorVersionLinkTargetDirectory(
821 self.target_directory.clone(),
822 ));
823 }
824 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
825 Err(err) => {
826 return Err(Error::PythonMinorVersionLinkDirectory(err));
827 }
828 }
829 Ok(())
830 }
831
832 pub fn exists(&self) -> bool {
839 #[cfg(unix)]
840 {
841 self.symlink_directory
842 .symlink_metadata()
843 .is_ok_and(|metadata| metadata.file_type().is_symlink())
844 && self
845 .read_target()
846 .is_some_and(|target| target == self.target_directory)
847 }
848 #[cfg(windows)]
849 {
850 self.symlink_directory
851 .symlink_metadata()
852 .is_ok_and(|metadata| {
853 (metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0) != 0
856 })
857 && self
858 .read_target()
859 .is_some_and(|target| target == self.target_directory)
860 }
861 }
862
863 pub fn read_target(&self) -> Option<PathBuf> {
869 #[cfg(unix)]
870 {
871 self.symlink_directory.read_link().ok()
872 }
873 #[cfg(windows)]
874 {
875 junction::get_target(&self.symlink_directory).ok()
876 }
877 }
878}
879
880fn executable_path_from_base(
884 base: &Path,
885 executable_name: &str,
886 implementation: &LenientImplementationName,
887 os: Os,
888) -> PathBuf {
889 if matches!(
890 implementation,
891 &LenientImplementationName::Known(ImplementationName::GraalPy)
892 ) {
893 base.join("bin").join(executable_name)
895 } else if os.is_emscripten()
896 || matches!(
897 implementation,
898 &LenientImplementationName::Known(ImplementationName::Pyodide)
899 )
900 {
901 base.join(executable_name)
903 } else if os.is_windows() {
904 base.join(executable_name)
906 } else {
907 base.join("bin").join(executable_name)
909 }
910}
911
912pub fn create_link_to_executable(link: &Path, executable: &Path) -> Result<(), Error> {
916 let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?;
917 fs_err::create_dir_all(link_parent).map_err(Error::ExecutableDirectory)?;
918
919 if cfg!(unix) {
920 match symlink_or_copy_file(executable, link) {
922 Ok(()) => Ok(()),
923 Err(err) if err.kind() == io::ErrorKind::NotFound => {
924 Err(Error::MissingExecutable(executable.to_path_buf()))
925 }
926 Err(err) => Err(Error::LinkExecutable(err)),
927 }
928 } else if cfg!(windows) {
929 use uv_trampoline_builder::windows_python_launcher;
930
931 let launcher = windows_python_launcher(executable, false)?;
933
934 #[expect(clippy::disallowed_types)]
937 {
938 std::fs::File::create_new(link)
939 .and_then(|mut file| file.write_all(launcher.as_ref()))
940 .map_err(Error::LinkExecutable)
941 }
942 } else {
943 unimplemented!("Only Windows and Unix are supported.")
944 }
945}
946
947pub fn replace_link_to_executable(link: &Path, executable: &Path) -> Result<(), Error> {
953 let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?;
954 fs_err::create_dir_all(link_parent).map_err(Error::ExecutableDirectory)?;
955
956 if cfg!(unix) {
957 replace_symlink(executable, link).map_err(Error::LinkExecutable)
958 } else if cfg!(windows) {
959 use uv_trampoline_builder::windows_python_launcher;
960
961 let launcher = windows_python_launcher(executable, false)?;
962
963 uv_fs::write_atomic_sync(link, &*launcher).map_err(Error::LinkExecutable)
964 } else {
965 unimplemented!("Only Windows and Unix are supported.")
966 }
967}
968
969pub fn platform_key_from_env() -> Result<String, Error> {
972 Ok(Platform::from_env()?.to_string().to_lowercase())
973}
974
975impl fmt::Display for ManagedPythonInstallation {
976 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
977 write!(
978 f,
979 "{}",
980 self.path
981 .file_name()
982 .unwrap_or(self.path.as_os_str())
983 .to_string_lossy()
984 )
985 }
986}
987
988pub fn python_executable_dir() -> Result<PathBuf, Error> {
990 uv_dirs::user_executable_directory(Some(EnvVars::UV_PYTHON_BIN_DIR))
991 .ok_or(Error::NoExecutableDirectory)
992}
993
994#[cfg(test)]
995mod tests {
996 use super::*;
997 use crate::implementation::LenientImplementationName;
998 use crate::installation::PythonInstallationKey;
999 use crate::{ImplementationName, PythonVariant};
1000 use std::path::PathBuf;
1001 use std::str::FromStr;
1002 use uv_pep440::{Prerelease, PrereleaseKind};
1003 use uv_platform::Platform;
1004
1005 fn create_test_installation(
1006 implementation: ImplementationName,
1007 major: u8,
1008 minor: u8,
1009 patch: u8,
1010 prerelease: Option<Prerelease>,
1011 variant: PythonVariant,
1012 build: Option<&str>,
1013 ) -> ManagedPythonInstallation {
1014 let platform = Platform::from_str("linux-x86_64-gnu").unwrap();
1015 let key = PythonInstallationKey::new(
1016 LenientImplementationName::Known(implementation),
1017 major,
1018 minor,
1019 patch,
1020 prerelease,
1021 platform,
1022 variant,
1023 );
1024 ManagedPythonInstallation {
1025 path: PathBuf::from("/test/path"),
1026 key,
1027 url: None,
1028 sha256: None,
1029 build: build.map(|s| Cow::Owned(s.to_owned())),
1030 }
1031 }
1032
1033 #[test]
1034 fn test_is_upgrade_of_same_version() {
1035 let installation = create_test_installation(
1036 ImplementationName::CPython,
1037 3,
1038 10,
1039 8,
1040 None,
1041 PythonVariant::Default,
1042 None,
1043 );
1044
1045 assert!(!installation.is_upgrade_of(&installation));
1047 }
1048
1049 #[test]
1050 fn test_is_upgrade_of_patch_version() {
1051 let older = create_test_installation(
1052 ImplementationName::CPython,
1053 3,
1054 10,
1055 8,
1056 None,
1057 PythonVariant::Default,
1058 None,
1059 );
1060 let newer = create_test_installation(
1061 ImplementationName::CPython,
1062 3,
1063 10,
1064 9,
1065 None,
1066 PythonVariant::Default,
1067 None,
1068 );
1069
1070 assert!(newer.is_upgrade_of(&older));
1072 assert!(!older.is_upgrade_of(&newer));
1074 }
1075
1076 #[test]
1077 fn test_is_upgrade_of_different_minor_version() {
1078 let py310 = create_test_installation(
1079 ImplementationName::CPython,
1080 3,
1081 10,
1082 8,
1083 None,
1084 PythonVariant::Default,
1085 None,
1086 );
1087 let py311 = create_test_installation(
1088 ImplementationName::CPython,
1089 3,
1090 11,
1091 0,
1092 None,
1093 PythonVariant::Default,
1094 None,
1095 );
1096
1097 assert!(!py311.is_upgrade_of(&py310));
1099 assert!(!py310.is_upgrade_of(&py311));
1100 }
1101
1102 #[test]
1103 fn test_is_upgrade_of_different_implementation() {
1104 let cpython = create_test_installation(
1105 ImplementationName::CPython,
1106 3,
1107 10,
1108 8,
1109 None,
1110 PythonVariant::Default,
1111 None,
1112 );
1113 let pypy = create_test_installation(
1114 ImplementationName::PyPy,
1115 3,
1116 10,
1117 9,
1118 None,
1119 PythonVariant::Default,
1120 None,
1121 );
1122
1123 assert!(!pypy.is_upgrade_of(&cpython));
1125 assert!(!cpython.is_upgrade_of(&pypy));
1126 }
1127
1128 #[test]
1129 fn test_is_upgrade_of_different_variant() {
1130 let default = create_test_installation(
1131 ImplementationName::CPython,
1132 3,
1133 10,
1134 8,
1135 None,
1136 PythonVariant::Default,
1137 None,
1138 );
1139 let freethreaded = create_test_installation(
1140 ImplementationName::CPython,
1141 3,
1142 10,
1143 9,
1144 None,
1145 PythonVariant::Freethreaded,
1146 None,
1147 );
1148
1149 assert!(!freethreaded.is_upgrade_of(&default));
1151 assert!(!default.is_upgrade_of(&freethreaded));
1152 }
1153
1154 #[test]
1155 fn test_is_upgrade_of_prerelease() {
1156 let stable = create_test_installation(
1157 ImplementationName::CPython,
1158 3,
1159 10,
1160 8,
1161 None,
1162 PythonVariant::Default,
1163 None,
1164 );
1165 let prerelease = create_test_installation(
1166 ImplementationName::CPython,
1167 3,
1168 10,
1169 8,
1170 Some(Prerelease {
1171 kind: PrereleaseKind::Alpha,
1172 number: 1,
1173 }),
1174 PythonVariant::Default,
1175 None,
1176 );
1177
1178 assert!(stable.is_upgrade_of(&prerelease));
1180
1181 assert!(!prerelease.is_upgrade_of(&stable));
1183 }
1184
1185 #[test]
1186 fn test_is_upgrade_of_prerelease_to_prerelease() {
1187 let alpha1 = create_test_installation(
1188 ImplementationName::CPython,
1189 3,
1190 10,
1191 8,
1192 Some(Prerelease {
1193 kind: PrereleaseKind::Alpha,
1194 number: 1,
1195 }),
1196 PythonVariant::Default,
1197 None,
1198 );
1199 let alpha2 = create_test_installation(
1200 ImplementationName::CPython,
1201 3,
1202 10,
1203 8,
1204 Some(Prerelease {
1205 kind: PrereleaseKind::Alpha,
1206 number: 2,
1207 }),
1208 PythonVariant::Default,
1209 None,
1210 );
1211
1212 assert!(alpha2.is_upgrade_of(&alpha1));
1214 assert!(!alpha1.is_upgrade_of(&alpha2));
1216 }
1217
1218 #[test]
1219 fn test_is_upgrade_of_prerelease_same_patch() {
1220 let prerelease = create_test_installation(
1221 ImplementationName::CPython,
1222 3,
1223 10,
1224 8,
1225 Some(Prerelease {
1226 kind: PrereleaseKind::Alpha,
1227 number: 1,
1228 }),
1229 PythonVariant::Default,
1230 None,
1231 );
1232
1233 assert!(!prerelease.is_upgrade_of(&prerelease));
1235 }
1236
1237 #[test]
1238 fn test_is_upgrade_of_build_version() {
1239 let older_build = create_test_installation(
1240 ImplementationName::CPython,
1241 3,
1242 10,
1243 8,
1244 None,
1245 PythonVariant::Default,
1246 Some("20240101"),
1247 );
1248 let newer_build = create_test_installation(
1249 ImplementationName::CPython,
1250 3,
1251 10,
1252 8,
1253 None,
1254 PythonVariant::Default,
1255 Some("20240201"),
1256 );
1257
1258 assert!(newer_build.is_upgrade_of(&older_build));
1260 assert!(!older_build.is_upgrade_of(&newer_build));
1262 }
1263
1264 #[test]
1265 fn test_is_upgrade_of_build_version_same() {
1266 let installation = create_test_installation(
1267 ImplementationName::CPython,
1268 3,
1269 10,
1270 8,
1271 None,
1272 PythonVariant::Default,
1273 Some("20240101"),
1274 );
1275
1276 assert!(!installation.is_upgrade_of(&installation));
1278 }
1279
1280 #[test]
1281 fn test_is_upgrade_of_build_with_legacy_installation() {
1282 let legacy = create_test_installation(
1283 ImplementationName::CPython,
1284 3,
1285 10,
1286 8,
1287 None,
1288 PythonVariant::Default,
1289 None,
1290 );
1291 let with_build = create_test_installation(
1292 ImplementationName::CPython,
1293 3,
1294 10,
1295 8,
1296 None,
1297 PythonVariant::Default,
1298 Some("20240101"),
1299 );
1300
1301 assert!(with_build.is_upgrade_of(&legacy));
1303 assert!(!legacy.is_upgrade_of(&with_build));
1305 }
1306
1307 #[test]
1308 fn test_is_upgrade_of_patch_takes_precedence_over_build() {
1309 let older_patch_newer_build = create_test_installation(
1310 ImplementationName::CPython,
1311 3,
1312 10,
1313 8,
1314 None,
1315 PythonVariant::Default,
1316 Some("20240201"),
1317 );
1318 let newer_patch_older_build = create_test_installation(
1319 ImplementationName::CPython,
1320 3,
1321 10,
1322 9,
1323 None,
1324 PythonVariant::Default,
1325 Some("20240101"),
1326 );
1327
1328 assert!(newer_patch_older_build.is_upgrade_of(&older_patch_newer_build));
1330 assert!(!older_patch_newer_build.is_upgrade_of(&newer_patch_older_build));
1332 }
1333
1334 #[test]
1335 fn test_find_version_matching() {
1336 use crate::PythonVersion;
1337
1338 let platform = Platform::from_env().unwrap();
1339 let temp_dir = tempfile::tempdir().unwrap();
1340
1341 fs::create_dir(temp_dir.path().join(format!("cpython-3.10.0-{platform}"))).unwrap();
1343
1344 temp_env::with_var(
1345 uv_static::EnvVars::UV_PYTHON_INSTALL_DIR,
1346 Some(temp_dir.path()),
1347 || {
1348 let installations = ManagedPythonInstallations::from_settings(None).unwrap();
1349
1350 let v3_1 = PythonVersion::from_str("3.1").unwrap();
1352 let matched: Vec<_> = installations.find_version(&v3_1).unwrap().collect();
1353 assert_eq!(matched.len(), 0);
1354
1355 let v3_10 = PythonVersion::from_str("3.10").unwrap();
1357 let matched: Vec<_> = installations.find_version(&v3_10).unwrap().collect();
1358 assert_eq!(matched.len(), 1);
1359 },
1360 );
1361 }
1362}