| | """Build Environment used for isolation during sdist building""" |
| |
|
| | from __future__ import annotations |
| |
|
| | import logging |
| | import os |
| | import pathlib |
| | import site |
| | import sys |
| | import textwrap |
| | from collections import OrderedDict |
| | from collections.abc import Iterable |
| | from types import TracebackType |
| | from typing import TYPE_CHECKING, Protocol, TypedDict |
| |
|
| | 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.deprecation import deprecated |
| | from pip._internal.utils.logging import VERBOSE |
| | from pip._internal.utils.packaging import get_requirement |
| | 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 |
| | from pip._internal.req.req_install import InstallRequirement |
| |
|
| | class ExtraEnviron(TypedDict, total=False): |
| | extra_environ: dict[str, str] |
| |
|
| |
|
| | logger = logging.getLogger(__name__) |
| |
|
| |
|
| | def _dedup(a: str, b: str) -> 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 BuildEnvironmentInstaller(Protocol): |
| | """ |
| | Interface for installing build dependencies into an isolated build |
| | environment. |
| | """ |
| |
|
| | def install( |
| | self, |
| | requirements: Iterable[str], |
| | prefix: _Prefix, |
| | *, |
| | kind: str, |
| | for_req: InstallRequirement | None, |
| | ) -> None: ... |
| |
|
| |
|
| | class SubprocessBuildEnvironmentInstaller: |
| | """ |
| | Install build dependencies by calling pip in a subprocess. |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | finder: PackageFinder, |
| | build_constraints: list[str] | None = None, |
| | build_constraint_feature_enabled: bool = False, |
| | ) -> None: |
| | self.finder = finder |
| | self._build_constraints = build_constraints or [] |
| | self._build_constraint_feature_enabled = build_constraint_feature_enabled |
| |
|
| | def _deprecation_constraint_check(self) -> None: |
| | """ |
| | Check for deprecation warning: PIP_CONSTRAINT affecting build environments. |
| | |
| | This warns when build-constraint feature is NOT enabled and PIP_CONSTRAINT |
| | is not empty. |
| | """ |
| | if self._build_constraint_feature_enabled or self._build_constraints: |
| | return |
| |
|
| | pip_constraint = os.environ.get("PIP_CONSTRAINT") |
| | if not pip_constraint or not pip_constraint.strip(): |
| | return |
| |
|
| | deprecated( |
| | reason=( |
| | "Setting PIP_CONSTRAINT will not affect " |
| | "build constraints in the future," |
| | ), |
| | replacement=( |
| | "to specify build constraints using --build-constraint or " |
| | "PIP_BUILD_CONSTRAINT. To disable this warning without " |
| | "any build constraints set --use-feature=build-constraint or " |
| | 'PIP_USE_FEATURE="build-constraint"' |
| | ), |
| | gone_in="26.2", |
| | issue=None, |
| | ) |
| |
|
| | def install( |
| | self, |
| | requirements: Iterable[str], |
| | prefix: _Prefix, |
| | *, |
| | kind: str, |
| | for_req: InstallRequirement | None, |
| | ) -> None: |
| | self._deprecation_constraint_check() |
| |
|
| | finder = self.finder |
| | args: list[str] = [ |
| | sys.executable, |
| | get_runnable_pip(), |
| | "install", |
| | "--ignore-installed", |
| | "--no-user", |
| | "--prefix", |
| | prefix.path, |
| | "--no-warn-script-location", |
| | "--disable-pip-version-check", |
| | |
| | |
| | |
| | "--no-compile", |
| | |
| | |
| | "--target", |
| | "", |
| | ] |
| | if logger.getEffectiveLevel() <= logging.DEBUG: |
| | args.append("-vv") |
| | elif logger.getEffectiveLevel() <= VERBOSE: |
| | 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]) |
| |
|
| | if finder.proxy: |
| | args.extend(["--proxy", finder.proxy]) |
| | for host in finder.trusted_hosts: |
| | args.extend(["--trusted-host", host]) |
| | if finder.custom_cert: |
| | args.extend(["--cert", finder.custom_cert]) |
| | if finder.client_cert: |
| | args.extend(["--client-cert", finder.client_cert]) |
| | if finder.allow_all_prereleases: |
| | args.append("--pre") |
| | if finder.prefer_binary: |
| | args.append("--prefer-binary") |
| |
|
| | |
| | if self._build_constraint_feature_enabled: |
| | args.extend(["--use-feature", "build-constraint"]) |
| |
|
| | if self._build_constraints: |
| | |
| | |
| | |
| | for constraint_file in self._build_constraints: |
| | args.extend(["--constraint", constraint_file]) |
| | args.extend(["--build-constraint", constraint_file]) |
| |
|
| | extra_environ: ExtraEnviron = {} |
| | if self._build_constraint_feature_enabled and not self._build_constraints: |
| | |
| | |
| | |
| | extra_environ = {"extra_environ": {"_PIP_IN_BUILD_IGNORE_CONSTRAINTS": "1"}} |
| |
|
| | args.append("--") |
| | args.extend(requirements) |
| |
|
| | identify_requirement = ( |
| | f" for {for_req.name}" if for_req and for_req.name else "" |
| | ) |
| | with open_spinner(f"Installing {kind}") as spinner: |
| | call_subprocess( |
| | args, |
| | command_desc=f"installing {kind}{identify_requirement}", |
| | spinner=spinner, |
| | **extra_environ, |
| | ) |
| |
|
| |
|
| | class BuildEnvironment: |
| | """Creates and manages an isolated environment to install build deps""" |
| |
|
| | def __init__(self, installer: BuildEnvironmentInstaller) -> None: |
| | self.installer = installer |
| | 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: type[BaseException] | None, |
| | exc_val: BaseException | None, |
| | exc_tb: TracebackType | None, |
| | ) -> 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 = get_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, |
| | requirements: Iterable[str], |
| | prefix_as_string: str, |
| | *, |
| | kind: str, |
| | for_req: InstallRequirement | None = None, |
| | ) -> None: |
| | prefix = self._prefixes[prefix_as_string] |
| | assert not prefix.setup |
| | prefix.setup = True |
| | if not requirements: |
| | return |
| | self.installer.install(requirements, prefix, kind=kind, for_req=for_req) |
| |
|
| |
|
| | 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: type[BaseException] | None, |
| | exc_val: BaseException | None, |
| | exc_tb: TracebackType | None, |
| | ) -> None: |
| | pass |
| |
|
| | def cleanup(self) -> None: |
| | pass |
| |
|
| | def install_requirements( |
| | self, |
| | requirements: Iterable[str], |
| | prefix_as_string: str, |
| | *, |
| | kind: str, |
| | for_req: InstallRequirement | None = None, |
| | ) -> None: |
| | raise NotImplementedError() |
| |
|