actualizado 3-sept
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",
|
||||
]
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
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