From c2ef825d7b2af7946c7cdd2a3c7bb9d938eb6443 Mon Sep 17 00:00:00 2001 From: Silvano Cerza <3314350+silvanocerza@users.noreply.github.com> Date: Mon, 15 Jul 2024 20:28:31 +0200 Subject: [PATCH] Add `pypy` executables when calling `uv venv` (#5047) ## Summary Should fix #2092. This PR changes `uv venv` so it also creates symlinks to `pypy` on Unix and copies executables on Windows when creating a new environment using PyPy. I found a bit of discrepancy between creation of a venv using `python` and `uv`, as using `python` brings all the executables with it. While `uv` brings only those without any version number, at least on Windows. The behaviour is different on Unix as we take the versioned symlinks too. Some examples below. `python -m venv` generates the following `Scripts` folder. ``` Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 7/14/2024 15:41 2031 activate -a---- 7/14/2024 15:41 1029 activate.bat -a---- 7/14/2024 15:41 9033 Activate.ps1 -a---- 7/14/2024 15:41 393 deactivate.bat -a---- 7/14/2024 15:40 27648 libffi-8.dll -a---- 7/14/2024 15:41 44290560 libpypy3.10-c.dll -a---- 7/14/2024 15:41 108424 pip.exe -a---- 7/14/2024 15:41 108424 pip3.10.exe -a---- 7/14/2024 15:41 108424 pip3.exe -a---- 7/14/2024 15:41 79360 pypy.exe -a---- 7/14/2024 15:41 79360 pypy3.10.exe -a---- 7/14/2024 15:41 79360 pypy3.10w.exe -a---- 7/14/2024 15:41 79360 pypy3.exe -a---- 7/14/2024 15:41 79360 pypyw.exe -a---- 7/14/2024 15:41 79360 python.exe -a---- 7/14/2024 15:41 79360 python3.10.exe -a---- 7/14/2024 15:41 79360 python3.exe -a---- 7/14/2024 15:41 79360 pythonw.exe ``` `uv venv` instead generates this. ``` -a---- 7/14/2024 16:27 3360 activate -a---- 7/14/2024 16:27 2251 activate.bat -a---- 7/14/2024 16:27 2627 activate.csh -a---- 7/14/2024 16:27 4191 activate.fish -a---- 7/14/2024 16:27 3875 activate.nu -a---- 7/14/2024 16:27 2766 activate.ps1 -a---- 7/14/2024 16:27 2378 activate_this.py -a---- 7/14/2024 16:27 1728 deactivate.bat -a---- 7/13/2024 19:19 27648 libffi-8.dll -a---- 7/13/2024 19:19 44290560 libpypy3.10-c.dll -a---- 7/14/2024 16:27 1215 pydoc.bat -a---- 7/13/2024 19:19 79360 pypy.exe -a---- 7/13/2024 19:19 79360 pypyw.exe -a---- 7/13/2024 19:19 79360 python.exe -a---- 7/13/2024 19:19 79360 pythonw.exe ``` ## Test Plan To verify the correct behaviour: 1. Download and install PyPy from [official website](https://www.pypy.org/download.html) 2. Call `uv venv -p ` 3. Run `.\.venv\Scripts\activate` on Windows or `./.venv/Scripts/activate` on Unix 4. Run `pypy` I thought of writing some automated tests but I couldn't rely on `uv python install` command to install PyPy as it's not in the list of installable Python builds. --- .github/workflows/ci.yml | 4 +- crates/uv-virtualenv/src/bare.rs | 130 ++++++++++++++++++++++++++++--- 2 files changed, 123 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdc84531b..ac9b2b4a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -571,13 +571,13 @@ jobs: fi } - executables=("python") + executables=("pypy" "pypy3" "python") all_found=true for executable_name in "${executables[@]}"; do check_in_bin "$executable_name" "$folder_path" result=$? - + if [[ $result -ne 0 ]]; then all_found=false fi diff --git a/crates/uv-virtualenv/src/bare.rs b/crates/uv-virtualenv/src/bare.rs index 8094833bd..348278071 100644 --- a/crates/uv-virtualenv/src/bare.rs +++ b/crates/uv-virtualenv/src/bare.rs @@ -170,6 +170,14 @@ pub fn create_bare_venv( interpreter.python_minor(), )), )?; + + if interpreter.markers().implementation_name() == "pypy" { + uv_fs::replace_symlink( + "python", + scripts.join(format!("pypy{}", interpreter.python_major())), + )?; + uv_fs::replace_symlink("python", scripts.join("pypy"))?; + } } // No symlinking on Windows, at least not on a regular non-dev non-admin Windows install. @@ -188,6 +196,58 @@ pub fn create_bare_venv( &scripts, python_home, )?; + + if interpreter.markers().implementation_name() == "pypy" { + copy_launcher_windows( + WindowsExecutable::PythonMajor, + interpreter, + &base_python, + &scripts, + python_home, + )?; + copy_launcher_windows( + WindowsExecutable::PythonMajorMinor, + interpreter, + &base_python, + &scripts, + python_home, + )?; + copy_launcher_windows( + WindowsExecutable::PyPy, + interpreter, + &base_python, + &scripts, + python_home, + )?; + copy_launcher_windows( + WindowsExecutable::PyPyMajor, + interpreter, + &base_python, + &scripts, + python_home, + )?; + copy_launcher_windows( + WindowsExecutable::PyPyMajorMinor, + interpreter, + &base_python, + &scripts, + python_home, + )?; + copy_launcher_windows( + WindowsExecutable::PyPyw, + interpreter, + &base_python, + &scripts, + python_home, + )?; + copy_launcher_windows( + WindowsExecutable::PyPyMajorMinorw, + interpreter, + &base_python, + &scripts, + python_home, + )?; + } } #[cfg(not(any(unix, windows)))] @@ -304,16 +364,59 @@ pub fn create_bare_venv( enum WindowsExecutable { /// The `python.exe` executable (or `venvlauncher.exe` launcher shim). Python, + /// The `python3.exe` executable (or `venvlauncher.exe` launcher shim). + PythonMajor, + /// The `python3..exe` executable (or `venvlauncher.exe` launcher shim). + PythonMajorMinor, /// The `pythonw.exe` executable (or `venvwlauncher.exe` launcher shim). Pythonw, + // The `pypy.exe` executable + PyPy, + // The `pypy3.exe` executable + PyPyMajor, + // The `pypy3..exe` executable + PyPyMajorMinor, + // The `pypyw.exe` executable + PyPyw, + // The `pypy3.w.exe` executable + PyPyMajorMinorw, } impl WindowsExecutable { /// The name of the Python executable. - fn exe(self) -> &'static str { + fn exe(self, interpreter: &Interpreter) -> String { match self { - WindowsExecutable::Python => "python.exe", - WindowsExecutable::Pythonw => "pythonw.exe", + WindowsExecutable::Python => String::from("python.exe"), + WindowsExecutable::PythonMajor => { + format!("python{}.exe", interpreter.python_major()) + } + WindowsExecutable::PythonMajorMinor => { + format!( + "python{}.{}.exe", + interpreter.python_major(), + interpreter.python_minor() + ) + } + WindowsExecutable::Pythonw => String::from("pythonw.exe"), + WindowsExecutable::PyPy => String::from("pypy.exe"), + WindowsExecutable::PyPyMajor => { + format!("pypy{}.exe", interpreter.python_major()) + } + WindowsExecutable::PyPyMajorMinor => { + format!( + "pypy{}.{}.exe", + interpreter.python_major(), + interpreter.python_minor() + ) + } + WindowsExecutable::PyPyw => String::from("pypyw.exe"), + WindowsExecutable::PyPyMajorMinorw => { + format!( + "pypy{}.{}w.exe", + interpreter.python_major(), + interpreter.python_minor() + ) + } } } @@ -321,7 +424,16 @@ impl WindowsExecutable { fn launcher(self) -> &'static str { match self { WindowsExecutable::Python => "venvlauncher.exe", + WindowsExecutable::PythonMajor => "venvlauncher.exe", + WindowsExecutable::PythonMajorMinor => "venvlauncher.exe", WindowsExecutable::Pythonw => "venvwlauncher.exe", + // From 3.13 on these should replace the `python.exe` and `pythonw.exe` shims. + // These are not relevant as of now for PyPy as it doesn't yet support Python 3.13. + WindowsExecutable::PyPy => "venvlauncher.exe", + WindowsExecutable::PyPyMajor => "venvlauncher.exe", + WindowsExecutable::PyPyMajorMinor => "venvlauncher.exe", + WindowsExecutable::PyPyw => "venvwlauncher.exe", + WindowsExecutable::PyPyMajorMinorw => "venvwlauncher.exe", } } } @@ -344,8 +456,8 @@ fn copy_launcher_windows( .join("venv") .join("scripts") .join("nt") - .join(executable.exe()); - match fs_err::copy(shim, scripts.join(executable.exe())) { + .join(executable.exe(interpreter)); + match fs_err::copy(shim, scripts.join(executable.exe(interpreter))) { Ok(_) => return Ok(()), Err(err) if err.kind() == io::ErrorKind::NotFound => {} Err(err) => { @@ -362,7 +474,7 @@ fn copy_launcher_windows( .join("scripts") .join("nt") .join(executable.launcher()); - match fs_err::copy(shim, scripts.join(executable.exe())) { + match fs_err::copy(shim, scripts.join(executable.exe(interpreter))) { Ok(_) => return Ok(()), Err(err) if err.kind() == io::ErrorKind::NotFound => {} Err(err) => { @@ -373,7 +485,7 @@ fn copy_launcher_windows( // Third priority: on Conda at least, we can look for the launcher shim next to // the Python executable itself. let shim = base_python.with_file_name(executable.launcher()); - match fs_err::copy(shim, scripts.join(executable.exe())) { + match fs_err::copy(shim, scripts.join(executable.exe(interpreter))) { Ok(_) => return Ok(()), Err(err) if err.kind() == io::ErrorKind::NotFound => {} Err(err) => { @@ -385,8 +497,8 @@ fn copy_launcher_windows( // an embedded Python. Copy the Python executable itself, along with // the DLLs, `.pyd` files, and `.zip` files in the same directory. match fs_err::copy( - base_python.with_file_name(executable.exe()), - scripts.join(executable.exe()), + base_python.with_file_name(executable.exe(interpreter)), + scripts.join(executable.exe(interpreter)), ) { Ok(_) => { // Copy `.dll` and `.pyd` files from the top-level, and from the