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("Failed to copy to: {0}", to.user_display())]
58 CopyError {
59 to: PathBuf,
60 #[source]
61 err: io::Error,
62 },
63 #[error("Missing expected Python executable at {}", _0.user_display())]
64 MissingExecutable(PathBuf),
65 #[error("Missing expected target directory for Python minor version link at {}", _0.user_display())]
66 MissingPythonMinorVersionLinkTargetDirectory(PathBuf),
67 #[error("Failed to create canonical Python executable at {} from {}", to.user_display(), from.user_display())]
68 CanonicalizeExecutable {
69 from: PathBuf,
70 to: PathBuf,
71 #[source]
72 err: io::Error,
73 },
74 #[error("Failed to create Python executable link at {} from {}", to.user_display(), from.user_display())]
75 LinkExecutable {
76 from: PathBuf,
77 to: PathBuf,
78 #[source]
79 err: io::Error,
80 },
81 #[error("Failed to create Python minor version link directory at {} from {}", to.user_display(), from.user_display())]
82 PythonMinorVersionLinkDirectory {
83 from: PathBuf,
84 to: PathBuf,
85 #[source]
86 err: io::Error,
87 },
88 #[error("Failed to create directory for Python executable link at {}", to.user_display())]
89 ExecutableDirectory {
90 to: PathBuf,
91 #[source]
92 err: io::Error,
93 },
94 #[error("Failed to read Python installation directory: {0}", dir.user_display())]
95 ReadError {
96 dir: PathBuf,
97 #[source]
98 err: io::Error,
99 },
100 #[error("Failed to find a directory to install executables into")]
101 NoExecutableDirectory,
102 #[error(transparent)]
103 LauncherError(#[from] uv_trampoline_builder::Error),
104 #[error("Failed to read managed Python directory name: {0}")]
105 NameError(String),
106 #[error("Failed to construct absolute path to managed Python directory: {}", _0.user_display())]
107 AbsolutePath(PathBuf, #[source] io::Error),
108 #[error(transparent)]
109 NameParseError(#[from] installation::PythonInstallationKeyError),
110 #[error("Failed to determine the libc used on the current platform")]
111 LibcDetection(#[from] LibcDetectionError),
112 #[error(transparent)]
113 MacOsDylib(#[from] macos_dylib::Error),
114}
115
116pub fn compare_build_versions(a: &str, b: &str) -> Ordering {
121 match (a.parse::<u64>(), b.parse::<u64>()) {
122 (Ok(a_num), Ok(b_num)) => a_num.cmp(&b_num),
123 _ => a.cmp(b),
124 }
125}
126
127#[derive(Debug, Clone, Eq, PartialEq)]
129pub struct ManagedPythonInstallations {
130 root: PathBuf,
132}
133
134impl ManagedPythonInstallations {
135 fn from_path(root: impl Into<PathBuf>) -> Self {
137 Self { root: root.into() }
138 }
139
140 pub async fn lock(&self) -> Result<LockedFile, Error> {
143 Ok(LockedFile::acquire(
144 self.root.join(".lock"),
145 LockedFileMode::Exclusive,
146 self.root.user_display(),
147 )
148 .await?)
149 }
150
151 pub fn from_settings(install_dir: Option<PathBuf>) -> Result<Self, Error> {
158 if let Some(install_dir) = install_dir {
159 Ok(Self::from_path(install_dir))
160 } else if let Some(install_dir) =
161 std::env::var_os(EnvVars::UV_PYTHON_INSTALL_DIR).filter(|s| !s.is_empty())
162 {
163 Ok(Self::from_path(install_dir))
164 } else {
165 Ok(Self::from_path(
166 StateStore::from_settings(None)?.bucket(StateBucket::ManagedPython),
167 ))
168 }
169 }
170
171 pub fn temp() -> Result<Self, Error> {
173 Ok(Self::from_path(
174 StateStore::temp()?.bucket(StateBucket::ManagedPython),
175 ))
176 }
177
178 pub fn scratch(&self) -> PathBuf {
180 self.root.join(".temp")
181 }
182
183 pub fn init(self) -> Result<Self, Error> {
187 let root = &self.root;
188
189 if !root.exists()
191 && root
192 .parent()
193 .is_some_and(|parent| parent.join("toolchains").exists())
194 {
195 let deprecated = root.parent().unwrap().join("toolchains");
196 fs::rename(&deprecated, root)?;
198 uv_fs::replace_symlink(root, &deprecated)?;
200 } else {
201 fs::create_dir_all(root)?;
202 }
203
204 fs::create_dir_all(root)?;
206
207 let scratch = self.scratch();
209 fs::create_dir_all(&scratch)?;
210
211 match fs::OpenOptions::new()
213 .write(true)
214 .create_new(true)
215 .open(root.join(".gitignore"))
216 {
217 Ok(mut file) => file.write_all(b"*")?,
218 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
219 Err(err) => return Err(err.into()),
220 }
221
222 Ok(self)
223 }
224
225 pub fn find_all(
230 &self,
231 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + use<>, Error> {
232 let dirs = match fs_err::read_dir(&self.root) {
233 Ok(installation_dirs) => {
234 let directories: Vec<_> = installation_dirs
236 .filter_map(|read_dir| match read_dir {
237 Ok(entry) => match entry.file_type() {
238 Ok(file_type) => file_type.is_dir().then_some(Ok(entry.path())),
239 Err(err) => Some(Err(err)),
240 },
241 Err(err) => Some(Err(err)),
242 })
243 .collect::<Result<_, io::Error>>()
244 .map_err(|err| Error::ReadError {
245 dir: self.root.clone(),
246 err,
247 })?;
248 directories
249 }
250 Err(err) if err.kind() == io::ErrorKind::NotFound => vec![],
251 Err(err) => {
252 return Err(Error::ReadError {
253 dir: self.root.clone(),
254 err,
255 });
256 }
257 };
258 let scratch = self.scratch();
259 Ok(dirs
260 .into_iter()
261 .filter(|path| *path != scratch)
263 .filter(|path| {
265 path.file_name()
266 .and_then(OsStr::to_str)
267 .map(|name| !name.starts_with('.'))
268 .unwrap_or(true)
269 })
270 .filter_map(|path| {
271 ManagedPythonInstallation::from_path(path)
272 .inspect_err(|err| {
273 warn!("Ignoring malformed managed Python entry:\n {err}");
274 })
275 .ok()
276 })
277 .sorted_unstable_by_key(|installation| Reverse(installation.key().clone())))
278 }
279
280 pub fn find_matching_current_platform(
282 &self,
283 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + use<>, Error> {
284 let platform = Platform::from_env()?;
285
286 let iter = Self::from_settings(None)?
287 .find_all()?
288 .filter(move |installation| {
289 if !platform.supports(installation.platform()) {
290 debug!("Skipping managed installation `{installation}`: not supported by current platform `{platform}`");
291 return false;
292 }
293 true
294 });
295
296 Ok(iter)
297 }
298
299 pub fn find_version<'a>(
306 &'a self,
307 version: &'a PythonVersion,
308 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + 'a, Error> {
309 let request = VersionRequest::from(version);
310 Ok(self
311 .find_matching_current_platform()?
312 .filter(move |installation| request.matches_installation_key(installation.key())))
313 }
314
315 pub fn root(&self) -> &Path {
316 &self.root
317 }
318}
319
320static EXTERNALLY_MANAGED: &str = "[externally-managed]
321Error=This Python installation is managed by uv and should not be modified.
322";
323
324#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
326pub struct ManagedPythonInstallation {
327 path: PathBuf,
329 key: PythonInstallationKey,
331 url: Option<Cow<'static, str>>,
335 sha256: Option<Cow<'static, str>>,
339 build: Option<Cow<'static, str>>,
343}
344
345impl ManagedPythonInstallation {
346 pub fn new(path: PathBuf, download: &ManagedPythonDownload) -> Self {
347 Self {
348 path,
349 key: download.key().clone(),
350 url: Some(download.url().clone()),
351 sha256: download.sha256().cloned(),
352 build: download.build().map(Cow::Borrowed),
353 }
354 }
355
356 pub(crate) fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
357 let path = path.as_ref();
358
359 let key = PythonInstallationKey::from_str(
360 path.file_name()
361 .ok_or(Error::NameError("name is empty".to_string()))?
362 .to_str()
363 .ok_or(Error::NameError("not a valid string".to_string()))?,
364 )?;
365
366 let path = std::path::absolute(path)
367 .map_err(|err| Error::AbsolutePath(path.to_path_buf(), err))?;
368
369 let build = match fs::read_to_string(path.join("BUILD")) {
371 Ok(content) => Some(Cow::Owned(content.trim().to_string())),
372 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
373 Err(err) => return Err(err.into()),
374 };
375
376 Ok(Self {
377 path,
378 key,
379 url: None,
380 sha256: None,
381 build,
382 })
383 }
384
385 pub fn try_from_interpreter(interpreter: &Interpreter) -> Option<Self> {
389 let managed_root = ManagedPythonInstallations::from_settings(None).ok()?;
390
391 let sys_base_prefix = dunce::canonicalize(interpreter.sys_base_prefix())
395 .unwrap_or_else(|_| interpreter.sys_base_prefix().to_path_buf());
396 let root = dunce::canonicalize(managed_root.root())
397 .unwrap_or_else(|_| managed_root.root().to_path_buf());
398
399 let suffix = sys_base_prefix.strip_prefix(&root).ok()?;
401
402 let first_component = suffix.components().next()?;
403 let name = first_component.as_os_str().to_str()?;
404
405 PythonInstallationKey::from_str(name).ok()?;
407
408 let path = managed_root.root().join(name);
410 Self::from_path(path).ok()
411 }
412
413 pub fn executable(&self, windowed: bool) -> PathBuf {
422 let version = match self.implementation() {
423 ImplementationName::CPython => {
424 if cfg!(unix) {
425 format!("{}.{}", self.key.major, self.key.minor)
426 } else {
427 String::new()
428 }
429 }
430 ImplementationName::PyPy => format!("{}.{}", self.key.major, self.key.minor),
432 ImplementationName::Pyodide => String::new(),
434 ImplementationName::GraalPy => String::new(),
435 };
436
437 let variant = if self.implementation() == ImplementationName::GraalPy {
440 ""
441 } else if cfg!(unix) {
442 self.key.variant.executable_suffix()
443 } else if cfg!(windows) && windowed {
444 "w"
446 } else {
447 ""
448 };
449
450 let name = format!(
451 "{implementation}{version}{variant}{exe}",
452 implementation = self.implementation().executable_name(),
453 exe = std::env::consts::EXE_SUFFIX
454 );
455
456 let executable = executable_path_from_base(
457 self.python_dir().as_path(),
458 &name,
459 &LenientImplementationName::from(self.implementation()),
460 *self.key.os(),
461 );
462
463 if cfg!(windows)
468 && matches!(self.key.variant, PythonVariant::Freethreaded)
469 && !executable.exists()
470 {
471 return self.python_dir().join(format!(
473 "python{}.{}t{}",
474 self.key.major,
475 self.key.minor,
476 std::env::consts::EXE_SUFFIX
477 ));
478 }
479
480 executable
481 }
482
483 fn python_dir(&self) -> PathBuf {
484 let install = self.path.join("install");
485 if install.is_dir() {
486 install
487 } else {
488 self.path.clone()
489 }
490 }
491
492 pub fn version(&self) -> PythonVersion {
494 self.key.version()
495 }
496
497 pub fn implementation(&self) -> ImplementationName {
498 match self.key.implementation().into_owned() {
499 LenientImplementationName::Known(implementation) => implementation,
500 LenientImplementationName::Unknown(_) => {
501 panic!("Managed Python installations should have a known implementation")
502 }
503 }
504 }
505
506 pub fn path(&self) -> &Path {
507 &self.path
508 }
509
510 pub fn key(&self) -> &PythonInstallationKey {
511 &self.key
512 }
513
514 pub fn platform(&self) -> &Platform {
515 self.key.platform()
516 }
517
518 pub fn build(&self) -> Option<&str> {
520 self.build.as_deref()
521 }
522
523 pub fn minor_version_key(&self) -> &PythonInstallationMinorVersionKey {
524 PythonInstallationMinorVersionKey::ref_cast(&self.key)
525 }
526
527 pub fn satisfies(&self, request: &PythonRequest) -> bool {
528 match request {
529 PythonRequest::File(path) => self.executable(false) == *path,
530 PythonRequest::Default | PythonRequest::Any => true,
531 PythonRequest::Directory(path) => self.path() == *path,
532 PythonRequest::ExecutableName(name) => self
533 .executable(false)
534 .file_name()
535 .is_some_and(|filename| filename.to_string_lossy() == *name),
536 PythonRequest::Implementation(implementation) => {
537 *implementation == self.implementation()
538 }
539 PythonRequest::ImplementationVersion(implementation, version) => {
540 *implementation == self.implementation() && version.matches_version(&self.version())
541 }
542 PythonRequest::Version(version) => version.matches_version(&self.version()),
543 PythonRequest::Key(request) => request.satisfied_by_key(self.key()),
544 }
545 }
546
547 pub fn ensure_canonical_executables(&self) -> Result<(), Error> {
549 let python = self.executable(false);
550
551 let canonical_names = &["python"];
552
553 for name in canonical_names {
554 let executable =
555 python.with_file_name(format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX));
556
557 if executable == python {
560 continue;
561 }
562
563 match symlink_or_copy_file(&python, &executable) {
564 Ok(()) => {
565 debug!(
566 "Created link {} -> {}",
567 executable.user_display(),
568 python.user_display(),
569 );
570 }
571 Err(err) if err.kind() == io::ErrorKind::NotFound => {
572 return Err(Error::MissingExecutable(python.clone()));
573 }
574 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
575 Err(err) => {
576 return Err(Error::CanonicalizeExecutable {
577 from: executable,
578 to: python,
579 err,
580 });
581 }
582 }
583 }
584
585 Ok(())
586 }
587
588 pub fn ensure_minor_version_link(&self) -> Result<(), Error> {
591 if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self) {
592 minor_version_link.create_directory()?;
593 }
594 Ok(())
595 }
596
597 pub fn ensure_externally_managed(&self) -> Result<(), Error> {
600 if self.key.os().is_emscripten() {
601 return Ok(());
604 }
605 let stdlib = if self.key.os().is_windows() {
607 self.python_dir().join("Lib")
608 } else {
609 let lib_suffix = self.key.variant.lib_suffix();
610 let python = if matches!(
611 self.key.implementation,
612 LenientImplementationName::Known(ImplementationName::PyPy)
613 ) {
614 format!("pypy{}", self.key.version().python_version())
615 } else {
616 format!("python{}{lib_suffix}", self.key.version().python_version())
617 };
618 self.python_dir().join("lib").join(python)
619 };
620
621 let file = stdlib.join("EXTERNALLY-MANAGED");
622 fs_err::write(file, EXTERNALLY_MANAGED)?;
623
624 Ok(())
625 }
626
627 pub fn ensure_sysconfig_patched(&self) -> Result<(), Error> {
629 if cfg!(unix) {
630 if self.key.os().is_emscripten() {
631 return Ok(());
634 }
635 if self.implementation() == ImplementationName::CPython {
636 sysconfig::update_sysconfig(
637 self.path(),
638 self.key.major,
639 self.key.minor,
640 self.key.variant.lib_suffix(),
641 )?;
642 }
643 }
644 Ok(())
645 }
646
647 pub fn ensure_dylib_patched(&self) -> Result<(), macos_dylib::Error> {
654 if cfg!(target_os = "macos") {
655 if self.key().os().is_like_darwin() {
656 if self.implementation() == ImplementationName::CPython {
657 let dylib_path = self.python_dir().join("lib").join(format!(
658 "{}python{}{}{}",
659 std::env::consts::DLL_PREFIX,
660 self.key.version().python_version(),
661 self.key.variant().executable_suffix(),
662 std::env::consts::DLL_SUFFIX
663 ));
664 macos_dylib::patch_dylib_install_name(dylib_path)?;
665 }
666 }
667 }
668 Ok(())
669 }
670
671 pub fn ensure_build_file(&self) -> Result<(), Error> {
673 if let Some(ref build) = self.build {
674 let build_file = self.path.join("BUILD");
675 fs::write(&build_file, build.as_ref())?;
676 }
677 Ok(())
678 }
679
680 pub fn is_bin_link(&self, path: &Path) -> bool {
683 if cfg!(unix) {
684 same_file::is_same_file(path, self.executable(false)).unwrap_or_default()
685 } else if cfg!(windows) {
686 let Some(launcher) = Launcher::try_from_path(path).unwrap_or_default() else {
687 return false;
688 };
689 if !matches!(launcher.kind, LauncherKind::Python) {
690 return false;
691 }
692 dunce::canonicalize(&launcher.python_path).unwrap_or(launcher.python_path)
696 == self.executable(false)
697 } else {
698 unreachable!("Only Windows and Unix are supported")
699 }
700 }
701
702 pub fn is_upgrade_of(&self, other: &Self) -> bool {
704 if self.key.implementation != other.key.implementation {
706 return false;
707 }
708 if self.key.variant != other.key.variant {
710 return false;
711 }
712 if (self.key.major, self.key.minor) != (other.key.major, other.key.minor) {
714 return false;
715 }
716 if self.key.patch == other.key.patch {
719 return match (self.key.prerelease, other.key.prerelease) {
720 (Some(self_pre), Some(other_pre)) => self_pre > other_pre,
722 (None, Some(_)) => true,
724 (Some(_), None) => false,
726 (None, None) => match (self.build.as_deref(), other.build.as_deref()) {
728 (Some(_), None) => true,
730 (Some(self_build), Some(other_build)) => {
732 compare_build_versions(self_build, other_build) == Ordering::Greater
733 }
734 (None, _) => false,
736 },
737 };
738 }
739 if self.key.patch < other.key.patch {
741 return false;
742 }
743 true
744 }
745
746 pub fn url(&self) -> Option<&str> {
747 self.url.as_deref()
748 }
749
750 pub fn sha256(&self) -> Option<&str> {
751 self.sha256.as_deref()
752 }
753}
754
755#[derive(Clone, Debug)]
758pub struct PythonMinorVersionLink {
759 pub symlink_directory: PathBuf,
761 pub symlink_executable: PathBuf,
764 pub target_directory: PathBuf,
767}
768
769impl PythonMinorVersionLink {
770 fn from_executable(executable: &Path, key: &PythonInstallationKey) -> Option<Self> {
790 let implementation = key.implementation();
791 if !matches!(
792 implementation.as_ref(),
793 LenientImplementationName::Known(ImplementationName::CPython)
794 ) {
795 return None;
797 }
798 let executable_name = executable
799 .file_name()
800 .expect("Executable file name should exist");
801 let symlink_directory_name = PythonInstallationMinorVersionKey::ref_cast(key).to_string();
802 let parent = executable
803 .parent()
804 .expect("Executable should have parent directory");
805
806 let target_directory = if cfg!(unix) {
808 if parent
809 .components()
810 .next_back()
811 .is_some_and(|c| c.as_os_str() == "bin")
812 {
813 parent.parent()?.to_path_buf()
814 } else {
815 return None;
816 }
817 } else if cfg!(windows) {
818 parent.to_path_buf()
819 } else {
820 unimplemented!("Only Windows and Unix systems are supported.")
821 };
822 let symlink_directory = target_directory.with_file_name(symlink_directory_name);
823 if target_directory == symlink_directory {
825 return None;
826 }
827 let symlink_executable = executable_path_from_base(
829 symlink_directory.as_path(),
830 &executable_name.to_string_lossy(),
831 &implementation,
832 *key.os(),
833 );
834 let minor_version_link = Self {
835 symlink_directory,
836 symlink_executable,
837 target_directory,
838 };
839 Some(minor_version_link)
840 }
841
842 pub fn from_installation(installation: &ManagedPythonInstallation) -> Option<Self> {
843 Self::from_executable(installation.executable(false).as_path(), installation.key())
844 }
845
846 pub fn create_directory(&self) -> Result<(), Error> {
847 match replace_symlink(
848 self.target_directory.as_path(),
849 self.symlink_directory.as_path(),
850 ) {
851 Ok(()) => {
852 debug!(
853 "Created link {} -> {}",
854 &self.symlink_directory.user_display(),
855 &self.target_directory.user_display(),
856 );
857 }
858 Err(err) if err.kind() == io::ErrorKind::NotFound => {
859 return Err(Error::MissingPythonMinorVersionLinkTargetDirectory(
860 self.target_directory.clone(),
861 ));
862 }
863 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
864 Err(err) => {
865 return Err(Error::PythonMinorVersionLinkDirectory {
866 from: self.symlink_directory.clone(),
867 to: self.target_directory.clone(),
868 err,
869 });
870 }
871 }
872 Ok(())
873 }
874
875 pub fn exists(&self) -> bool {
882 #[cfg(unix)]
883 {
884 self.symlink_directory
885 .symlink_metadata()
886 .is_ok_and(|metadata| metadata.file_type().is_symlink())
887 && self
888 .read_target()
889 .is_some_and(|target| target == self.target_directory)
890 }
891 #[cfg(windows)]
892 {
893 self.symlink_directory
894 .symlink_metadata()
895 .is_ok_and(|metadata| {
896 (metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0) != 0
899 })
900 && self
901 .read_target()
902 .is_some_and(|target| target == self.target_directory)
903 }
904 }
905
906 pub fn read_target(&self) -> Option<PathBuf> {
912 #[cfg(unix)]
913 {
914 self.symlink_directory.read_link().ok()
915 }
916 #[cfg(windows)]
917 {
918 junction::get_target(&self.symlink_directory).ok()
919 }
920 }
921}
922
923fn executable_path_from_base(
927 base: &Path,
928 executable_name: &str,
929 implementation: &LenientImplementationName,
930 os: Os,
931) -> PathBuf {
932 if matches!(
933 implementation,
934 &LenientImplementationName::Known(ImplementationName::GraalPy)
935 ) {
936 base.join("bin").join(executable_name)
938 } else if os.is_emscripten()
939 || matches!(
940 implementation,
941 &LenientImplementationName::Known(ImplementationName::Pyodide)
942 )
943 {
944 base.join(executable_name)
946 } else if os.is_windows() {
947 base.join(executable_name)
949 } else {
950 base.join("bin").join(executable_name)
952 }
953}
954
955pub fn create_link_to_executable(link: &Path, executable: &Path) -> Result<(), Error> {
959 let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?;
960 fs_err::create_dir_all(link_parent).map_err(|err| Error::ExecutableDirectory {
961 to: link_parent.to_path_buf(),
962 err,
963 })?;
964
965 if cfg!(unix) {
966 match symlink_or_copy_file(executable, link) {
968 Ok(()) => Ok(()),
969 Err(err) if err.kind() == io::ErrorKind::NotFound => {
970 Err(Error::MissingExecutable(executable.to_path_buf()))
971 }
972 Err(err) => Err(Error::LinkExecutable {
973 from: executable.to_path_buf(),
974 to: link.to_path_buf(),
975 err,
976 }),
977 }
978 } else if cfg!(windows) {
979 use uv_trampoline_builder::windows_python_launcher;
980
981 let launcher = windows_python_launcher(executable, false)?;
983
984 #[expect(clippy::disallowed_types)]
987 {
988 std::fs::File::create_new(link)
989 .and_then(|mut file| file.write_all(launcher.as_ref()))
990 .map_err(|err| Error::LinkExecutable {
991 from: executable.to_path_buf(),
992 to: link.to_path_buf(),
993 err,
994 })
995 }
996 } else {
997 unimplemented!("Only Windows and Unix are supported.")
998 }
999}
1000
1001pub fn replace_link_to_executable(link: &Path, executable: &Path) -> Result<(), Error> {
1007 let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?;
1008 fs_err::create_dir_all(link_parent).map_err(|err| Error::ExecutableDirectory {
1009 to: link_parent.to_path_buf(),
1010 err,
1011 })?;
1012
1013 if cfg!(unix) {
1014 replace_symlink(executable, link).map_err(|err| Error::LinkExecutable {
1015 from: executable.to_path_buf(),
1016 to: link.to_path_buf(),
1017 err,
1018 })
1019 } else if cfg!(windows) {
1020 use uv_trampoline_builder::windows_python_launcher;
1021
1022 let launcher = windows_python_launcher(executable, false)?;
1023
1024 uv_fs::write_atomic_sync(link, &*launcher).map_err(|err| Error::LinkExecutable {
1025 from: executable.to_path_buf(),
1026 to: link.to_path_buf(),
1027 err,
1028 })
1029 } else {
1030 unimplemented!("Only Windows and Unix are supported.")
1031 }
1032}
1033
1034pub fn platform_key_from_env() -> Result<String, Error> {
1037 Ok(Platform::from_env()?.to_string().to_lowercase())
1038}
1039
1040impl fmt::Display for ManagedPythonInstallation {
1041 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1042 write!(
1043 f,
1044 "{}",
1045 self.path
1046 .file_name()
1047 .unwrap_or(self.path.as_os_str())
1048 .to_string_lossy()
1049 )
1050 }
1051}
1052
1053pub fn python_executable_dir() -> Result<PathBuf, Error> {
1055 uv_dirs::user_executable_directory(Some(EnvVars::UV_PYTHON_BIN_DIR))
1056 .ok_or(Error::NoExecutableDirectory)
1057}
1058
1059#[cfg(test)]
1060mod tests {
1061 use super::*;
1062 use crate::implementation::LenientImplementationName;
1063 use crate::installation::PythonInstallationKey;
1064 use crate::{ImplementationName, PythonVariant};
1065 use std::path::PathBuf;
1066 use std::str::FromStr;
1067 use uv_pep440::{Prerelease, PrereleaseKind};
1068 use uv_platform::Platform;
1069
1070 fn create_test_installation(
1071 implementation: ImplementationName,
1072 major: u8,
1073 minor: u8,
1074 patch: u8,
1075 prerelease: Option<Prerelease>,
1076 variant: PythonVariant,
1077 build: Option<&str>,
1078 ) -> ManagedPythonInstallation {
1079 let platform = Platform::from_str("linux-x86_64-gnu").unwrap();
1080 let key = PythonInstallationKey::new(
1081 LenientImplementationName::Known(implementation),
1082 major,
1083 minor,
1084 patch,
1085 prerelease,
1086 platform,
1087 variant,
1088 );
1089 ManagedPythonInstallation {
1090 path: PathBuf::from("/test/path"),
1091 key,
1092 url: None,
1093 sha256: None,
1094 build: build.map(|s| Cow::Owned(s.to_owned())),
1095 }
1096 }
1097
1098 #[test]
1099 fn test_is_upgrade_of_same_version() {
1100 let installation = create_test_installation(
1101 ImplementationName::CPython,
1102 3,
1103 10,
1104 8,
1105 None,
1106 PythonVariant::Default,
1107 None,
1108 );
1109
1110 assert!(!installation.is_upgrade_of(&installation));
1112 }
1113
1114 #[test]
1115 fn test_is_upgrade_of_patch_version() {
1116 let older = create_test_installation(
1117 ImplementationName::CPython,
1118 3,
1119 10,
1120 8,
1121 None,
1122 PythonVariant::Default,
1123 None,
1124 );
1125 let newer = create_test_installation(
1126 ImplementationName::CPython,
1127 3,
1128 10,
1129 9,
1130 None,
1131 PythonVariant::Default,
1132 None,
1133 );
1134
1135 assert!(newer.is_upgrade_of(&older));
1137 assert!(!older.is_upgrade_of(&newer));
1139 }
1140
1141 #[test]
1142 fn test_is_upgrade_of_different_minor_version() {
1143 let py310 = create_test_installation(
1144 ImplementationName::CPython,
1145 3,
1146 10,
1147 8,
1148 None,
1149 PythonVariant::Default,
1150 None,
1151 );
1152 let py311 = create_test_installation(
1153 ImplementationName::CPython,
1154 3,
1155 11,
1156 0,
1157 None,
1158 PythonVariant::Default,
1159 None,
1160 );
1161
1162 assert!(!py311.is_upgrade_of(&py310));
1164 assert!(!py310.is_upgrade_of(&py311));
1165 }
1166
1167 #[test]
1168 fn test_is_upgrade_of_different_implementation() {
1169 let cpython = create_test_installation(
1170 ImplementationName::CPython,
1171 3,
1172 10,
1173 8,
1174 None,
1175 PythonVariant::Default,
1176 None,
1177 );
1178 let pypy = create_test_installation(
1179 ImplementationName::PyPy,
1180 3,
1181 10,
1182 9,
1183 None,
1184 PythonVariant::Default,
1185 None,
1186 );
1187
1188 assert!(!pypy.is_upgrade_of(&cpython));
1190 assert!(!cpython.is_upgrade_of(&pypy));
1191 }
1192
1193 #[test]
1194 fn test_is_upgrade_of_different_variant() {
1195 let default = create_test_installation(
1196 ImplementationName::CPython,
1197 3,
1198 10,
1199 8,
1200 None,
1201 PythonVariant::Default,
1202 None,
1203 );
1204 let freethreaded = create_test_installation(
1205 ImplementationName::CPython,
1206 3,
1207 10,
1208 9,
1209 None,
1210 PythonVariant::Freethreaded,
1211 None,
1212 );
1213
1214 assert!(!freethreaded.is_upgrade_of(&default));
1216 assert!(!default.is_upgrade_of(&freethreaded));
1217 }
1218
1219 #[test]
1220 fn test_is_upgrade_of_prerelease() {
1221 let stable = create_test_installation(
1222 ImplementationName::CPython,
1223 3,
1224 10,
1225 8,
1226 None,
1227 PythonVariant::Default,
1228 None,
1229 );
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!(stable.is_upgrade_of(&prerelease));
1245
1246 assert!(!prerelease.is_upgrade_of(&stable));
1248 }
1249
1250 #[test]
1251 fn test_is_upgrade_of_prerelease_to_prerelease() {
1252 let alpha1 = create_test_installation(
1253 ImplementationName::CPython,
1254 3,
1255 10,
1256 8,
1257 Some(Prerelease {
1258 kind: PrereleaseKind::Alpha,
1259 number: 1,
1260 }),
1261 PythonVariant::Default,
1262 None,
1263 );
1264 let alpha2 = create_test_installation(
1265 ImplementationName::CPython,
1266 3,
1267 10,
1268 8,
1269 Some(Prerelease {
1270 kind: PrereleaseKind::Alpha,
1271 number: 2,
1272 }),
1273 PythonVariant::Default,
1274 None,
1275 );
1276
1277 assert!(alpha2.is_upgrade_of(&alpha1));
1279 assert!(!alpha1.is_upgrade_of(&alpha2));
1281 }
1282
1283 #[test]
1284 fn test_is_upgrade_of_prerelease_same_patch() {
1285 let prerelease = create_test_installation(
1286 ImplementationName::CPython,
1287 3,
1288 10,
1289 8,
1290 Some(Prerelease {
1291 kind: PrereleaseKind::Alpha,
1292 number: 1,
1293 }),
1294 PythonVariant::Default,
1295 None,
1296 );
1297
1298 assert!(!prerelease.is_upgrade_of(&prerelease));
1300 }
1301
1302 #[test]
1303 fn test_is_upgrade_of_build_version() {
1304 let older_build = create_test_installation(
1305 ImplementationName::CPython,
1306 3,
1307 10,
1308 8,
1309 None,
1310 PythonVariant::Default,
1311 Some("20240101"),
1312 );
1313 let newer_build = create_test_installation(
1314 ImplementationName::CPython,
1315 3,
1316 10,
1317 8,
1318 None,
1319 PythonVariant::Default,
1320 Some("20240201"),
1321 );
1322
1323 assert!(newer_build.is_upgrade_of(&older_build));
1325 assert!(!older_build.is_upgrade_of(&newer_build));
1327 }
1328
1329 #[test]
1330 fn test_is_upgrade_of_build_version_same() {
1331 let installation = create_test_installation(
1332 ImplementationName::CPython,
1333 3,
1334 10,
1335 8,
1336 None,
1337 PythonVariant::Default,
1338 Some("20240101"),
1339 );
1340
1341 assert!(!installation.is_upgrade_of(&installation));
1343 }
1344
1345 #[test]
1346 fn test_is_upgrade_of_build_with_legacy_installation() {
1347 let legacy = create_test_installation(
1348 ImplementationName::CPython,
1349 3,
1350 10,
1351 8,
1352 None,
1353 PythonVariant::Default,
1354 None,
1355 );
1356 let with_build = create_test_installation(
1357 ImplementationName::CPython,
1358 3,
1359 10,
1360 8,
1361 None,
1362 PythonVariant::Default,
1363 Some("20240101"),
1364 );
1365
1366 assert!(with_build.is_upgrade_of(&legacy));
1368 assert!(!legacy.is_upgrade_of(&with_build));
1370 }
1371
1372 #[test]
1373 fn test_is_upgrade_of_patch_takes_precedence_over_build() {
1374 let older_patch_newer_build = create_test_installation(
1375 ImplementationName::CPython,
1376 3,
1377 10,
1378 8,
1379 None,
1380 PythonVariant::Default,
1381 Some("20240201"),
1382 );
1383 let newer_patch_older_build = create_test_installation(
1384 ImplementationName::CPython,
1385 3,
1386 10,
1387 9,
1388 None,
1389 PythonVariant::Default,
1390 Some("20240101"),
1391 );
1392
1393 assert!(newer_patch_older_build.is_upgrade_of(&older_patch_newer_build));
1395 assert!(!older_patch_newer_build.is_upgrade_of(&newer_patch_older_build));
1397 }
1398
1399 #[test]
1400 fn test_find_version_matching() {
1401 use crate::PythonVersion;
1402
1403 let platform = Platform::from_env().unwrap();
1404 let temp_dir = tempfile::tempdir().unwrap();
1405
1406 fs::create_dir(temp_dir.path().join(format!("cpython-3.10.0-{platform}"))).unwrap();
1408
1409 temp_env::with_var(
1410 uv_static::EnvVars::UV_PYTHON_INSTALL_DIR,
1411 Some(temp_dir.path()),
1412 || {
1413 let installations = ManagedPythonInstallations::from_settings(None).unwrap();
1414
1415 let v3_1 = PythonVersion::from_str("3.1").unwrap();
1417 let matched: Vec<_> = installations.find_version(&v3_1).unwrap().collect();
1418 assert_eq!(matched.len(), 0);
1419
1420 let v3_10 = PythonVersion::from_str("3.10").unwrap();
1422 let matched: Vec<_> = installations.find_version(&v3_10).unwrap().collect();
1423 assert_eq!(matched.len(), 1);
1424 },
1425 );
1426 }
1427}