Deduplicate symbolic links between purelib and platlib (#3002)

## Summary

This PR adds system install tests to verify the behavior described in
#2798. It turns out this behavior _also_ affects Fedora and Amazon
Linux, we just didn't have the right conditions enabled (specifically,
you need to create the virtualenv with `python -m venv` to get these
symlinks), so the test suite was expanded to capture that.

The issue itself is also fixed by way of deduplicating the
`site-packages` entries.

Closes: https://github.com/astral-sh/uv/issues/2798
This commit is contained in:
Charlie Marsh 2024-04-12 17:08:56 -04:00 committed by GitHub
parent 3ae35adc8e
commit ab9cc78b7a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 85 additions and 5 deletions

View file

@ -371,6 +371,31 @@ jobs:
- name: "Validate global Python install" - name: "Validate global Python install"
run: python scripts/check_system_python.py --uv ./uv run: python scripts/check_system_python.py --uv ./uv
system-test-opensuse:
needs: build-binary-linux
name: "check system | python on opensuse"
runs-on: ubuntu-latest
container: opensuse/tumbleweed
steps:
- uses: actions/checkout@v4
- name: "Install Python"
run: zypper install -y python310 which && python3.10 -m ensurepip && mv /usr/bin/python3.10 /usr/bin/python3
- name: "Download binary"
uses: actions/download-artifact@v4
with:
name: uv-linux-${{ github.sha }}
- name: "Prepare binary"
run: chmod +x ./uv
- name: "Print Python path"
run: echo $(which python3)
- name: "Validate global Python install"
run: python3 scripts/check_system_python.py --uv ./uv
# Note: rockylinux:8 is a 1-1 code compatible distro to rhel-8 # Note: rockylinux:8 is a 1-1 code compatible distro to rhel-8
# rockylinux:8 mimics centos-8 but with added maintenance stability # rockylinux:8 mimics centos-8 but with added maintenance stability
# and avoids issues with centos stream uptime concerns # and avoids issues with centos stream uptime concerns

View file

@ -1,6 +1,7 @@
use std::env; use std::env;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use same_file::is_same_file;
use tracing::{debug, info}; use tracing::{debug, info};
use uv_cache::Cache; use uv_cache::Cache;
@ -92,12 +93,17 @@ impl PythonEnvironment {
/// ///
/// In most cases, `purelib` and `platlib` will be the same, and so the iterator will contain /// In most cases, `purelib` and `platlib` will be the same, and so the iterator will contain
/// a single element; however, in some distributions, they may be different. /// a single element; however, in some distributions, they may be different.
///
/// Some distributions also create symbolic links from `purelib` to `platlib`; in such cases, we
/// still deduplicate the entries, returning a single path.
pub fn site_packages(&self) -> impl Iterator<Item = &Path> { pub fn site_packages(&self) -> impl Iterator<Item = &Path> {
std::iter::once(self.interpreter.purelib()).chain( let purelib = self.interpreter.purelib();
if self.interpreter.purelib() == self.interpreter.platlib() { let platlib = self.interpreter.platlib();
std::iter::once(purelib).chain(
if purelib == platlib || is_same_file(purelib, platlib).unwrap_or(false) {
None None
} else { } else {
Some(self.interpreter.platlib()) Some(platlib)
}, },
) )
} }

View file

@ -8,6 +8,7 @@ To run locally, create a venv with seed packages.
import argparse import argparse
import logging import logging
import os import os
import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
@ -107,7 +108,7 @@ if __name__ == "__main__":
raise Exception("The package `pylint` is installed (but shouldn't be).") raise Exception("The package `pylint` is installed (but shouldn't be).")
# Create a virtual environment with `uv`. # Create a virtual environment with `uv`.
logging.info("Creating virtual environment...") logging.info("Creating virtual environment with `uv`...")
subprocess.run( subprocess.run(
[uv, "venv", ".venv", "--seed", "--python", sys.executable], [uv, "venv", ".venv", "--seed", "--python", sys.executable],
cwd=temp_dir, cwd=temp_dir,
@ -126,7 +127,7 @@ if __name__ == "__main__":
check=True, check=True,
) )
logging.info("Installing into virtual environment...") logging.info("Installing into `uv` virtual environment...")
# Disable the `CONDA_PREFIX` and `VIRTUAL_ENV` environment variables, so that # Disable the `CONDA_PREFIX` and `VIRTUAL_ENV` environment variables, so that
# we only rely on virtual environment discovery via the `.venv` directory. # we only rely on virtual environment discovery via the `.venv` directory.
@ -163,6 +164,26 @@ if __name__ == "__main__":
"The package `pylint` isn't installed in the virtual environment." "The package `pylint` isn't installed in the virtual environment."
) )
# Uninstall the package (`pylint`).
logging.info("Uninstalling the package `pylint`.")
subprocess.run(
[uv, "pip", "uninstall", "pylint", "--verbose"],
cwd=temp_dir,
check=True,
env=env,
)
# Ensure that the package (`pylint`) isn't installed in the virtual environment.
logging.info("Checking that `pylint` isn't installed.")
code = subprocess.run(
[executable, "-m", "pip", "show", "pylint"],
cwd=temp_dir,
)
if code.returncode == 0:
raise Exception(
"The package `pylint` is installed in the virtual environment (but shouldn't be)."
)
# Attempt to install NumPy. # Attempt to install NumPy.
# This ensures that we can successfully install a package with native libraries. # This ensures that we can successfully install a package with native libraries.
# #
@ -178,3 +199,31 @@ if __name__ == "__main__":
# for Python 3.13 (at time of writing). # for Python 3.13 (at time of writing).
if sys.version_info < (3, 13) and sys.implementation.name == "cpython": if sys.version_info < (3, 13) and sys.implementation.name == "cpython":
install_package(uv=uv, package="pydantic_core") install_package(uv=uv, package="pydantic_core")
# Next, create a virtual environment with `venv`, to ensure that `uv` can
# interoperate with `venv` virtual environments.
shutil.rmtree(os.path.join(temp_dir, ".venv"))
logging.info("Creating virtual environment with `venv`...")
subprocess.run(
[sys.executable, "-m", "venv", ".venv"],
cwd=temp_dir,
check=True,
)
# Install the package (`pylint`) into the virtual environment.
logging.info("Installing into `venv` virtual environment...")
subprocess.run(
[uv, "pip", "install", "pylint", "--verbose"],
cwd=temp_dir,
check=True,
env=env,
)
# Uninstall the package (`pylint`).
logging.info("Uninstalling the package `pylint`.")
subprocess.run(
[uv, "pip", "uninstall", "pylint", "--verbose"],
cwd=temp_dir,
check=True,
env=env,
)