1111from collections import OrderedDict
1212from collections .abc import Iterable
1313from types import TracebackType
14- from typing import TYPE_CHECKING
14+ from typing import TYPE_CHECKING , Protocol
1515
1616from pip ._vendor .packaging .version import Version
1717
2626
2727if TYPE_CHECKING :
2828 from pip ._internal .index .package_finder import PackageFinder
29+ from pip ._internal .req .req_install import InstallRequirement
2930
3031logger = 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+
82180class 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
300321class 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 ()
0 commit comments