1from __future__ import annotations
2
3import abc
4from collections.abc import Iterable
5from contextlib import AbstractContextManager as ContextManager
6from typing import TYPE_CHECKING, Literal, Protocol
7
8from pip._vendor.packaging.version import Version
9
10from pip._internal.locations import get_scheme
11from pip._internal.metadata import get_default_environment, get_environment
12from pip._internal.utils.packaging import get_requirement
13
14if TYPE_CHECKING:
15 from pip._internal.req.req_install import InstallRequirement
16
17
18BuildIsolationMode = Literal["off", "virtual", "venv"]
19
20
21def _dedup(a: str, b: str) -> tuple[str] | tuple[str, str]:
22 return (a, b) if a != b else (a,)
23
24
25class Prefix:
26 # TODO: simplify this data model when the legacy subprocess installer is removed
27 def __init__(self, path: str, *, venv_executable: str | None = None) -> None:
28 self.path = path
29 self.setup = False
30 scheme = get_scheme("", prefix=path)
31 self.bin_dir = scheme.scripts
32 self.lib_dirs = _dedup(scheme.purelib, scheme.platlib)
33 self.venv_executable = venv_executable
34
35
36class BuildEnvironmentInstaller(Protocol):
37 """
38 Interface for installing build dependencies into an isolated build
39 environment.
40 """
41
42 def install(
43 self,
44 requirements: Iterable[str],
45 prefix: Prefix,
46 *,
47 kind: str,
48 for_req: InstallRequirement | None,
49 ) -> None: ...
50
51
52class BuildEnvironment(ContextManager[None], metaclass=abc.ABCMeta):
53 """Creates and manages an isolated environment to install build deps"""
54
55 lib_dirs: list[str]
56 python_executable: str
57
58 @abc.abstractmethod
59 def __init__(self, installer: BuildEnvironmentInstaller): ...
60
61 def check_requirements(
62 self, reqs: Iterable[str]
63 ) -> tuple[set[tuple[str, str]], set[str]]:
64 """Return 2 sets:
65 - conflicting requirements: set of (installed, wanted) reqs tuples
66 - missing requirements: set of reqs
67 """
68 missing = set()
69 conflicting = set()
70 if reqs:
71 env = (
72 get_environment(self.lib_dirs)
73 if hasattr(self, "lib_dirs")
74 else get_default_environment()
75 )
76 for req_str in reqs:
77 req = get_requirement(req_str)
78 # We're explicitly evaluating with an empty extra value, since build
79 # environments are not provided any mechanism to select specific extras.
80 if req.marker is not None and not req.marker.evaluate({"extra": ""}):
81 continue
82 dist = env.get_distribution(req.name)
83 if not dist:
84 missing.add(req_str)
85 continue
86 if isinstance(dist.version, Version):
87 installed_req_str = f"{req.name}=={dist.version}"
88 else:
89 installed_req_str = f"{req.name}==={dist.version}"
90 if not req.specifier.contains(dist.version, prereleases=True):
91 conflicting.add((installed_req_str, req_str))
92 # FIXME: Consider direct URL?
93 return conflicting, missing
94
95 @abc.abstractmethod
96 def install_requirements(
97 self,
98 requirements: Iterable[str],
99 prefix_as_string: str,
100 *,
101 kind: str,
102 for_req: InstallRequirement | None = None,
103 ) -> None: ...