diff --git a/news/8214.bugfix.rst b/news/8214.bugfix.rst new file mode 100644 index 00000000000..22224f380e5 --- /dev/null +++ b/news/8214.bugfix.rst @@ -0,0 +1,2 @@ +Prevent packages already-installed alongside with pip to be injected into an +isolated build environment during build-time dependency population. diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index fa22d6377c6..755dbb1f712 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -1,15 +1,19 @@ """Build Environment used for isolation during sdist building """ +import contextlib import logging import os +import pathlib import sys import textwrap +import zipfile from collections import OrderedDict from sysconfig import get_paths from types import TracebackType -from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type +from typing import TYPE_CHECKING, Iterable, Iterator, List, Optional, Set, Tuple, Type +from pip._vendor.certifi import where from pip._vendor.pkg_resources import Requirement, VersionConflict, WorkingSet from pip import __file__ as pip_location @@ -37,6 +41,29 @@ def __init__(self, path): self.lib_dirs = get_prefixed_libs(path) +@contextlib.contextmanager +def _create_standalone_pip() -> Iterator[str]: + """Create a "standalone pip" zip file. + + The zip file's content is identical to the currently-running pip. + It will be used to install requirements into the build environment. + """ + source = pathlib.Path(pip_location).resolve().parent + + # Return the current instance if it is already a zip file. This can happen + # if a PEP 517 requirement is an sdist itself. + if not source.is_dir() and source.parent.name == "__env_pip__.zip": + yield str(source) + return + + with TempDirectory(kind="standalone-pip") as tmp_dir: + pip_zip = os.path.join(tmp_dir.path, "__env_pip__.zip") + with zipfile.ZipFile(pip_zip, "w") as zf: + for child in source.rglob("*"): + zf.write(child, child.relative_to(source.parent).as_posix()) + yield os.path.join(pip_zip, "pip") + + class BuildEnvironment: """Creates and manages an isolated environment to install build deps """ @@ -160,8 +187,25 @@ def install_requirements( prefix.setup = True if not requirements: return + with _create_standalone_pip() as standalone_pip: + self._install_requirements( + standalone_pip, + finder, + requirements, + prefix, + message, + ) + + @staticmethod + def _install_requirements( + standalone_pip: str, + finder: "PackageFinder", + requirements: Iterable[str], + prefix: _Prefix, + message: str, + ) -> None: args = [ - sys.executable, os.path.dirname(pip_location), 'install', + sys.executable, standalone_pip, 'install', '--ignore-installed', '--no-user', '--prefix', prefix.path, '--no-warn-script-location', ] # type: List[str] @@ -190,8 +234,9 @@ def install_requirements( args.append('--prefer-binary') args.append('--') args.extend(requirements) + extra_environ = {"_PIP_STANDALONE_CERT": where()} with open_spinner(message) as spinner: - call_subprocess(args, spinner=spinner) + call_subprocess(args, spinner=spinner, extra_environ=extra_environ) class NoOpBuildEnvironment(BuildEnvironment): diff --git a/src/pip/_vendor/certifi/core.py b/src/pip/_vendor/certifi/core.py index 8987449f6b5..b8140cf1ae7 100644 --- a/src/pip/_vendor/certifi/core.py +++ b/src/pip/_vendor/certifi/core.py @@ -8,7 +8,21 @@ """ import os + +class _PipPatchedCertificate(Exception): + pass + + try: + # Return a certificate file on disk for a standalone pip zipapp running in + # an isolated build environment to use. Passing --cert to the standalone + # pip does not work since requests calls where() unconditionally on import. + _PIP_STANDALONE_CERT = os.environ.get("_PIP_STANDALONE_CERT") + if _PIP_STANDALONE_CERT: + def where(): + return _PIP_STANDALONE_CERT + raise _PipPatchedCertificate() + from importlib.resources import path as get_path, read_text _CACERT_CTX = None @@ -38,6 +52,8 @@ def where(): return _CACERT_PATH +except _PipPatchedCertificate: + pass except ImportError: # This fallback will work for Python versions prior to 3.7 that lack the diff --git a/tools/vendoring/patches/certifi.patch b/tools/vendoring/patches/certifi.patch index 9d5395a7b6b..a36a0020ff5 100644 --- a/tools/vendoring/patches/certifi.patch +++ b/tools/vendoring/patches/certifi.patch @@ -1,13 +1,41 @@ diff --git a/src/pip/_vendor/certifi/core.py b/src/pip/_vendor/certifi/core.py -index 5d2b8cd32..8987449f6 100644 +index 5d2b8cd32..b8140cf1a 100644 --- a/src/pip/_vendor/certifi/core.py +++ b/src/pip/_vendor/certifi/core.py -@@ -33,7 +33,7 @@ try: +@@ -8,7 +8,21 @@ This module returns the installation location of cacert.pem or its contents. + """ + import os + ++ ++class _PipPatchedCertificate(Exception): ++ pass ++ ++ + try: ++ # Return a certificate file on disk for a standalone pip zipapp running in ++ # an isolated build environment to use. Passing --cert to the standalone ++ # pip does not work since requests calls where() unconditionally on import. ++ _PIP_STANDALONE_CERT = os.environ.get("_PIP_STANDALONE_CERT") ++ if _PIP_STANDALONE_CERT: ++ def where(): ++ return _PIP_STANDALONE_CERT ++ raise _PipPatchedCertificate() ++ + from importlib.resources import path as get_path, read_text + + _CACERT_CTX = None +@@ -33,11 +47,13 @@ try: # We also have to hold onto the actual context manager, because # it will do the cleanup whenever it gets garbage collected, so # we will also store that at the global level as well. - _CACERT_CTX = get_path("certifi", "cacert.pem") + _CACERT_CTX = get_path("pip._vendor.certifi", "cacert.pem") _CACERT_PATH = str(_CACERT_CTX.__enter__()) - + return _CACERT_PATH + ++except _PipPatchedCertificate: ++ pass + + except ImportError: + # This fallback will work for Python versions prior to 3.7 that lack the