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(crate) fn find_matching_current_platform()
248 -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + use<>, Error> {
249 let platform = Platform::from_env()?;
250
251 let iter = Self::from_settings(None)?
252 .find_all()?
253 .filter(move |installation| {
254 if !platform.supports(installation.platform()) {
255 debug!("Skipping managed installation `{installation}`: not supported by current platform `{platform}`");
256 return false;
257 }
258 true
259 });
260
261 Ok(iter)
262 }
263
264 pub fn find_version<'a>(
271 &'a self,
272 version: &'a PythonVersion,
273 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + 'a, Error> {
274 let request = VersionRequest::from(version);
275 Ok(Self::find_matching_current_platform()?
276 .filter(move |installation| request.matches_installation_key(installation.key())))
277 }
278
279 pub fn root(&self) -> &Path {
280 &self.root
281 }
282
283 pub(crate) fn absolute_root(&self) -> Result<PathBuf, Error> {
284 let root = if self.root.is_absolute() {
285 self.root.clone()
286 } else {
287 crate::current_dir()?.join(&self.root)
288 };
289
290 normalize_absolute_path(&root).map_err(|err| Error::AbsolutePath(self.root.clone(), err))
291 }
292}
293
294static EXTERNALLY_MANAGED: &str = "[externally-managed]
295Error=This Python installation is managed by uv and should not be modified.
296";
297
298#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
300pub struct ManagedPythonInstallation {
301 path: PathBuf,
303 key: PythonInstallationKey,
305 url: Option<Cow<'static, str>>,
309 sha256: Option<Cow<'static, str>>,
313 build: Option<Cow<'static, str>>,
317}
318
319impl ManagedPythonInstallation {
320 pub fn new(path: PathBuf, download: &ManagedPythonDownload) -> Self {
321 Self {
322 path,
323 key: download.key().clone(),
324 url: Some(download.url().clone()),
325 sha256: download.sha256().cloned(),
326 build: download.build().map(Cow::Borrowed),
327 }
328 }
329
330 pub(crate) fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
331 let path = path.as_ref();
332
333 let key = PythonInstallationKey::from_str(
334 path.file_name()
335 .ok_or(Error::NameError("name is empty".to_string()))?
336 .to_str()
337 .ok_or(Error::NameError("not a valid string".to_string()))?,
338 )?;
339
340 let path = std::path::absolute(path)
341 .map_err(|err| Error::AbsolutePath(path.to_path_buf(), err))?;
342
343 let build = match fs::read_to_string(path.join("BUILD")) {
345 Ok(content) => Some(Cow::Owned(content.trim().to_string())),
346 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
347 Err(err) => return Err(err.into()),
348 };
349
350 Ok(Self {
351 path,
352 key,
353 url: None,
354 sha256: None,
355 build,
356 })
357 }
358
359 pub fn try_from_interpreter(interpreter: &Interpreter) -> Option<Self> {
363 let managed_root = ManagedPythonInstallations::from_settings(None).ok()?;
364 let root = managed_root.absolute_root().ok()?;
365
366 let sys_base_prefix = dunce::canonicalize(interpreter.sys_base_prefix())
370 .unwrap_or_else(|_| interpreter.sys_base_prefix().to_path_buf());
371 let root = dunce::canonicalize(&root).unwrap_or(root);
372
373 let suffix = sys_base_prefix.strip_prefix(&root).ok()?;
375
376 let first_component = suffix.components().next()?;
377 let name = first_component.as_os_str().to_str()?;
378
379 PythonInstallationKey::from_str(name).ok()?;
381
382 let path = root.join(name);
384 Self::from_path(path).ok()
385 }
386
387 pub fn executable(&self, windowed: bool) -> PathBuf {
396 let version = match self.implementation() {
397 ImplementationName::CPython => {
398 if cfg!(unix) {
399 format!("{}.{}", self.key.major, self.key.minor)
400 } else {
401 String::new()
402 }
403 }
404 ImplementationName::PyPy => format!("{}.{}", self.key.major, self.key.minor),
406 ImplementationName::Pyodide => String::new(),
408 ImplementationName::GraalPy => String::new(),
409 };
410
411 let variant = if self.implementation() == ImplementationName::GraalPy {
414 ""
415 } else if cfg!(unix) {
416 self.key.variant.executable_suffix()
417 } else if cfg!(windows) && windowed {
418 "w"
420 } else {
421 ""
422 };
423
424 let name = format!(
425 "{implementation}{version}{variant}{exe}",
426 implementation = self.implementation().executable_name(),
427 exe = std::env::consts::EXE_SUFFIX
428 );
429
430 let executable = executable_path_from_base(
431 self.python_dir().as_path(),
432 &name,
433 &LenientImplementationName::from(self.implementation()),
434 *self.key.os(),
435 );
436
437 if cfg!(windows)
442 && matches!(self.key.variant, PythonVariant::Freethreaded)
443 && !executable.exists()
444 {
445 return self.python_dir().join(format!(
447 "python{}.{}t{}",
448 self.key.major,
449 self.key.minor,
450 std::env::consts::EXE_SUFFIX
451 ));
452 }
453
454 executable
455 }
456
457 fn python_dir(&self) -> PathBuf {
458 let install = self.path.join("install");
459 if install.is_dir() {
460 install
461 } else {
462 self.path.clone()
463 }
464 }
465
466 pub fn version(&self) -> PythonVersion {
468 self.key.version()
469 }
470
471 pub fn implementation(&self) -> ImplementationName {
472 match self.key.implementation().into_owned() {
473 LenientImplementationName::Known(implementation) => implementation,
474 LenientImplementationName::Unknown(_) => {
475 panic!("Managed Python installations should have a known implementation")
476 }
477 }
478 }
479
480 pub fn path(&self) -> &Path {
481 &self.path
482 }
483
484 pub fn key(&self) -> &PythonInstallationKey {
485 &self.key
486 }
487
488 pub fn platform(&self) -> &Platform {
489 self.key.platform()
490 }
491
492 pub fn build(&self) -> Option<&str> {
494 self.build.as_deref()
495 }
496
497 pub fn minor_version_key(&self) -> &PythonInstallationMinorVersionKey {
498 PythonInstallationMinorVersionKey::ref_cast(&self.key)
499 }
500
501 pub fn satisfies(&self, request: &PythonRequest) -> bool {
502 match request {
503 PythonRequest::File(path) => self.executable(false) == *path,
504 PythonRequest::Default | PythonRequest::Any => true,
505 PythonRequest::Directory(path) => self.path() == *path,
506 PythonRequest::ExecutableName(name) => self
507 .executable(false)
508 .file_name()
509 .is_some_and(|filename| filename.to_string_lossy() == *name),
510 PythonRequest::Implementation(implementation) => {
511 *implementation == self.implementation()
512 }
513 PythonRequest::ImplementationVersion(implementation, version) => {
514 *implementation == self.implementation() && version.matches_version(&self.version())
515 }
516 PythonRequest::Version(version) => version.matches_version(&self.version()),
517 PythonRequest::Key(request) => request.satisfied_by_key(self.key()),
518 }
519 }
520
521 pub fn ensure_canonical_executables(&self) -> Result<(), Error> {
523 let python = self.executable(false);
524
525 let canonical_names = &["python"];
526
527 for name in canonical_names {
528 let executable =
529 python.with_file_name(format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX));
530
531 if executable == python {
534 continue;
535 }
536
537 match symlink_or_copy_file(&python, &executable) {
538 Ok(()) => {
539 debug!(
540 "Created link {} -> {}",
541 executable.user_display(),
542 python.user_display(),
543 );
544 }
545 Err(err) if err.kind() == io::ErrorKind::NotFound => {
546 return Err(Error::MissingExecutable(python.clone()));
547 }
548 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
549 Err(err) => {
550 return Err(Error::CanonicalizeExecutable(err));
551 }
552 }
553 }
554
555 Ok(())
556 }
557
558 pub fn ensure_minor_version_link(&self) -> Result<(), Error> {
561 if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self) {
562 minor_version_link.create_directory()?;
563 }
564 Ok(())
565 }
566
567 pub fn ensure_externally_managed(&self) -> Result<(), Error> {
570 if self.key.os().is_emscripten() {
571 return Ok(());
574 }
575 let stdlib = if self.key.os().is_windows() {
577 self.python_dir().join("Lib")
578 } else {
579 let lib_suffix = self.key.variant.lib_suffix();
580 let python = if matches!(
581 self.key.implementation,
582 LenientImplementationName::Known(ImplementationName::PyPy)
583 ) {
584 format!("pypy{}", self.key.version().python_version())
585 } else {
586 format!("python{}{lib_suffix}", self.key.version().python_version())
587 };
588 self.python_dir().join("lib").join(python)
589 };
590
591 let file = stdlib.join("EXTERNALLY-MANAGED");
592 fs_err::write(file, EXTERNALLY_MANAGED)?;
593
594 Ok(())
595 }
596
597 pub fn ensure_sysconfig_patched(&self) -> Result<(), Error> {
599 if cfg!(unix) {
600 if self.key.os().is_emscripten() {
601 return Ok(());
604 }
605 if self.implementation() == ImplementationName::CPython {
606 sysconfig::update_sysconfig(
607 self.path(),
608 self.key.major,
609 self.key.minor,
610 self.key.variant.lib_suffix(),
611 )?;
612 }
613 }
614 Ok(())
615 }
616
617 pub fn ensure_dylib_patched(&self) -> Result<(), macos_dylib::Error> {
624 if cfg!(target_os = "macos") {
625 if self.key().os().is_like_darwin() {
626 if self.implementation() == ImplementationName::CPython {
627 let dylib_path = self.python_dir().join("lib").join(format!(
628 "{}python{}{}{}",
629 std::env::consts::DLL_PREFIX,
630 self.key.version().python_version(),
631 self.key.variant().executable_suffix(),
632 std::env::consts::DLL_SUFFIX
633 ));
634 macos_dylib::patch_dylib_install_name(dylib_path)?;
635 }
636 }
637 }
638 Ok(())
639 }
640
641 pub fn ensure_build_file(&self) -> Result<(), Error> {
643 if let Some(ref build) = self.build {
644 let build_file = self.path.join("BUILD");
645 fs::write(&build_file, build.as_ref())?;
646 }
647 Ok(())
648 }
649
650 pub fn is_bin_link(&self, path: &Path) -> bool {
653 if cfg!(unix) {
654 same_file::is_same_file(path, self.executable(false)).unwrap_or_default()
655 } else if cfg!(windows) {
656 let Some(launcher) = Launcher::try_from_path(path).unwrap_or_default() else {
657 return false;
658 };
659 if !matches!(launcher.kind, LauncherKind::Python) {
660 return false;
661 }
662 dunce::canonicalize(&launcher.python_path).unwrap_or(launcher.python_path)
666 == self.executable(false)
667 } else {
668 unreachable!("Only Windows and Unix are supported")
669 }
670 }
671
672 pub fn is_upgrade_of(&self, other: &Self) -> bool {
674 if self.key.implementation != other.key.implementation {
676 return false;
677 }
678 if self.key.variant != other.key.variant {
680 return false;
681 }
682 if (self.key.major, self.key.minor) != (other.key.major, other.key.minor) {
684 return false;
685 }
686 if self.key.patch == other.key.patch {
689 return match (self.key.prerelease, other.key.prerelease) {
690 (Some(self_pre), Some(other_pre)) => self_pre > other_pre,
692 (None, Some(_)) => true,
694 (Some(_), None) => false,
696 (None, None) => match (self.build.as_deref(), other.build.as_deref()) {
698 (Some(_), None) => true,
700 (Some(self_build), Some(other_build)) => {
702 compare_build_versions(self_build, other_build) == Ordering::Greater
703 }
704 (None, _) => false,
706 },
707 };
708 }
709 if self.key.patch < other.key.patch {
711 return false;
712 }
713 true
714 }
715
716 pub fn url(&self) -> Option<&str> {
717 self.url.as_deref()
718 }
719
720 pub fn sha256(&self) -> Option<&str> {
721 self.sha256.as_deref()
722 }
723}
724
725#[derive(Clone, Debug)]
728pub struct PythonMinorVersionLink {
729 pub symlink_directory: PathBuf,
731 pub symlink_executable: PathBuf,
734 pub target_directory: PathBuf,
737}
738
739impl PythonMinorVersionLink {
740 fn from_executable(executable: &Path, key: &PythonInstallationKey) -> Option<Self> {
760 let implementation = key.implementation();
761 if !matches!(
762 implementation.as_ref(),
763 LenientImplementationName::Known(ImplementationName::CPython)
764 ) {
765 return None;
767 }
768 let executable_name = executable
769 .file_name()
770 .expect("Executable file name should exist");
771 let symlink_directory_name = PythonInstallationMinorVersionKey::ref_cast(key).to_string();
772 let parent = executable
773 .parent()
774 .expect("Executable should have parent directory");
775
776 let target_directory = if cfg!(unix) {
778 if parent
779 .components()
780 .next_back()
781 .is_some_and(|c| c.as_os_str() == "bin")
782 {
783 parent.parent()?.to_path_buf()
784 } else {
785 return None;
786 }
787 } else if cfg!(windows) {
788 parent.to_path_buf()
789 } else {
790 unimplemented!("Only Windows and Unix systems are supported.")
791 };
792 let symlink_directory = target_directory.with_file_name(symlink_directory_name);
793 if target_directory == symlink_directory {
795 return None;
796 }
797 let symlink_executable = executable_path_from_base(
799 symlink_directory.as_path(),
800 &executable_name.to_string_lossy(),
801 &implementation,
802 *key.os(),
803 );
804 let minor_version_link = Self {
805 symlink_directory,
806 symlink_executable,
807 target_directory,
808 };
809 Some(minor_version_link)
810 }
811
812 pub fn from_installation(installation: &ManagedPythonInstallation) -> Option<Self> {
813 Self::from_executable(installation.executable(false).as_path(), installation.key())
814 }
815
816 fn create_directory(&self) -> Result<(), Error> {
817 match replace_symlink(
818 self.target_directory.as_path(),
819 self.symlink_directory.as_path(),
820 ) {
821 Ok(()) => {
822 debug!(
823 "Created link {} -> {}",
824 &self.symlink_directory.user_display(),
825 &self.target_directory.user_display(),
826 );
827 }
828 Err(err) if err.kind() == io::ErrorKind::NotFound => {
829 return Err(Error::MissingPythonMinorVersionLinkTargetDirectory(
830 self.target_directory.clone(),
831 ));
832 }
833 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
834 Err(err) => {
835 return Err(Error::PythonMinorVersionLinkDirectory(err));
836 }
837 }
838 Ok(())
839 }
840
841 pub fn exists(&self) -> bool {
848 #[cfg(unix)]
849 {
850 self.symlink_directory
851 .symlink_metadata()
852 .is_ok_and(|metadata| metadata.file_type().is_symlink())
853 && self
854 .read_target()
855 .is_some_and(|target| target == self.target_directory)
856 }
857 #[cfg(windows)]
858 {
859 self.symlink_directory
860 .symlink_metadata()
861 .is_ok_and(|metadata| {
862 (metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0) != 0
865 })
866 && self
867 .read_target()
868 .is_some_and(|target| target == self.target_directory)
869 }
870 }
871
872 fn read_target(&self) -> Option<PathBuf> {
877 #[cfg(unix)]
878 {
879 self.symlink_directory.read_link().ok()
880 }
881 #[cfg(windows)]
882 {
883 uv_fs::read_link(&self.symlink_directory).ok()
884 }
885 }
886}
887
888fn executable_path_from_base(
892 base: &Path,
893 executable_name: &str,
894 implementation: &LenientImplementationName,
895 os: Os,
896) -> PathBuf {
897 if matches!(
898 implementation,
899 &LenientImplementationName::Known(ImplementationName::GraalPy)
900 ) {
901 base.join("bin").join(executable_name)
903 } else if os.is_emscripten()
904 || matches!(
905 implementation,
906 &LenientImplementationName::Known(ImplementationName::Pyodide)
907 )
908 {
909 base.join(executable_name)
911 } else if os.is_windows() {
912 base.join(executable_name)
914 } else {
915 base.join("bin").join(executable_name)
917 }
918}
919
920pub fn create_link_to_executable(link: &Path, executable: &Path) -> Result<(), Error> {
924 let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?;
925 fs_err::create_dir_all(link_parent).map_err(Error::ExecutableDirectory)?;
926
927 if cfg!(unix) {
928 match symlink_or_copy_file(executable, link) {
930 Ok(()) => Ok(()),
931 Err(err) if err.kind() == io::ErrorKind::NotFound => {
932 Err(Error::MissingExecutable(executable.to_path_buf()))
933 }
934 Err(err) => Err(Error::LinkExecutable(err)),
935 }
936 } else if cfg!(windows) {
937 use uv_trampoline_builder::windows_python_launcher;
938
939 let launcher = windows_python_launcher(executable, false)?;
941
942 #[expect(clippy::disallowed_types)]
945 {
946 std::fs::File::create_new(link)
947 .and_then(|mut file| file.write_all(launcher.as_ref()))
948 .map_err(Error::LinkExecutable)
949 }
950 } else {
951 unimplemented!("Only Windows and Unix are supported.")
952 }
953}
954
955pub fn replace_link_to_executable(link: &Path, executable: &Path) -> Result<(), Error> {
961 let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?;
962 fs_err::create_dir_all(link_parent).map_err(Error::ExecutableDirectory)?;
963
964 if cfg!(unix) {
965 replace_symlink(executable, link).map_err(Error::LinkExecutable)
966 } else if cfg!(windows) {
967 use uv_trampoline_builder::windows_python_launcher;
968
969 let launcher = windows_python_launcher(executable, false)?;
970
971 uv_fs::write_atomic_sync(link, &*launcher).map_err(Error::LinkExecutable)
972 } else {
973 unimplemented!("Only Windows and Unix are supported.")
974 }
975}
976
977pub fn platform_key_from_env() -> Result<String, Error> {
980 Ok(Platform::from_env()?.to_string().to_lowercase())
981}
982
983impl fmt::Display for ManagedPythonInstallation {
984 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
985 write!(
986 f,
987 "{}",
988 self.path
989 .file_name()
990 .unwrap_or(self.path.as_os_str())
991 .to_string_lossy()
992 )
993 }
994}
995
996pub fn python_executable_dir() -> Result<PathBuf, Error> {
998 uv_dirs::user_executable_directory(Some(EnvVars::UV_PYTHON_BIN_DIR))
999 .ok_or(Error::NoExecutableDirectory)
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004 use super::*;
1005 use crate::implementation::LenientImplementationName;
1006 use crate::installation::PythonInstallationKey;
1007 use crate::{ImplementationName, PythonVariant};
1008 use std::path::PathBuf;
1009 use std::str::FromStr;
1010 use uv_pep440::{Prerelease, PrereleaseKind};
1011 use uv_platform::Platform;
1012
1013 fn create_test_installation(
1014 implementation: ImplementationName,
1015 major: u8,
1016 minor: u8,
1017 patch: u8,
1018 prerelease: Option<Prerelease>,
1019 variant: PythonVariant,
1020 build: Option<&str>,
1021 ) -> ManagedPythonInstallation {
1022 let platform = Platform::from_str("linux-x86_64-gnu").unwrap();
1023 let key = PythonInstallationKey::new(
1024 LenientImplementationName::Known(implementation),
1025 major,
1026 minor,
1027 patch,
1028 prerelease,
1029 platform,
1030 variant,
1031 );
1032 ManagedPythonInstallation {
1033 path: PathBuf::from("/test/path"),
1034 key,
1035 url: None,
1036 sha256: None,
1037 build: build.map(|s| Cow::Owned(s.to_owned())),
1038 }
1039 }
1040
1041 #[test]
1042 fn test_is_upgrade_of_same_version() {
1043 let installation = create_test_installation(
1044 ImplementationName::CPython,
1045 3,
1046 10,
1047 8,
1048 None,
1049 PythonVariant::Default,
1050 None,
1051 );
1052
1053 assert!(!installation.is_upgrade_of(&installation));
1055 }
1056
1057 #[test]
1058 fn test_is_upgrade_of_patch_version() {
1059 let older = create_test_installation(
1060 ImplementationName::CPython,
1061 3,
1062 10,
1063 8,
1064 None,
1065 PythonVariant::Default,
1066 None,
1067 );
1068 let newer = create_test_installation(
1069 ImplementationName::CPython,
1070 3,
1071 10,
1072 9,
1073 None,
1074 PythonVariant::Default,
1075 None,
1076 );
1077
1078 assert!(newer.is_upgrade_of(&older));
1080 assert!(!older.is_upgrade_of(&newer));
1082 }
1083
1084 #[test]
1085 fn test_is_upgrade_of_different_minor_version() {
1086 let py310 = create_test_installation(
1087 ImplementationName::CPython,
1088 3,
1089 10,
1090 8,
1091 None,
1092 PythonVariant::Default,
1093 None,
1094 );
1095 let py311 = create_test_installation(
1096 ImplementationName::CPython,
1097 3,
1098 11,
1099 0,
1100 None,
1101 PythonVariant::Default,
1102 None,
1103 );
1104
1105 assert!(!py311.is_upgrade_of(&py310));
1107 assert!(!py310.is_upgrade_of(&py311));
1108 }
1109
1110 #[test]
1111 fn test_is_upgrade_of_different_implementation() {
1112 let cpython = create_test_installation(
1113 ImplementationName::CPython,
1114 3,
1115 10,
1116 8,
1117 None,
1118 PythonVariant::Default,
1119 None,
1120 );
1121 let pypy = create_test_installation(
1122 ImplementationName::PyPy,
1123 3,
1124 10,
1125 9,
1126 None,
1127 PythonVariant::Default,
1128 None,
1129 );
1130
1131 assert!(!pypy.is_upgrade_of(&cpython));
1133 assert!(!cpython.is_upgrade_of(&pypy));
1134 }
1135
1136 #[test]
1137 fn test_is_upgrade_of_different_variant() {
1138 let default = create_test_installation(
1139 ImplementationName::CPython,
1140 3,
1141 10,
1142 8,
1143 None,
1144 PythonVariant::Default,
1145 None,
1146 );
1147 let freethreaded = create_test_installation(
1148 ImplementationName::CPython,
1149 3,
1150 10,
1151 9,
1152 None,
1153 PythonVariant::Freethreaded,
1154 None,
1155 );
1156
1157 assert!(!freethreaded.is_upgrade_of(&default));
1159 assert!(!default.is_upgrade_of(&freethreaded));
1160 }
1161
1162 #[test]
1163 fn test_is_upgrade_of_prerelease() {
1164 let stable = create_test_installation(
1165 ImplementationName::CPython,
1166 3,
1167 10,
1168 8,
1169 None,
1170 PythonVariant::Default,
1171 None,
1172 );
1173 let prerelease = create_test_installation(
1174 ImplementationName::CPython,
1175 3,
1176 10,
1177 8,
1178 Some(Prerelease {
1179 kind: PrereleaseKind::Alpha,
1180 number: 1,
1181 }),
1182 PythonVariant::Default,
1183 None,
1184 );
1185
1186 assert!(stable.is_upgrade_of(&prerelease));
1188
1189 assert!(!prerelease.is_upgrade_of(&stable));
1191 }
1192
1193 #[test]
1194 fn test_is_upgrade_of_prerelease_to_prerelease() {
1195 let alpha1 = create_test_installation(
1196 ImplementationName::CPython,
1197 3,
1198 10,
1199 8,
1200 Some(Prerelease {
1201 kind: PrereleaseKind::Alpha,
1202 number: 1,
1203 }),
1204 PythonVariant::Default,
1205 None,
1206 );
1207 let alpha2 = create_test_installation(
1208 ImplementationName::CPython,
1209 3,
1210 10,
1211 8,
1212 Some(Prerelease {
1213 kind: PrereleaseKind::Alpha,
1214 number: 2,
1215 }),
1216 PythonVariant::Default,
1217 None,
1218 );
1219
1220 assert!(alpha2.is_upgrade_of(&alpha1));
1222 assert!(!alpha1.is_upgrade_of(&alpha2));
1224 }
1225
1226 #[test]
1227 fn test_is_upgrade_of_prerelease_same_patch() {
1228 let prerelease = create_test_installation(
1229 ImplementationName::CPython,
1230 3,
1231 10,
1232 8,
1233 Some(Prerelease {
1234 kind: PrereleaseKind::Alpha,
1235 number: 1,
1236 }),
1237 PythonVariant::Default,
1238 None,
1239 );
1240
1241 assert!(!prerelease.is_upgrade_of(&prerelease));
1243 }
1244
1245 #[test]
1246 fn test_is_upgrade_of_build_version() {
1247 let older_build = create_test_installation(
1248 ImplementationName::CPython,
1249 3,
1250 10,
1251 8,
1252 None,
1253 PythonVariant::Default,
1254 Some("20240101"),
1255 );
1256 let newer_build = create_test_installation(
1257 ImplementationName::CPython,
1258 3,
1259 10,
1260 8,
1261 None,
1262 PythonVariant::Default,
1263 Some("20240201"),
1264 );
1265
1266 assert!(newer_build.is_upgrade_of(&older_build));
1268 assert!(!older_build.is_upgrade_of(&newer_build));
1270 }
1271
1272 #[test]
1273 fn test_is_upgrade_of_build_version_same() {
1274 let installation = create_test_installation(
1275 ImplementationName::CPython,
1276 3,
1277 10,
1278 8,
1279 None,
1280 PythonVariant::Default,
1281 Some("20240101"),
1282 );
1283
1284 assert!(!installation.is_upgrade_of(&installation));
1286 }
1287
1288 #[test]
1289 fn test_is_upgrade_of_build_with_legacy_installation() {
1290 let legacy = create_test_installation(
1291 ImplementationName::CPython,
1292 3,
1293 10,
1294 8,
1295 None,
1296 PythonVariant::Default,
1297 None,
1298 );
1299 let with_build = create_test_installation(
1300 ImplementationName::CPython,
1301 3,
1302 10,
1303 8,
1304 None,
1305 PythonVariant::Default,
1306 Some("20240101"),
1307 );
1308
1309 assert!(with_build.is_upgrade_of(&legacy));
1311 assert!(!legacy.is_upgrade_of(&with_build));
1313 }
1314
1315 #[test]
1316 fn test_is_upgrade_of_patch_takes_precedence_over_build() {
1317 let older_patch_newer_build = create_test_installation(
1318 ImplementationName::CPython,
1319 3,
1320 10,
1321 8,
1322 None,
1323 PythonVariant::Default,
1324 Some("20240201"),
1325 );
1326 let newer_patch_older_build = create_test_installation(
1327 ImplementationName::CPython,
1328 3,
1329 10,
1330 9,
1331 None,
1332 PythonVariant::Default,
1333 Some("20240101"),
1334 );
1335
1336 assert!(newer_patch_older_build.is_upgrade_of(&older_patch_newer_build));
1338 assert!(!older_patch_newer_build.is_upgrade_of(&newer_patch_older_build));
1340 }
1341
1342 #[test]
1343 fn test_find_version_matching() {
1344 use crate::PythonVersion;
1345
1346 let platform = Platform::from_env().unwrap();
1347 let temp_dir = tempfile::tempdir().unwrap();
1348
1349 fs::create_dir(temp_dir.path().join(format!("cpython-3.10.0-{platform}"))).unwrap();
1351
1352 temp_env::with_var(
1353 uv_static::EnvVars::UV_PYTHON_INSTALL_DIR,
1354 Some(temp_dir.path()),
1355 || {
1356 let installations = ManagedPythonInstallations::from_settings(None).unwrap();
1357
1358 let v3_1 = PythonVersion::from_str("3.1").unwrap();
1360 let matched: Vec<_> = installations.find_version(&v3_1).unwrap().collect();
1361 assert_eq!(matched.len(), 0);
1362
1363 let v3_10 = PythonVersion::from_str("3.10").unwrap();
1365 let matched: Vec<_> = installations.find_version(&v3_10).unwrap().collect();
1366 assert_eq!(matched.len(), 1);
1367 },
1368 );
1369 }
1370
1371 #[test]
1372 fn test_relative_install_dir_resolves_against_pwd() {
1373 let temp_dir = tempfile::tempdir().unwrap();
1374 let workdir = temp_dir.path().join("workdir");
1375 fs::create_dir(&workdir).unwrap();
1376
1377 temp_env::with_vars(
1378 [
1379 (
1380 uv_static::EnvVars::UV_PYTHON_INSTALL_DIR,
1381 Some(std::ffi::OsStr::new(".python-installs")),
1382 ),
1383 (uv_static::EnvVars::PWD, Some(workdir.as_os_str())),
1384 ],
1385 || {
1386 let installations = ManagedPythonInstallations::from_settings(None).unwrap();
1387 assert_eq!(
1388 installations.absolute_root().unwrap(),
1389 workdir.join(".python-installs")
1390 );
1391 },
1392 );
1393 }
1394}