First Commit
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
"""Build environments used for isolation during build backend calls."""
|
||||
|
||||
from pip._internal.build_env.base import (
|
||||
BuildEnvironment,
|
||||
BuildEnvironmentInstaller,
|
||||
BuildIsolationMode,
|
||||
)
|
||||
from pip._internal.build_env.installer import (
|
||||
InprocessBuildEnvironmentInstaller,
|
||||
SubprocessBuildEnvironmentInstaller,
|
||||
)
|
||||
from pip._internal.build_env.noop import NoOpBuildEnvironment
|
||||
from pip._internal.build_env.venv import VenvBuildEnvironment
|
||||
from pip._internal.build_env.virtual import VirtualBuildEnvironment
|
||||
|
||||
__all__ = [
|
||||
"BuildEnvironment",
|
||||
"BuildEnvironmentInstaller",
|
||||
"BuildIsolationMode",
|
||||
"InprocessBuildEnvironmentInstaller",
|
||||
"NoOpBuildEnvironment",
|
||||
"SubprocessBuildEnvironmentInstaller",
|
||||
"VenvBuildEnvironment",
|
||||
"VirtualBuildEnvironment",
|
||||
]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,103 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
from collections.abc import Iterable
|
||||
from contextlib import AbstractContextManager as ContextManager
|
||||
from typing import TYPE_CHECKING, Literal, Protocol
|
||||
|
||||
from pip._vendor.packaging.version import Version
|
||||
|
||||
from pip._internal.locations import get_scheme
|
||||
from pip._internal.metadata import get_default_environment, get_environment
|
||||
from pip._internal.utils.packaging import get_requirement
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
|
||||
|
||||
BuildIsolationMode = Literal["off", "virtual", "venv"]
|
||||
|
||||
|
||||
def _dedup(a: str, b: str) -> tuple[str] | tuple[str, str]:
|
||||
return (a, b) if a != b else (a,)
|
||||
|
||||
|
||||
class Prefix:
|
||||
# TODO: simplify this data model when the legacy subprocess installer is removed
|
||||
def __init__(self, path: str, *, venv_executable: str | None = None) -> 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)
|
||||
self.venv_executable = venv_executable
|
||||
|
||||
|
||||
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 BuildEnvironment(ContextManager[None], metaclass=abc.ABCMeta):
|
||||
"""Creates and manages an isolated environment to install build deps"""
|
||||
|
||||
lib_dirs: list[str]
|
||||
python_executable: str
|
||||
|
||||
@abc.abstractmethod
|
||||
def __init__(self, installer: BuildEnvironmentInstaller): ...
|
||||
|
||||
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)
|
||||
# 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)
|
||||
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))
|
||||
# FIXME: Consider direct URL?
|
||||
return conflicting, missing
|
||||
|
||||
@abc.abstractmethod
|
||||
def install_requirements(
|
||||
self,
|
||||
requirements: Iterable[str],
|
||||
prefix_as_string: str,
|
||||
*,
|
||||
kind: str,
|
||||
for_req: InstallRequirement | None = None,
|
||||
) -> None: ...
|
||||
@@ -0,0 +1,333 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import textwrap
|
||||
from collections.abc import Iterable, Sequence
|
||||
from contextlib import AbstractContextManager as ContextManager
|
||||
from contextlib import nullcontext
|
||||
from io import StringIO
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pip._internal.build_env.base import Prefix
|
||||
from pip._internal.cli.spinners import open_rich_spinner, open_spinner
|
||||
from pip._internal.exceptions import (
|
||||
BuildDependencyInstallError,
|
||||
DiagnosticPipError,
|
||||
InstallWheelBuildError,
|
||||
PipError,
|
||||
)
|
||||
from pip._internal.metadata import get_environment
|
||||
from pip._internal.utils.logging import VERBOSE, capture_logging
|
||||
from pip._internal.utils.misc import get_runnable_pip
|
||||
from pip._internal.utils.subprocess import call_subprocess
|
||||
from pip._internal.utils.temp_dir import TempDirectory
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pip._internal.cache import WheelCache
|
||||
from pip._internal.index.package_finder import PackageFinder
|
||||
from pip._internal.operations.build.build_tracker import BuildTracker
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
from pip._internal.resolution.base import BaseResolver
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SubprocessBuildEnvironmentInstaller:
|
||||
"""
|
||||
Install build dependencies by calling pip in a subprocess.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
finder: PackageFinder,
|
||||
build_constraints: list[str] | None = None,
|
||||
) -> None:
|
||||
self.finder = finder
|
||||
self._build_constraints = build_constraints or []
|
||||
|
||||
def install(
|
||||
self,
|
||||
requirements: Iterable[str],
|
||||
prefix: Prefix,
|
||||
*,
|
||||
kind: str,
|
||||
for_req: InstallRequirement | None,
|
||||
) -> None:
|
||||
finder = self.finder
|
||||
args: list[str] = [
|
||||
get_runnable_pip(),
|
||||
"install",
|
||||
# HACK: --prefix shouldn't be necessary for venv environments, but
|
||||
# we set it anyway so if it's set via an envvar or configuration
|
||||
# file, it won't break things, *sigh*.
|
||||
"--prefix",
|
||||
prefix.path,
|
||||
"--no-user",
|
||||
"--no-warn-script-location",
|
||||
"--disable-pip-version-check",
|
||||
# As the build environment is ephemeral, it's wasteful to
|
||||
# pre-compile everything, especially as not every Python
|
||||
# module will be used/compiled in most cases.
|
||||
"--no-compile",
|
||||
# The prefix specified two lines above, thus
|
||||
# target from config file or env var should be ignored
|
||||
"--target",
|
||||
"",
|
||||
]
|
||||
if prefix.venv_executable:
|
||||
args.insert(0, prefix.venv_executable)
|
||||
else:
|
||||
args.insert(0, sys.executable)
|
||||
args.append("--ignore-installed")
|
||||
|
||||
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:"})),
|
||||
)
|
||||
)
|
||||
|
||||
if finder.release_control is not None:
|
||||
# Use ordered args to preserve the user's original command-line order
|
||||
# This is important because later flags can override earlier ones
|
||||
for attr_name, value in finder.release_control.get_ordered_args():
|
||||
args.extend(("--" + attr_name.replace("_", "-"), value))
|
||||
|
||||
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])
|
||||
|
||||
# is not None: forward an empty --proxy "" (disable proxying) too.
|
||||
if finder.proxy is not None:
|
||||
args.extend(["--proxy", finder.proxy])
|
||||
if finder.no_proxy_env:
|
||||
args.append("--no-proxy-env")
|
||||
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.prefer_binary:
|
||||
args.append("--prefer-binary")
|
||||
if finder.refresh_package:
|
||||
args.extend(["--refresh-package", ",".join(finder.refresh_package)])
|
||||
|
||||
# Only build constraints apply in the isolated build environment.
|
||||
# _PIP_IN_BUILD_IGNORE_CONSTRAINTS tells the subprocess to ignore the
|
||||
# regular constraints it inherits (via PIP_CONSTRAINT or config files).
|
||||
# Build constraints reach it through --build-constraint, which also
|
||||
# constrains any nested builds.
|
||||
for constraint_file in self._build_constraints:
|
||||
args.extend(["--build-constraint", constraint_file])
|
||||
|
||||
if finder.uploaded_prior_to:
|
||||
args.extend(["--uploaded-prior-to", finder.uploaded_prior_to.isoformat()])
|
||||
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={"_PIP_IN_BUILD_IGNORE_CONSTRAINTS": "1"},
|
||||
)
|
||||
|
||||
|
||||
class InprocessBuildEnvironmentInstaller:
|
||||
"""
|
||||
Install build dependencies via the already running pip process.
|
||||
|
||||
This contains a stripped down version of the install command with
|
||||
only the logic necessary for installing build dependencies. The
|
||||
finder, session, build tracker, and wheel cache are reused, but new
|
||||
instances of everything else are created as needed.
|
||||
|
||||
Options are inherited from the parent install command unless
|
||||
they don't make sense for build dependencies (in which case, they
|
||||
are hard-coded, see comments below).
|
||||
"""
|
||||
|
||||
# TODO: this plays poorly with venv-based build environments, but cannot be
|
||||
# fixed until pip gains better support for operating within a Python
|
||||
# environment that isn't the running environment.
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
finder: PackageFinder,
|
||||
build_tracker: BuildTracker,
|
||||
wheel_cache: WheelCache,
|
||||
build_constraints: Sequence[InstallRequirement] = (),
|
||||
verbosity: int = 0,
|
||||
) -> None:
|
||||
from pip._internal.operations.prepare import RequirementPreparer
|
||||
|
||||
self._finder = finder
|
||||
self._build_constraints = build_constraints
|
||||
self._wheel_cache = wheel_cache
|
||||
self._level = 0
|
||||
|
||||
build_dir = TempDirectory(kind="build-env-install", globally_managed=True)
|
||||
self._preparer = RequirementPreparer(
|
||||
build_isolation_installer=self,
|
||||
# Inherited options or state.
|
||||
finder=finder,
|
||||
session=finder._link_collector.session,
|
||||
build_dir=build_dir.path,
|
||||
build_tracker=build_tracker,
|
||||
verbosity=verbosity,
|
||||
# This is irrelevant as it only applies to editable requirements.
|
||||
src_dir="",
|
||||
# Hard-coded options (that should NOT be inherited).
|
||||
download_dir=None,
|
||||
build_isolation="virtual",
|
||||
check_build_deps=False,
|
||||
progress_bar="off",
|
||||
# TODO: hash-checking should be extended to build deps, but that is
|
||||
# deferred for later as it'd be a breaking change.
|
||||
require_hashes=False,
|
||||
use_user_site=False,
|
||||
lazy_wheel=False,
|
||||
legacy_resolver=False,
|
||||
allow_editables=True,
|
||||
)
|
||||
|
||||
def install(
|
||||
self,
|
||||
requirements: Iterable[str],
|
||||
prefix: Prefix,
|
||||
*,
|
||||
kind: str,
|
||||
for_req: InstallRequirement | None,
|
||||
) -> None:
|
||||
"""Install entrypoint. Manages output capturing and error handling."""
|
||||
capture_logs = not logger.isEnabledFor(VERBOSE) and self._level == 0
|
||||
if capture_logs:
|
||||
# Hide the logs from the installation of build dependencies.
|
||||
# They will be shown only if an error occurs.
|
||||
capture_ctx: ContextManager[StringIO] = capture_logging()
|
||||
spinner: ContextManager[None] = open_rich_spinner(f"Installing {kind}")
|
||||
else:
|
||||
# Otherwise, pass-through all logs (with a header).
|
||||
capture_ctx, spinner = nullcontext(StringIO()), nullcontext()
|
||||
logger.info("Installing %s ...", kind)
|
||||
|
||||
try:
|
||||
self._level += 1
|
||||
with spinner, capture_ctx as stream:
|
||||
self._install_impl(requirements, prefix)
|
||||
|
||||
except DiagnosticPipError as exc:
|
||||
# Format similar to a nested subprocess error, where the
|
||||
# causing error is shown first, followed by the build error.
|
||||
logger.info(textwrap.dedent(stream.getvalue()))
|
||||
logger.error("%s", exc, extra={"rich": True})
|
||||
logger.info("")
|
||||
raise BuildDependencyInstallError(
|
||||
for_req, requirements, cause=exc, log_lines=None
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
logs: list[str] | None = textwrap.dedent(stream.getvalue()).splitlines()
|
||||
if not capture_logs:
|
||||
# If logs aren't being captured, then display the error inline
|
||||
# with the rest of the logs.
|
||||
logs = None
|
||||
if isinstance(exc, PipError):
|
||||
logger.error("%s", exc)
|
||||
else:
|
||||
logger.exception("pip crashed unexpectedly")
|
||||
raise BuildDependencyInstallError(
|
||||
for_req, requirements, cause=exc, log_lines=logs
|
||||
)
|
||||
|
||||
finally:
|
||||
self._level -= 1
|
||||
|
||||
def _install_impl(self, requirements: Iterable[str], prefix: Prefix) -> None:
|
||||
"""Core build dependency install logic."""
|
||||
from pip._internal.commands.install import installed_packages_summary
|
||||
from pip._internal.req import install_given_reqs
|
||||
from pip._internal.req.constructors import install_req_from_line
|
||||
from pip._internal.wheel_builder import build
|
||||
|
||||
ireqs = [install_req_from_line(req, user_supplied=True) for req in requirements]
|
||||
ireqs.extend(self._build_constraints)
|
||||
|
||||
resolver = self._make_resolver()
|
||||
resolved_set = resolver.resolve(ireqs, check_supported_wheels=True)
|
||||
self._preparer.prepare_linked_requirements_more(
|
||||
resolved_set.requirements.values()
|
||||
)
|
||||
|
||||
reqs_to_build = [
|
||||
r for r in resolved_set.requirements_to_install if not r.is_wheel
|
||||
]
|
||||
_, build_failures = build(
|
||||
reqs_to_build, self._wheel_cache, verify=True, allow_editables=True
|
||||
)
|
||||
if build_failures:
|
||||
raise InstallWheelBuildError(build_failures)
|
||||
|
||||
installed = install_given_reqs(
|
||||
resolver.get_installation_order(resolved_set),
|
||||
prefix=prefix.path,
|
||||
# Hard-coded options (that should NOT be inherited).
|
||||
root=None,
|
||||
home=None,
|
||||
warn_script_location=False,
|
||||
use_user_site=False,
|
||||
# As the build environment is ephemeral, it's wasteful to
|
||||
# pre-compile everything since not all modules will be used.
|
||||
pycompile=False,
|
||||
progress_bar="off",
|
||||
# Link console scripts to the build env's interpreter, not pip's.
|
||||
script_executable=prefix.venv_executable,
|
||||
)
|
||||
|
||||
env = get_environment(list(prefix.lib_dirs))
|
||||
if summary := installed_packages_summary(installed, env):
|
||||
logger.info(summary)
|
||||
|
||||
def _make_resolver(self) -> BaseResolver:
|
||||
"""Create a new resolver for one time use."""
|
||||
# Legacy installer never used the legacy resolver so create a
|
||||
# resolvelib resolver directly. Yuck.
|
||||
from pip._internal.req.constructors import install_req_from_req_string
|
||||
from pip._internal.resolution.resolvelib.resolver import Resolver
|
||||
|
||||
return Resolver(
|
||||
make_install_req=install_req_from_req_string,
|
||||
# Inherited state.
|
||||
preparer=self._preparer,
|
||||
finder=self._finder,
|
||||
wheel_cache=self._wheel_cache,
|
||||
# Hard-coded options (that should NOT be inherited).
|
||||
ignore_requires_python=False,
|
||||
use_user_site=False,
|
||||
ignore_dependencies=False,
|
||||
only_dependencies=False,
|
||||
ignore_installed=True,
|
||||
force_reinstall=False,
|
||||
upgrade_strategy="to-satisfy-only",
|
||||
py_version_info=None,
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections.abc import Iterable
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pip._internal.build_env.base import BuildEnvironment
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
|
||||
|
||||
class NoOpBuildEnvironment(BuildEnvironment):
|
||||
"""A no-op drop-in replacement for BuildEnvironment"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.python_executable = sys.executable
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,145 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import sysconfig
|
||||
from collections.abc import Iterable
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pip._internal.build_env.base import (
|
||||
BuildEnvironment,
|
||||
BuildEnvironmentInstaller,
|
||||
Prefix,
|
||||
)
|
||||
from pip._internal.exceptions import VenvCreationError, VenvImportError
|
||||
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
|
||||
|
||||
def _get_venv_path_from_sysconfig(name: str, env_dir: str) -> str:
|
||||
vars = {
|
||||
"base": env_dir,
|
||||
"platbase": env_dir,
|
||||
}
|
||||
return sysconfig.get_path(name, scheme="venv", vars=vars)
|
||||
|
||||
|
||||
class VenvBuildEnvironment(BuildEnvironment):
|
||||
"""A venv-based build environment."""
|
||||
|
||||
def __init__(self, installer: BuildEnvironmentInstaller) -> None:
|
||||
# We defer this import because certain distributions of Python do not include
|
||||
# a functional venv out of the box.
|
||||
try:
|
||||
import venv
|
||||
except ImportError:
|
||||
raise VenvImportError
|
||||
|
||||
self._env_path = TempDirectory(
|
||||
kind=tempdir_kinds.BUILD_ENV, globally_managed=True
|
||||
).path
|
||||
# Use symlinks to support relocatable Python installations on POSIX, including
|
||||
# python-build-standalone. This matches upstream venv CLI's behaviour.
|
||||
env = venv.EnvBuilder(symlinks=(os.name != "nt"))
|
||||
try:
|
||||
context = env.ensure_directories(self._env_path)
|
||||
env.create(self._env_path)
|
||||
except OSError as e:
|
||||
raise VenvCreationError(str(e))
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
# The context object was only documented in Python 3.12
|
||||
self.lib_dirs = [context.lib_path]
|
||||
self._bin_path = context.bin_path
|
||||
elif sys.version_info[:2] == (3, 11):
|
||||
# On Python 3.11, we can use sysconfig.
|
||||
self.lib_dirs = [_get_venv_path_from_sysconfig("purelib", self._env_path)]
|
||||
self._bin_path = _get_venv_path_from_sysconfig("scripts", self._env_path)
|
||||
else:
|
||||
# Otherwise, we need to manually construct all the paths... sigh.
|
||||
if sys.platform == "win32":
|
||||
libpath = os.path.join(self._env_path, "Lib", "site-packages")
|
||||
else:
|
||||
python = "pypy" if sys.implementation.name == "pypy" else "python"
|
||||
libpath = os.path.join(
|
||||
self._env_path,
|
||||
"lib",
|
||||
f"{python}{sys.version_info.major}.{sys.version_info.minor}",
|
||||
"site-packages",
|
||||
)
|
||||
self.lib_dirs = [libpath]
|
||||
# Same reasoning for try-except as for python_executable below.
|
||||
try:
|
||||
self._bin_path = context.bin_path
|
||||
except AttributeError:
|
||||
scripts_dir = "Scripts" if os.name == "nt" else "bin"
|
||||
self._bin_path = os.path.join(self._env_path, scripts_dir)
|
||||
|
||||
# There are enough ways trying to construct the Python executable path can go
|
||||
# wrong that we're better off assuming that the context object has the right
|
||||
# attributes, and only when they don't exist do we try to guess.
|
||||
#
|
||||
# These attributes seem to exist in every CPython version after 3.10.1 and
|
||||
# are documented to exist on 3.12 and higher.
|
||||
try:
|
||||
self.python_executable = context.env_exec_cmd
|
||||
except AttributeError:
|
||||
try:
|
||||
self.python_executable = context.env_exe
|
||||
except AttributeError:
|
||||
executable_name = "python.exe" if os.name == "nt" else "python"
|
||||
self.python_executable = os.path.join(self._bin_path, executable_name)
|
||||
|
||||
self._save_env: dict[str, str | None] = {}
|
||||
self._installer = installer
|
||||
|
||||
if not os.path.exists(self.python_executable):
|
||||
# This error is only likely on Windows due to interference from AV software.
|
||||
raise VenvCreationError(
|
||||
f"Python executable failed to copy to {self.python_executable}"
|
||||
)
|
||||
|
||||
def __enter__(self) -> None:
|
||||
# We want backend calls to be able to use binaries installed as if this
|
||||
# virtual environment was "activated".
|
||||
self._save_env = {
|
||||
name: os.environ.get(name, None) for name in ("PATH", "PYTHONPATH")
|
||||
}
|
||||
|
||||
new_path = [self._bin_path]
|
||||
if old_path := self._save_env["PATH"]:
|
||||
new_path.extend(old_path.split(os.pathsep))
|
||||
# However, we don't want a pre-existing PYTHONPATH to influence the
|
||||
# backend calls.
|
||||
os.environ.update({"PATH": os.pathsep.join(new_path), "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 install_requirements(
|
||||
self,
|
||||
requirements: Iterable[str],
|
||||
prefix_as_string: str,
|
||||
*,
|
||||
kind: str,
|
||||
for_req: InstallRequirement | None = None,
|
||||
) -> None:
|
||||
if not requirements:
|
||||
return
|
||||
|
||||
# TODO: when better support for installing to arbitrary Python environments
|
||||
# is added, replace this prefix hack with that.
|
||||
prefix = Prefix(self._env_path, venv_executable=self.python_executable)
|
||||
self._installer.install(requirements, prefix, kind=kind, for_req=for_req)
|
||||
@@ -0,0 +1,139 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import site
|
||||
import sys
|
||||
import textwrap
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Iterable
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pip._internal.build_env.base import (
|
||||
BuildEnvironment,
|
||||
BuildEnvironmentInstaller,
|
||||
Prefix,
|
||||
)
|
||||
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
|
||||
|
||||
def get_system_sitepackages() -> set[str]:
|
||||
"""Get system site packages, as a normalized set of strings."""
|
||||
system_sites = site.getsitepackages()
|
||||
return {os.path.normcase(path) for path in system_sites}
|
||||
|
||||
|
||||
class VirtualBuildEnvironment(BuildEnvironment):
|
||||
"""Legacy build environment implementation.
|
||||
|
||||
Patches sys.path and uses sitecustomize.py to isolate Python processes. It has
|
||||
known bugs and weird edge cases, but it exists as a fallback for platforms where
|
||||
the venv module is unavailable or dysfunctional.
|
||||
"""
|
||||
|
||||
def __init__(self, installer: BuildEnvironmentInstaller) -> None:
|
||||
self.python_executable = sys.executable
|
||||
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)
|
||||
|
||||
# Customize site to:
|
||||
# - ensure .pth files are honored
|
||||
# - prevent access to system site packages
|
||||
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, discover all system-sites related paths.
|
||||
original_sys_path = sys.path[:]
|
||||
# Clear sys.path so addsitedir() will add system site paths and paths
|
||||
# added by contained .pth files to sys.path reliably. This is necessary
|
||||
# since Python 3.15, which notably no longer re-executes .pth files for
|
||||
# known paths.
|
||||
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)
|
||||
|
||||
# Drop discovered system-sites related paths.
|
||||
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 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)
|
||||
Reference in New Issue
Block a user