|
1 | 1 | //! Create a virtual environment.
|
2 | 2 |
|
| 3 | +use std::borrow::Cow; |
3 | 4 | use std::env::consts::EXE_SUFFIX;
|
4 | 5 | use std::io;
|
5 | 6 | use std::io::{BufWriter, Write};
|
6 |
| -use std::path::Path; |
| 7 | +use std::path::{Path, PathBuf}; |
7 | 8 |
|
8 | 9 | use fs_err as fs;
|
9 | 10 | use fs_err::File;
|
10 | 11 | use itertools::Itertools;
|
11 |
| -use tracing::debug; |
| 12 | +use tracing::{debug, warn}; |
12 | 13 |
|
13 | 14 | use uv_fs::{cachedir, Simplified, CWD};
|
14 | 15 | use uv_pypi_types::Scheme;
|
@@ -58,22 +59,39 @@ pub(crate) fn create(
|
58 | 59 | // considered the "base" for the virtual environment. This is typically the Python executable
|
59 | 60 | // from the [`Interpreter`]; however, if the interpreter is a virtual environment itself, then
|
60 | 61 | // the base Python executable is the Python executable of the interpreter's base interpreter.
|
| 62 | + let base_executable = interpreter |
| 63 | + .sys_base_executable() |
| 64 | + .unwrap_or(interpreter.sys_executable()); |
61 | 65 | let base_python = if cfg!(unix) && interpreter.is_standalone() {
|
62 | 66 | // In `python-build-standalone`, a symlinked interpreter will return its own executable path
|
63 |
| - // as `sys._base_executable`. Using the symlinked path as the base Python executable is |
64 |
| - // incorrect, since it will cause `home` to point to something that is _not_ a Python |
65 |
| - // installation. |
| 67 | + // as `sys._base_executable`. Using the symlinked path as the base Python executable can be |
| 68 | + // incorrect, since it could cause `home` to point to something that is _not_ a Python |
| 69 | + // installation. Specifically, if the interpreter _itself_ is symlinked to an arbitrary |
| 70 | + // location, we need to fully resolve it to the actual Python executable; however, if the |
| 71 | + // entire standalone interpreter is symlinked, then we can use the symlinked path. |
66 | 72 | //
|
67 |
| - // Instead, we want to fully resolve the symlink to the actual Python executable. |
68 |
| - uv_fs::canonicalize_executable(interpreter.sys_executable())? |
| 73 | + // We emulate CPython's `getpath.py` to ensure that the base executable results in a valid |
| 74 | + // Python prefix when converted into the `home` key for `pyvenv.cfg`. |
| 75 | + match find_base_python( |
| 76 | + base_executable, |
| 77 | + interpreter.python_major(), |
| 78 | + interpreter.python_minor(), |
| 79 | + ) { |
| 80 | + Ok(path) => path, |
| 81 | + Err(err) => { |
| 82 | + warn!("Failed to find base Python executable: {err}"); |
| 83 | + uv_fs::canonicalize_executable(base_executable)? |
| 84 | + } |
| 85 | + } |
69 | 86 | } else {
|
70 |
| - std::path::absolute( |
71 |
| - interpreter |
72 |
| - .sys_base_executable() |
73 |
| - .unwrap_or(interpreter.sys_executable()), |
74 |
| - )? |
| 87 | + std::path::absolute(base_executable)? |
75 | 88 | };
|
76 | 89 |
|
| 90 | + debug!( |
| 91 | + "Using base executable for virtual environment: {}", |
| 92 | + base_python.display() |
| 93 | + ); |
| 94 | + |
77 | 95 | // Validate the existing location.
|
78 | 96 | match location.metadata() {
|
79 | 97 | Ok(metadata) => {
|
@@ -610,3 +628,82 @@ fn copy_launcher_windows(
|
610 | 628 |
|
611 | 629 | Err(Error::NotFound(base_python.user_display().to_string()))
|
612 | 630 | }
|
| 631 | + |
| 632 | +/// Find the Python executable that should be considered the "base" for a virtual environment. |
| 633 | +/// |
| 634 | +/// Assumes that the provided executable is that of a standalone Python interpreter. |
| 635 | +/// |
| 636 | +/// The strategy here mimics that of `getpath.py`: we search up the ancestor path to determine |
| 637 | +/// whether a given executable will convert into a valid Python prefix; if not, we resolve the |
| 638 | +/// symlink and try again. |
| 639 | +/// |
| 640 | +/// This ensures that: |
| 641 | +/// |
| 642 | +/// 1. We avoid using symlinks to arbitrary locations as the base Python executable. For example, |
| 643 | +/// if a user symlinks a Python _executable_ to `/Users/user/foo`, we want to avoid using |
| 644 | +/// `/Users/user` as `home`, since it's not a Python installation, and so the relevant libraries |
| 645 | +/// and headers won't be found when it's used as the executable directory. |
| 646 | +/// See: <https://github.com/python/cpython/blob/a03efb533a58fd13fb0cc7f4a5c02c8406a407bd/Modules/getpath.py#L367-L400> |
| 647 | +/// |
| 648 | +/// 2. We use the "first" resolved symlink that _is_ a valid Python prefix, and thereby preserve |
| 649 | +/// symlinks. For example, if a user symlinks a Python _installation_ to `/Users/user/foo`, such |
| 650 | +/// that `/Users/user/foo/bin/python` is the resulting executable, we want to use `/Users/user/foo` |
| 651 | +/// as `home`, rather than resolving to the symlink target. Concretely, this allows users to |
| 652 | +/// symlink patch versions (like `cpython-3.12.6-macos-aarch64-none`) to minor version aliases |
| 653 | +/// (like `cpython-3.12-macos-aarch64-none`) and preserve those aliases in the resulting virtual |
| 654 | +/// environments. |
| 655 | +/// |
| 656 | +/// See: <https://github.com/python/cpython/blob/a03efb533a58fd13fb0cc7f4a5c02c8406a407bd/Modules/getpath.py#L591-L594> |
| 657 | +fn find_base_python(executable: &Path, major: u8, minor: u8) -> Result<PathBuf, io::Error> { |
| 658 | + /// Determining whether `dir` is a valid Python prefix by searching for a "landmark". |
| 659 | + /// |
| 660 | + /// See: <https://github.com/python/cpython/blob/a03efb533a58fd13fb0cc7f4a5c02c8406a407bd/Modules/getpath.py#L183> |
| 661 | + fn is_prefix(dir: &Path, major: u8, minor: u8) -> bool { |
| 662 | + if cfg!(windows) { |
| 663 | + dir.join("Lib").join("os.py").is_file() |
| 664 | + } else { |
| 665 | + dir.join("lib") |
| 666 | + .join(format!("python{major}.{minor}")) |
| 667 | + .join("os.py") |
| 668 | + .is_file() |
| 669 | + } |
| 670 | + } |
| 671 | + |
| 672 | + let mut executable = Cow::Borrowed(executable); |
| 673 | + |
| 674 | + loop { |
| 675 | + debug!( |
| 676 | + "Assessing Python executable as base candidate: {}", |
| 677 | + executable.display() |
| 678 | + ); |
| 679 | + |
| 680 | + // Determine whether this executable will produce a valid `home` for a virtual environment. |
| 681 | + for prefix in executable.ancestors() { |
| 682 | + if is_prefix(prefix, major, minor) { |
| 683 | + return Ok(executable.into_owned()); |
| 684 | + } |
| 685 | + } |
| 686 | + |
| 687 | + // If not, resolve the symlink. |
| 688 | + let resolved = fs_err::read_link(&executable)?; |
| 689 | + |
| 690 | + // If the symlink is relative, resolve it relative to the executable. |
| 691 | + let resolved = if resolved.is_relative() { |
| 692 | + if let Some(parent) = executable.parent() { |
| 693 | + parent.join(resolved) |
| 694 | + } else { |
| 695 | + return Err(io::Error::new( |
| 696 | + io::ErrorKind::Other, |
| 697 | + "Symlink has no parent directory", |
| 698 | + )); |
| 699 | + } |
| 700 | + } else { |
| 701 | + resolved |
| 702 | + }; |
| 703 | + |
| 704 | + // Normalize the resolved path. |
| 705 | + let resolved = uv_fs::normalize_absolute_path(&resolved)?; |
| 706 | + |
| 707 | + executable = Cow::Owned(resolved); |
| 708 | + } |
| 709 | +} |
0 commit comments