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::downloads::{Error as DownloadError, ManagedPythonDownload};
28use crate::implementation::{
29 Error as ImplementationError, ImplementationName, LenientImplementationName,
30};
31use crate::installation::{self, PythonInstallationKey};
32use crate::interpreter::Interpreter;
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: impl AsRef<Path>) -> Result<Self, Error> {
361 let path = path.as_ref();
362
363 let key = PythonInstallationKey::from_str(
364 path.file_name()
365 .ok_or(Error::NameError("name is empty".to_string()))?
366 .to_str()
367 .ok_or(Error::NameError("not a valid string".to_string()))?,
368 )?;
369
370 let path = std::path::absolute(path)
371 .map_err(|err| Error::AbsolutePath(path.to_path_buf(), err))?;
372
373 let build = match fs::read_to_string(path.join("BUILD")) {
375 Ok(content) => Some(Cow::Owned(content.trim().to_string())),
376 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
377 Err(err) => return Err(err.into()),
378 };
379
380 Ok(Self {
381 path,
382 key,
383 url: None,
384 sha256: None,
385 build,
386 })
387 }
388
389 pub fn try_from_interpreter(interpreter: &Interpreter) -> Option<Self> {
393 let managed_root = ManagedPythonInstallations::from_settings(None).ok()?;
394
395 let sys_base_prefix = dunce::canonicalize(interpreter.sys_base_prefix())
399 .unwrap_or_else(|_| interpreter.sys_base_prefix().to_path_buf());
400 let root = dunce::canonicalize(managed_root.root())
401 .unwrap_or_else(|_| managed_root.root().to_path_buf());
402
403 let suffix = sys_base_prefix.strip_prefix(&root).ok()?;
405
406 let first_component = suffix.components().next()?;
407 let name = first_component.as_os_str().to_str()?;
408
409 PythonInstallationKey::from_str(name).ok()?;
411
412 let path = managed_root.root().join(name);
414 Self::from_path(path).ok()
415 }
416
417 pub fn executable(&self, windowed: bool) -> PathBuf {
426 let version = match self.implementation() {
427 ImplementationName::CPython => {
428 if cfg!(unix) {
429 format!("{}.{}", self.key.major, self.key.minor)
430 } else {
431 String::new()
432 }
433 }
434 ImplementationName::PyPy => format!("{}.{}", self.key.major, self.key.minor),
436 ImplementationName::Pyodide => String::new(),
438 ImplementationName::GraalPy => String::new(),
439 };
440
441 let variant = if self.implementation() == ImplementationName::GraalPy {
444 ""
445 } else if cfg!(unix) {
446 self.key.variant.executable_suffix()
447 } else if cfg!(windows) && windowed {
448 "w"
450 } else {
451 ""
452 };
453
454 let name = format!(
455 "{implementation}{version}{variant}{exe}",
456 implementation = self.implementation().executable_name(),
457 exe = std::env::consts::EXE_SUFFIX
458 );
459
460 let executable = executable_path_from_base(
461 self.python_dir().as_path(),
462 &name,
463 &LenientImplementationName::from(self.implementation()),
464 *self.key.os(),
465 );
466
467 if cfg!(windows)
472 && matches!(self.key.variant, PythonVariant::Freethreaded)
473 && !executable.exists()
474 {
475 return self.python_dir().join(format!(
477 "python{}.{}t{}",
478 self.key.major,
479 self.key.minor,
480 std::env::consts::EXE_SUFFIX
481 ));
482 }
483
484 executable
485 }
486
487 fn python_dir(&self) -> PathBuf {
488 let install = self.path.join("install");
489 if install.is_dir() {
490 install
491 } else {
492 self.path.clone()
493 }
494 }
495
496 pub fn version(&self) -> PythonVersion {
498 self.key.version()
499 }
500
501 pub fn implementation(&self) -> ImplementationName {
502 match self.key.implementation().into_owned() {
503 LenientImplementationName::Known(implementation) => implementation,
504 LenientImplementationName::Unknown(_) => {
505 panic!("Managed Python installations should have a known implementation")
506 }
507 }
508 }
509
510 pub fn path(&self) -> &Path {
511 &self.path
512 }
513
514 pub fn key(&self) -> &PythonInstallationKey {
515 &self.key
516 }
517
518 pub fn platform(&self) -> &Platform {
519 self.key.platform()
520 }
521
522 pub fn build(&self) -> Option<&str> {
524 self.build.as_deref()
525 }
526
527 pub fn minor_version_key(&self) -> &PythonInstallationMinorVersionKey {
528 PythonInstallationMinorVersionKey::ref_cast(&self.key)
529 }
530
531 pub fn satisfies(&self, request: &PythonRequest) -> bool {
532 match request {
533 PythonRequest::File(path) => self.executable(false) == *path,
534 PythonRequest::Default | PythonRequest::Any => true,
535 PythonRequest::Directory(path) => self.path() == *path,
536 PythonRequest::ExecutableName(name) => self
537 .executable(false)
538 .file_name()
539 .is_some_and(|filename| filename.to_string_lossy() == *name),
540 PythonRequest::Implementation(implementation) => {
541 *implementation == self.implementation()
542 }
543 PythonRequest::ImplementationVersion(implementation, version) => {
544 *implementation == self.implementation() && version.matches_version(&self.version())
545 }
546 PythonRequest::Version(version) => version.matches_version(&self.version()),
547 PythonRequest::Key(request) => request.satisfied_by_key(self.key()),
548 }
549 }
550
551 pub fn ensure_canonical_executables(&self) -> Result<(), Error> {
553 let python = self.executable(false);
554
555 let canonical_names = &["python"];
556
557 for name in canonical_names {
558 let executable =
559 python.with_file_name(format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX));
560
561 if executable == python {
564 continue;
565 }
566
567 match symlink_or_copy_file(&python, &executable) {
568 Ok(()) => {
569 debug!(
570 "Created link {} -> {}",
571 executable.user_display(),
572 python.user_display(),
573 );
574 }
575 Err(err) if err.kind() == io::ErrorKind::NotFound => {
576 return Err(Error::MissingExecutable(python.clone()));
577 }
578 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
579 Err(err) => {
580 return Err(Error::CanonicalizeExecutable {
581 from: executable,
582 to: python,
583 err,
584 });
585 }
586 }
587 }
588
589 Ok(())
590 }
591
592 pub fn ensure_minor_version_link(&self) -> Result<(), Error> {
595 if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self) {
596 minor_version_link.create_directory()?;
597 }
598 Ok(())
599 }
600
601 pub fn ensure_externally_managed(&self) -> Result<(), Error> {
604 if self.key.os().is_emscripten() {
605 return Ok(());
608 }
609 let stdlib = if self.key.os().is_windows() {
611 self.python_dir().join("Lib")
612 } else {
613 let lib_suffix = self.key.variant.lib_suffix();
614 let python = if matches!(
615 self.key.implementation,
616 LenientImplementationName::Known(ImplementationName::PyPy)
617 ) {
618 format!("pypy{}", self.key.version().python_version())
619 } else {
620 format!("python{}{lib_suffix}", self.key.version().python_version())
621 };
622 self.python_dir().join("lib").join(python)
623 };
624
625 let file = stdlib.join("EXTERNALLY-MANAGED");
626 fs_err::write(file, EXTERNALLY_MANAGED)?;
627
628 Ok(())
629 }
630
631 pub fn ensure_sysconfig_patched(&self) -> Result<(), Error> {
633 if cfg!(unix) {
634 if self.key.os().is_emscripten() {
635 return Ok(());
638 }
639 if self.implementation() == ImplementationName::CPython {
640 sysconfig::update_sysconfig(
641 self.path(),
642 self.key.major,
643 self.key.minor,
644 self.key.variant.lib_suffix(),
645 )?;
646 }
647 }
648 Ok(())
649 }
650
651 pub fn ensure_dylib_patched(&self) -> Result<(), macos_dylib::Error> {
658 if cfg!(target_os = "macos") {
659 if self.key().os().is_like_darwin() {
660 if self.implementation() == ImplementationName::CPython {
661 let dylib_path = self.python_dir().join("lib").join(format!(
662 "{}python{}{}{}",
663 std::env::consts::DLL_PREFIX,
664 self.key.version().python_version(),
665 self.key.variant().executable_suffix(),
666 std::env::consts::DLL_SUFFIX
667 ));
668 macos_dylib::patch_dylib_install_name(dylib_path)?;
669 }
670 }
671 }
672 Ok(())
673 }
674
675 pub fn ensure_build_file(&self) -> Result<(), Error> {
677 if let Some(ref build) = self.build {
678 let build_file = self.path.join("BUILD");
679 fs::write(&build_file, build.as_ref())?;
680 }
681 Ok(())
682 }
683
684 pub fn is_bin_link(&self, path: &Path) -> bool {
687 if cfg!(unix) {
688 same_file::is_same_file(path, self.executable(false)).unwrap_or_default()
689 } else if cfg!(windows) {
690 let Some(launcher) = Launcher::try_from_path(path).unwrap_or_default() else {
691 return false;
692 };
693 if !matches!(launcher.kind, LauncherKind::Python) {
694 return false;
695 }
696 dunce::canonicalize(&launcher.python_path).unwrap_or(launcher.python_path)
700 == self.executable(false)
701 } else {
702 unreachable!("Only Windows and Unix are supported")
703 }
704 }
705
706 pub fn is_upgrade_of(&self, other: &Self) -> bool {
708 if self.key.implementation != other.key.implementation {
710 return false;
711 }
712 if self.key.variant != other.key.variant {
714 return false;
715 }
716 if (self.key.major, self.key.minor) != (other.key.major, other.key.minor) {
718 return false;
719 }
720 if self.key.patch == other.key.patch {
723 return match (self.key.prerelease, other.key.prerelease) {
724 (Some(self_pre), Some(other_pre)) => self_pre > other_pre,
726 (None, Some(_)) => true,
728 (Some(_), None) => false,
730 (None, None) => match (self.build.as_deref(), other.build.as_deref()) {
732 (Some(_), None) => true,
734 (Some(self_build), Some(other_build)) => {
736 compare_build_versions(self_build, other_build) == Ordering::Greater
737 }
738 (None, _) => false,
740 },
741 };
742 }
743 if self.key.patch < other.key.patch {
745 return false;
746 }
747 true
748 }
749
750 pub fn url(&self) -> Option<&str> {
751 self.url.as_deref()
752 }
753
754 pub fn sha256(&self) -> Option<&str> {
755 self.sha256.as_deref()
756 }
757}
758
759#[derive(Clone, Debug)]
762pub struct PythonMinorVersionLink {
763 pub symlink_directory: PathBuf,
765 pub symlink_executable: PathBuf,
768 pub target_directory: PathBuf,
771}
772
773impl PythonMinorVersionLink {
774 fn from_executable(executable: &Path, key: &PythonInstallationKey) -> Option<Self> {
794 let implementation = key.implementation();
795 if !matches!(
796 implementation.as_ref(),
797 LenientImplementationName::Known(ImplementationName::CPython)
798 ) {
799 return None;
801 }
802 let executable_name = executable
803 .file_name()
804 .expect("Executable file name should exist");
805 let symlink_directory_name = PythonInstallationMinorVersionKey::ref_cast(key).to_string();
806 let parent = executable
807 .parent()
808 .expect("Executable should have parent directory");
809
810 let target_directory = if cfg!(unix) {
812 if parent
813 .components()
814 .next_back()
815 .is_some_and(|c| c.as_os_str() == "bin")
816 {
817 parent.parent()?.to_path_buf()
818 } else {
819 return None;
820 }
821 } else if cfg!(windows) {
822 parent.to_path_buf()
823 } else {
824 unimplemented!("Only Windows and Unix systems are supported.")
825 };
826 let symlink_directory = target_directory.with_file_name(symlink_directory_name);
827 if target_directory == symlink_directory {
829 return None;
830 }
831 let symlink_executable = executable_path_from_base(
833 symlink_directory.as_path(),
834 &executable_name.to_string_lossy(),
835 &implementation,
836 *key.os(),
837 );
838 let minor_version_link = Self {
839 symlink_directory,
840 symlink_executable,
841 target_directory,
842 };
843 Some(minor_version_link)
844 }
845
846 pub fn from_installation(installation: &ManagedPythonInstallation) -> Option<Self> {
847 Self::from_executable(installation.executable(false).as_path(), installation.key())
848 }
849
850 pub fn create_directory(&self) -> Result<(), Error> {
851 match replace_symlink(
852 self.target_directory.as_path(),
853 self.symlink_directory.as_path(),
854 ) {
855 Ok(()) => {
856 debug!(
857 "Created link {} -> {}",
858 &self.symlink_directory.user_display(),
859 &self.target_directory.user_display(),
860 );
861 }
862 Err(err) if err.kind() == io::ErrorKind::NotFound => {
863 return Err(Error::MissingPythonMinorVersionLinkTargetDirectory(
864 self.target_directory.clone(),
865 ));
866 }
867 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
868 Err(err) => {
869 return Err(Error::PythonMinorVersionLinkDirectory {
870 from: self.symlink_directory.clone(),
871 to: self.target_directory.clone(),
872 err,
873 });
874 }
875 }
876 Ok(())
877 }
878
879 pub fn exists(&self) -> bool {
886 #[cfg(unix)]
887 {
888 self.symlink_directory
889 .symlink_metadata()
890 .is_ok_and(|metadata| metadata.file_type().is_symlink())
891 && self
892 .read_target()
893 .is_some_and(|target| target == self.target_directory)
894 }
895 #[cfg(windows)]
896 {
897 self.symlink_directory
898 .symlink_metadata()
899 .is_ok_and(|metadata| {
900 (metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0) != 0
903 })
904 && self
905 .read_target()
906 .is_some_and(|target| target == self.target_directory)
907 }
908 }
909
910 pub fn read_target(&self) -> Option<PathBuf> {
916 #[cfg(unix)]
917 {
918 self.symlink_directory.read_link().ok()
919 }
920 #[cfg(windows)]
921 {
922 junction::get_target(&self.symlink_directory).ok()
923 }
924 }
925}
926
927fn executable_path_from_base(
931 base: &Path,
932 executable_name: &str,
933 implementation: &LenientImplementationName,
934 os: Os,
935) -> PathBuf {
936 if matches!(
937 implementation,
938 &LenientImplementationName::Known(ImplementationName::GraalPy)
939 ) {
940 base.join("bin").join(executable_name)
942 } else if os.is_emscripten()
943 || matches!(
944 implementation,
945 &LenientImplementationName::Known(ImplementationName::Pyodide)
946 )
947 {
948 base.join(executable_name)
950 } else if os.is_windows() {
951 base.join(executable_name)
953 } else {
954 base.join("bin").join(executable_name)
956 }
957}
958
959pub fn create_link_to_executable(link: &Path, executable: &Path) -> Result<(), Error> {
963 let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?;
964 fs_err::create_dir_all(link_parent).map_err(|err| Error::ExecutableDirectory {
965 to: link_parent.to_path_buf(),
966 err,
967 })?;
968
969 if cfg!(unix) {
970 match symlink_or_copy_file(executable, link) {
972 Ok(()) => Ok(()),
973 Err(err) if err.kind() == io::ErrorKind::NotFound => {
974 Err(Error::MissingExecutable(executable.to_path_buf()))
975 }
976 Err(err) => Err(Error::LinkExecutable {
977 from: executable.to_path_buf(),
978 to: link.to_path_buf(),
979 err,
980 }),
981 }
982 } else if cfg!(windows) {
983 use uv_trampoline_builder::windows_python_launcher;
984
985 let launcher = windows_python_launcher(executable, false)?;
987
988 #[expect(clippy::disallowed_types)]
991 {
992 std::fs::File::create_new(link)
993 .and_then(|mut file| file.write_all(launcher.as_ref()))
994 .map_err(|err| Error::LinkExecutable {
995 from: executable.to_path_buf(),
996 to: link.to_path_buf(),
997 err,
998 })
999 }
1000 } else {
1001 unimplemented!("Only Windows and Unix are supported.")
1002 }
1003}
1004
1005pub fn platform_key_from_env() -> Result<String, Error> {
1008 Ok(Platform::from_env()?.to_string().to_lowercase())
1009}
1010
1011impl fmt::Display for ManagedPythonInstallation {
1012 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1013 write!(
1014 f,
1015 "{}",
1016 self.path
1017 .file_name()
1018 .unwrap_or(self.path.as_os_str())
1019 .to_string_lossy()
1020 )
1021 }
1022}
1023
1024pub fn python_executable_dir() -> Result<PathBuf, Error> {
1026 uv_dirs::user_executable_directory(Some(EnvVars::UV_PYTHON_BIN_DIR))
1027 .ok_or(Error::NoExecutableDirectory)
1028}
1029
1030#[cfg(test)]
1031mod tests {
1032 use super::*;
1033 use crate::implementation::LenientImplementationName;
1034 use crate::installation::PythonInstallationKey;
1035 use crate::{ImplementationName, PythonVariant};
1036 use std::path::PathBuf;
1037 use std::str::FromStr;
1038 use uv_pep440::{Prerelease, PrereleaseKind};
1039 use uv_platform::Platform;
1040
1041 fn create_test_installation(
1042 implementation: ImplementationName,
1043 major: u8,
1044 minor: u8,
1045 patch: u8,
1046 prerelease: Option<Prerelease>,
1047 variant: PythonVariant,
1048 build: Option<&str>,
1049 ) -> ManagedPythonInstallation {
1050 let platform = Platform::from_str("linux-x86_64-gnu").unwrap();
1051 let key = PythonInstallationKey::new(
1052 LenientImplementationName::Known(implementation),
1053 major,
1054 minor,
1055 patch,
1056 prerelease,
1057 platform,
1058 variant,
1059 );
1060 ManagedPythonInstallation {
1061 path: PathBuf::from("/test/path"),
1062 key,
1063 url: None,
1064 sha256: None,
1065 build: build.map(|s| Cow::Owned(s.to_owned())),
1066 }
1067 }
1068
1069 #[test]
1070 fn test_is_upgrade_of_same_version() {
1071 let installation = create_test_installation(
1072 ImplementationName::CPython,
1073 3,
1074 10,
1075 8,
1076 None,
1077 PythonVariant::Default,
1078 None,
1079 );
1080
1081 assert!(!installation.is_upgrade_of(&installation));
1083 }
1084
1085 #[test]
1086 fn test_is_upgrade_of_patch_version() {
1087 let older = create_test_installation(
1088 ImplementationName::CPython,
1089 3,
1090 10,
1091 8,
1092 None,
1093 PythonVariant::Default,
1094 None,
1095 );
1096 let newer = create_test_installation(
1097 ImplementationName::CPython,
1098 3,
1099 10,
1100 9,
1101 None,
1102 PythonVariant::Default,
1103 None,
1104 );
1105
1106 assert!(newer.is_upgrade_of(&older));
1108 assert!(!older.is_upgrade_of(&newer));
1110 }
1111
1112 #[test]
1113 fn test_is_upgrade_of_different_minor_version() {
1114 let py310 = create_test_installation(
1115 ImplementationName::CPython,
1116 3,
1117 10,
1118 8,
1119 None,
1120 PythonVariant::Default,
1121 None,
1122 );
1123 let py311 = create_test_installation(
1124 ImplementationName::CPython,
1125 3,
1126 11,
1127 0,
1128 None,
1129 PythonVariant::Default,
1130 None,
1131 );
1132
1133 assert!(!py311.is_upgrade_of(&py310));
1135 assert!(!py310.is_upgrade_of(&py311));
1136 }
1137
1138 #[test]
1139 fn test_is_upgrade_of_different_implementation() {
1140 let cpython = create_test_installation(
1141 ImplementationName::CPython,
1142 3,
1143 10,
1144 8,
1145 None,
1146 PythonVariant::Default,
1147 None,
1148 );
1149 let pypy = create_test_installation(
1150 ImplementationName::PyPy,
1151 3,
1152 10,
1153 9,
1154 None,
1155 PythonVariant::Default,
1156 None,
1157 );
1158
1159 assert!(!pypy.is_upgrade_of(&cpython));
1161 assert!(!cpython.is_upgrade_of(&pypy));
1162 }
1163
1164 #[test]
1165 fn test_is_upgrade_of_different_variant() {
1166 let default = create_test_installation(
1167 ImplementationName::CPython,
1168 3,
1169 10,
1170 8,
1171 None,
1172 PythonVariant::Default,
1173 None,
1174 );
1175 let freethreaded = create_test_installation(
1176 ImplementationName::CPython,
1177 3,
1178 10,
1179 9,
1180 None,
1181 PythonVariant::Freethreaded,
1182 None,
1183 );
1184
1185 assert!(!freethreaded.is_upgrade_of(&default));
1187 assert!(!default.is_upgrade_of(&freethreaded));
1188 }
1189
1190 #[test]
1191 fn test_is_upgrade_of_prerelease() {
1192 let stable = create_test_installation(
1193 ImplementationName::CPython,
1194 3,
1195 10,
1196 8,
1197 None,
1198 PythonVariant::Default,
1199 None,
1200 );
1201 let prerelease = create_test_installation(
1202 ImplementationName::CPython,
1203 3,
1204 10,
1205 8,
1206 Some(Prerelease {
1207 kind: PrereleaseKind::Alpha,
1208 number: 1,
1209 }),
1210 PythonVariant::Default,
1211 None,
1212 );
1213
1214 assert!(stable.is_upgrade_of(&prerelease));
1216
1217 assert!(!prerelease.is_upgrade_of(&stable));
1219 }
1220
1221 #[test]
1222 fn test_is_upgrade_of_prerelease_to_prerelease() {
1223 let alpha1 = create_test_installation(
1224 ImplementationName::CPython,
1225 3,
1226 10,
1227 8,
1228 Some(Prerelease {
1229 kind: PrereleaseKind::Alpha,
1230 number: 1,
1231 }),
1232 PythonVariant::Default,
1233 None,
1234 );
1235 let alpha2 = create_test_installation(
1236 ImplementationName::CPython,
1237 3,
1238 10,
1239 8,
1240 Some(Prerelease {
1241 kind: PrereleaseKind::Alpha,
1242 number: 2,
1243 }),
1244 PythonVariant::Default,
1245 None,
1246 );
1247
1248 assert!(alpha2.is_upgrade_of(&alpha1));
1250 assert!(!alpha1.is_upgrade_of(&alpha2));
1252 }
1253
1254 #[test]
1255 fn test_is_upgrade_of_prerelease_same_patch() {
1256 let prerelease = create_test_installation(
1257 ImplementationName::CPython,
1258 3,
1259 10,
1260 8,
1261 Some(Prerelease {
1262 kind: PrereleaseKind::Alpha,
1263 number: 1,
1264 }),
1265 PythonVariant::Default,
1266 None,
1267 );
1268
1269 assert!(!prerelease.is_upgrade_of(&prerelease));
1271 }
1272
1273 #[test]
1274 fn test_is_upgrade_of_build_version() {
1275 let older_build = create_test_installation(
1276 ImplementationName::CPython,
1277 3,
1278 10,
1279 8,
1280 None,
1281 PythonVariant::Default,
1282 Some("20240101"),
1283 );
1284 let newer_build = create_test_installation(
1285 ImplementationName::CPython,
1286 3,
1287 10,
1288 8,
1289 None,
1290 PythonVariant::Default,
1291 Some("20240201"),
1292 );
1293
1294 assert!(newer_build.is_upgrade_of(&older_build));
1296 assert!(!older_build.is_upgrade_of(&newer_build));
1298 }
1299
1300 #[test]
1301 fn test_is_upgrade_of_build_version_same() {
1302 let installation = create_test_installation(
1303 ImplementationName::CPython,
1304 3,
1305 10,
1306 8,
1307 None,
1308 PythonVariant::Default,
1309 Some("20240101"),
1310 );
1311
1312 assert!(!installation.is_upgrade_of(&installation));
1314 }
1315
1316 #[test]
1317 fn test_is_upgrade_of_build_with_legacy_installation() {
1318 let legacy = create_test_installation(
1319 ImplementationName::CPython,
1320 3,
1321 10,
1322 8,
1323 None,
1324 PythonVariant::Default,
1325 None,
1326 );
1327 let with_build = create_test_installation(
1328 ImplementationName::CPython,
1329 3,
1330 10,
1331 8,
1332 None,
1333 PythonVariant::Default,
1334 Some("20240101"),
1335 );
1336
1337 assert!(with_build.is_upgrade_of(&legacy));
1339 assert!(!legacy.is_upgrade_of(&with_build));
1341 }
1342
1343 #[test]
1344 fn test_is_upgrade_of_patch_takes_precedence_over_build() {
1345 let older_patch_newer_build = create_test_installation(
1346 ImplementationName::CPython,
1347 3,
1348 10,
1349 8,
1350 None,
1351 PythonVariant::Default,
1352 Some("20240201"),
1353 );
1354 let newer_patch_older_build = create_test_installation(
1355 ImplementationName::CPython,
1356 3,
1357 10,
1358 9,
1359 None,
1360 PythonVariant::Default,
1361 Some("20240101"),
1362 );
1363
1364 assert!(newer_patch_older_build.is_upgrade_of(&older_patch_newer_build));
1366 assert!(!older_patch_newer_build.is_upgrade_of(&newer_patch_older_build));
1368 }
1369}