Tipragot
628be439b8
Cela permet de ne pas avoir de problèmes de compatibilité car python est dans le git.
312 lines
10 KiB
Python
312 lines
10 KiB
Python
"""Build Environment used for isolation during sdist building
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import pathlib
|
|
import site
|
|
import sys
|
|
import textwrap
|
|
from collections import OrderedDict
|
|
from types import TracebackType
|
|
from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type, Union
|
|
|
|
from pip._vendor.certifi import where
|
|
from pip._vendor.packaging.requirements import Requirement
|
|
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.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
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _dedup(a: str, b: str) -> Union[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():
|
|
# This would happen if someone is using pip from inside a zip file. In that
|
|
# case, we can use that directly.
|
|
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:
|
|
# virtualenv < 20 overwrites site.py without getsitepackages
|
|
# fallback on get_purelib/get_platlib.
|
|
# this is known to miss things, but shouldn't in the cases
|
|
# where getsitepackages() has been removed (inside a virtualenv)
|
|
system_sites = [get_purelib(), get_platlib()]
|
|
return {os.path.normcase(path) for path in system_sites}
|
|
|
|
|
|
class BuildEnvironment:
|
|
"""Creates and manages an isolated environment to install build deps"""
|
|
|
|
def __init__(self) -> None:
|
|
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, 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: Optional[Type[BaseException]],
|
|
exc_val: Optional[BaseException],
|
|
exc_tb: Optional[TracebackType],
|
|
) -> 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 = 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
|
|
|
|
def install_requirements(
|
|
self,
|
|
finder: "PackageFinder",
|
|
requirements: Iterable[str],
|
|
prefix_as_string: str,
|
|
*,
|
|
kind: str,
|
|
) -> None:
|
|
prefix = self._prefixes[prefix_as_string]
|
|
assert not prefix.setup
|
|
prefix.setup = True
|
|
if not requirements:
|
|
return
|
|
self._install_requirements(
|
|
get_runnable_pip(),
|
|
finder,
|
|
requirements,
|
|
prefix,
|
|
kind=kind,
|
|
)
|
|
|
|
@staticmethod
|
|
def _install_requirements(
|
|
pip_runnable: str,
|
|
finder: "PackageFinder",
|
|
requirements: Iterable[str],
|
|
prefix: _Prefix,
|
|
*,
|
|
kind: str,
|
|
) -> None:
|
|
args: List[str] = [
|
|
sys.executable,
|
|
pip_runnable,
|
|
"install",
|
|
"--ignore-installed",
|
|
"--no-user",
|
|
"--prefix",
|
|
prefix.path,
|
|
"--no-warn-script-location",
|
|
]
|
|
if logger.getEffectiveLevel() <= logging.DEBUG:
|
|
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])
|
|
|
|
for host in finder.trusted_hosts:
|
|
args.extend(["--trusted-host", host])
|
|
if finder.allow_all_prereleases:
|
|
args.append("--pre")
|
|
if finder.prefer_binary:
|
|
args.append("--prefer-binary")
|
|
args.append("--")
|
|
args.extend(requirements)
|
|
extra_environ = {"_PIP_STANDALONE_CERT": where()}
|
|
with open_spinner(f"Installing {kind}") as spinner:
|
|
call_subprocess(
|
|
args,
|
|
command_desc=f"pip subprocess to install {kind}",
|
|
spinner=spinner,
|
|
extra_environ=extra_environ,
|
|
)
|
|
|
|
|
|
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: Optional[Type[BaseException]],
|
|
exc_val: Optional[BaseException],
|
|
exc_tb: Optional[TracebackType],
|
|
) -> None:
|
|
pass
|
|
|
|
def cleanup(self) -> None:
|
|
pass
|
|
|
|
def install_requirements(
|
|
self,
|
|
finder: "PackageFinder",
|
|
requirements: Iterable[str],
|
|
prefix_as_string: str,
|
|
*,
|
|
kind: str,
|
|
) -> None:
|
|
raise NotImplementedError()
|