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};
15use uv_preview::{Preview, PreviewFeature};
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
115pub fn compare_build_versions(a: &str, b: &str) -> Ordering {
120 match (a.parse::<u64>(), b.parse::<u64>()) {
121 (Ok(a_num), Ok(b_num)) => a_num.cmp(&b_num),
122 _ => a.cmp(b),
123 }
124}
125
126#[derive(Debug, Clone, Eq, PartialEq)]
128pub struct ManagedPythonInstallations {
129 root: PathBuf,
131}
132
133impl ManagedPythonInstallations {
134 fn from_path(root: impl Into<PathBuf>) -> Self {
136 Self { root: root.into() }
137 }
138
139 pub async fn lock(&self) -> Result<LockedFile, Error> {
142 Ok(LockedFile::acquire(
143 self.root.join(".lock"),
144 LockedFileMode::Exclusive,
145 self.root.user_display(),
146 )
147 .await?)
148 }
149
150 pub fn from_settings(install_dir: Option<PathBuf>) -> Result<Self, Error> {
157 if let Some(install_dir) = install_dir {
158 Ok(Self::from_path(install_dir))
159 } else if let Some(install_dir) =
160 std::env::var_os(EnvVars::UV_PYTHON_INSTALL_DIR).filter(|s| !s.is_empty())
161 {
162 Ok(Self::from_path(install_dir))
163 } else {
164 Ok(Self::from_path(
165 StateStore::from_settings(None)?.bucket(StateBucket::ManagedPython),
166 ))
167 }
168 }
169
170 pub fn temp() -> Result<Self, Error> {
172 Ok(Self::from_path(
173 StateStore::temp()?.bucket(StateBucket::ManagedPython),
174 ))
175 }
176
177 pub fn scratch(&self) -> PathBuf {
179 self.root.join(".temp")
180 }
181
182 pub fn init(self) -> Result<Self, Error> {
186 let root = &self.root;
187
188 if !root.exists()
190 && root
191 .parent()
192 .is_some_and(|parent| parent.join("toolchains").exists())
193 {
194 let deprecated = root.parent().unwrap().join("toolchains");
195 fs::rename(&deprecated, root)?;
197 uv_fs::replace_symlink(root, &deprecated)?;
199 } else {
200 fs::create_dir_all(root)?;
201 }
202
203 fs::create_dir_all(root)?;
205
206 let scratch = self.scratch();
208 fs::create_dir_all(&scratch)?;
209
210 match fs::OpenOptions::new()
212 .write(true)
213 .create_new(true)
214 .open(root.join(".gitignore"))
215 {
216 Ok(mut file) => file.write_all(b"*")?,
217 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
218 Err(err) => return Err(err.into()),
219 }
220
221 Ok(self)
222 }
223
224 pub fn find_all(
229 &self,
230 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + use<>, Error> {
231 let dirs = match fs_err::read_dir(&self.root) {
232 Ok(installation_dirs) => {
233 let directories: Vec<_> = installation_dirs
235 .filter_map(|read_dir| match read_dir {
236 Ok(entry) => match entry.file_type() {
237 Ok(file_type) => file_type.is_dir().then_some(Ok(entry.path())),
238 Err(err) => Some(Err(err)),
239 },
240 Err(err) => Some(Err(err)),
241 })
242 .collect::<Result<_, io::Error>>()
243 .map_err(|err| Error::ReadError {
244 dir: self.root.clone(),
245 err,
246 })?;
247 directories
248 }
249 Err(err) if err.kind() == io::ErrorKind::NotFound => vec![],
250 Err(err) => {
251 return Err(Error::ReadError {
252 dir: self.root.clone(),
253 err,
254 });
255 }
256 };
257 let scratch = self.scratch();
258 Ok(dirs
259 .into_iter()
260 .filter(|path| *path != scratch)
262 .filter(|path| {
264 path.file_name()
265 .and_then(OsStr::to_str)
266 .map(|name| !name.starts_with('.'))
267 .unwrap_or(true)
268 })
269 .filter_map(|path| {
270 ManagedPythonInstallation::from_path(path)
271 .inspect_err(|err| {
272 warn!("Ignoring malformed managed Python entry:\n {err}");
273 })
274 .ok()
275 })
276 .sorted_unstable_by_key(|installation| Reverse(installation.key().clone())))
277 }
278
279 pub fn find_matching_current_platform(
281 &self,
282 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + use<>, Error> {
283 let platform = Platform::from_env()?;
284
285 let iter = Self::from_settings(None)?
286 .find_all()?
287 .filter(move |installation| {
288 if !platform.supports(installation.platform()) {
289 debug!("Skipping managed installation `{installation}`: not supported by current platform `{platform}`");
290 return false;
291 }
292 true
293 });
294
295 Ok(iter)
296 }
297
298 pub fn find_version<'a>(
305 &'a self,
306 version: &'a PythonVersion,
307 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + 'a, Error> {
308 Ok(self
309 .find_matching_current_platform()?
310 .filter(move |installation| {
311 installation
312 .path
313 .file_name()
314 .map(OsStr::to_string_lossy)
315 .is_some_and(|filename| filename.starts_with(&format!("cpython-{version}")))
316 }))
317 }
318
319 pub fn root(&self) -> &Path {
320 &self.root
321 }
322}
323
324static EXTERNALLY_MANAGED: &str = "[externally-managed]
325Error=This Python installation is managed by uv and should not be modified.
326";
327
328#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
330pub struct ManagedPythonInstallation {
331 path: PathBuf,
333 key: PythonInstallationKey,
335 url: Option<Cow<'static, str>>,
339 sha256: Option<Cow<'static, str>>,
343 build: Option<Cow<'static, str>>,
347}
348
349impl ManagedPythonInstallation {
350 pub fn new(path: PathBuf, download: &ManagedPythonDownload) -> Self {
351 Self {
352 path,
353 key: download.key().clone(),
354 url: Some(download.url().clone()),
355 sha256: download.sha256().cloned(),
356 build: download.build().map(Cow::Borrowed),
357 }
358 }
359
360 pub(crate) fn from_path(path: PathBuf) -> Result<Self, Error> {
361 let key = PythonInstallationKey::from_str(
362 path.file_name()
363 .ok_or(Error::NameError("name is empty".to_string()))?
364 .to_str()
365 .ok_or(Error::NameError("not a valid string".to_string()))?,
366 )?;
367
368 let path = std::path::absolute(&path).map_err(|err| Error::AbsolutePath(path, err))?;
369
370 let build = match fs::read_to_string(path.join("BUILD")) {
372 Ok(content) => Some(Cow::Owned(content.trim().to_string())),
373 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
374 Err(err) => return Err(err.into()),
375 };
376
377 Ok(Self {
378 path,
379 key,
380 url: None,
381 sha256: None,
382 build,
383 })
384 }
385
386 pub fn executable(&self, windowed: bool) -> PathBuf {
395 let version = match self.implementation() {
396 ImplementationName::CPython => {
397 if cfg!(unix) {
398 format!("{}.{}", self.key.major, self.key.minor)
399 } else {
400 String::new()
401 }
402 }
403 ImplementationName::PyPy => format!("{}.{}", self.key.major, self.key.minor),
405 ImplementationName::Pyodide => String::new(),
407 ImplementationName::GraalPy => String::new(),
408 };
409
410 let variant = if self.implementation() == ImplementationName::GraalPy {
413 ""
414 } else if cfg!(unix) {
415 self.key.variant.executable_suffix()
416 } else if cfg!(windows) && windowed {
417 "w"
419 } else {
420 ""
421 };
422
423 let name = format!(
424 "{implementation}{version}{variant}{exe}",
425 implementation = self.implementation().executable_name(),
426 exe = std::env::consts::EXE_SUFFIX
427 );
428
429 let executable = executable_path_from_base(
430 self.python_dir().as_path(),
431 &name,
432 &LenientImplementationName::from(self.implementation()),
433 *self.key.os(),
434 );
435
436 if cfg!(windows)
441 && matches!(self.key.variant, PythonVariant::Freethreaded)
442 && !executable.exists()
443 {
444 return self.python_dir().join(format!(
446 "python{}.{}t{}",
447 self.key.major,
448 self.key.minor,
449 std::env::consts::EXE_SUFFIX
450 ));
451 }
452
453 executable
454 }
455
456 fn python_dir(&self) -> PathBuf {
457 let install = self.path.join("install");
458 if install.is_dir() {
459 install
460 } else {
461 self.path.clone()
462 }
463 }
464
465 pub fn version(&self) -> PythonVersion {
467 self.key.version()
468 }
469
470 pub fn implementation(&self) -> ImplementationName {
471 match self.key.implementation().into_owned() {
472 LenientImplementationName::Known(implementation) => implementation,
473 LenientImplementationName::Unknown(_) => {
474 panic!("Managed Python installations should have a known implementation")
475 }
476 }
477 }
478
479 pub fn path(&self) -> &Path {
480 &self.path
481 }
482
483 pub fn key(&self) -> &PythonInstallationKey {
484 &self.key
485 }
486
487 pub fn platform(&self) -> &Platform {
488 self.key.platform()
489 }
490
491 pub fn build(&self) -> Option<&str> {
493 self.build.as_deref()
494 }
495
496 pub fn minor_version_key(&self) -> &PythonInstallationMinorVersionKey {
497 PythonInstallationMinorVersionKey::ref_cast(&self.key)
498 }
499
500 pub fn satisfies(&self, request: &PythonRequest) -> bool {
501 match request {
502 PythonRequest::File(path) => self.executable(false) == *path,
503 PythonRequest::Default | PythonRequest::Any => true,
504 PythonRequest::Directory(path) => self.path() == *path,
505 PythonRequest::ExecutableName(name) => self
506 .executable(false)
507 .file_name()
508 .is_some_and(|filename| filename.to_string_lossy() == *name),
509 PythonRequest::Implementation(implementation) => {
510 *implementation == self.implementation()
511 }
512 PythonRequest::ImplementationVersion(implementation, version) => {
513 *implementation == self.implementation() && version.matches_version(&self.version())
514 }
515 PythonRequest::Version(version) => version.matches_version(&self.version()),
516 PythonRequest::Key(request) => request.satisfied_by_key(self.key()),
517 }
518 }
519
520 pub fn ensure_canonical_executables(&self) -> Result<(), Error> {
522 let python = self.executable(false);
523
524 let canonical_names = &["python"];
525
526 for name in canonical_names {
527 let executable =
528 python.with_file_name(format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX));
529
530 if executable == python {
533 continue;
534 }
535
536 match symlink_or_copy_file(&python, &executable) {
537 Ok(()) => {
538 debug!(
539 "Created link {} -> {}",
540 executable.user_display(),
541 python.user_display(),
542 );
543 }
544 Err(err) if err.kind() == io::ErrorKind::NotFound => {
545 return Err(Error::MissingExecutable(python.clone()));
546 }
547 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
548 Err(err) => {
549 return Err(Error::CanonicalizeExecutable {
550 from: executable,
551 to: python,
552 err,
553 });
554 }
555 }
556 }
557
558 Ok(())
559 }
560
561 pub fn ensure_minor_version_link(&self, preview: Preview) -> Result<(), Error> {
564 if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self, preview) {
565 minor_version_link.create_directory()?;
566 }
567 Ok(())
568 }
569
570 pub fn update_minor_version_link(&self, preview: Preview) -> Result<(), Error> {
576 if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self, preview) {
577 if !minor_version_link.exists() {
578 return Ok(());
579 }
580 minor_version_link.create_directory()?;
581 }
582 Ok(())
583 }
584
585 pub fn ensure_externally_managed(&self) -> Result<(), Error> {
588 if self.key.os().is_emscripten() {
589 return Ok(());
592 }
593 let stdlib = if self.key.os().is_windows() {
595 self.python_dir().join("Lib")
596 } else {
597 let lib_suffix = self.key.variant.lib_suffix();
598 let python = if matches!(
599 self.key.implementation,
600 LenientImplementationName::Known(ImplementationName::PyPy)
601 ) {
602 format!("pypy{}", self.key.version().python_version())
603 } else {
604 format!("python{}{lib_suffix}", self.key.version().python_version())
605 };
606 self.python_dir().join("lib").join(python)
607 };
608
609 let file = stdlib.join("EXTERNALLY-MANAGED");
610 fs_err::write(file, EXTERNALLY_MANAGED)?;
611
612 Ok(())
613 }
614
615 pub fn ensure_sysconfig_patched(&self) -> Result<(), Error> {
617 if cfg!(unix) {
618 if self.key.os().is_emscripten() {
619 return Ok(());
622 }
623 if self.implementation() == ImplementationName::CPython {
624 sysconfig::update_sysconfig(
625 self.path(),
626 self.key.major,
627 self.key.minor,
628 self.key.variant.lib_suffix(),
629 )?;
630 }
631 }
632 Ok(())
633 }
634
635 pub fn ensure_dylib_patched(&self) -> Result<(), macos_dylib::Error> {
642 if cfg!(target_os = "macos") {
643 if self.key().os().is_like_darwin() {
644 if self.implementation() == ImplementationName::CPython {
645 let dylib_path = self.python_dir().join("lib").join(format!(
646 "{}python{}{}{}",
647 std::env::consts::DLL_PREFIX,
648 self.key.version().python_version(),
649 self.key.variant().executable_suffix(),
650 std::env::consts::DLL_SUFFIX
651 ));
652 macos_dylib::patch_dylib_install_name(dylib_path)?;
653 }
654 }
655 }
656 Ok(())
657 }
658
659 pub fn ensure_build_file(&self) -> Result<(), Error> {
661 if let Some(ref build) = self.build {
662 let build_file = self.path.join("BUILD");
663 fs::write(&build_file, build.as_ref())?;
664 }
665 Ok(())
666 }
667
668 pub fn is_bin_link(&self, path: &Path) -> bool {
671 if cfg!(unix) {
672 same_file::is_same_file(path, self.executable(false)).unwrap_or_default()
673 } else if cfg!(windows) {
674 let Some(launcher) = Launcher::try_from_path(path).unwrap_or_default() else {
675 return false;
676 };
677 if !matches!(launcher.kind, LauncherKind::Python) {
678 return false;
679 }
680 dunce::canonicalize(&launcher.python_path).unwrap_or(launcher.python_path)
684 == self.executable(false)
685 } else {
686 unreachable!("Only Windows and Unix are supported")
687 }
688 }
689
690 pub fn is_upgrade_of(&self, other: &Self) -> bool {
692 if self.key.implementation != other.key.implementation {
694 return false;
695 }
696 if self.key.variant != other.key.variant {
698 return false;
699 }
700 if (self.key.major, self.key.minor) != (other.key.major, other.key.minor) {
702 return false;
703 }
704 if self.key.patch == other.key.patch {
707 return match (self.key.prerelease, other.key.prerelease) {
708 (Some(self_pre), Some(other_pre)) => self_pre > other_pre,
710 (None, Some(_)) => true,
712 (Some(_), None) => false,
714 (None, None) => match (self.build.as_deref(), other.build.as_deref()) {
716 (Some(_), None) => true,
718 (Some(self_build), Some(other_build)) => {
720 compare_build_versions(self_build, other_build) == Ordering::Greater
721 }
722 (None, _) => false,
724 },
725 };
726 }
727 if self.key.patch < other.key.patch {
729 return false;
730 }
731 true
732 }
733
734 pub fn url(&self) -> Option<&str> {
735 self.url.as_deref()
736 }
737
738 pub fn sha256(&self) -> Option<&str> {
739 self.sha256.as_deref()
740 }
741}
742
743#[derive(Clone, Debug)]
746pub struct PythonMinorVersionLink {
747 pub symlink_directory: PathBuf,
749 pub symlink_executable: PathBuf,
752 pub target_directory: PathBuf,
755}
756
757impl PythonMinorVersionLink {
758 pub fn from_executable(
778 executable: &Path,
779 key: &PythonInstallationKey,
780 preview: Preview,
781 ) -> Option<Self> {
782 let implementation = key.implementation();
783 if !matches!(
784 implementation.as_ref(),
785 LenientImplementationName::Known(ImplementationName::CPython)
786 ) {
787 return None;
789 }
790 let executable_name = executable
791 .file_name()
792 .expect("Executable file name should exist");
793 let symlink_directory_name = PythonInstallationMinorVersionKey::ref_cast(key).to_string();
794 let parent = executable
795 .parent()
796 .expect("Executable should have parent directory");
797
798 let target_directory = if cfg!(unix) {
800 if parent
801 .components()
802 .next_back()
803 .is_some_and(|c| c.as_os_str() == "bin")
804 {
805 parent.parent()?.to_path_buf()
806 } else {
807 return None;
808 }
809 } else if cfg!(windows) {
810 parent.to_path_buf()
811 } else {
812 unimplemented!("Only Windows and Unix systems are supported.")
813 };
814 let symlink_directory = target_directory.with_file_name(symlink_directory_name);
815 if target_directory == symlink_directory {
817 return None;
818 }
819 let symlink_executable = executable_path_from_base(
821 symlink_directory.as_path(),
822 &executable_name.to_string_lossy(),
823 &implementation,
824 *key.os(),
825 );
826 let minor_version_link = Self {
827 symlink_directory,
828 symlink_executable,
829 target_directory,
830 };
831 if !preview.is_enabled(PreviewFeature::PythonUpgrade) && !minor_version_link.exists() {
835 return None;
836 }
837 Some(minor_version_link)
838 }
839
840 pub fn from_installation(
841 installation: &ManagedPythonInstallation,
842 preview: Preview,
843 ) -> Option<Self> {
844 Self::from_executable(
845 installation.executable(false).as_path(),
846 installation.key(),
847 preview,
848 )
849 }
850
851 pub fn create_directory(&self) -> Result<(), Error> {
852 match replace_symlink(
853 self.target_directory.as_path(),
854 self.symlink_directory.as_path(),
855 ) {
856 Ok(()) => {
857 debug!(
858 "Created link {} -> {}",
859 &self.symlink_directory.user_display(),
860 &self.target_directory.user_display(),
861 );
862 }
863 Err(err) if err.kind() == io::ErrorKind::NotFound => {
864 return Err(Error::MissingPythonMinorVersionLinkTargetDirectory(
865 self.target_directory.clone(),
866 ));
867 }
868 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
869 Err(err) => {
870 return Err(Error::PythonMinorVersionLinkDirectory {
871 from: self.symlink_directory.clone(),
872 to: self.target_directory.clone(),
873 err,
874 });
875 }
876 }
877 Ok(())
878 }
879
880 pub fn exists(&self) -> bool {
881 #[cfg(unix)]
882 {
883 self.symlink_directory
884 .symlink_metadata()
885 .map(|metadata| metadata.file_type().is_symlink())
886 .unwrap_or(false)
887 }
888 #[cfg(windows)]
889 {
890 self.symlink_directory
891 .symlink_metadata()
892 .is_ok_and(|metadata| {
893 (metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0) != 0
896 })
897 }
898 }
899}
900
901fn executable_path_from_base(
905 base: &Path,
906 executable_name: &str,
907 implementation: &LenientImplementationName,
908 os: Os,
909) -> PathBuf {
910 if matches!(
911 implementation,
912 &LenientImplementationName::Known(ImplementationName::GraalPy)
913 ) {
914 base.join("bin").join(executable_name)
916 } else if os.is_emscripten()
917 || matches!(
918 implementation,
919 &LenientImplementationName::Known(ImplementationName::Pyodide)
920 )
921 {
922 base.join(executable_name)
924 } else if os.is_windows() {
925 base.join(executable_name)
927 } else {
928 base.join("bin").join(executable_name)
930 }
931}
932
933pub fn create_link_to_executable(link: &Path, executable: &Path) -> Result<(), Error> {
937 let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?;
938 fs_err::create_dir_all(link_parent).map_err(|err| Error::ExecutableDirectory {
939 to: link_parent.to_path_buf(),
940 err,
941 })?;
942
943 if cfg!(unix) {
944 match symlink_or_copy_file(executable, link) {
946 Ok(()) => Ok(()),
947 Err(err) if err.kind() == io::ErrorKind::NotFound => {
948 Err(Error::MissingExecutable(executable.to_path_buf()))
949 }
950 Err(err) => Err(Error::LinkExecutable {
951 from: executable.to_path_buf(),
952 to: link.to_path_buf(),
953 err,
954 }),
955 }
956 } else if cfg!(windows) {
957 use uv_trampoline_builder::windows_python_launcher;
958
959 let launcher = windows_python_launcher(executable, false)?;
961
962 #[expect(clippy::disallowed_types)]
965 {
966 std::fs::File::create_new(link)
967 .and_then(|mut file| file.write_all(launcher.as_ref()))
968 .map_err(|err| Error::LinkExecutable {
969 from: executable.to_path_buf(),
970 to: link.to_path_buf(),
971 err,
972 })
973 }
974 } else {
975 unimplemented!("Only Windows and Unix are supported.")
976 }
977}
978
979pub fn platform_key_from_env() -> Result<String, Error> {
982 Ok(Platform::from_env()?.to_string().to_lowercase())
983}
984
985impl fmt::Display for ManagedPythonInstallation {
986 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
987 write!(
988 f,
989 "{}",
990 self.path
991 .file_name()
992 .unwrap_or(self.path.as_os_str())
993 .to_string_lossy()
994 )
995 }
996}
997
998pub fn python_executable_dir() -> Result<PathBuf, Error> {
1000 uv_dirs::user_executable_directory(Some(EnvVars::UV_PYTHON_BIN_DIR))
1001 .ok_or(Error::NoExecutableDirectory)
1002}
1003
1004#[cfg(test)]
1005mod tests {
1006 use super::*;
1007 use crate::implementation::LenientImplementationName;
1008 use crate::installation::PythonInstallationKey;
1009 use crate::{ImplementationName, PythonVariant};
1010 use std::path::PathBuf;
1011 use std::str::FromStr;
1012 use uv_pep440::{Prerelease, PrereleaseKind};
1013 use uv_platform::Platform;
1014
1015 fn create_test_installation(
1016 implementation: ImplementationName,
1017 major: u8,
1018 minor: u8,
1019 patch: u8,
1020 prerelease: Option<Prerelease>,
1021 variant: PythonVariant,
1022 build: Option<&str>,
1023 ) -> ManagedPythonInstallation {
1024 let platform = Platform::from_str("linux-x86_64-gnu").unwrap();
1025 let key = PythonInstallationKey::new(
1026 LenientImplementationName::Known(implementation),
1027 major,
1028 minor,
1029 patch,
1030 prerelease,
1031 platform,
1032 variant,
1033 );
1034 ManagedPythonInstallation {
1035 path: PathBuf::from("/test/path"),
1036 key,
1037 url: None,
1038 sha256: None,
1039 build: build.map(|s| Cow::Owned(s.to_owned())),
1040 }
1041 }
1042
1043 #[test]
1044 fn test_is_upgrade_of_same_version() {
1045 let installation = create_test_installation(
1046 ImplementationName::CPython,
1047 3,
1048 10,
1049 8,
1050 None,
1051 PythonVariant::Default,
1052 None,
1053 );
1054
1055 assert!(!installation.is_upgrade_of(&installation));
1057 }
1058
1059 #[test]
1060 fn test_is_upgrade_of_patch_version() {
1061 let older = create_test_installation(
1062 ImplementationName::CPython,
1063 3,
1064 10,
1065 8,
1066 None,
1067 PythonVariant::Default,
1068 None,
1069 );
1070 let newer = create_test_installation(
1071 ImplementationName::CPython,
1072 3,
1073 10,
1074 9,
1075 None,
1076 PythonVariant::Default,
1077 None,
1078 );
1079
1080 assert!(newer.is_upgrade_of(&older));
1082 assert!(!older.is_upgrade_of(&newer));
1084 }
1085
1086 #[test]
1087 fn test_is_upgrade_of_different_minor_version() {
1088 let py310 = create_test_installation(
1089 ImplementationName::CPython,
1090 3,
1091 10,
1092 8,
1093 None,
1094 PythonVariant::Default,
1095 None,
1096 );
1097 let py311 = create_test_installation(
1098 ImplementationName::CPython,
1099 3,
1100 11,
1101 0,
1102 None,
1103 PythonVariant::Default,
1104 None,
1105 );
1106
1107 assert!(!py311.is_upgrade_of(&py310));
1109 assert!(!py310.is_upgrade_of(&py311));
1110 }
1111
1112 #[test]
1113 fn test_is_upgrade_of_different_implementation() {
1114 let cpython = create_test_installation(
1115 ImplementationName::CPython,
1116 3,
1117 10,
1118 8,
1119 None,
1120 PythonVariant::Default,
1121 None,
1122 );
1123 let pypy = create_test_installation(
1124 ImplementationName::PyPy,
1125 3,
1126 10,
1127 9,
1128 None,
1129 PythonVariant::Default,
1130 None,
1131 );
1132
1133 assert!(!pypy.is_upgrade_of(&cpython));
1135 assert!(!cpython.is_upgrade_of(&pypy));
1136 }
1137
1138 #[test]
1139 fn test_is_upgrade_of_different_variant() {
1140 let default = create_test_installation(
1141 ImplementationName::CPython,
1142 3,
1143 10,
1144 8,
1145 None,
1146 PythonVariant::Default,
1147 None,
1148 );
1149 let freethreaded = create_test_installation(
1150 ImplementationName::CPython,
1151 3,
1152 10,
1153 9,
1154 None,
1155 PythonVariant::Freethreaded,
1156 None,
1157 );
1158
1159 assert!(!freethreaded.is_upgrade_of(&default));
1161 assert!(!default.is_upgrade_of(&freethreaded));
1162 }
1163
1164 #[test]
1165 fn test_is_upgrade_of_prerelease() {
1166 let stable = create_test_installation(
1167 ImplementationName::CPython,
1168 3,
1169 10,
1170 8,
1171 None,
1172 PythonVariant::Default,
1173 None,
1174 );
1175 let prerelease = create_test_installation(
1176 ImplementationName::CPython,
1177 3,
1178 10,
1179 8,
1180 Some(Prerelease {
1181 kind: PrereleaseKind::Alpha,
1182 number: 1,
1183 }),
1184 PythonVariant::Default,
1185 None,
1186 );
1187
1188 assert!(stable.is_upgrade_of(&prerelease));
1190
1191 assert!(!prerelease.is_upgrade_of(&stable));
1193 }
1194
1195 #[test]
1196 fn test_is_upgrade_of_prerelease_to_prerelease() {
1197 let alpha1 = create_test_installation(
1198 ImplementationName::CPython,
1199 3,
1200 10,
1201 8,
1202 Some(Prerelease {
1203 kind: PrereleaseKind::Alpha,
1204 number: 1,
1205 }),
1206 PythonVariant::Default,
1207 None,
1208 );
1209 let alpha2 = create_test_installation(
1210 ImplementationName::CPython,
1211 3,
1212 10,
1213 8,
1214 Some(Prerelease {
1215 kind: PrereleaseKind::Alpha,
1216 number: 2,
1217 }),
1218 PythonVariant::Default,
1219 None,
1220 );
1221
1222 assert!(alpha2.is_upgrade_of(&alpha1));
1224 assert!(!alpha1.is_upgrade_of(&alpha2));
1226 }
1227
1228 #[test]
1229 fn test_is_upgrade_of_prerelease_same_patch() {
1230 let prerelease = create_test_installation(
1231 ImplementationName::CPython,
1232 3,
1233 10,
1234 8,
1235 Some(Prerelease {
1236 kind: PrereleaseKind::Alpha,
1237 number: 1,
1238 }),
1239 PythonVariant::Default,
1240 None,
1241 );
1242
1243 assert!(!prerelease.is_upgrade_of(&prerelease));
1245 }
1246
1247 #[test]
1248 fn test_is_upgrade_of_build_version() {
1249 let older_build = create_test_installation(
1250 ImplementationName::CPython,
1251 3,
1252 10,
1253 8,
1254 None,
1255 PythonVariant::Default,
1256 Some("20240101"),
1257 );
1258 let newer_build = create_test_installation(
1259 ImplementationName::CPython,
1260 3,
1261 10,
1262 8,
1263 None,
1264 PythonVariant::Default,
1265 Some("20240201"),
1266 );
1267
1268 assert!(newer_build.is_upgrade_of(&older_build));
1270 assert!(!older_build.is_upgrade_of(&newer_build));
1272 }
1273
1274 #[test]
1275 fn test_is_upgrade_of_build_version_same() {
1276 let installation = create_test_installation(
1277 ImplementationName::CPython,
1278 3,
1279 10,
1280 8,
1281 None,
1282 PythonVariant::Default,
1283 Some("20240101"),
1284 );
1285
1286 assert!(!installation.is_upgrade_of(&installation));
1288 }
1289
1290 #[test]
1291 fn test_is_upgrade_of_build_with_legacy_installation() {
1292 let legacy = create_test_installation(
1293 ImplementationName::CPython,
1294 3,
1295 10,
1296 8,
1297 None,
1298 PythonVariant::Default,
1299 None,
1300 );
1301 let with_build = create_test_installation(
1302 ImplementationName::CPython,
1303 3,
1304 10,
1305 8,
1306 None,
1307 PythonVariant::Default,
1308 Some("20240101"),
1309 );
1310
1311 assert!(with_build.is_upgrade_of(&legacy));
1313 assert!(!legacy.is_upgrade_of(&with_build));
1315 }
1316
1317 #[test]
1318 fn test_is_upgrade_of_patch_takes_precedence_over_build() {
1319 let older_patch_newer_build = create_test_installation(
1320 ImplementationName::CPython,
1321 3,
1322 10,
1323 8,
1324 None,
1325 PythonVariant::Default,
1326 Some("20240201"),
1327 );
1328 let newer_patch_older_build = create_test_installation(
1329 ImplementationName::CPython,
1330 3,
1331 10,
1332 9,
1333 None,
1334 PythonVariant::Default,
1335 Some("20240101"),
1336 );
1337
1338 assert!(newer_patch_older_build.is_upgrade_of(&older_patch_newer_build));
1340 assert!(!older_patch_newer_build.is_upgrade_of(&newer_patch_older_build));
1342 }
1343}