actualizado 3-sept

This commit is contained in:
2026-09-03 19:40:44 +02:00
parent 89992823b4
commit 5d2912f789
4338 changed files with 350157 additions and 386 deletions
@@ -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",
]
@@ -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)