Path: blob/main/test/lib/python3.9/site-packages/pip/_internal/build_env.py
4799 views
"""Build Environment used for isolation during sdist building1"""23import contextlib4import logging5import os6import pathlib7import sys8import textwrap9import zipfile10from collections import OrderedDict11from sysconfig import get_paths12from types import TracebackType13from typing import TYPE_CHECKING, Generator, Iterable, List, Optional, Set, Tuple, Type1415from pip._vendor.certifi import where16from pip._vendor.packaging.requirements import Requirement17from pip._vendor.packaging.version import Version1819from pip import __file__ as pip_location20from pip._internal.cli.spinners import open_spinner21from pip._internal.locations import get_platlib, get_prefixed_libs, get_purelib22from pip._internal.metadata import get_default_environment, get_environment23from pip._internal.utils.subprocess import call_subprocess24from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds2526if TYPE_CHECKING:27from pip._internal.index.package_finder import PackageFinder2829logger = logging.getLogger(__name__)303132class _Prefix:33def __init__(self, path: str) -> None:34self.path = path35self.setup = False36self.bin_dir = get_paths(37"nt" if os.name == "nt" else "posix_prefix",38vars={"base": path, "platbase": path},39)["scripts"]40self.lib_dirs = get_prefixed_libs(path)414243@contextlib.contextmanager44def _create_standalone_pip() -> Generator[str, None, None]:45"""Create a "standalone pip" zip file.4647The zip file's content is identical to the currently-running pip.48It will be used to install requirements into the build environment.49"""50source = pathlib.Path(pip_location).resolve().parent5152# Return the current instance if `source` is not a directory. We can't build53# a zip from this, and it likely means the instance is already standalone.54if not source.is_dir():55yield str(source)56return5758with TempDirectory(kind="standalone-pip") as tmp_dir:59pip_zip = os.path.join(tmp_dir.path, "__env_pip__.zip")60kwargs = {}61if sys.version_info >= (3, 8):62kwargs["strict_timestamps"] = False63with zipfile.ZipFile(pip_zip, "w", **kwargs) as zf:64for child in source.rglob("*"):65zf.write(child, child.relative_to(source.parent).as_posix())66yield os.path.join(pip_zip, "pip")676869class BuildEnvironment:70"""Creates and manages an isolated environment to install build deps"""7172def __init__(self) -> None:73temp_dir = TempDirectory(kind=tempdir_kinds.BUILD_ENV, globally_managed=True)7475self._prefixes = OrderedDict(76(name, _Prefix(os.path.join(temp_dir.path, name)))77for name in ("normal", "overlay")78)7980self._bin_dirs: List[str] = []81self._lib_dirs: List[str] = []82for prefix in reversed(list(self._prefixes.values())):83self._bin_dirs.append(prefix.bin_dir)84self._lib_dirs.extend(prefix.lib_dirs)8586# Customize site to:87# - ensure .pth files are honored88# - prevent access to system site packages89system_sites = {90os.path.normcase(site) for site in (get_purelib(), get_platlib())91}92self._site_dir = os.path.join(temp_dir.path, "site")93if not os.path.exists(self._site_dir):94os.mkdir(self._site_dir)95with open(96os.path.join(self._site_dir, "sitecustomize.py"), "w", encoding="utf-8"97) as fp:98fp.write(99textwrap.dedent(100"""101import os, site, sys102103# First, drop system-sites related paths.104original_sys_path = sys.path[:]105known_paths = set()106for path in {system_sites!r}:107site.addsitedir(path, known_paths=known_paths)108system_paths = set(109os.path.normcase(path)110for path in sys.path[len(original_sys_path):]111)112original_sys_path = [113path for path in original_sys_path114if os.path.normcase(path) not in system_paths115]116sys.path = original_sys_path117118# Second, add lib directories.119# ensuring .pth file are processed.120for path in {lib_dirs!r}:121assert not path in sys.path122site.addsitedir(path)123"""124).format(system_sites=system_sites, lib_dirs=self._lib_dirs)125)126127def __enter__(self) -> None:128self._save_env = {129name: os.environ.get(name, None)130for name in ("PATH", "PYTHONNOUSERSITE", "PYTHONPATH")131}132133path = self._bin_dirs[:]134old_path = self._save_env["PATH"]135if old_path:136path.extend(old_path.split(os.pathsep))137138pythonpath = [self._site_dir]139140os.environ.update(141{142"PATH": os.pathsep.join(path),143"PYTHONNOUSERSITE": "1",144"PYTHONPATH": os.pathsep.join(pythonpath),145}146)147148def __exit__(149self,150exc_type: Optional[Type[BaseException]],151exc_val: Optional[BaseException],152exc_tb: Optional[TracebackType],153) -> None:154for varname, old_value in self._save_env.items():155if old_value is None:156os.environ.pop(varname, None)157else:158os.environ[varname] = old_value159160def check_requirements(161self, reqs: Iterable[str]162) -> Tuple[Set[Tuple[str, str]], Set[str]]:163"""Return 2 sets:164- conflicting requirements: set of (installed, wanted) reqs tuples165- missing requirements: set of reqs166"""167missing = set()168conflicting = set()169if reqs:170env = (171get_environment(self._lib_dirs)172if hasattr(self, "_lib_dirs")173else get_default_environment()174)175for req_str in reqs:176req = Requirement(req_str)177# We're explicitly evaluating with an empty extra value, since build178# environments are not provided any mechanism to select specific extras.179if req.marker is not None and not req.marker.evaluate({"extra": ""}):180continue181dist = env.get_distribution(req.name)182if not dist:183missing.add(req_str)184continue185if isinstance(dist.version, Version):186installed_req_str = f"{req.name}=={dist.version}"187else:188installed_req_str = f"{req.name}==={dist.version}"189if not req.specifier.contains(dist.version, prereleases=True):190conflicting.add((installed_req_str, req_str))191# FIXME: Consider direct URL?192return conflicting, missing193194def install_requirements(195self,196finder: "PackageFinder",197requirements: Iterable[str],198prefix_as_string: str,199*,200kind: str,201) -> None:202prefix = self._prefixes[prefix_as_string]203assert not prefix.setup204prefix.setup = True205if not requirements:206return207with contextlib.ExitStack() as ctx:208pip_runnable = ctx.enter_context(_create_standalone_pip())209self._install_requirements(210pip_runnable,211finder,212requirements,213prefix,214kind=kind,215)216217@staticmethod218def _install_requirements(219pip_runnable: str,220finder: "PackageFinder",221requirements: Iterable[str],222prefix: _Prefix,223*,224kind: str,225) -> None:226args: List[str] = [227sys.executable,228pip_runnable,229"install",230"--ignore-installed",231"--no-user",232"--prefix",233prefix.path,234"--no-warn-script-location",235]236if logger.getEffectiveLevel() <= logging.DEBUG:237args.append("-v")238for format_control in ("no_binary", "only_binary"):239formats = getattr(finder.format_control, format_control)240args.extend(241(242"--" + format_control.replace("_", "-"),243",".join(sorted(formats or {":none:"})),244)245)246247index_urls = finder.index_urls248if index_urls:249args.extend(["-i", index_urls[0]])250for extra_index in index_urls[1:]:251args.extend(["--extra-index-url", extra_index])252else:253args.append("--no-index")254for link in finder.find_links:255args.extend(["--find-links", link])256257for host in finder.trusted_hosts:258args.extend(["--trusted-host", host])259if finder.allow_all_prereleases:260args.append("--pre")261if finder.prefer_binary:262args.append("--prefer-binary")263args.append("--")264args.extend(requirements)265extra_environ = {"_PIP_STANDALONE_CERT": where()}266with open_spinner(f"Installing {kind}") as spinner:267call_subprocess(268args,269command_desc=f"pip subprocess to install {kind}",270spinner=spinner,271extra_environ=extra_environ,272)273274275class NoOpBuildEnvironment(BuildEnvironment):276"""A no-op drop-in replacement for BuildEnvironment"""277278def __init__(self) -> None:279pass280281def __enter__(self) -> None:282pass283284def __exit__(285self,286exc_type: Optional[Type[BaseException]],287exc_val: Optional[BaseException],288exc_tb: Optional[TracebackType],289) -> None:290pass291292def cleanup(self) -> None:293pass294295def install_requirements(296self,297finder: "PackageFinder",298requirements: Iterable[str],299prefix_as_string: str,300*,301kind: str,302) -> None:303raise NotImplementedError()304305306