1use core::fmt;
2use std::borrow::Cow;
3use std::cmp::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};
15use uv_preview::{Preview, PreviewFeatures};
16#[cfg(windows)]
17use windows::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
18
19use uv_fs::{
20 LockedFile, LockedFileError, LockedFileMode, Simplified, replace_symlink, symlink_or_copy_file,
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::downloads::{Error as DownloadError, ManagedPythonDownload};
29use crate::implementation::{
30 Error as ImplementationError, ImplementationName, LenientImplementationName,
31};
32use crate::installation::{self, PythonInstallationKey};
33use crate::python_version::PythonVersion;
34use crate::{
35 PythonInstallationMinorVersionKey, PythonRequest, PythonVariant, macos_dylib, sysconfig,
36};
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("Failed to copy to: {0}", to.user_display())]
57 CopyError {
58 to: PathBuf,
59 #[source]
60 err: io::Error,
61 },
62 #[error("Missing expected Python executable at {}", _0.user_display())]
63 MissingExecutable(PathBuf),
64 #[error("Missing expected target directory for Python minor version link at {}", _0.user_display())]
65 MissingPythonMinorVersionLinkTargetDirectory(PathBuf),
66 #[error("Failed to create canonical Python executable at {} from {}", to.user_display(), from.user_display())]
67 CanonicalizeExecutable {
68 from: PathBuf,
69 to: PathBuf,
70 #[source]
71 err: io::Error,
72 },
73 #[error("Failed to create Python executable link at {} from {}", to.user_display(), from.user_display())]
74 LinkExecutable {
75 from: PathBuf,
76 to: PathBuf,
77 #[source]
78 err: io::Error,
79 },
80 #[error("Failed to create Python minor version link directory at {} from {}", to.user_display(), from.user_display())]
81 PythonMinorVersionLinkDirectory {
82 from: PathBuf,
83 to: PathBuf,
84 #[source]
85 err: io::Error,
86 },
87 #[error("Failed to create directory for Python executable link at {}", to.user_display())]
88 ExecutableDirectory {
89 to: PathBuf,
90 #[source]
91 err: io::Error,
92 },
93 #[error("Failed to read Python installation directory: {0}", dir.user_display())]
94 ReadError {
95 dir: PathBuf,
96 #[source]
97 err: io::Error,
98 },
99 #[error("Failed to find a directory to install executables into")]
100 NoExecutableDirectory,
101 #[error(transparent)]
102 LauncherError(#[from] uv_trampoline_builder::Error),
103 #[error("Failed to read managed Python directory name: {0}")]
104 NameError(String),
105 #[error("Failed to construct absolute path to managed Python directory: {}", _0.user_display())]
106 AbsolutePath(PathBuf, #[source] io::Error),
107 #[error(transparent)]
108 NameParseError(#[from] installation::PythonInstallationKeyError),
109 #[error("Failed to determine the libc used on the current platform")]
110 LibcDetection(#[from] LibcDetectionError),
111 #[error(transparent)]
112 MacOsDylib(#[from] macos_dylib::Error),
113}
114#[derive(Debug, Clone, Eq, PartialEq)]
116pub struct ManagedPythonInstallations {
117 root: PathBuf,
119}
120
121impl ManagedPythonInstallations {
122 fn from_path(root: impl Into<PathBuf>) -> Self {
124 Self { root: root.into() }
125 }
126
127 pub async fn lock(&self) -> Result<LockedFile, Error> {
130 Ok(LockedFile::acquire(
131 self.root.join(".lock"),
132 LockedFileMode::Exclusive,
133 self.root.user_display(),
134 )
135 .await?)
136 }
137
138 pub fn from_settings(install_dir: Option<PathBuf>) -> Result<Self, Error> {
145 if let Some(install_dir) = install_dir {
146 Ok(Self::from_path(install_dir))
147 } else if let Some(install_dir) =
148 std::env::var_os(EnvVars::UV_PYTHON_INSTALL_DIR).filter(|s| !s.is_empty())
149 {
150 Ok(Self::from_path(install_dir))
151 } else {
152 Ok(Self::from_path(
153 StateStore::from_settings(None)?.bucket(StateBucket::ManagedPython),
154 ))
155 }
156 }
157
158 pub fn temp() -> Result<Self, Error> {
160 Ok(Self::from_path(
161 StateStore::temp()?.bucket(StateBucket::ManagedPython),
162 ))
163 }
164
165 pub fn scratch(&self) -> PathBuf {
167 self.root.join(".temp")
168 }
169
170 pub fn init(self) -> Result<Self, Error> {
174 let root = &self.root;
175
176 if !root.exists()
178 && root
179 .parent()
180 .is_some_and(|parent| parent.join("toolchains").exists())
181 {
182 let deprecated = root.parent().unwrap().join("toolchains");
183 fs::rename(&deprecated, root)?;
185 uv_fs::replace_symlink(root, &deprecated)?;
187 } else {
188 fs::create_dir_all(root)?;
189 }
190
191 fs::create_dir_all(root)?;
193
194 let scratch = self.scratch();
196 fs::create_dir_all(&scratch)?;
197
198 match fs::OpenOptions::new()
200 .write(true)
201 .create_new(true)
202 .open(root.join(".gitignore"))
203 {
204 Ok(mut file) => file.write_all(b"*")?,
205 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
206 Err(err) => return Err(err.into()),
207 }
208
209 Ok(self)
210 }
211
212 pub fn find_all(
217 &self,
218 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + use<>, Error> {
219 let dirs = match fs_err::read_dir(&self.root) {
220 Ok(installation_dirs) => {
221 let directories: Vec<_> = installation_dirs
223 .filter_map(|read_dir| match read_dir {
224 Ok(entry) => match entry.file_type() {
225 Ok(file_type) => file_type.is_dir().then_some(Ok(entry.path())),
226 Err(err) => Some(Err(err)),
227 },
228 Err(err) => Some(Err(err)),
229 })
230 .collect::<Result<_, io::Error>>()
231 .map_err(|err| Error::ReadError {
232 dir: self.root.clone(),
233 err,
234 })?;
235 directories
236 }
237 Err(err) if err.kind() == io::ErrorKind::NotFound => vec![],
238 Err(err) => {
239 return Err(Error::ReadError {
240 dir: self.root.clone(),
241 err,
242 });
243 }
244 };
245 let scratch = self.scratch();
246 Ok(dirs
247 .into_iter()
248 .filter(|path| *path != scratch)
250 .filter(|path| {
252 path.file_name()
253 .and_then(OsStr::to_str)
254 .map(|name| !name.starts_with('.'))
255 .unwrap_or(true)
256 })
257 .filter_map(|path| {
258 ManagedPythonInstallation::from_path(path)
259 .inspect_err(|err| {
260 warn!("Ignoring malformed managed Python entry:\n {err}");
261 })
262 .ok()
263 })
264 .sorted_unstable_by_key(|installation| Reverse(installation.key().clone())))
265 }
266
267 pub fn find_matching_current_platform(
269 &self,
270 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + use<>, Error> {
271 let platform = Platform::from_env()?;
272
273 let iter = Self::from_settings(None)?
274 .find_all()?
275 .filter(move |installation| {
276 if !platform.supports(installation.platform()) {
277 debug!("Skipping managed installation `{installation}`: not supported by current platform `{platform}`");
278 return false;
279 }
280 true
281 });
282
283 Ok(iter)
284 }
285
286 pub fn find_version<'a>(
293 &'a self,
294 version: &'a PythonVersion,
295 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + 'a, Error> {
296 Ok(self
297 .find_matching_current_platform()?
298 .filter(move |installation| {
299 installation
300 .path
301 .file_name()
302 .map(OsStr::to_string_lossy)
303 .is_some_and(|filename| filename.starts_with(&format!("cpython-{version}")))
304 }))
305 }
306
307 pub fn root(&self) -> &Path {
308 &self.root
309 }
310}
311
312static EXTERNALLY_MANAGED: &str = "[externally-managed]
313Error=This Python installation is managed by uv and should not be modified.
314";
315
316#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
318pub struct ManagedPythonInstallation {
319 path: PathBuf,
321 key: PythonInstallationKey,
323 url: Option<Cow<'static, str>>,
327 sha256: Option<Cow<'static, str>>,
331 build: Option<Cow<'static, str>>,
335}
336
337impl ManagedPythonInstallation {
338 pub fn new(path: PathBuf, download: &ManagedPythonDownload) -> Self {
339 Self {
340 path,
341 key: download.key().clone(),
342 url: Some(download.url().clone()),
343 sha256: download.sha256().cloned(),
344 build: download.build().map(Cow::Borrowed),
345 }
346 }
347
348 pub(crate) fn from_path(path: PathBuf) -> Result<Self, Error> {
349 let key = PythonInstallationKey::from_str(
350 path.file_name()
351 .ok_or(Error::NameError("name is empty".to_string()))?
352 .to_str()
353 .ok_or(Error::NameError("not a valid string".to_string()))?,
354 )?;
355
356 let path = std::path::absolute(&path).map_err(|err| Error::AbsolutePath(path, err))?;
357
358 let build = match fs::read_to_string(path.join("BUILD")) {
360 Ok(content) => Some(Cow::Owned(content.trim().to_string())),
361 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
362 Err(err) => return Err(err.into()),
363 };
364
365 Ok(Self {
366 path,
367 key,
368 url: None,
369 sha256: None,
370 build,
371 })
372 }
373
374 pub fn executable(&self, windowed: bool) -> PathBuf {
383 let version = match self.implementation() {
384 ImplementationName::CPython => {
385 if cfg!(unix) {
386 format!("{}.{}", self.key.major, self.key.minor)
387 } else {
388 String::new()
389 }
390 }
391 ImplementationName::PyPy => format!("{}.{}", self.key.major, self.key.minor),
393 ImplementationName::Pyodide => String::new(),
395 ImplementationName::GraalPy => String::new(),
396 };
397
398 let variant = if self.implementation() == ImplementationName::GraalPy {
401 ""
402 } else if cfg!(unix) {
403 self.key.variant.executable_suffix()
404 } else if cfg!(windows) && windowed {
405 "w"
407 } else {
408 ""
409 };
410
411 let name = format!(
412 "{implementation}{version}{variant}{exe}",
413 implementation = self.implementation().executable_name(),
414 exe = std::env::consts::EXE_SUFFIX
415 );
416
417 let executable = executable_path_from_base(
418 self.python_dir().as_path(),
419 &name,
420 &LenientImplementationName::from(self.implementation()),
421 *self.key.os(),
422 );
423
424 if cfg!(windows)
429 && matches!(self.key.variant, PythonVariant::Freethreaded)
430 && !executable.exists()
431 {
432 return self.python_dir().join(format!(
434 "python{}.{}t{}",
435 self.key.major,
436 self.key.minor,
437 std::env::consts::EXE_SUFFIX
438 ));
439 }
440
441 executable
442 }
443
444 fn python_dir(&self) -> PathBuf {
445 let install = self.path.join("install");
446 if install.is_dir() {
447 install
448 } else {
449 self.path.clone()
450 }
451 }
452
453 pub fn version(&self) -> PythonVersion {
455 self.key.version()
456 }
457
458 pub fn implementation(&self) -> ImplementationName {
459 match self.key.implementation().into_owned() {
460 LenientImplementationName::Known(implementation) => implementation,
461 LenientImplementationName::Unknown(_) => {
462 panic!("Managed Python installations should have a known implementation")
463 }
464 }
465 }
466
467 pub fn path(&self) -> &Path {
468 &self.path
469 }
470
471 pub fn key(&self) -> &PythonInstallationKey {
472 &self.key
473 }
474
475 pub fn platform(&self) -> &Platform {
476 self.key.platform()
477 }
478
479 pub fn build(&self) -> Option<&str> {
481 self.build.as_deref()
482 }
483
484 pub fn minor_version_key(&self) -> &PythonInstallationMinorVersionKey {
485 PythonInstallationMinorVersionKey::ref_cast(&self.key)
486 }
487
488 pub fn satisfies(&self, request: &PythonRequest) -> bool {
489 match request {
490 PythonRequest::File(path) => self.executable(false) == *path,
491 PythonRequest::Default | PythonRequest::Any => true,
492 PythonRequest::Directory(path) => self.path() == *path,
493 PythonRequest::ExecutableName(name) => self
494 .executable(false)
495 .file_name()
496 .is_some_and(|filename| filename.to_string_lossy() == *name),
497 PythonRequest::Implementation(implementation) => {
498 *implementation == self.implementation()
499 }
500 PythonRequest::ImplementationVersion(implementation, version) => {
501 *implementation == self.implementation() && version.matches_version(&self.version())
502 }
503 PythonRequest::Version(version) => version.matches_version(&self.version()),
504 PythonRequest::Key(request) => request.satisfied_by_key(self.key()),
505 }
506 }
507
508 pub fn ensure_canonical_executables(&self) -> Result<(), Error> {
510 let python = self.executable(false);
511
512 let canonical_names = &["python"];
513
514 for name in canonical_names {
515 let executable =
516 python.with_file_name(format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX));
517
518 if executable == python {
521 continue;
522 }
523
524 match symlink_or_copy_file(&python, &executable) {
525 Ok(()) => {
526 debug!(
527 "Created link {} -> {}",
528 executable.user_display(),
529 python.user_display(),
530 );
531 }
532 Err(err) if err.kind() == io::ErrorKind::NotFound => {
533 return Err(Error::MissingExecutable(python.clone()));
534 }
535 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
536 Err(err) => {
537 return Err(Error::CanonicalizeExecutable {
538 from: executable,
539 to: python,
540 err,
541 });
542 }
543 }
544 }
545
546 Ok(())
547 }
548
549 pub fn ensure_minor_version_link(&self, preview: Preview) -> Result<(), Error> {
552 if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self, preview) {
553 minor_version_link.create_directory()?;
554 }
555 Ok(())
556 }
557
558 pub fn update_minor_version_link(&self, preview: Preview) -> Result<(), Error> {
564 if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self, preview) {
565 if !minor_version_link.exists() {
566 return Ok(());
567 }
568 minor_version_link.create_directory()?;
569 }
570 Ok(())
571 }
572
573 pub fn ensure_externally_managed(&self) -> Result<(), Error> {
576 if self.key.os().is_emscripten() {
577 return Ok(());
580 }
581 let stdlib = if self.key.os().is_windows() {
583 self.python_dir().join("Lib")
584 } else {
585 let lib_suffix = self.key.variant.lib_suffix();
586 let python = if matches!(
587 self.key.implementation,
588 LenientImplementationName::Known(ImplementationName::PyPy)
589 ) {
590 format!("pypy{}", self.key.version().python_version())
591 } else {
592 format!("python{}{lib_suffix}", self.key.version().python_version())
593 };
594 self.python_dir().join("lib").join(python)
595 };
596
597 let file = stdlib.join("EXTERNALLY-MANAGED");
598 fs_err::write(file, EXTERNALLY_MANAGED)?;
599
600 Ok(())
601 }
602
603 pub fn ensure_sysconfig_patched(&self) -> Result<(), Error> {
605 if cfg!(unix) {
606 if self.key.os().is_emscripten() {
607 return Ok(());
610 }
611 if self.implementation() == ImplementationName::CPython {
612 sysconfig::update_sysconfig(
613 self.path(),
614 self.key.major,
615 self.key.minor,
616 self.key.variant.lib_suffix(),
617 )?;
618 }
619 }
620 Ok(())
621 }
622
623 pub fn ensure_dylib_patched(&self) -> Result<(), macos_dylib::Error> {
630 if cfg!(target_os = "macos") {
631 if self.key().os().is_like_darwin() {
632 if self.implementation() == ImplementationName::CPython {
633 let dylib_path = self.python_dir().join("lib").join(format!(
634 "{}python{}{}{}",
635 std::env::consts::DLL_PREFIX,
636 self.key.version().python_version(),
637 self.key.variant().executable_suffix(),
638 std::env::consts::DLL_SUFFIX
639 ));
640 macos_dylib::patch_dylib_install_name(dylib_path)?;
641 }
642 }
643 }
644 Ok(())
645 }
646
647 pub fn ensure_build_file(&self) -> Result<(), Error> {
649 if let Some(ref build) = self.build {
650 let build_file = self.path.join("BUILD");
651 fs::write(&build_file, build.as_ref())?;
652 }
653 Ok(())
654 }
655
656 pub fn is_bin_link(&self, path: &Path) -> bool {
659 if cfg!(unix) {
660 same_file::is_same_file(path, self.executable(false)).unwrap_or_default()
661 } else if cfg!(windows) {
662 let Some(launcher) = Launcher::try_from_path(path).unwrap_or_default() else {
663 return false;
664 };
665 if !matches!(launcher.kind, LauncherKind::Python) {
666 return false;
667 }
668 dunce::canonicalize(&launcher.python_path).unwrap_or(launcher.python_path)
672 == self.executable(false)
673 } else {
674 unreachable!("Only Windows and Unix are supported")
675 }
676 }
677
678 pub fn is_upgrade_of(&self, other: &Self) -> bool {
680 if self.key.implementation != other.key.implementation {
682 return false;
683 }
684 if self.key.variant != other.key.variant {
686 return false;
687 }
688 if (self.key.major, self.key.minor) != (other.key.major, other.key.minor) {
690 return false;
691 }
692 if self.key.patch == other.key.patch {
694 return match (self.key.prerelease, other.key.prerelease) {
695 (Some(self_pre), Some(other_pre)) => self_pre > other_pre,
697 (None, Some(_)) => true,
699 (_, None) => false,
701 };
702 }
703 if self.key.patch < other.key.patch {
705 return false;
706 }
707 true
708 }
709
710 pub fn url(&self) -> Option<&str> {
711 self.url.as_deref()
712 }
713
714 pub fn sha256(&self) -> Option<&str> {
715 self.sha256.as_deref()
716 }
717}
718
719#[derive(Clone, Debug)]
722pub struct PythonMinorVersionLink {
723 pub symlink_directory: PathBuf,
725 pub symlink_executable: PathBuf,
728 pub target_directory: PathBuf,
731}
732
733impl PythonMinorVersionLink {
734 pub fn from_executable(
754 executable: &Path,
755 key: &PythonInstallationKey,
756 preview: Preview,
757 ) -> Option<Self> {
758 let implementation = key.implementation();
759 if !matches!(
760 implementation.as_ref(),
761 LenientImplementationName::Known(ImplementationName::CPython)
762 ) {
763 return None;
765 }
766 let executable_name = executable
767 .file_name()
768 .expect("Executable file name should exist");
769 let symlink_directory_name = PythonInstallationMinorVersionKey::ref_cast(key).to_string();
770 let parent = executable
771 .parent()
772 .expect("Executable should have parent directory");
773
774 let target_directory = if cfg!(unix) {
776 if parent
777 .components()
778 .next_back()
779 .is_some_and(|c| c.as_os_str() == "bin")
780 {
781 parent.parent()?.to_path_buf()
782 } else {
783 return None;
784 }
785 } else if cfg!(windows) {
786 parent.to_path_buf()
787 } else {
788 unimplemented!("Only Windows and Unix systems are supported.")
789 };
790 let symlink_directory = target_directory.with_file_name(symlink_directory_name);
791 if target_directory == symlink_directory {
793 return None;
794 }
795 let symlink_executable = executable_path_from_base(
797 symlink_directory.as_path(),
798 &executable_name.to_string_lossy(),
799 &implementation,
800 *key.os(),
801 );
802 let minor_version_link = Self {
803 symlink_directory,
804 symlink_executable,
805 target_directory,
806 };
807 if !preview.is_enabled(PreviewFeatures::PYTHON_UPGRADE) && !minor_version_link.exists() {
811 return None;
812 }
813 Some(minor_version_link)
814 }
815
816 pub fn from_installation(
817 installation: &ManagedPythonInstallation,
818 preview: Preview,
819 ) -> Option<Self> {
820 Self::from_executable(
821 installation.executable(false).as_path(),
822 installation.key(),
823 preview,
824 )
825 }
826
827 pub fn create_directory(&self) -> Result<(), Error> {
828 match replace_symlink(
829 self.target_directory.as_path(),
830 self.symlink_directory.as_path(),
831 ) {
832 Ok(()) => {
833 debug!(
834 "Created link {} -> {}",
835 &self.symlink_directory.user_display(),
836 &self.target_directory.user_display(),
837 );
838 }
839 Err(err) if err.kind() == io::ErrorKind::NotFound => {
840 return Err(Error::MissingPythonMinorVersionLinkTargetDirectory(
841 self.target_directory.clone(),
842 ));
843 }
844 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
845 Err(err) => {
846 return Err(Error::PythonMinorVersionLinkDirectory {
847 from: self.symlink_directory.clone(),
848 to: self.target_directory.clone(),
849 err,
850 });
851 }
852 }
853 Ok(())
854 }
855
856 pub fn exists(&self) -> bool {
857 #[cfg(unix)]
858 {
859 self.symlink_directory
860 .symlink_metadata()
861 .map(|metadata| metadata.file_type().is_symlink())
862 .unwrap_or(false)
863 }
864 #[cfg(windows)]
865 {
866 self.symlink_directory
867 .symlink_metadata()
868 .is_ok_and(|metadata| {
869 (metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0) != 0
872 })
873 }
874 }
875}
876
877fn executable_path_from_base(
881 base: &Path,
882 executable_name: &str,
883 implementation: &LenientImplementationName,
884 os: Os,
885) -> PathBuf {
886 if matches!(
887 implementation,
888 &LenientImplementationName::Known(ImplementationName::GraalPy)
889 ) {
890 base.join("bin").join(executable_name)
892 } else if os.is_emscripten()
893 || matches!(
894 implementation,
895 &LenientImplementationName::Known(ImplementationName::Pyodide)
896 )
897 {
898 base.join(executable_name)
900 } else if os.is_windows() {
901 base.join(executable_name)
903 } else {
904 base.join("bin").join(executable_name)
906 }
907}
908
909pub fn create_link_to_executable(link: &Path, executable: &Path) -> Result<(), Error> {
913 let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?;
914 fs_err::create_dir_all(link_parent).map_err(|err| Error::ExecutableDirectory {
915 to: link_parent.to_path_buf(),
916 err,
917 })?;
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 {
927 from: executable.to_path_buf(),
928 to: link.to_path_buf(),
929 err,
930 }),
931 }
932 } else if cfg!(windows) {
933 use uv_trampoline_builder::windows_python_launcher;
934
935 let launcher = windows_python_launcher(executable, false)?;
937
938 #[allow(clippy::disallowed_types)]
941 {
942 std::fs::File::create_new(link)
943 .and_then(|mut file| file.write_all(launcher.as_ref()))
944 .map_err(|err| Error::LinkExecutable {
945 from: executable.to_path_buf(),
946 to: link.to_path_buf(),
947 err,
948 })
949 }
950 } else {
951 unimplemented!("Only Windows and Unix are supported.")
952 }
953}
954
955pub fn platform_key_from_env() -> Result<String, Error> {
958 Ok(Platform::from_env()?.to_string().to_lowercase())
959}
960
961impl fmt::Display for ManagedPythonInstallation {
962 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
963 write!(
964 f,
965 "{}",
966 self.path
967 .file_name()
968 .unwrap_or(self.path.as_os_str())
969 .to_string_lossy()
970 )
971 }
972}
973
974pub fn python_executable_dir() -> Result<PathBuf, Error> {
976 uv_dirs::user_executable_directory(Some(EnvVars::UV_PYTHON_BIN_DIR))
977 .ok_or(Error::NoExecutableDirectory)
978}
979
980#[cfg(test)]
981mod tests {
982 use super::*;
983 use crate::implementation::LenientImplementationName;
984 use crate::installation::PythonInstallationKey;
985 use crate::{ImplementationName, PythonVariant};
986 use std::path::PathBuf;
987 use std::str::FromStr;
988 use uv_pep440::{Prerelease, PrereleaseKind};
989 use uv_platform::Platform;
990
991 fn create_test_installation(
992 implementation: ImplementationName,
993 major: u8,
994 minor: u8,
995 patch: u8,
996 prerelease: Option<Prerelease>,
997 variant: PythonVariant,
998 ) -> ManagedPythonInstallation {
999 let platform = Platform::from_str("linux-x86_64-gnu").unwrap();
1000 let key = PythonInstallationKey::new(
1001 LenientImplementationName::Known(implementation),
1002 major,
1003 minor,
1004 patch,
1005 prerelease,
1006 platform,
1007 variant,
1008 );
1009 ManagedPythonInstallation {
1010 path: PathBuf::from("/test/path"),
1011 key,
1012 url: None,
1013 sha256: None,
1014 build: None,
1015 }
1016 }
1017
1018 #[test]
1019 fn test_is_upgrade_of_same_version() {
1020 let installation = create_test_installation(
1021 ImplementationName::CPython,
1022 3,
1023 10,
1024 8,
1025 None,
1026 PythonVariant::Default,
1027 );
1028
1029 assert!(!installation.is_upgrade_of(&installation));
1031 }
1032
1033 #[test]
1034 fn test_is_upgrade_of_patch_version() {
1035 let older = create_test_installation(
1036 ImplementationName::CPython,
1037 3,
1038 10,
1039 8,
1040 None,
1041 PythonVariant::Default,
1042 );
1043 let newer = create_test_installation(
1044 ImplementationName::CPython,
1045 3,
1046 10,
1047 9,
1048 None,
1049 PythonVariant::Default,
1050 );
1051
1052 assert!(newer.is_upgrade_of(&older));
1054 assert!(!older.is_upgrade_of(&newer));
1056 }
1057
1058 #[test]
1059 fn test_is_upgrade_of_different_minor_version() {
1060 let py310 = create_test_installation(
1061 ImplementationName::CPython,
1062 3,
1063 10,
1064 8,
1065 None,
1066 PythonVariant::Default,
1067 );
1068 let py311 = create_test_installation(
1069 ImplementationName::CPython,
1070 3,
1071 11,
1072 0,
1073 None,
1074 PythonVariant::Default,
1075 );
1076
1077 assert!(!py311.is_upgrade_of(&py310));
1079 assert!(!py310.is_upgrade_of(&py311));
1080 }
1081
1082 #[test]
1083 fn test_is_upgrade_of_different_implementation() {
1084 let cpython = create_test_installation(
1085 ImplementationName::CPython,
1086 3,
1087 10,
1088 8,
1089 None,
1090 PythonVariant::Default,
1091 );
1092 let pypy = create_test_installation(
1093 ImplementationName::PyPy,
1094 3,
1095 10,
1096 9,
1097 None,
1098 PythonVariant::Default,
1099 );
1100
1101 assert!(!pypy.is_upgrade_of(&cpython));
1103 assert!(!cpython.is_upgrade_of(&pypy));
1104 }
1105
1106 #[test]
1107 fn test_is_upgrade_of_different_variant() {
1108 let default = create_test_installation(
1109 ImplementationName::CPython,
1110 3,
1111 10,
1112 8,
1113 None,
1114 PythonVariant::Default,
1115 );
1116 let freethreaded = create_test_installation(
1117 ImplementationName::CPython,
1118 3,
1119 10,
1120 9,
1121 None,
1122 PythonVariant::Freethreaded,
1123 );
1124
1125 assert!(!freethreaded.is_upgrade_of(&default));
1127 assert!(!default.is_upgrade_of(&freethreaded));
1128 }
1129
1130 #[test]
1131 fn test_is_upgrade_of_prerelease() {
1132 let stable = create_test_installation(
1133 ImplementationName::CPython,
1134 3,
1135 10,
1136 8,
1137 None,
1138 PythonVariant::Default,
1139 );
1140 let prerelease = create_test_installation(
1141 ImplementationName::CPython,
1142 3,
1143 10,
1144 8,
1145 Some(Prerelease {
1146 kind: PrereleaseKind::Alpha,
1147 number: 1,
1148 }),
1149 PythonVariant::Default,
1150 );
1151
1152 assert!(stable.is_upgrade_of(&prerelease));
1154
1155 assert!(!prerelease.is_upgrade_of(&stable));
1157 }
1158
1159 #[test]
1160 fn test_is_upgrade_of_prerelease_to_prerelease() {
1161 let alpha1 = create_test_installation(
1162 ImplementationName::CPython,
1163 3,
1164 10,
1165 8,
1166 Some(Prerelease {
1167 kind: PrereleaseKind::Alpha,
1168 number: 1,
1169 }),
1170 PythonVariant::Default,
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 );
1183
1184 assert!(alpha2.is_upgrade_of(&alpha1));
1186 assert!(!alpha1.is_upgrade_of(&alpha2));
1188 }
1189
1190 #[test]
1191 fn test_is_upgrade_of_prerelease_same_patch() {
1192 let prerelease = create_test_installation(
1193 ImplementationName::CPython,
1194 3,
1195 10,
1196 8,
1197 Some(Prerelease {
1198 kind: PrereleaseKind::Alpha,
1199 number: 1,
1200 }),
1201 PythonVariant::Default,
1202 );
1203
1204 assert!(!prerelease.is_upgrade_of(&prerelease));
1206 }
1207}