| | """Build Environment used for isolation during sdist building |
| | """ |
| |
|
| | import logging |
| | import os |
| | import pathlib |
| | import site |
| | import sys |
| | import textwrap |
| | from collections import OrderedDict |
| | from types import TracebackType |
| | 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 |
| | 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_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 |
| |
|
| | if TYPE_CHECKING: |
| | from pip._internal.index.package_finder import PackageFinder |
| |
|
| | 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 |
| | scheme = get_scheme("", prefix=path) |
| | self.bin_dir = scheme.scripts |
| | self.lib_dirs = _dedup(scheme.purelib, scheme.platlib) |
| |
|
| |
|
| | def get_runnable_pip() -> str: |
| | """Get a file to pass to a Python executable, to run the currently-running pip. |
| | |
| | This is used to run a pip subprocess, for installing requirements into the build |
| | environment. |
| | """ |
| | source = pathlib.Path(pip_location).resolve().parent |
| |
|
| | if not source.is_dir(): |
| | |
| | |
| | return str(source) |
| |
|
| | 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: |
| | |
| | |
| | |
| | |
| | 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""" |
| |
|
| | def __init__(self) -> None: |
| | temp_dir = TempDirectory(kind=tempdir_kinds.BUILD_ENV, globally_managed=True) |
| |
|
| | self._prefixes = OrderedDict( |
| | (name, _Prefix(os.path.join(temp_dir.path, name))) |
| | for name in ("normal", "overlay") |
| | ) |
| |
|
| | self._bin_dirs: List[str] = [] |
| | self._lib_dirs: List[str] = [] |
| | for prefix in reversed(list(self._prefixes.values())): |
| | self._bin_dirs.append(prefix.bin_dir) |
| | self._lib_dirs.extend(prefix.lib_dirs) |
| |
|
| | |
| | |
| | |
| | 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) |
| | with open( |
| | os.path.join(self._site_dir, "sitecustomize.py"), "w", encoding="utf-8" |
| | ) as fp: |
| | fp.write( |
| | textwrap.dedent( |
| | """ |
| | import os, site, sys |
| | |
| | # First, drop system-sites related paths. |
| | original_sys_path = sys.path[:] |
| | known_paths = set() |
| | for path in {system_sites!r}: |
| | site.addsitedir(path, known_paths=known_paths) |
| | system_paths = set( |
| | os.path.normcase(path) |
| | for path in sys.path[len(original_sys_path):] |
| | ) |
| | original_sys_path = [ |
| | path for path in original_sys_path |
| | if os.path.normcase(path) not in system_paths |
| | ] |
| | sys.path = original_sys_path |
| | |
| | # Second, add lib directories. |
| | # ensuring .pth file are processed. |
| | for path in {lib_dirs!r}: |
| | assert not path in sys.path |
| | site.addsitedir(path) |
| | """ |
| | ).format(system_sites=system_sites, lib_dirs=self._lib_dirs) |
| | ) |
| |
|
| | def __enter__(self) -> None: |
| | self._save_env = { |
| | name: os.environ.get(name, None) |
| | for name in ("PATH", "PYTHONNOUSERSITE", "PYTHONPATH") |
| | } |
| |
|
| | path = self._bin_dirs[:] |
| | old_path = self._save_env["PATH"] |
| | if old_path: |
| | path.extend(old_path.split(os.pathsep)) |
| |
|
| | pythonpath = [self._site_dir] |
| |
|
| | os.environ.update( |
| | { |
| | "PATH": os.pathsep.join(path), |
| | "PYTHONNOUSERSITE": "1", |
| | "PYTHONPATH": os.pathsep.join(pythonpath), |
| | } |
| | ) |
| |
|
| | def __exit__( |
| | self, |
| | exc_type: Optional[Type[BaseException]], |
| | exc_val: Optional[BaseException], |
| | exc_tb: Optional[TracebackType], |
| | ) -> None: |
| | for varname, old_value in self._save_env.items(): |
| | if old_value is None: |
| | os.environ.pop(varname, None) |
| | else: |
| | os.environ[varname] = old_value |
| |
|
| | def check_requirements( |
| | self, reqs: Iterable[str] |
| | ) -> Tuple[Set[Tuple[str, str]], Set[str]]: |
| | """Return 2 sets: |
| | - conflicting requirements: set of (installed, wanted) reqs tuples |
| | - missing requirements: set of reqs |
| | """ |
| | missing = set() |
| | conflicting = set() |
| | if reqs: |
| | env = ( |
| | get_environment(self._lib_dirs) |
| | if hasattr(self, "_lib_dirs") |
| | else get_default_environment() |
| | ) |
| | for req_str in reqs: |
| | req = Requirement(req_str) |
| | |
| | |
| | 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) |
| | continue |
| | if isinstance(dist.version, Version): |
| | installed_req_str = f"{req.name}=={dist.version}" |
| | else: |
| | installed_req_str = f"{req.name}==={dist.version}" |
| | if not req.specifier.contains(dist.version, prereleases=True): |
| | conflicting.add((installed_req_str, req_str)) |
| | |
| | return conflicting, missing |
| |
|
| | def install_requirements( |
| | self, |
| | finder: "PackageFinder", |
| | requirements: Iterable[str], |
| | prefix_as_string: str, |
| | *, |
| | kind: str, |
| | ) -> None: |
| | prefix = self._prefixes[prefix_as_string] |
| | assert not prefix.setup |
| | prefix.setup = True |
| | if not requirements: |
| | return |
| | self._install_requirements( |
| | get_runnable_pip(), |
| | finder, |
| | requirements, |
| | prefix, |
| | kind=kind, |
| | ) |
| |
|
| | @staticmethod |
| | def _install_requirements( |
| | pip_runnable: str, |
| | finder: "PackageFinder", |
| | requirements: Iterable[str], |
| | prefix: _Prefix, |
| | *, |
| | kind: str, |
| | ) -> None: |
| | args: List[str] = [ |
| | sys.executable, |
| | pip_runnable, |
| | "install", |
| | "--ignore-installed", |
| | "--no-user", |
| | "--prefix", |
| | prefix.path, |
| | "--no-warn-script-location", |
| | ] |
| | if logger.getEffectiveLevel() <= logging.DEBUG: |
| | args.append("-v") |
| | for format_control in ("no_binary", "only_binary"): |
| | formats = getattr(finder.format_control, format_control) |
| | args.extend( |
| | ( |
| | "--" + format_control.replace("_", "-"), |
| | ",".join(sorted(formats or {":none:"})), |
| | ) |
| | ) |
| |
|
| | index_urls = finder.index_urls |
| | if index_urls: |
| | args.extend(["-i", index_urls[0]]) |
| | for extra_index in index_urls[1:]: |
| | args.extend(["--extra-index-url", extra_index]) |
| | else: |
| | args.append("--no-index") |
| | for link in finder.find_links: |
| | args.extend(["--find-links", link]) |
| |
|
| | for host in finder.trusted_hosts: |
| | args.extend(["--trusted-host", host]) |
| | if finder.allow_all_prereleases: |
| | args.append("--pre") |
| | if finder.prefer_binary: |
| | args.append("--prefer-binary") |
| | args.append("--") |
| | args.extend(requirements) |
| | extra_environ = {"_PIP_STANDALONE_CERT": where()} |
| | 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): |
| | """A no-op drop-in replacement for BuildEnvironment""" |
| |
|
| | def __init__(self) -> None: |
| | pass |
| |
|
| | def __enter__(self) -> None: |
| | pass |
| |
|
| | def __exit__( |
| | self, |
| | exc_type: Optional[Type[BaseException]], |
| | exc_val: Optional[BaseException], |
| | exc_tb: Optional[TracebackType], |
| | ) -> None: |
| | pass |
| |
|
| | def cleanup(self) -> None: |
| | pass |
| |
|
| | def install_requirements( |
| | self, |
| | finder: "PackageFinder", |
| | requirements: Iterable[str], |
| | prefix_as_string: str, |
| | *, |
| | kind: str, |
| | ) -> None: |
| | raise NotImplementedError() |
| |
|