summaryrefslogtreecommitdiff
path: root/src/pip/_internal/build_env.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/pip/_internal/build_env.py')
-rw-r--r--src/pip/_internal/build_env.py126
1 files changed, 71 insertions, 55 deletions
diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py
index a30b45826..4f704a354 100644
--- a/src/pip/_internal/build_env.py
+++ b/src/pip/_internal/build_env.py
@@ -1,17 +1,15 @@
"""Build Environment used for isolation during sdist building
"""
-import contextlib
import logging
import os
import pathlib
+import site
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, Iterator, List, Optional, Set, Tuple, Type
+from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type, Union
from pip._vendor.certifi import where
from pip._vendor.packaging.requirements import Requirement
@@ -19,8 +17,8 @@ from pip._vendor.packaging.version import Version
from pip import __file__ as pip_location
from pip._internal.cli.spinners import open_spinner
-from pip._internal.locations import get_platlib, get_prefixed_libs, get_purelib
-from pip._internal.metadata import get_environment
+from pip._internal.locations import get_platlib, get_purelib, get_scheme
+from pip._internal.metadata import get_default_environment, get_environment
from pip._internal.utils.subprocess import call_subprocess
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
@@ -30,41 +28,53 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
+def _dedup(a: str, b: str) -> Union[Tuple[str], Tuple[str, str]]:
+ return (a, b) if a != b else (a,)
+
+
class _Prefix:
def __init__(self, path: str) -> None:
self.path = path
self.setup = False
- self.bin_dir = get_paths(
- "nt" if os.name == "nt" else "posix_prefix",
- vars={"base": path, "platbase": path},
- )["scripts"]
- self.lib_dirs = get_prefixed_libs(path)
+ scheme = get_scheme("", prefix=path)
+ self.bin_dir = scheme.scripts
+ self.lib_dirs = _dedup(scheme.purelib, scheme.platlib)
-@contextlib.contextmanager
-def _create_standalone_pip() -> Iterator[str]:
- """Create a "standalone pip" zip file.
+def get_runnable_pip() -> str:
+ """Get a file to pass to a Python executable, to run the currently-running pip.
- The zip file's content is identical to the currently-running pip.
- It will be used to install requirements into the build environment.
+ This is used to run a pip subprocess, for installing requirements into the build
+ environment.
"""
source = pathlib.Path(pip_location).resolve().parent
- # Return the current instance if `source` is not a directory. We can't build
- # a zip from this, and it likely means the instance is already standalone.
if not source.is_dir():
- yield str(source)
- return
+ # This would happen if someone is using pip from inside a zip file. In that
+ # case, we can use that directly.
+ return str(source)
+
+ return os.fsdecode(source / "__pip-runner__.py")
+
- with TempDirectory(kind="standalone-pip") as tmp_dir:
- pip_zip = os.path.join(tmp_dir.path, "__env_pip__.zip")
- kwargs = {}
- if sys.version_info >= (3, 8):
- kwargs["strict_timestamps"] = False
- with zipfile.ZipFile(pip_zip, "w", **kwargs) as zf:
- for child in source.rglob("*"):
- zf.write(child, child.relative_to(source.parent).as_posix())
- yield os.path.join(pip_zip, "pip")
+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:
@@ -87,9 +97,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)
@@ -168,9 +177,17 @@ class BuildEnvironment:
missing = set()
conflicting = set()
if reqs:
- env = get_environment(self._lib_dirs)
+ env = (
+ get_environment(self._lib_dirs)
+ if hasattr(self, "_lib_dirs")
+ else get_default_environment()
+ )
for req_str in reqs:
req = Requirement(req_str)
+ # We're explicitly evaluating with an empty extra value, since build
+ # environments are not provided any mechanism to select specific extras.
+ if req.marker is not None and not req.marker.evaluate({"extra": ""}):
+ continue
dist = env.get_distribution(req.name)
if not dist:
missing.add(req_str)
@@ -179,7 +196,7 @@ class BuildEnvironment:
installed_req_str = f"{req.name}=={dist.version}"
else:
installed_req_str = f"{req.name}==={dist.version}"
- if dist.version not in req.specifier:
+ if not req.specifier.contains(dist.version, prereleases=True):
conflicting.add((installed_req_str, req_str))
# FIXME: Consider direct URL?
return conflicting, missing
@@ -189,29 +206,21 @@ class BuildEnvironment:
finder: "PackageFinder",
requirements: Iterable[str],
prefix_as_string: str,
- message: str,
+ *,
+ kind: str,
) -> None:
prefix = self._prefixes[prefix_as_string]
assert not prefix.setup
prefix.setup = True
if not requirements:
return
- with contextlib.ExitStack() as ctx:
- # TODO: Remove this block when dropping 3.6 support. Python 3.6
- # lacks importlib.resources and pep517 has issues loading files in
- # a zip, so we fallback to the "old" method by adding the current
- # pip directory to the child process's sys.path.
- if sys.version_info < (3, 7):
- pip_runnable = os.path.dirname(pip_location)
- else:
- pip_runnable = ctx.enter_context(_create_standalone_pip())
- self._install_requirements(
- pip_runnable,
- finder,
- requirements,
- prefix,
- message,
- )
+ self._install_requirements(
+ get_runnable_pip(),
+ finder,
+ requirements,
+ prefix,
+ kind=kind,
+ )
@staticmethod
def _install_requirements(
@@ -219,7 +228,8 @@ class BuildEnvironment:
finder: "PackageFinder",
requirements: Iterable[str],
prefix: _Prefix,
- message: str,
+ *,
+ kind: str,
) -> None:
args: List[str] = [
sys.executable,
@@ -261,8 +271,13 @@ class BuildEnvironment:
args.append("--")
args.extend(requirements)
extra_environ = {"_PIP_STANDALONE_CERT": where()}
- with open_spinner(message) as spinner:
- call_subprocess(args, spinner=spinner, extra_environ=extra_environ)
+ with open_spinner(f"Installing {kind}") as spinner:
+ call_subprocess(
+ args,
+ command_desc=f"pip subprocess to install {kind}",
+ spinner=spinner,
+ extra_environ=extra_environ,
+ )
class NoOpBuildEnvironment(BuildEnvironment):
@@ -290,6 +305,7 @@ class NoOpBuildEnvironment(BuildEnvironment):
finder: "PackageFinder",
requirements: Iterable[str],
prefix_as_string: str,
- message: str,
+ *,
+ kind: str,
) -> None:
raise NotImplementedError()