Repository: pypa/pip (python)

Issue:
virtualenv with --system-site-packages breaks pip's build isolation
It seems that if you create a virtualenv with `--system-site-packages`, the system packages (but *not* the user packages) will be on the `PYTHONPATH` in the PEP 517 isolated build environment that `pip` creates (it does not effect `python -m pep517.build`), and it seems they will be on there with *higher precedence* than the requirements installed in the build environment. It only affects `pip >= 19.0.0`.

The most common way I've seen this cause problems is with the new `setuptools.build_meta:__legacy__` backend. Since the most recent version of `pip` requires a recent version of `setuptools`, if you have an older version of `setuptools` installed on the system, `pip install` will fail due to the missing `setuptools.build_meta:__legacy__` backend. It is possible to reproduce this by crafting a deliberately bad package and installing it, then creating a wheel for a newer version of the package (which allowed me to test that this failure was actually introduced with `pip==19.0.0`), but for the MWE we can stick with `setuptools`.

To reproduce, create a package like this:

```bash
cd /tmp
mkdir demo
cd demo
mkdir badpkg

touch badpkg/pyproject.toml
echo 'from setuptools import setup; setup(name="foo", version="0.1.0")' \
    > badpkg/setup.py
```

Then install an older version of `setuptools` *on your system* (can't be `--user` or in the virtualenv), `pip install 'setuptools < 40.8.0'` (I did this in a `pyenv` environment). If your system already has an older version of `setuptools` on it, you're already good.

Next create a virtualenv with `--system-site-packages` and activate it:

```bash
virtualenv venv --system-site-packages
source venv/bin/activate
```

Finally try to install `badpkg` (`pip wheel` or `pip install -t tmp` also works to demonstrate the problem):

```bash
pip install ./badpkg
```

You should get a traceback the ends like this:

>  `AttributeError: module 'setuptools.build_meta' has no attribute '__legacy__'`

At first I thought this was because the affected packages had too loose bounds on `build-system.requires` (e.g. `requires=["setuptools"]`) and that `pip` was failing to install a more recent version in the isolated environment, but this bug still occurs even if you specify `"setuptools>=40.8.0"`, so I believe it's not a problem at install-time, it's a problem with the path resolution at build time.

At the moment it's unclear if this is a problem with `pip` or `virtualenv`, but since it does not affect `python -m pep517.build`, I'm reporting it here. It could be a combination of both.

CC: @gaborbernat 

Pull request description:
Fix build isolation on Pythons where purelib/platlib aren't enough
use `site.getsitepackages()` where available instead of just purelib/platlib, which is sometimes insufficient on e.g. System Pythons for Debian/macOS.

handle `virtualenv < 20` overwriting site.py without getsitepackages() by preserving current behavior. I _think_ the cases where this fix is needed and site.py is overwritten are mutually exclusive, so this should be okay. At the very least, it should be an improvement in all cases where behavior is changed.

I don't know how to write a great test, since it needs to actually run against system Python on Mac or Debian outside a virtualenv in order to directly test this. As it is, the added test only assumes that `site.getsitepackages()` is right to remove and then removed, which is a bit tautological.

closes #6264

Patch:
diff --git a/news/6264.bugfix.rst b/news/6264.bugfix.rst
new file mode 100644
index 000000000..66554a473
--- /dev/null
+++ b/news/6264.bugfix.rst
@@ -0,0 +1,1 @@
+Fix build environment isolation on some system Pythons.
diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py
index 6213eedd1..cc2b38bab 100644
--- a/src/pip/_internal/build_env.py
+++ b/src/pip/_internal/build_env.py
@@ -4,6 +4,7 @@
 import logging
 import os
 import pathlib
+import site
 import sys
 import textwrap
 from collections import OrderedDict
@@ -55,6 +56,26 @@ def get_runnable_pip() -> str:
     return os.fsdecode(source / "__pip-runner__.py")
 
 
+def _get_system_sitepackages() -> Set[str]:
+    """Get system site packages
+
+    Usually from site.getsitepackages,
+    but fallback on `get_purelib()/get_platlib()` if unavailable
+    (e.g. in a virtualenv created by virtualenv<20)
+
+    Returns normalized set of strings.
+    """
+    if hasattr(site, "getsitepackages"):
+        system_sites = site.getsitepackages()
+    else:
+        # virtualenv < 20 overwrites site.py without getsitepackages
+        # fallback on get_purelib/get_platlib.
+        # this is known to miss things, but shouldn't in the cases
+        # where getsitepackages() has been removed (inside a virtualenv)
+        system_sites = [get_purelib(), get_platlib()]
+    return {os.path.normcase(path) for path in system_sites}
+
+
 class BuildEnvironment:
     """Creates and manages an isolated environment to install build deps"""
 
@@ -75,9 +96,8 @@ class BuildEnvironment:
         # Customize site to:
         # - ensure .pth files are honored
         # - prevent access to system site packages
-        system_sites = {
-            os.path.normcase(site) for site in (get_purelib(), get_platlib())
-        }
+        system_sites = _get_system_sitepackages()
+
         self._site_dir = os.path.join(temp_dir.path, "site")
         if not os.path.exists(self._site_dir):
             os.mkdir(self._site_dir)


Test patch:
diff --git a/tests/functional/test_build_env.py b/tests/functional/test_build_env.py
index 437adb995..869e8ad92 100644
--- a/tests/functional/test_build_env.py
+++ b/tests/functional/test_build_env.py
@@ -4,7 +4,7 @@ from typing import Optional
 
 import pytest
 
-from pip._internal.build_env import BuildEnvironment
+from pip._internal.build_env import BuildEnvironment, _get_system_sitepackages
 from tests.lib import (
     PipTestEnvironment,
     TestPipResult,
@@ -226,6 +226,10 @@ def test_build_env_isolation(script: PipTestEnvironment) -> None:
     script.pip_install_local("-t", target, pkg_whl)
     script.environ["PYTHONPATH"] = target
 
+    system_sites = _get_system_sitepackages()
+    # there should always be something to exclude
+    assert system_sites
+
     run_with_build_env(
         script,
         "",
@@ -247,5 +251,14 @@ def test_build_env_isolation(script: PipTestEnvironment) -> None:
                     })), file=sys.stderr)
             print('sys.path:\n  ' + '\n  '.join(sys.path), file=sys.stderr)
             sys.exit(1)
+        """
+        f"""
+        # second check: direct check of exclusion of system site packages
+        import os
+
+        normalized_path = [os.path.normcase(path) for path in sys.path]
+        for system_path in {system_sites!r}:
+            assert system_path not in normalized_path, \
+            f"{{system_path}} found in {{normalized_path}}"
         """,
     )