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("Missing expected Python executable at {}", _0.user_display())]
58 MissingExecutable(PathBuf),
59 #[error("Missing expected target directory for Python minor version link at {}", _0.user_display())]
60 MissingPythonMinorVersionLinkTargetDirectory(PathBuf),
61 #[error("Failed to create canonical Python executable")]
62 CanonicalizeExecutable(#[source] io::Error),
63 #[error("Failed to create Python executable link")]
64 LinkExecutable {
65 to: PathBuf,
66 #[source]
67 err: io::Error,
68 },
69 #[error("Failed to create Python minor version link directory")]
70 PythonMinorVersionLinkDirectory(#[source] io::Error),
71 #[error("Failed to create directory for Python executable link")]
72 ExecutableDirectory(#[source] io::Error),
73 #[error("Failed to read Python installation directory")]
74 ReadError(#[source] io::Error),
75 #[error("Failed to find a directory to install executables into")]
76 NoExecutableDirectory,
77 #[error(transparent)]
78 LauncherError(#[from] uv_trampoline_builder::Error),
79 #[error("Failed to read managed Python directory name: {0}")]
80 NameError(String),
81 #[error("Failed to construct absolute path to managed Python directory: {}", _0.user_display())]
82 AbsolutePath(PathBuf, #[source] io::Error),
83 #[error(transparent)]
84 NameParseError(#[from] installation::PythonInstallationKeyError),
85 #[error("Failed to determine the libc used on the current platform")]
86 LibcDetection(#[from] LibcDetectionError),
87 #[error(transparent)]
88 MacOsDylib(#[from] macos_dylib::Error),
89}
90
91pub fn compare_build_versions(a: &str, b: &str) -> Ordering {
96 match (a.parse::<u64>(), b.parse::<u64>()) {
97 (Ok(a_num), Ok(b_num)) => a_num.cmp(&b_num),
98 _ => a.cmp(b),
99 }
100}
101
102#[derive(Debug, Clone, Eq, PartialEq)]
104pub struct ManagedPythonInstallations {
105 root: PathBuf,
107}
108
109impl ManagedPythonInstallations {
110 fn from_path(root: impl Into<PathBuf>) -> Self {
112 Self { root: root.into() }
113 }
114
115 pub async fn lock(&self) -> Result<LockedFile, Error> {
118 Ok(LockedFile::acquire(
119 self.root.join(".lock"),
120 LockedFileMode::Exclusive,
121 self.root.user_display(),
122 )
123 .await?)
124 }
125
126 pub fn from_settings(install_dir: Option<PathBuf>) -> Result<Self, Error> {
133 if let Some(install_dir) = install_dir {
134 Ok(Self::from_path(install_dir))
135 } else if let Some(install_dir) =
136 std::env::var_os(EnvVars::UV_PYTHON_INSTALL_DIR).filter(|s| !s.is_empty())
137 {
138 Ok(Self::from_path(install_dir))
139 } else {
140 Ok(Self::from_path(
141 StateStore::from_settings(None)?.bucket(StateBucket::ManagedPython),
142 ))
143 }
144 }
145
146 pub fn temp() -> Result<Self, Error> {
148 Ok(Self::from_path(
149 StateStore::temp()?.bucket(StateBucket::ManagedPython),
150 ))
151 }
152
153 pub fn scratch(&self) -> PathBuf {
155 self.root.join(".temp")
156 }
157
158 pub fn init(self) -> Result<Self, Error> {
162 let root = &self.root;
163
164 if !root.exists()
166 && root
167 .parent()
168 .is_some_and(|parent| parent.join("toolchains").exists())
169 {
170 let deprecated = root.parent().unwrap().join("toolchains");
171 fs::rename(&deprecated, root)?;
173 uv_fs::replace_symlink(root, &deprecated)?;
175 } else {
176 fs::create_dir_all(root)?;
177 }
178
179 fs::create_dir_all(root)?;
181
182 let scratch = self.scratch();
184 fs::create_dir_all(&scratch)?;
185
186 match fs::OpenOptions::new()
188 .write(true)
189 .create_new(true)
190 .open(root.join(".gitignore"))
191 {
192 Ok(mut file) => file.write_all(b"*")?,
193 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
194 Err(err) => return Err(err.into()),
195 }
196
197 Ok(self)
198 }
199
200 pub fn find_all(
205 &self,
206 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + use<>, Error> {
207 let dirs = match fs_err::read_dir(&self.root) {
208 Ok(installation_dirs) => {
209 let directories: Vec<_> = installation_dirs
211 .filter_map(|read_dir| match read_dir {
212 Ok(entry) => match entry.file_type() {
213 Ok(file_type) => file_type.is_dir().then_some(Ok(entry.path())),
214 Err(err) => Some(Err(err)),
215 },
216 Err(err) => Some(Err(err)),
217 })
218 .collect::<Result<_, io::Error>>()
219 .map_err(Error::ReadError)?;
220 directories
221 }
222 Err(err) if err.kind() == io::ErrorKind::NotFound => vec![],
223 Err(err) => {
224 return Err(Error::ReadError(err));
225 }
226 };
227 let scratch = self.scratch();
228 Ok(dirs
229 .into_iter()
230 .filter(|path| *path != scratch)
232 .filter(|path| {
234 path.file_name()
235 .and_then(OsStr::to_str)
236 .map(|name| !name.starts_with('.'))
237 .unwrap_or(true)
238 })
239 .filter_map(|path| {
240 ManagedPythonInstallation::from_path(path)
241 .inspect_err(|err| {
242 warn!("Ignoring malformed managed Python entry:\n {err}");
243 })
244 .ok()
245 })
246 .sorted_unstable_by_key(|installation| Reverse(installation.key().clone())))
247 }
248
249 pub fn find_matching_current_platform(
251 &self,
252 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + use<>, Error> {
253 let platform = Platform::from_env()?;
254
255 let iter = Self::from_settings(None)?
256 .find_all()?
257 .filter(move |installation| {
258 if !platform.supports(installation.platform()) {
259 debug!("Skipping managed installation `{installation}`: not supported by current platform `{platform}`");
260 return false;
261 }
262 true
263 });
264
265 Ok(iter)
266 }
267
268 pub fn find_version<'a>(
275 &'a self,
276 version: &'a PythonVersion,
277 ) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + 'a, Error> {
278 let request = VersionRequest::from(version);
279 Ok(self
280 .find_matching_current_platform()?
281 .filter(move |installation| request.matches_installation_key(installation.key())))
282 }
283
284 pub fn root(&self) -> &Path {
285 &self.root
286 }
287}
288
289static EXTERNALLY_MANAGED: &str = "[externally-managed]
290Error=This Python installation is managed by uv and should not be modified.
291";
292
293#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
295pub struct ManagedPythonInstallation {
296 path: PathBuf,
298 key: PythonInstallationKey,
300 url: Option<Cow<'static, str>>,
304 sha256: Option<Cow<'static, str>>,
308 build: Option<Cow<'static, str>>,
312}
313
314impl ManagedPythonInstallation {
315 pub fn new(path: PathBuf, download: &ManagedPythonDownload) -> Self {
316 Self {
317 path,
318 key: download.key().clone(),
319 url: Some(download.url().clone()),
320 sha256: download.sha256().cloned(),
321 build: download.build().map(Cow::Borrowed),
322 }
323 }
324
325 pub(crate) fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
326 let path = path.as_ref();
327
328 let key = PythonInstallationKey::from_str(
329 path.file_name()
330 .ok_or(Error::NameError("name is empty".to_string()))?
331 .to_str()
332 .ok_or(Error::NameError("not a valid string".to_string()))?,
333 )?;
334
335 let path = std::path::absolute(path)
336 .map_err(|err| Error::AbsolutePath(path.to_path_buf(), err))?;
337
338 let build = match fs::read_to_string(path.join("BUILD")) {
340 Ok(content) => Some(Cow::Owned(content.trim().to_string())),
341 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
342 Err(err) => return Err(err.into()),
343 };
344
345 Ok(Self {
346 path,
347 key,
348 url: None,
349 sha256: None,
350 build,
351 })
352 }
353
354 pub fn try_from_interpreter(interpreter: &Interpreter) -> Option<Self> {
358 let managed_root = ManagedPythonInstallations::from_settings(None).ok()?;
359
360 let sys_base_prefix = dunce::canonicalize(interpreter.sys_base_prefix())
364 .unwrap_or_else(|_| interpreter.sys_base_prefix().to_path_buf());
365 let root = dunce::canonicalize(managed_root.root())
366 .unwrap_or_else(|_| managed_root.root().to_path_buf());
367
368 let suffix = sys_base_prefix.strip_prefix(&root).ok()?;
370
371 let first_component = suffix.components().next()?;
372 let name = first_component.as_os_str().to_str()?;
373
374 PythonInstallationKey::from_str(name).ok()?;
376
377 let path = managed_root.root().join(name);
379 Self::from_path(path).ok()
380 }
381
382 pub fn executable(&self, windowed: bool) -> PathBuf {
391 let version = match self.implementation() {
392 ImplementationName::CPython => {
393 if cfg!(unix) {
394 format!("{}.{}", self.key.major, self.key.minor)
395 } else {
396 String::new()
397 }
398 }
399 ImplementationName::PyPy => format!("{}.{}", self.key.major, self.key.minor),
401 ImplementationName::Pyodide => String::new(),
403 ImplementationName::GraalPy => String::new(),
404 };
405
406 let variant = if self.implementation() == ImplementationName::GraalPy {
409 ""
410 } else if cfg!(unix) {
411 self.key.variant.executable_suffix()
412 } else if cfg!(windows) && windowed {
413 "w"
415 } else {
416 ""
417 };
418
419 let name = format!(
420 "{implementation}{version}{variant}{exe}",
421 implementation = self.implementation().executable_name(),
422 exe = std::env::consts::EXE_SUFFIX
423 );
424
425 let executable = executable_path_from_base(
426 self.python_dir().as_path(),
427 &name,
428 &LenientImplementationName::from(self.implementation()),
429 *self.key.os(),
430 );
431
432 if cfg!(windows)
437 && matches!(self.key.variant, PythonVariant::Freethreaded)
438 && !executable.exists()
439 {
440 return self.python_dir().join(format!(
442 "python{}.{}t{}",
443 self.key.major,
444 self.key.minor,
445 std::env::consts::EXE_SUFFIX
446 ));
447 }
448
449 executable
450 }
451
452 fn python_dir(&self) -> PathBuf {
453 let install = self.path.join("install");
454 if install.is_dir() {
455 install
456 } else {
457 self.path.clone()
458 }
459 }
460
461 pub fn version(&self) -> PythonVersion {
463 self.key.version()
464 }
465
466 pub fn implementation(&self) -> ImplementationName {
467 match self.key.implementation().into_owned() {
468 LenientImplementationName::Known(implementation) => implementation,
469 LenientImplementationName::Unknown(_) => {
470 panic!("Managed Python installations should have a known implementation")
471 }
472 }
473 }
474
475 pub fn path(&self) -> &Path {
476 &self.path
477 }
478
479 pub fn key(&self) -> &PythonInstallationKey {
480 &self.key
481 }
482
483 pub fn platform(&self) -> &Platform {
484 self.key.platform()
485 }
486
487 pub fn build(&self) -> Option<&str> {
489 self.build.as_deref()
490 }
491
492 pub fn minor_version_key(&self) -> &PythonInstallationMinorVersionKey {
493 PythonInstallationMinorVersionKey::ref_cast(&self.key)
494 }
495
496 pub fn satisfies(&self, request: &PythonRequest) -> bool {
497 match request {
498 PythonRequest::File(path) => self.executable(false) == *path,
499 PythonRequest::Default | PythonRequest::Any => true,
500 PythonRequest::Directory(path) => self.path() == *path,
501 PythonRequest::ExecutableName(name) => self
502 .executable(false)
503 .file_name()
504 .is_some_and(|filename| filename.to_string_lossy() == *name),
505 PythonRequest::Implementation(implementation) => {
506 *implementation == self.implementation()
507 }
508 PythonRequest::ImplementationVersion(implementation, version) => {
509 *implementation == self.implementation() && version.matches_version(&self.version())
510 }
511 PythonRequest::Version(version) => version.matches_version(&self.version()),
512 PythonRequest::Key(request) => request.satisfied_by_key(self.key()),
513 }
514 }
515
516 pub fn ensure_canonical_executables(&self) -> Result<(), Error> {
518 let python = self.executable(false);
519
520 let canonical_names = &["python"];
521
522 for name in canonical_names {
523 let executable =
524 python.with_file_name(format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX));
525
526 if executable == python {
529 continue;
530 }
531
532 match symlink_or_copy_file(&python, &executable) {
533 Ok(()) => {
534 debug!(
535 "Created link {} -> {}",
536 executable.user_display(),
537 python.user_display(),
538 );
539 }
540 Err(err) if err.kind() == io::ErrorKind::NotFound => {
541 return Err(Error::MissingExecutable(python.clone()));
542 }
543 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
544 Err(err) => {
545 return Err(Error::CanonicalizeExecutable(err));
546 }
547 }
548 }
549
550 Ok(())
551 }
552
553 pub fn ensure_minor_version_link(&self) -> Result<(), Error> {
556 if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self) {
557 minor_version_link.create_directory()?;
558 }
559 Ok(())
560 }
561
562 pub fn ensure_externally_managed(&self) -> Result<(), Error> {
565 if self.key.os().is_emscripten() {
566 return Ok(());
569 }
570 let stdlib = if self.key.os().is_windows() {
572 self.python_dir().join("Lib")
573 } else {
574 let lib_suffix = self.key.variant.lib_suffix();
575 let python = if matches!(
576 self.key.implementation,
577 LenientImplementationName::Known(ImplementationName::PyPy)
578 ) {
579 format!("pypy{}", self.key.version().python_version())
580 } else {
581 format!("python{}{lib_suffix}", self.key.version().python_version())
582 };
583 self.python_dir().join("lib").join(python)
584 };
585
586 let file = stdlib.join("EXTERNALLY-MANAGED");
587 fs_err::write(file, EXTERNALLY_MANAGED)?;
588
589 Ok(())
590 }
591
592 pub fn ensure_sysconfig_patched(&self) -> Result<(), Error> {
594 if cfg!(unix) {
595 if self.key.os().is_emscripten() {
596 return Ok(());
599 }
600 if self.implementation() == ImplementationName::CPython {
601 sysconfig::update_sysconfig(
602 self.path(),
603 self.key.major,
604 self.key.minor,
605 self.key.variant.lib_suffix(),
606 )?;
607 }
608 }
609 Ok(())
610 }
611
612 pub fn ensure_dylib_patched(&self) -> Result<(), macos_dylib::Error> {
619 if cfg!(target_os = "macos") {
620 if self.key().os().is_like_darwin() {
621 if self.implementation() == ImplementationName::CPython {
622 let dylib_path = self.python_dir().join("lib").join(format!(
623 "{}python{}{}{}",
624 std::env::consts::DLL_PREFIX,
625 self.key.version().python_version(),
626 self.key.variant().executable_suffix(),
627 std::env::consts::DLL_SUFFIX
628 ));
629 macos_dylib::patch_dylib_install_name(dylib_path)?;
630 }
631 }
632 }
633 Ok(())
634 }
635
636 pub fn ensure_build_file(&self) -> Result<(), Error> {
638 if let Some(ref build) = self.build {
639 let build_file = self.path.join("BUILD");
640 fs::write(&build_file, build.as_ref())?;
641 }
642 Ok(())
643 }
644
645 pub fn is_bin_link(&self, path: &Path) -> bool {
648 if cfg!(unix) {
649 same_file::is_same_file(path, self.executable(false)).unwrap_or_default()
650 } else if cfg!(windows) {
651 let Some(launcher) = Launcher::try_from_path(path).unwrap_or_default() else {
652 return false;
653 };
654 if !matches!(launcher.kind, LauncherKind::Python) {
655 return false;
656 }
657 dunce::canonicalize(&launcher.python_path).unwrap_or(launcher.python_path)
661 == self.executable(false)
662 } else {
663 unreachable!("Only Windows and Unix are supported")
664 }
665 }
666
667 pub fn is_upgrade_of(&self, other: &Self) -> bool {
669 if self.key.implementation != other.key.implementation {
671 return false;
672 }
673 if self.key.variant != other.key.variant {
675 return false;
676 }
677 if (self.key.major, self.key.minor) != (other.key.major, other.key.minor) {
679 return false;
680 }
681 if self.key.patch == other.key.patch {
684 return match (self.key.prerelease, other.key.prerelease) {
685 (Some(self_pre), Some(other_pre)) => self_pre > other_pre,
687 (None, Some(_)) => true,
689 (Some(_), None) => false,
691 (None, None) => match (self.build.as_deref(), other.build.as_deref()) {
693 (Some(_), None) => true,
695 (Some(self_build), Some(other_build)) => {
697 compare_build_versions(self_build, other_build) == Ordering::Greater
698 }
699 (None, _) => false,
701 },
702 };
703 }
704 if self.key.patch < other.key.patch {
706 return false;
707 }
708 true
709 }
710
711 pub fn url(&self) -> Option<&str> {
712 self.url.as_deref()
713 }
714
715 pub fn sha256(&self) -> Option<&str> {
716 self.sha256.as_deref()
717 }
718}
719
720#[derive(Clone, Debug)]
723pub struct PythonMinorVersionLink {
724 pub symlink_directory: PathBuf,
726 pub symlink_executable: PathBuf,
729 pub target_directory: PathBuf,
732}
733
734impl PythonMinorVersionLink {
735 fn from_executable(executable: &Path, key: &PythonInstallationKey) -> Option<Self> {
755 let implementation = key.implementation();
756 if !matches!(
757 implementation.as_ref(),
758 LenientImplementationName::Known(ImplementationName::CPython)
759 ) {
760 return None;
762 }
763 let executable_name = executable
764 .file_name()
765 .expect("Executable file name should exist");
766 let symlink_directory_name = PythonInstallationMinorVersionKey::ref_cast(key).to_string();
767 let parent = executable
768 .parent()
769 .expect("Executable should have parent directory");
770
771 let target_directory = if cfg!(unix) {
773 if parent
774 .components()
775 .next_back()
776 .is_some_and(|c| c.as_os_str() == "bin")
777 {
778 parent.parent()?.to_path_buf()
779 } else {
780 return None;
781 }
782 } else if cfg!(windows) {
783 parent.to_path_buf()
784 } else {
785 unimplemented!("Only Windows and Unix systems are supported.")
786 };
787 let symlink_directory = target_directory.with_file_name(symlink_directory_name);
788 if target_directory == symlink_directory {
790 return None;
791 }
792 let symlink_executable = executable_path_from_base(
794 symlink_directory.as_path(),
795 &executable_name.to_string_lossy(),
796 &implementation,
797 *key.os(),
798 );
799 let minor_version_link = Self {
800 symlink_directory,
801 symlink_executable,
802 target_directory,
803 };
804 Some(minor_version_link)
805 }
806
807 pub fn from_installation(installation: &ManagedPythonInstallation) -> Option<Self> {
808 Self::from_executable(installation.executable(false).as_path(), installation.key())
809 }
810
811 pub fn create_directory(&self) -> Result<(), Error> {
812 match replace_symlink(
813 self.target_directory.as_path(),
814 self.symlink_directory.as_path(),
815 ) {
816 Ok(()) => {
817 debug!(
818 "Created link {} -> {}",
819 &self.symlink_directory.user_display(),
820 &self.target_directory.user_display(),
821 );
822 }
823 Err(err) if err.kind() == io::ErrorKind::NotFound => {
824 return Err(Error::MissingPythonMinorVersionLinkTargetDirectory(
825 self.target_directory.clone(),
826 ));
827 }
828 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
829 Err(err) => {
830 return Err(Error::PythonMinorVersionLinkDirectory(err));
831 }
832 }
833 Ok(())
834 }
835
836 pub fn exists(&self) -> bool {
843 #[cfg(unix)]
844 {
845 self.symlink_directory
846 .symlink_metadata()
847 .is_ok_and(|metadata| metadata.file_type().is_symlink())
848 && self
849 .read_target()
850 .is_some_and(|target| target == self.target_directory)
851 }
852 #[cfg(windows)]
853 {
854 self.symlink_directory
855 .symlink_metadata()
856 .is_ok_and(|metadata| {
857 (metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0) != 0
860 })
861 && self
862 .read_target()
863 .is_some_and(|target| target == self.target_directory)
864 }
865 }
866
867 pub fn read_target(&self) -> Option<PathBuf> {
873 #[cfg(unix)]
874 {
875 self.symlink_directory.read_link().ok()
876 }
877 #[cfg(windows)]
878 {
879 junction::get_target(&self.symlink_directory).ok()
880 }
881 }
882}
883
884fn executable_path_from_base(
888 base: &Path,
889 executable_name: &str,
890 implementation: &LenientImplementationName,
891 os: Os,
892) -> PathBuf {
893 if matches!(
894 implementation,
895 &LenientImplementationName::Known(ImplementationName::GraalPy)
896 ) {
897 base.join("bin").join(executable_name)
899 } else if os.is_emscripten()
900 || matches!(
901 implementation,
902 &LenientImplementationName::Known(ImplementationName::Pyodide)
903 )
904 {
905 base.join(executable_name)
907 } else if os.is_windows() {
908 base.join(executable_name)
910 } else {
911 base.join("bin").join(executable_name)
913 }
914}
915
916pub fn create_link_to_executable(link: &Path, executable: &Path) -> Result<(), Error> {
920 let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?;
921 fs_err::create_dir_all(link_parent).map_err(Error::ExecutableDirectory)?;
922
923 if cfg!(unix) {
924 match symlink_or_copy_file(executable, link) {
926 Ok(()) => Ok(()),
927 Err(err) if err.kind() == io::ErrorKind::NotFound => {
928 Err(Error::MissingExecutable(executable.to_path_buf()))
929 }
930 Err(err) => Err(Error::LinkExecutable {
931 to: link.to_path_buf(),
932 err,
933 }),
934 }
935 } else if cfg!(windows) {
936 use uv_trampoline_builder::windows_python_launcher;
937
938 let launcher = windows_python_launcher(executable, false)?;
940
941 #[expect(clippy::disallowed_types)]
944 {
945 std::fs::File::create_new(link)
946 .and_then(|mut file| file.write_all(launcher.as_ref()))
947 .map_err(|err| Error::LinkExecutable {
948 to: link.to_path_buf(),
949 err,
950 })
951 }
952 } else {
953 unimplemented!("Only Windows and Unix are supported.")
954 }
955}
956
957pub fn replace_link_to_executable(link: &Path, executable: &Path) -> Result<(), Error> {
963 let link_parent = link.parent().ok_or(Error::NoExecutableDirectory)?;
964 fs_err::create_dir_all(link_parent).map_err(Error::ExecutableDirectory)?;
965
966 if cfg!(unix) {
967 replace_symlink(executable, link).map_err(|err| Error::LinkExecutable {
968 to: link.to_path_buf(),
969 err,
970 })
971 } else if cfg!(windows) {
972 use uv_trampoline_builder::windows_python_launcher;
973
974 let launcher = windows_python_launcher(executable, false)?;
975
976 uv_fs::write_atomic_sync(link, &*launcher).map_err(|err| Error::LinkExecutable {
977 to: link.to_path_buf(),
978 err,
979 })
980 } else {
981 unimplemented!("Only Windows and Unix are supported.")
982 }
983}
984
985pub fn platform_key_from_env() -> Result<String, Error> {
988 Ok(Platform::from_env()?.to_string().to_lowercase())
989}
990
991impl fmt::Display for ManagedPythonInstallation {
992 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
993 write!(
994 f,
995 "{}",
996 self.path
997 .file_name()
998 .unwrap_or(self.path.as_os_str())
999 .to_string_lossy()
1000 )
1001 }
1002}
1003
1004pub fn python_executable_dir() -> Result<PathBuf, Error> {
1006 uv_dirs::user_executable_directory(Some(EnvVars::UV_PYTHON_BIN_DIR))
1007 .ok_or(Error::NoExecutableDirectory)
1008}
1009
1010#[cfg(test)]
1011mod tests {
1012 use super::*;
1013 use crate::implementation::LenientImplementationName;
1014 use crate::installation::PythonInstallationKey;
1015 use crate::{ImplementationName, PythonVariant};
1016 use std::path::PathBuf;
1017 use std::str::FromStr;
1018 use uv_pep440::{Prerelease, PrereleaseKind};
1019 use uv_platform::Platform;
1020
1021 fn create_test_installation(
1022 implementation: ImplementationName,
1023 major: u8,
1024 minor: u8,
1025 patch: u8,
1026 prerelease: Option<Prerelease>,
1027 variant: PythonVariant,
1028 build: Option<&str>,
1029 ) -> ManagedPythonInstallation {
1030 let platform = Platform::from_str("linux-x86_64-gnu").unwrap();
1031 let key = PythonInstallationKey::new(
1032 LenientImplementationName::Known(implementation),
1033 major,
1034 minor,
1035 patch,
1036 prerelease,
1037 platform,
1038 variant,
1039 );
1040 ManagedPythonInstallation {
1041 path: PathBuf::from("/test/path"),
1042 key,
1043 url: None,
1044 sha256: None,
1045 build: build.map(|s| Cow::Owned(s.to_owned())),
1046 }
1047 }
1048
1049 #[test]
1050 fn test_is_upgrade_of_same_version() {
1051 let installation = create_test_installation(
1052 ImplementationName::CPython,
1053 3,
1054 10,
1055 8,
1056 None,
1057 PythonVariant::Default,
1058 None,
1059 );
1060
1061 assert!(!installation.is_upgrade_of(&installation));
1063 }
1064
1065 #[test]
1066 fn test_is_upgrade_of_patch_version() {
1067 let older = create_test_installation(
1068 ImplementationName::CPython,
1069 3,
1070 10,
1071 8,
1072 None,
1073 PythonVariant::Default,
1074 None,
1075 );
1076 let newer = create_test_installation(
1077 ImplementationName::CPython,
1078 3,
1079 10,
1080 9,
1081 None,
1082 PythonVariant::Default,
1083 None,
1084 );
1085
1086 assert!(newer.is_upgrade_of(&older));
1088 assert!(!older.is_upgrade_of(&newer));
1090 }
1091
1092 #[test]
1093 fn test_is_upgrade_of_different_minor_version() {
1094 let py310 = create_test_installation(
1095 ImplementationName::CPython,
1096 3,
1097 10,
1098 8,
1099 None,
1100 PythonVariant::Default,
1101 None,
1102 );
1103 let py311 = create_test_installation(
1104 ImplementationName::CPython,
1105 3,
1106 11,
1107 0,
1108 None,
1109 PythonVariant::Default,
1110 None,
1111 );
1112
1113 assert!(!py311.is_upgrade_of(&py310));
1115 assert!(!py310.is_upgrade_of(&py311));
1116 }
1117
1118 #[test]
1119 fn test_is_upgrade_of_different_implementation() {
1120 let cpython = create_test_installation(
1121 ImplementationName::CPython,
1122 3,
1123 10,
1124 8,
1125 None,
1126 PythonVariant::Default,
1127 None,
1128 );
1129 let pypy = create_test_installation(
1130 ImplementationName::PyPy,
1131 3,
1132 10,
1133 9,
1134 None,
1135 PythonVariant::Default,
1136 None,
1137 );
1138
1139 assert!(!pypy.is_upgrade_of(&cpython));
1141 assert!(!cpython.is_upgrade_of(&pypy));
1142 }
1143
1144 #[test]
1145 fn test_is_upgrade_of_different_variant() {
1146 let default = create_test_installation(
1147 ImplementationName::CPython,
1148 3,
1149 10,
1150 8,
1151 None,
1152 PythonVariant::Default,
1153 None,
1154 );
1155 let freethreaded = create_test_installation(
1156 ImplementationName::CPython,
1157 3,
1158 10,
1159 9,
1160 None,
1161 PythonVariant::Freethreaded,
1162 None,
1163 );
1164
1165 assert!(!freethreaded.is_upgrade_of(&default));
1167 assert!(!default.is_upgrade_of(&freethreaded));
1168 }
1169
1170 #[test]
1171 fn test_is_upgrade_of_prerelease() {
1172 let stable = create_test_installation(
1173 ImplementationName::CPython,
1174 3,
1175 10,
1176 8,
1177 None,
1178 PythonVariant::Default,
1179 None,
1180 );
1181 let prerelease = create_test_installation(
1182 ImplementationName::CPython,
1183 3,
1184 10,
1185 8,
1186 Some(Prerelease {
1187 kind: PrereleaseKind::Alpha,
1188 number: 1,
1189 }),
1190 PythonVariant::Default,
1191 None,
1192 );
1193
1194 assert!(stable.is_upgrade_of(&prerelease));
1196
1197 assert!(!prerelease.is_upgrade_of(&stable));
1199 }
1200
1201 #[test]
1202 fn test_is_upgrade_of_prerelease_to_prerelease() {
1203 let alpha1 = create_test_installation(
1204 ImplementationName::CPython,
1205 3,
1206 10,
1207 8,
1208 Some(Prerelease {
1209 kind: PrereleaseKind::Alpha,
1210 number: 1,
1211 }),
1212 PythonVariant::Default,
1213 None,
1214 );
1215 let alpha2 = create_test_installation(
1216 ImplementationName::CPython,
1217 3,
1218 10,
1219 8,
1220 Some(Prerelease {
1221 kind: PrereleaseKind::Alpha,
1222 number: 2,
1223 }),
1224 PythonVariant::Default,
1225 None,
1226 );
1227
1228 assert!(alpha2.is_upgrade_of(&alpha1));
1230 assert!(!alpha1.is_upgrade_of(&alpha2));
1232 }
1233
1234 #[test]
1235 fn test_is_upgrade_of_prerelease_same_patch() {
1236 let prerelease = create_test_installation(
1237 ImplementationName::CPython,
1238 3,
1239 10,
1240 8,
1241 Some(Prerelease {
1242 kind: PrereleaseKind::Alpha,
1243 number: 1,
1244 }),
1245 PythonVariant::Default,
1246 None,
1247 );
1248
1249 assert!(!prerelease.is_upgrade_of(&prerelease));
1251 }
1252
1253 #[test]
1254 fn test_is_upgrade_of_build_version() {
1255 let older_build = create_test_installation(
1256 ImplementationName::CPython,
1257 3,
1258 10,
1259 8,
1260 None,
1261 PythonVariant::Default,
1262 Some("20240101"),
1263 );
1264 let newer_build = create_test_installation(
1265 ImplementationName::CPython,
1266 3,
1267 10,
1268 8,
1269 None,
1270 PythonVariant::Default,
1271 Some("20240201"),
1272 );
1273
1274 assert!(newer_build.is_upgrade_of(&older_build));
1276 assert!(!older_build.is_upgrade_of(&newer_build));
1278 }
1279
1280 #[test]
1281 fn test_is_upgrade_of_build_version_same() {
1282 let installation = create_test_installation(
1283 ImplementationName::CPython,
1284 3,
1285 10,
1286 8,
1287 None,
1288 PythonVariant::Default,
1289 Some("20240101"),
1290 );
1291
1292 assert!(!installation.is_upgrade_of(&installation));
1294 }
1295
1296 #[test]
1297 fn test_is_upgrade_of_build_with_legacy_installation() {
1298 let legacy = create_test_installation(
1299 ImplementationName::CPython,
1300 3,
1301 10,
1302 8,
1303 None,
1304 PythonVariant::Default,
1305 None,
1306 );
1307 let with_build = create_test_installation(
1308 ImplementationName::CPython,
1309 3,
1310 10,
1311 8,
1312 None,
1313 PythonVariant::Default,
1314 Some("20240101"),
1315 );
1316
1317 assert!(with_build.is_upgrade_of(&legacy));
1319 assert!(!legacy.is_upgrade_of(&with_build));
1321 }
1322
1323 #[test]
1324 fn test_is_upgrade_of_patch_takes_precedence_over_build() {
1325 let older_patch_newer_build = create_test_installation(
1326 ImplementationName::CPython,
1327 3,
1328 10,
1329 8,
1330 None,
1331 PythonVariant::Default,
1332 Some("20240201"),
1333 );
1334 let newer_patch_older_build = create_test_installation(
1335 ImplementationName::CPython,
1336 3,
1337 10,
1338 9,
1339 None,
1340 PythonVariant::Default,
1341 Some("20240101"),
1342 );
1343
1344 assert!(newer_patch_older_build.is_upgrade_of(&older_patch_newer_build));
1346 assert!(!older_patch_newer_build.is_upgrade_of(&newer_patch_older_build));
1348 }
1349
1350 #[test]
1351 fn test_find_version_matching() {
1352 use crate::PythonVersion;
1353
1354 let platform = Platform::from_env().unwrap();
1355 let temp_dir = tempfile::tempdir().unwrap();
1356
1357 fs::create_dir(temp_dir.path().join(format!("cpython-3.10.0-{platform}"))).unwrap();
1359
1360 temp_env::with_var(
1361 uv_static::EnvVars::UV_PYTHON_INSTALL_DIR,
1362 Some(temp_dir.path()),
1363 || {
1364 let installations = ManagedPythonInstallations::from_settings(None).unwrap();
1365
1366 let v3_1 = PythonVersion::from_str("3.1").unwrap();
1368 let matched: Vec<_> = installations.find_version(&v3_1).unwrap().collect();
1369 assert_eq!(matched.len(), 0);
1370
1371 let v3_10 = PythonVersion::from_str("3.10").unwrap();
1373 let matched: Vec<_> = installations.find_version(&v3_10).unwrap().collect();
1374 assert_eq!(matched.len(), 1);
1375 },
1376 );
1377 }
1378}