Skip to content

Commit b817b2b

Browse files
authored
refactor: Add BuildEnvironmentInstaller protocol (#13452)
This enables alternative build dependency installers to be added. This is a step towards installing build dependencies in-process.
1 parent 18694f6 commit b817b2b

File tree

10 files changed

+173
-128
lines changed

10 files changed

+173
-128
lines changed

src/pip/_internal/build_env.py

Lines changed: 104 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from collections import OrderedDict
1212
from collections.abc import Iterable
1313
from types import TracebackType
14-
from typing import TYPE_CHECKING
14+
from typing import TYPE_CHECKING, Protocol
1515

1616
from pip._vendor.packaging.version import Version
1717

@@ -26,6 +26,7 @@
2626

2727
if TYPE_CHECKING:
2828
from pip._internal.index.package_finder import PackageFinder
29+
from pip._internal.req.req_install import InstallRequirement
2930

3031
logger = logging.getLogger(__name__)
3132

@@ -79,10 +80,108 @@ def _get_system_sitepackages() -> set[str]:
7980
return {os.path.normcase(path) for path in system_sites}
8081

8182

83+
class BuildEnvironmentInstaller(Protocol):
84+
"""
85+
Interface for installing build dependencies into an isolated build
86+
environment.
87+
"""
88+
89+
def install(
90+
self,
91+
requirements: Iterable[str],
92+
prefix: _Prefix,
93+
*,
94+
kind: str,
95+
for_req: InstallRequirement | None,
96+
) -> None: ...
97+
98+
99+
class SubprocessBuildEnvironmentInstaller:
100+
"""
101+
Install build dependencies by calling pip in a subprocess.
102+
"""
103+
104+
def __init__(self, finder: PackageFinder) -> None:
105+
self.finder = finder
106+
107+
def install(
108+
self,
109+
requirements: Iterable[str],
110+
prefix: _Prefix,
111+
*,
112+
kind: str,
113+
for_req: InstallRequirement | None,
114+
) -> None:
115+
finder = self.finder
116+
args: list[str] = [
117+
sys.executable,
118+
get_runnable_pip(),
119+
"install",
120+
"--ignore-installed",
121+
"--no-user",
122+
"--prefix",
123+
prefix.path,
124+
"--no-warn-script-location",
125+
"--disable-pip-version-check",
126+
# As the build environment is ephemeral, it's wasteful to
127+
# pre-compile everything, especially as not every Python
128+
# module will be used/compiled in most cases.
129+
"--no-compile",
130+
# The prefix specified two lines above, thus
131+
# target from config file or env var should be ignored
132+
"--target",
133+
"",
134+
]
135+
if logger.getEffectiveLevel() <= logging.DEBUG:
136+
args.append("-vv")
137+
elif logger.getEffectiveLevel() <= VERBOSE:
138+
args.append("-v")
139+
for format_control in ("no_binary", "only_binary"):
140+
formats = getattr(finder.format_control, format_control)
141+
args.extend(
142+
(
143+
"--" + format_control.replace("_", "-"),
144+
",".join(sorted(formats or {":none:"})),
145+
)
146+
)
147+
148+
index_urls = finder.index_urls
149+
if index_urls:
150+
args.extend(["-i", index_urls[0]])
151+
for extra_index in index_urls[1:]:
152+
args.extend(["--extra-index-url", extra_index])
153+
else:
154+
args.append("--no-index")
155+
for link in finder.find_links:
156+
args.extend(["--find-links", link])
157+
158+
if finder.proxy:
159+
args.extend(["--proxy", finder.proxy])
160+
for host in finder.trusted_hosts:
161+
args.extend(["--trusted-host", host])
162+
if finder.custom_cert:
163+
args.extend(["--cert", finder.custom_cert])
164+
if finder.client_cert:
165+
args.extend(["--client-cert", finder.client_cert])
166+
if finder.allow_all_prereleases:
167+
args.append("--pre")
168+
if finder.prefer_binary:
169+
args.append("--prefer-binary")
170+
args.append("--")
171+
args.extend(requirements)
172+
with open_spinner(f"Installing {kind}") as spinner:
173+
call_subprocess(
174+
args,
175+
command_desc=f"pip subprocess to install {kind}",
176+
spinner=spinner,
177+
)
178+
179+
82180
class BuildEnvironment:
83181
"""Creates and manages an isolated environment to install build deps"""
84182

85-
def __init__(self) -> None:
183+
def __init__(self, installer: BuildEnvironmentInstaller) -> None:
184+
self.installer = installer
86185
temp_dir = TempDirectory(kind=tempdir_kinds.BUILD_ENV, globally_managed=True)
87186

88187
self._prefixes = OrderedDict(
@@ -205,96 +304,18 @@ def check_requirements(
205304

206305
def install_requirements(
207306
self,
208-
finder: PackageFinder,
209307
requirements: Iterable[str],
210308
prefix_as_string: str,
211309
*,
212310
kind: str,
311+
for_req: InstallRequirement | None = None,
213312
) -> None:
214313
prefix = self._prefixes[prefix_as_string]
215314
assert not prefix.setup
216315
prefix.setup = True
217316
if not requirements:
218317
return
219-
self._install_requirements(
220-
get_runnable_pip(),
221-
finder,
222-
requirements,
223-
prefix,
224-
kind=kind,
225-
)
226-
227-
@staticmethod
228-
def _install_requirements(
229-
pip_runnable: str,
230-
finder: PackageFinder,
231-
requirements: Iterable[str],
232-
prefix: _Prefix,
233-
*,
234-
kind: str,
235-
) -> None:
236-
args: list[str] = [
237-
sys.executable,
238-
pip_runnable,
239-
"install",
240-
"--ignore-installed",
241-
"--no-user",
242-
"--prefix",
243-
prefix.path,
244-
"--no-warn-script-location",
245-
"--disable-pip-version-check",
246-
# As the build environment is ephemeral, it's wasteful to
247-
# pre-compile everything, especially as not every Python
248-
# module will be used/compiled in most cases.
249-
"--no-compile",
250-
# The prefix specified two lines above, thus
251-
# target from config file or env var should be ignored
252-
"--target",
253-
"",
254-
]
255-
if logger.getEffectiveLevel() <= logging.DEBUG:
256-
args.append("-vv")
257-
elif logger.getEffectiveLevel() <= VERBOSE:
258-
args.append("-v")
259-
for format_control in ("no_binary", "only_binary"):
260-
formats = getattr(finder.format_control, format_control)
261-
args.extend(
262-
(
263-
"--" + format_control.replace("_", "-"),
264-
",".join(sorted(formats or {":none:"})),
265-
)
266-
)
267-
268-
index_urls = finder.index_urls
269-
if index_urls:
270-
args.extend(["-i", index_urls[0]])
271-
for extra_index in index_urls[1:]:
272-
args.extend(["--extra-index-url", extra_index])
273-
else:
274-
args.append("--no-index")
275-
for link in finder.find_links:
276-
args.extend(["--find-links", link])
277-
278-
if finder.proxy:
279-
args.extend(["--proxy", finder.proxy])
280-
for host in finder.trusted_hosts:
281-
args.extend(["--trusted-host", host])
282-
if finder.custom_cert:
283-
args.extend(["--cert", finder.custom_cert])
284-
if finder.client_cert:
285-
args.extend(["--client-cert", finder.client_cert])
286-
if finder.allow_all_prereleases:
287-
args.append("--pre")
288-
if finder.prefer_binary:
289-
args.append("--prefer-binary")
290-
args.append("--")
291-
args.extend(requirements)
292-
with open_spinner(f"Installing {kind}") as spinner:
293-
call_subprocess(
294-
args,
295-
command_desc=f"pip subprocess to install {kind}",
296-
spinner=spinner,
297-
)
318+
self.installer.install(requirements, prefix, kind=kind, for_req=for_req)
298319

299320

300321
class NoOpBuildEnvironment(BuildEnvironment):
@@ -319,10 +340,10 @@ def cleanup(self) -> None:
319340

320341
def install_requirements(
321342
self,
322-
finder: PackageFinder,
323343
requirements: Iterable[str],
324344
prefix_as_string: str,
325345
*,
326346
kind: str,
347+
for_req: InstallRequirement | None = None,
327348
) -> None:
328349
raise NotImplementedError()

src/pip/_internal/cli/req_command.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from optparse import Values
1313
from typing import Any
1414

15+
from pip._internal.build_env import SubprocessBuildEnvironmentInstaller
1516
from pip._internal.cache import WheelCache
1617
from pip._internal.cli import cmdoptions
1718
from pip._internal.cli.index_command import IndexGroupCommand
@@ -136,6 +137,7 @@ def make_requirement_preparer(
136137
src_dir=options.src_dir,
137138
download_dir=download_dir,
138139
build_isolation=options.build_isolation,
140+
build_isolation_installer=SubprocessBuildEnvironmentInstaller(finder),
139141
check_build_deps=options.check_build_deps,
140142
build_tracker=build_tracker,
141143
session=session,

src/pip/_internal/distributions/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from pip._internal.req import InstallRequirement
88

99
if TYPE_CHECKING:
10-
from pip._internal.index.package_finder import PackageFinder
10+
from pip._internal.build_env import BuildEnvironmentInstaller
1111

1212

1313
class AbstractDistribution(metaclass=abc.ABCMeta):
@@ -48,7 +48,7 @@ def get_metadata_distribution(self) -> BaseDistribution:
4848
@abc.abstractmethod
4949
def prepare_distribution_metadata(
5050
self,
51-
finder: PackageFinder,
51+
build_env_installer: BuildEnvironmentInstaller,
5252
build_isolation: bool,
5353
check_build_deps: bool,
5454
) -> None:

src/pip/_internal/distributions/installed.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
from __future__ import annotations
22

3+
from typing import TYPE_CHECKING
4+
35
from pip._internal.distributions.base import AbstractDistribution
4-
from pip._internal.index.package_finder import PackageFinder
56
from pip._internal.metadata import BaseDistribution
67

8+
if TYPE_CHECKING:
9+
from pip._internal.build_env import BuildEnvironmentInstaller
10+
711

812
class InstalledDistribution(AbstractDistribution):
913
"""Represents an installed package.
@@ -22,7 +26,7 @@ def get_metadata_distribution(self) -> BaseDistribution:
2226

2327
def prepare_distribution_metadata(
2428
self,
25-
finder: PackageFinder,
29+
build_env_installer: BuildEnvironmentInstaller,
2630
build_isolation: bool,
2731
check_build_deps: bool,
2832
) -> None:

src/pip/_internal/distributions/sdist.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from pip._internal.utils.subprocess import runner_with_spinner_message
1212

1313
if TYPE_CHECKING:
14-
from pip._internal.index.package_finder import PackageFinder
14+
from pip._internal.build_env import BuildEnvironmentInstaller
1515

1616
logger = logging.getLogger(__name__)
1717

@@ -34,7 +34,7 @@ def get_metadata_distribution(self) -> BaseDistribution:
3434

3535
def prepare_distribution_metadata(
3636
self,
37-
finder: PackageFinder,
37+
build_env_installer: BuildEnvironmentInstaller,
3838
build_isolation: bool,
3939
check_build_deps: bool,
4040
) -> None:
@@ -46,7 +46,7 @@ def prepare_distribution_metadata(
4646
if should_isolate:
4747
# Setup an isolated environment and install the build backend static
4848
# requirements in it.
49-
self._prepare_build_backend(finder)
49+
self._prepare_build_backend(build_env_installer)
5050
# Check that if the requirement is editable, it either supports PEP 660 or
5151
# has a setup.py or a setup.cfg. This cannot be done earlier because we need
5252
# to setup the build backend to verify it supports build_editable, nor can
@@ -56,7 +56,7 @@ def prepare_distribution_metadata(
5656
# without setup.py nor setup.cfg.
5757
self.req.isolated_editable_sanity_check()
5858
# Install the dynamic build requirements.
59-
self._install_build_reqs(finder)
59+
self._install_build_reqs(build_env_installer)
6060
# Check if the current environment provides build dependencies
6161
should_check_deps = self.req.use_pep517 and check_build_deps
6262
if should_check_deps:
@@ -71,15 +71,17 @@ def prepare_distribution_metadata(
7171
self._raise_missing_reqs(missing)
7272
self.req.prepare_metadata()
7373

74-
def _prepare_build_backend(self, finder: PackageFinder) -> None:
74+
def _prepare_build_backend(
75+
self, build_env_installer: BuildEnvironmentInstaller
76+
) -> None:
7577
# Isolate in a BuildEnvironment and install the build-time
7678
# requirements.
7779
pyproject_requires = self.req.pyproject_requires
7880
assert pyproject_requires is not None
7981

80-
self.req.build_env = BuildEnvironment()
82+
self.req.build_env = BuildEnvironment(build_env_installer)
8183
self.req.build_env.install_requirements(
82-
finder, pyproject_requires, "overlay", kind="build dependencies"
84+
pyproject_requires, "overlay", kind="build dependencies", for_req=self.req
8385
)
8486
conflicting, missing = self.req.build_env.check_requirements(
8587
self.req.requirements_to_check
@@ -115,7 +117,9 @@ def _get_build_requires_editable(self) -> Iterable[str]:
115117
with backend.subprocess_runner(runner):
116118
return backend.get_requires_for_build_editable()
117119

118-
def _install_build_reqs(self, finder: PackageFinder) -> None:
120+
def _install_build_reqs(
121+
self, build_env_installer: BuildEnvironmentInstaller
122+
) -> None:
119123
# Install any extra build dependencies that the backend requests.
120124
# This must be done in a second pass, as the pyproject.toml
121125
# dependencies must be installed before we can call the backend.
@@ -131,7 +135,7 @@ def _install_build_reqs(self, finder: PackageFinder) -> None:
131135
if conflicting:
132136
self._raise_conflicts("the backend dependencies", conflicting)
133137
self.req.build_env.install_requirements(
134-
finder, missing, "normal", kind="backend dependencies"
138+
missing, "normal", kind="backend dependencies", for_req=self.req
135139
)
136140

137141
def _raise_conflicts(

src/pip/_internal/distributions/wheel.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
)
1313

1414
if TYPE_CHECKING:
15-
from pip._internal.index.package_finder import PackageFinder
15+
from pip._internal.build_env import BuildEnvironmentInstaller
1616

1717

1818
class WheelDistribution(AbstractDistribution):
@@ -37,7 +37,7 @@ def get_metadata_distribution(self) -> BaseDistribution:
3737

3838
def prepare_distribution_metadata(
3939
self,
40-
finder: PackageFinder,
40+
build_env_installer: BuildEnvironmentInstaller,
4141
build_isolation: bool,
4242
check_build_deps: bool,
4343
) -> None:

0 commit comments

Comments
 (0)