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, normalize_absolute_path,
20 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::discovery::VersionRequest;
29use crate::downloads::{Error as DownloadError, ManagedPythonDownload};
30use crate::implementation::{
31 Error as ImplementationError, ImplementationName, LenientImplementationName,
32};
33use crate::installation::{self, PythonInstallationKey};
34use crate::interpreter::Interpreter;
35use crate::python_version::PythonVersion;
36use crate::{
37 PythonInstallationMinorVersionKey, PythonRequest, PythonVariant, macos_dylib, sysconfig,
38};
39
40#[derive(Error, Debug)]
41pub enum Error {
42 #[error(transparent)]
43 Io(#[from] io::Error),
44 #[error(transparent)]
45 LockedFile(#[from] LockedFileError),
46 #[error(transparent)]
47 Download(#[from] DownloadError),
48 #[error(transparent)]
49 PlatformError(#[from] PlatformError),
50 #[error(transparent)]
51 ImplementationError(#[from] ImplementationError),
52 #[error("Invalid python version: {0}")]
53 InvalidPythonVersion(String),
54 #[error(transparent)]
55 ExtractError(#[from] uv_extract::Error),
56 #[error(transparent)]
57 SysconfigError(#[from] sysconfig::Error),
58 #[error("Missing expected Python executable at {}", _0.user_display())]
59 MissingExecutable(PathBuf),
60 #[error("Missing expected target directory for Python minor version link at {}", _0.user_display())]
61 MissingPythonMinorVersionLinkTargetDirectory(PathBuf),
62 #[error("Failed to create canonical Python executable")]
63 CanonicalizeExecutable(#[source] io::Error),
64 #[error("Failed to create Python executable link")]
65 LinkExecutable(#[source] io::Error),
66 #[error("Failed to create Python minor version link directory")]
67 PythonMinorVersionLinkDirectory(#[source] io::Error),
68 #[error("Failed to create directory for Python executable link")]
69 ExecutableDirectory(#[source] io::Error),
70 #[error("Failed to read Python installation directory")]
71 ReadError(#[source] io::Error),
72 #[error("Failed to find a directory to install executables into")]
73 NoExecutableDirectory,
74 #[error(transparent)]
75 LauncherError(#[from] uv_trampoline_builder::Error),
76 #[error("Failed to read managed Python directory name: {0}")]
77 NameError(String),
78 #[error("Failed to construct absolute path to managed Python directory: {}", _0.user_display())]
79 AbsolutePath(PathBuf, #[source] io::Error),
80 #[error(transparent)]
81 NameParseError(#[from] installation::PythonInstallationKeyError),
82 #[error("Failed to determine the libc used on the current platform")]
83 LibcDetection(#[from] LibcDetectionError),
84 #[error(transparent)]
85 MacOsDylib(#[from] macos_dylib::Error),
86}
87
88pub fn compare_build_versions(a: &str, b: &str) -> Ordering {
93 match (a.parse::<u64>(), b.parse::<u64>()) {
94 (Ok(a_num), Ok(b_num)) => a_num.cmp(&b_num),
95 _ => a.cmp(b),
96 }
97}
98
99#[derive(Debug, Clone, Eq, PartialEq)]
101pub struct ManagedPythonInstallations {
102 root: PathBuf,
104}
105
106impl ManagedPythonInstallations {
107 fn from_path(root: impl Into<PathBuf>) -> Self {
109 Self { root: root.into() }
110 }
111
112 pub async fn lock(&self) -> Result<LockedFile, Error> {
115 Ok(LockedFile::acquire(
116 self.root.join(".lock"),
117 LockedFileMode::Exclusive,
118 self.root.user_display(),
119 )
120 .await?)
121 }
122
123 pub fn from_settings(install_dir: Option<PathBuf>) -> Result<Self, Error> {
130 if let Some(install_dir) = install_dir {
131 Ok(Self::from_path(install_dir))
132 } else if let Some(install_dir) =
133 std::env::var_os(EnvVars::UV_PYTHON_INSTALL_DIR).filter(|s| !s.is_empty())
134 {
135 Ok(Self::from_path(install_dir))
136 } else {
137 Ok(Self::from_path(
138 StateStore::from_settings(None)?.bucket(StateBucket::ManagedPython),
139 ))
140 }
141 }
142
143 pub fn temp() -> Result<Self, Error> {
145 Ok(Self::from_path(
146 StateStore::temp()?.bucket(StateBucket::ManagedPython),
147 ))
148 }
149
150 pub fn scratch(&self) -> PathBuf {
152 self.root.join(".temp")
153 }
154
155 pub fn init(self) -> Result<Self, Error> {
159 let root = &self.root;
160
161 if !root.exists()
163 && root
164 .parent()
165 .is_some_and(|parent| parent.join("toolchains").exists())
166 {
167 let deprecated = root.parent().unwrap().join("toolchains");
168 fs::rename(&deprecated, root)?;
170 uv_fs::replace_symlink(root, &deprecated)?;
172 } else {
173 fs::create_dir_all(root)?;
174 }
175
176 fs::create_dir_all(root)?;
178
179 let scratch = self.scratch();
181 fs::create_dir_all(&scratch)?;
182
183 match fs::OpenOptions::new()
185 .write(true)
186 .create_new(true)
187 .open(root.join(".gitignore"))
188 {
189 Ok(mut file) => file.write_all(b"*")?,
190 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
191 Err(err) => return Err(err.into()),
192 }
193
194 Ok(self)
195 }
196
197 pub fn find_all(
202 &self,
203 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + use<>, Error> {
204 let dirs = match fs_err::read_dir(&self.root) {
205 Ok(installation_dirs) => {
206 let directories: Vec<_> = installation_dirs
208 .filter_map(|read_dir| match read_dir {
209 Ok(entry) => match entry.file_type() {
210 Ok(file_type) => file_type.is_dir().then_some(Ok(entry.path())),
211 Err(err) => Some(Err(err)),
212 },
213 Err(err) => Some(Err(err)),
214 })
215 .collect::<Result<_, io::Error>>()
216 .map_err(Error::ReadError)?;
217 directories
218 }
219 Err(err) if err.kind() == io::ErrorKind::NotFound => vec![],
220 Err(err) => {
221 return Err(Error::ReadError(err));
222 }
223 };
224 let scratch = self.scratch();
225 Ok(dirs
226 .into_iter()
227 .filter(|path| *path != scratch)
229 .filter(|path| {
231 path.file_name()
232 .and_then(OsStr::to_str)
233 .map(|name| !name.starts_with('.'))
234 .unwrap_or(true)
235 })
236 .filter_map(|path| {
237 ManagedPythonInstallation::from_path(path)
238 .inspect_err(|err| {
239 warn!("Ignoring malformed managed Python entry:\n {err}");
240 })
241 .ok()
242 })
243 .sorted_unstable_by_key(|installation| Reverse(installation.key().clone())))
244 }
245
246 pub fn find_matching_current_platform(
248 &self,
249 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + use<>, Error> {
250 let platform = Platform::from_env()?;
251
252 let iter = Self::from_settings(None)?
253 .find_all()?
254 .filter(move |installation| {
255 if !platform.supports(installation.platform()) {
256 debug!("Skipping managed installation `{installation}`: not supported by current platform `{platform}`");
257 return false;
258 }
259 true
260 });
261
262 Ok(iter)
263 }
264
265 pub fn find_version<'a>(
272 &'a self,
273 version: &'a PythonVersion,
274 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + 'a, Error> {
275 let request = VersionRequest::from(version);
276 Ok(self
277 .find_matching_current_platform()?
278 .filter(move |installation| request.matches_installation_key(installation.key())))
279 }
280
281 pub fn root(&self) -> &Path {
282 &self.root
283 }
284
285 pub(crate) fn absolute_root(&self) -> Result<PathBuf, Error> {
286 let root = if self.root.is_absolute() {
287 self.root.clone()
288 } else {
289 crate::current_dir()?.join(&self.root)
290 };
291
292 normalize_absolute_path(&root).map_err(|err| Error::AbsolutePath(self.root.clone(), err))
293 }
294}
295
296static EXTERNALLY_MANAGED: &str = "[externally-managed]
297Error=This Python installation is managed by uv and should not be modified.
298";
299
300#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
302pub struct ManagedPythonInstallation {
303 path: PathBuf,
305 key: PythonInstallationKey,
307 url: Option<Cow<'static, str>>,
311 sha256: Option<Cow<'static, str>>,
315 build: Option<Cow<'static, str>>,
319}
320
321impl ManagedPythonInstallation {
322 pub fn new(path: PathBuf, download: &ManagedPythonDownload) -> Self {
323 Self {
324 path,
325 key: download.key().clone(),
326 url: Some(download.url().clone()),
327 sha256: download.sha256().cloned(),
328 build: download.build().map(Cow::Borrowed),
329 }
330 }
331
332 pub(crate) fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
333 let path = path.as_ref();
334
335 let key = PythonInstallationKey::from_str(
336 path.file_name()
337 .ok_or(Error::NameError("name is empty".to_string()))?
338 .to_str()
339 .ok_or(Error::NameError("not a valid string".to_string()))?,
340 )?;
341
342 let path = std::path::absolute(path)
343 .map_err(|err| Error::AbsolutePath(path.to_path_buf(), err))?;
344
345 let build = match fs::read_to_string(path.join("BUILD")) {
347 Ok(content) => Some(Cow::Owned(content.trim().to_string())),
348 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
349 Err(err) => return Err(err.into()),
350 };
351
352 Ok(Self {
353 path,
354 key,
355 url: None,
356 sha256: None,
357 build,
358 })
359 }
360
361 pub fn try_from_interpreter(interpreter: &Interpreter) -> Option<Self> {
365 let managed_root = ManagedPythonInstallations::from_settings(None).ok()?;
366 let root = managed_root.absolute_root().ok()?;
367
368 let sys_base_prefix = dunce::canonicalize(interpreter.sys_base_prefix())
372 .unwrap_or_else(|_| interpreter.sys_base_prefix().to_path_buf());
373 let root = dunce::canonicalize(&root).unwrap_or(root);
374
375 let suffix = sys_base_prefix.strip_prefix(&root).ok()?;
377
378 let first_component = suffix.components().next()?;
379 let name = first_component.as_os_str().to_str()?;
380
381 PythonInstallationKey::from_str(name).ok()?;
383
384 let path = root.join(name);
386 Self::from_path(path).ok()
387 }
388
389 pub fn executable(&self, windowed: bool) -> PathBuf {
398 let version = match self.implementation() {
399 ImplementationName::CPython => {
400 if cfg!(unix) {
401 format!("{}.{}", self.key.major, self.key.minor)
402 } else {
403 String::new()
404 }
405 }
406 ImplementationName::PyPy => format!("{}.{}", self.key.major, self.key.minor),
408 ImplementationName::Pyodide => String::new(),
410 ImplementationName::GraalPy => String::new(),
411 };
412
413 let variant = if self.implementation() == ImplementationName::GraalPy {
416 ""
417 } else if cfg!(unix) {
418 self.key.variant.executable_suffix()
419 } else if cfg!(windows) && windowed {
420 "w"
422 } else {
423 ""
424 };
425
426 let name = format!(
427 "{implementation}{version}{variant}{exe}",
428 implementation = self.implementation().executable_name(),
429 exe = std::env::consts::EXE_SUFFIX
430 );
431
432 let executable = executable_path_from_base(
433 self.python_dir().as_path(),
434 &name,
435 &LenientImplementationName::from(self.implementation()),
436 *self.key.os(),
437 );
438
439 if cfg!(windows)
444 && matches!(self.key.variant, PythonVariant::Freethreaded)
445 && !executable.exists()
446 {
447 return self.python_dir().join(format!(
449 "python{}.{}t{}",
450 self.key.major,
451 self.key.minor,
452 std::env::consts::EXE_SUFFIX
453 ));
454 }
455
456 executable
457 }
458
459 fn python_dir(&self) -> PathBuf {
460 let install = self.path.join("install");
461 if install.is_dir() {
462 install
463 } else {
464 self.path.clone()
465 }
466 }
467
468 pub fn version(&self) -> PythonVersion {
470 self.key.version()
471 }
472
473 pub fn implementation(&self) -> ImplementationName {
474 match self.key.implementation().into_owned() {
475 LenientImplementationName::Known(implementation) => implementation,
476 LenientImplementationName::Unknown(_) => {
477 panic!("Managed Python installations should have a known implementation")
478 }
479 }
480 }
481
482 pub fn path(&self) -> &Path {
483 &self.path
484 }
485
486 pub fn key(&self) -> &PythonInstallationKey {
487 &self.key
488 }
489
490 pub fn platform(&self) -> &Platform {
491 self.key.platform()
492 }
493
494 pub fn build(&self) -> Option<&str> {
496 self.build.as_deref()
497 }
498
499 pub fn minor_version_key(&self) -> &PythonInstallationMinorVersionKey {
500 PythonInstallationMinorVersionKey::ref_cast(&self.key)
501 }
502
503 pub fn satisfies(&self, request: &PythonRequest) -> bool {
504 match request {
505 PythonRequest::File(path) => self.executable(false) == *path,
506 PythonRequest::Default | PythonRequest::Any => true,
507 PythonRequest::Directory(path) => self.path() == *path,
508 PythonRequest::ExecutableName(name) => self
509 .executable(false)
510 .file_name()
511 .is_some_and(|filename| filename.to_string_lossy() == *name),
512 PythonRequest::Implementation(implementation) => {
513 *implementation == self.implementation()
514 }
515 PythonRequest::ImplementationVersion(implementation, version) => {
516 *implementation == self.implementation() && version.matches_version(&self.version())
517 }
518 PythonRequest::Version(version) => version.matches_version(&self.version()),
519 PythonRequest::Key(request) => request.satisfied_by_key(self.key()),
520 }
521 }
522
523 pub fn ensure_canonical_executables(&self) -> Result<(), Error> {
525 let python = self.executable(false);
526
527 let canonical_names = &["python"];
528
529 for name in canonical_names {
530 let executable =
531 python.with_file_name(format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX));
532
533 if executable == python {
536 continue;
537 }
538
539 match symlink_or_copy_file(&python, &executable) {
540 Ok(()) => {
541 debug!(
542 "Created link {} -> {}",
543 executable.user_display(),
544 python.user_display(),
545 );
546 }
547 Err(err) if err.kind() == io::ErrorKind::NotFound => {
548 return Err(Error::MissingExecutable(python.clone()));
549 }
550 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
551 Err(err) => {
552 return Err(Error::CanonicalizeExecutable(err));
553 }
554 }
555 }
556
557 Ok(())
558 }
559
560 pub fn ensure_minor_version_link(&self) -> Result<(), Error> {
563 if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self) {
564 minor_version_link.create_directory()?;
565 }
566 Ok(())
567 }
568
569 pub fn ensure_externally_managed(&self) -> Result<(), Error> {
572 if self.key.os().is_emscripten() {
573 return Ok(());
576 }
577 let stdlib = if self.key.os().is_windows() {
579 self.python_dir().join("Lib")
580 } else {
581 let lib_suffix = self.key.variant.lib_suffix();
582 let python = if matches!(
583 self.key.implementation,
584 LenientImplementationName::Known(ImplementationName::PyPy)
585 ) {
586 format!("pypy{}", self.key.version().python_version())
587 } else {
588 format!("python{}{lib_suffix}", self.key.version().python_version())
589 };
590 self.python_dir().join("lib").join(python)
591 };
592
593 let file = stdlib.join("EXTERNALLY-MANAGED");
594 fs_err::write(file, EXTERNALLY_MANAGED)?;
595
596 Ok(())
597 }
598
599 pub fn ensure_sysconfig_patched(&self) -> Result<(), Error> {
601 if cfg!(unix) {
602 if self.key.os().is_emscripten() {
603 return Ok(());
606 }
607 if self.implementation() == ImplementationName::CPython {
608 sysconfig::update_sysconfig(
609 self.path(),
610 self.key.major,
611 self.key.minor,
612 self.key.variant.lib_suffix(),
613 )?;
614 }
615 }
616 Ok(())
617 }
618
619 pub fn ensure_dylib_patched(&self) -> Result<(), macos_dylib::Error> {
626 if cfg!(target_os = "macos") {
627 if self.key().os().is_like_darwin() {
628 if self.implementation() == ImplementationName::CPython {
629 let dylib_path = self.python_dir().join("lib").join(format!(
630 "{}python{}{}{}",
631 std::env::consts::DLL_PREFIX,
632 self.key.version().python_version(),
633 self.key.variant().executable_suffix(),
634 std::env::consts::DLL_SUFFIX
635 ));
636 macos_dylib::patch_dylib_install_name(dylib_path)?;
637 }
638 }
639 }
640 Ok(())
641 }
642
643 pub fn ensure_build_file(&self) -> Result<(), Error> {
645 if let Some(ref build) = self.build {
646 let build_file = self.path.join("BUILD");
647 fs::write(&build_file, build.as_ref())?;
648 }
649 Ok(())
650 }
651
652 pub fn is_bin_link(&self, path: &Path) -> bool {
655 if cfg!(unix) {
656 same_file::is_same_file(path, self.executable(false)).unwrap_or_default()
657 } else if cfg!(windows) {
658 let Some(launcher) = Launcher::try_from_path(path).unwrap_or_default() else {
659 return false;
660 };
661 if !matches!(launcher.kind, LauncherKind::Python) {
662 return false;
663 }
664 dunce::canonicalize(&launcher.python_path).unwrap_or(launcher.python_path)
668 == self.executable(false)
669 } else {
670 unreachable!("Only Windows and Unix are supported")
671 }
672 }
673
674 pub fn is_upgrade_of(&self, other: &Self) -> bool {
676 if self.key.implementation != other.key.implementation {
678 return false;
679 }
680 if self.key.variant != other.key.variant {
682 return false;
683 }
684 if (self.key.major, self.key.minor) != (other.key.major, other.key.minor) {
686 return false;
687 }
688 if self.key.patch == other.key.patch {
691 return match (self.key.prerelease, other.key.prerelease) {
692 (Some(self_pre), Some(other_pre)) => self_pre > other_pre,
694 (None, Some(_)) => true,
696 (Some(_), None) => false,
698 (None, None) => match (self.build.as_deref(), other.build.as_deref()) {
700 (Some(_), None) => true,
702 (Some(self_build), Some(other_build)) => {
704 compare_build_versions(self_build, other_build) == Ordering::Greater
705 }
706 (None, _) => false,
708 },
709 };
710 }
711 if self.key.patch < other.key.patch {
713 return false;
714 }
715 true
716 }
717
718 pub fn url(&self) -> Option<&str> {
719 self.url.as_deref()
720 }
721
722 pub fn sha256(&self) -> Option<&str> {
723 self.sha256.as_deref()
724 }
725}
726
727#[derive(Clone, Debug)]
730pub struct PythonMinorVersionLink {
731 pub symlink_directory: PathBuf,
733 pub symlink_executable: PathBuf,
736 pub target_directory: PathBuf,
739}
740
741impl PythonMinorVersionLink {
742 fn from_executable(executable: &Path, key: &PythonInstallationKey) -> Option<Self> {
762 let implementation = key.implementation();
763 if !matches!(
764 implementation.as_ref(),
765 LenientImplementationName::Known(ImplementationName::CPython)
766 ) {
767 return None;
769 }
770 let executable_name = executable
771 .file_name()
772 .expect("Executable file name should exist");
773 let symlink_directory_name = PythonInstallationMinorVersionKey::ref_cast(key).to_string();
774 let parent = executable
775 .parent()
776 .expect("Executable should have parent directory");
777
778 let target_directory = if cfg!(unix) {
780 if parent
781 .components()
782 .next_back()
783 .is_some_and(|c| c.as_os_str() == "bin")
784 {
785 parent.parent()?.to_path_buf()
786 } else {
787 return None;
788 }
789 } else if cfg!(windows) {
790 parent.to_path_buf()
791 } else {
792 unimplemented!("Only Windows and Unix systems are supported.")
793 };
794 let symlink_directory = target_directory.with_file_name(symlink_directory_name);
795 if target_directory == symlink_directory {
797 return None;
798 }
799 let symlink_executable = executable_path_from_base(
801 symlink_directory.as_path(),
802 &executable_name.to_string_lossy(),
803 &implementation,
804 *key.os(),
805 );
806 let minor_version_link = Self {
807 symlink_directory,
808 symlink_executable,
809 target_directory,
810 };
811 Some(minor_version_link)
812 }
813
814 pub fn from_installation(installation: &ManagedPythonInstallation) -> Option<Self> {
815 Self::from_executable(installation.executable(false).as_path(), installation.key())
816 }
817
818 pub fn create_directory(&self) -> Result<(), Error> {
819 match replace_symlink(
820 self.target_directory.as_path(),
821 self.symlink_directory.as_path(),
822 ) {
823 Ok(()) => {
824 debug!(
825 "Created link {} -> {}",
826 &self.symlink_directory.user_display(),
827 &self.target_directory.user_display(),
828 );
829 }
830 Err(err) if err.kind() == io::ErrorKind::NotFound => {
831 return Err(Error::MissingPythonMinorVersionLinkTargetDirectory(
832 self.target_directory.clone(),
833 ));
834 }
835 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
836 Err(err) => {
837 return Err(Error::PythonMinorVersionLinkDirectory(err));
838 }
839 }
840 Ok(())
841 }
842
843 pub fn exists(&self) -> bool {
850 #[cfg(unix)]
851 {
852 self.symlink_directory
853 .symlink_metadata()
854 .is_ok_and(|metadata| metadata.file_type().is_symlink())
855 && self
856 .read_target()
857 .is_some_and(|target| target == self.target_directory)
858 }
859 #[cfg(windows)]
860 {
861 self.symlink_directory
862 .symlink_metadata()
863 .is_ok_and(|metadata| {
864 (metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0) != 0
867 })
868 && self
869 .read_target()
870 .is_some_and(|target| target == self.target_directory)
871 }
872 }
873
874 pub fn read_target(&self) -> Option<PathBuf> {
880 #[cfg(unix)]
881 {
882 self.symlink_directory.read_link().ok()
883 }
884 #[cfg(windows)]
885 {
886 junction::get_target(&self.symlink_directory).ok()
887 }
888 }
889}
890
891fn executable_path_from_base(
895 base: &Path,
896 executable_name: &str,
897 implementation: &LenientImplementationName,
898 os: Os,
899) -> PathBuf {
900 if matches!(
901 implementation,
902 &LenientImplementationName::Known(ImplementationName::GraalPy)
903 ) {
904 base.join("bin").join(executable_name)
906 } else if os.is_emscripten()
907 || matches!(
908 implementation,
909 &LenientImplementationName::Known(ImplementationName::Pyodide)
910 )
911 {
912 base.join(executable_name)
914 } else if os.is_windows() {
915 base.join(executable_name)
917 } else {
918 base.join("bin").join(executable_name)
920 }
921}
922
923pub fn create_link_to_executable(link: &Path, executable: &Path) -> Result<(), Error> {
927 let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?;
928 fs_err::create_dir_all(link_parent).map_err(Error::ExecutableDirectory)?;
929
930 if cfg!(unix) {
931 match symlink_or_copy_file(executable, link) {
933 Ok(()) => Ok(()),
934 Err(err) if err.kind() == io::ErrorKind::NotFound => {
935 Err(Error::MissingExecutable(executable.to_path_buf()))
936 }
937 Err(err) => Err(Error::LinkExecutable(err)),
938 }
939 } else if cfg!(windows) {
940 use uv_trampoline_builder::windows_python_launcher;
941
942 let launcher = windows_python_launcher(executable, false)?;
944
945 #[expect(clippy::disallowed_types)]
948 {
949 std::fs::File::create_new(link)
950 .and_then(|mut file| file.write_all(launcher.as_ref()))
951 .map_err(Error::LinkExecutable)
952 }
953 } else {
954 unimplemented!("Only Windows and Unix are supported.")
955 }
956}
957
958pub fn replace_link_to_executable(link: &Path, executable: &Path) -> Result<(), Error> {
964 let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?;
965 fs_err::create_dir_all(link_parent).map_err(Error::ExecutableDirectory)?;
966
967 if cfg!(unix) {
968 replace_symlink(executable, link).map_err(Error::LinkExecutable)
969 } else if cfg!(windows) {
970 use uv_trampoline_builder::windows_python_launcher;
971
972 let launcher = windows_python_launcher(executable, false)?;
973
974 uv_fs::write_atomic_sync(link, &*launcher).map_err(Error::LinkExecutable)
975 } else {
976 unimplemented!("Only Windows and Unix are supported.")
977 }
978}
979
980pub fn platform_key_from_env() -> Result<String, Error> {
983 Ok(Platform::from_env()?.to_string().to_lowercase())
984}
985
986impl fmt::Display for ManagedPythonInstallation {
987 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
988 write!(
989 f,
990 "{}",
991 self.path
992 .file_name()
993 .unwrap_or(self.path.as_os_str())
994 .to_string_lossy()
995 )
996 }
997}
998
999pub fn python_executable_dir() -> Result<PathBuf, Error> {
1001 uv_dirs::user_executable_directory(Some(EnvVars::UV_PYTHON_BIN_DIR))
1002 .ok_or(Error::NoExecutableDirectory)
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007 use super::*;
1008 use crate::implementation::LenientImplementationName;
1009 use crate::installation::PythonInstallationKey;
1010 use crate::{ImplementationName, PythonVariant};
1011 use std::path::PathBuf;
1012 use std::str::FromStr;
1013 use uv_pep440::{Prerelease, PrereleaseKind};
1014 use uv_platform::Platform;
1015
1016 fn create_test_installation(
1017 implementation: ImplementationName,
1018 major: u8,
1019 minor: u8,
1020 patch: u8,
1021 prerelease: Option<Prerelease>,
1022 variant: PythonVariant,
1023 build: Option<&str>,
1024 ) -> ManagedPythonInstallation {
1025 let platform = Platform::from_str("linux-x86_64-gnu").unwrap();
1026 let key = PythonInstallationKey::new(
1027 LenientImplementationName::Known(implementation),
1028 major,
1029 minor,
1030 patch,
1031 prerelease,
1032 platform,
1033 variant,
1034 );
1035 ManagedPythonInstallation {
1036 path: PathBuf::from("/test/path"),
1037 key,
1038 url: None,
1039 sha256: None,
1040 build: build.map(|s| Cow::Owned(s.to_owned())),
1041 }
1042 }
1043
1044 #[test]
1045 fn test_is_upgrade_of_same_version() {
1046 let installation = create_test_installation(
1047 ImplementationName::CPython,
1048 3,
1049 10,
1050 8,
1051 None,
1052 PythonVariant::Default,
1053 None,
1054 );
1055
1056 assert!(!installation.is_upgrade_of(&installation));
1058 }
1059
1060 #[test]
1061 fn test_is_upgrade_of_patch_version() {
1062 let older = create_test_installation(
1063 ImplementationName::CPython,
1064 3,
1065 10,
1066 8,
1067 None,
1068 PythonVariant::Default,
1069 None,
1070 );
1071 let newer = create_test_installation(
1072 ImplementationName::CPython,
1073 3,
1074 10,
1075 9,
1076 None,
1077 PythonVariant::Default,
1078 None,
1079 );
1080
1081 assert!(newer.is_upgrade_of(&older));
1083 assert!(!older.is_upgrade_of(&newer));
1085 }
1086
1087 #[test]
1088 fn test_is_upgrade_of_different_minor_version() {
1089 let py310 = create_test_installation(
1090 ImplementationName::CPython,
1091 3,
1092 10,
1093 8,
1094 None,
1095 PythonVariant::Default,
1096 None,
1097 );
1098 let py311 = create_test_installation(
1099 ImplementationName::CPython,
1100 3,
1101 11,
1102 0,
1103 None,
1104 PythonVariant::Default,
1105 None,
1106 );
1107
1108 assert!(!py311.is_upgrade_of(&py310));
1110 assert!(!py310.is_upgrade_of(&py311));
1111 }
1112
1113 #[test]
1114 fn test_is_upgrade_of_different_implementation() {
1115 let cpython = create_test_installation(
1116 ImplementationName::CPython,
1117 3,
1118 10,
1119 8,
1120 None,
1121 PythonVariant::Default,
1122 None,
1123 );
1124 let pypy = create_test_installation(
1125 ImplementationName::PyPy,
1126 3,
1127 10,
1128 9,
1129 None,
1130 PythonVariant::Default,
1131 None,
1132 );
1133
1134 assert!(!pypy.is_upgrade_of(&cpython));
1136 assert!(!cpython.is_upgrade_of(&pypy));
1137 }
1138
1139 #[test]
1140 fn test_is_upgrade_of_different_variant() {
1141 let default = create_test_installation(
1142 ImplementationName::CPython,
1143 3,
1144 10,
1145 8,
1146 None,
1147 PythonVariant::Default,
1148 None,
1149 );
1150 let freethreaded = create_test_installation(
1151 ImplementationName::CPython,
1152 3,
1153 10,
1154 9,
1155 None,
1156 PythonVariant::Freethreaded,
1157 None,
1158 );
1159
1160 assert!(!freethreaded.is_upgrade_of(&default));
1162 assert!(!default.is_upgrade_of(&freethreaded));
1163 }
1164
1165 #[test]
1166 fn test_is_upgrade_of_prerelease() {
1167 let stable = create_test_installation(
1168 ImplementationName::CPython,
1169 3,
1170 10,
1171 8,
1172 None,
1173 PythonVariant::Default,
1174 None,
1175 );
1176 let prerelease = create_test_installation(
1177 ImplementationName::CPython,
1178 3,
1179 10,
1180 8,
1181 Some(Prerelease {
1182 kind: PrereleaseKind::Alpha,
1183 number: 1,
1184 }),
1185 PythonVariant::Default,
1186 None,
1187 );
1188
1189 assert!(stable.is_upgrade_of(&prerelease));
1191
1192 assert!(!prerelease.is_upgrade_of(&stable));
1194 }
1195
1196 #[test]
1197 fn test_is_upgrade_of_prerelease_to_prerelease() {
1198 let alpha1 = create_test_installation(
1199 ImplementationName::CPython,
1200 3,
1201 10,
1202 8,
1203 Some(Prerelease {
1204 kind: PrereleaseKind::Alpha,
1205 number: 1,
1206 }),
1207 PythonVariant::Default,
1208 None,
1209 );
1210 let alpha2 = create_test_installation(
1211 ImplementationName::CPython,
1212 3,
1213 10,
1214 8,
1215 Some(Prerelease {
1216 kind: PrereleaseKind::Alpha,
1217 number: 2,
1218 }),
1219 PythonVariant::Default,
1220 None,
1221 );
1222
1223 assert!(alpha2.is_upgrade_of(&alpha1));
1225 assert!(!alpha1.is_upgrade_of(&alpha2));
1227 }
1228
1229 #[test]
1230 fn test_is_upgrade_of_prerelease_same_patch() {
1231 let prerelease = create_test_installation(
1232 ImplementationName::CPython,
1233 3,
1234 10,
1235 8,
1236 Some(Prerelease {
1237 kind: PrereleaseKind::Alpha,
1238 number: 1,
1239 }),
1240 PythonVariant::Default,
1241 None,
1242 );
1243
1244 assert!(!prerelease.is_upgrade_of(&prerelease));
1246 }
1247
1248 #[test]
1249 fn test_is_upgrade_of_build_version() {
1250 let older_build = create_test_installation(
1251 ImplementationName::CPython,
1252 3,
1253 10,
1254 8,
1255 None,
1256 PythonVariant::Default,
1257 Some("20240101"),
1258 );
1259 let newer_build = create_test_installation(
1260 ImplementationName::CPython,
1261 3,
1262 10,
1263 8,
1264 None,
1265 PythonVariant::Default,
1266 Some("20240201"),
1267 );
1268
1269 assert!(newer_build.is_upgrade_of(&older_build));
1271 assert!(!older_build.is_upgrade_of(&newer_build));
1273 }
1274
1275 #[test]
1276 fn test_is_upgrade_of_build_version_same() {
1277 let installation = create_test_installation(
1278 ImplementationName::CPython,
1279 3,
1280 10,
1281 8,
1282 None,
1283 PythonVariant::Default,
1284 Some("20240101"),
1285 );
1286
1287 assert!(!installation.is_upgrade_of(&installation));
1289 }
1290
1291 #[test]
1292 fn test_is_upgrade_of_build_with_legacy_installation() {
1293 let legacy = create_test_installation(
1294 ImplementationName::CPython,
1295 3,
1296 10,
1297 8,
1298 None,
1299 PythonVariant::Default,
1300 None,
1301 );
1302 let with_build = create_test_installation(
1303 ImplementationName::CPython,
1304 3,
1305 10,
1306 8,
1307 None,
1308 PythonVariant::Default,
1309 Some("20240101"),
1310 );
1311
1312 assert!(with_build.is_upgrade_of(&legacy));
1314 assert!(!legacy.is_upgrade_of(&with_build));
1316 }
1317
1318 #[test]
1319 fn test_is_upgrade_of_patch_takes_precedence_over_build() {
1320 let older_patch_newer_build = create_test_installation(
1321 ImplementationName::CPython,
1322 3,
1323 10,
1324 8,
1325 None,
1326 PythonVariant::Default,
1327 Some("20240201"),
1328 );
1329 let newer_patch_older_build = create_test_installation(
1330 ImplementationName::CPython,
1331 3,
1332 10,
1333 9,
1334 None,
1335 PythonVariant::Default,
1336 Some("20240101"),
1337 );
1338
1339 assert!(newer_patch_older_build.is_upgrade_of(&older_patch_newer_build));
1341 assert!(!older_patch_newer_build.is_upgrade_of(&newer_patch_older_build));
1343 }
1344
1345 #[test]
1346 fn test_find_version_matching() {
1347 use crate::PythonVersion;
1348
1349 let platform = Platform::from_env().unwrap();
1350 let temp_dir = tempfile::tempdir().unwrap();
1351
1352 fs::create_dir(temp_dir.path().join(format!("cpython-3.10.0-{platform}"))).unwrap();
1354
1355 temp_env::with_var(
1356 uv_static::EnvVars::UV_PYTHON_INSTALL_DIR,
1357 Some(temp_dir.path()),
1358 || {
1359 let installations = ManagedPythonInstallations::from_settings(None).unwrap();
1360
1361 let v3_1 = PythonVersion::from_str("3.1").unwrap();
1363 let matched: Vec<_> = installations.find_version(&v3_1).unwrap().collect();
1364 assert_eq!(matched.len(), 0);
1365
1366 let v3_10 = PythonVersion::from_str("3.10").unwrap();
1368 let matched: Vec<_> = installations.find_version(&v3_10).unwrap().collect();
1369 assert_eq!(matched.len(), 1);
1370 },
1371 );
1372 }
1373
1374 #[test]
1375 fn test_relative_install_dir_resolves_against_pwd() {
1376 let temp_dir = tempfile::tempdir().unwrap();
1377 let workdir = temp_dir.path().join("workdir");
1378 fs::create_dir(&workdir).unwrap();
1379
1380 temp_env::with_vars(
1381 [
1382 (
1383 uv_static::EnvVars::UV_PYTHON_INSTALL_DIR,
1384 Some(std::ffi::OsStr::new(".python-installs")),
1385 ),
1386 (uv_static::EnvVars::PWD, Some(workdir.as_os_str())),
1387 ],
1388 || {
1389 let installations = ManagedPythonInstallations::from_settings(None).unwrap();
1390 assert_eq!(
1391 installations.absolute_root().unwrap(),
1392 workdir.join(".python-installs")
1393 );
1394 },
1395 );
1396 }
1397}