99import sys
1010import textwrap
1111from collections import OrderedDict
12- from collections .abc import Iterable
12+ from collections .abc import Iterable , Sequence
13+ from contextlib import AbstractContextManager as ContextManager
14+ from contextlib import nullcontext
15+ from io import StringIO
1316from types import TracebackType
1417from typing import TYPE_CHECKING , Protocol , TypedDict
1518
1619from pip ._vendor .packaging .version import Version
1720
1821from pip import __file__ as pip_location
19- from pip ._internal .cli .spinners import open_spinner
22+ from pip ._internal .cli .spinners import open_rich_spinner , open_spinner
23+ from pip ._internal .exceptions import (
24+ BuildDependencyInstallError ,
25+ DiagnosticPipError ,
26+ InstallWheelBuildError ,
27+ PipError ,
28+ )
2029from pip ._internal .locations import get_platlib , get_purelib , get_scheme
2130from pip ._internal .metadata import get_default_environment , get_environment
2231from pip ._internal .utils .deprecation import deprecated
23- from pip ._internal .utils .logging import VERBOSE
32+ from pip ._internal .utils .logging import VERBOSE , capture_logging
2433from pip ._internal .utils .packaging import get_requirement
2534from pip ._internal .utils .subprocess import call_subprocess
2635from pip ._internal .utils .temp_dir import TempDirectory , tempdir_kinds
2736
2837if TYPE_CHECKING :
38+ from pip ._internal .cache import WheelCache
2939 from pip ._internal .index .package_finder import PackageFinder
40+ from pip ._internal .operations .build .build_tracker import BuildTracker
3041 from pip ._internal .req .req_install import InstallRequirement
42+ from pip ._internal .resolution .base import BaseResolver
3143
3244 class ExtraEnviron (TypedDict , total = False ):
3345 extra_environ : dict [str , str ]
@@ -188,6 +200,12 @@ def install(
188200 )
189201 )
190202
203+ if finder .release_control is not None :
204+ # Use ordered args to preserve the user's original command-line order
205+ # This is important because later flags can override earlier ones
206+ for attr_name , value in finder .release_control .get_ordered_args ():
207+ args .extend (("--" + attr_name .replace ("_" , "-" ), value ))
208+
191209 index_urls = finder .index_urls
192210 if index_urls :
193211 args .extend (["-i" , index_urls [0 ]])
@@ -206,8 +224,6 @@ def install(
206224 args .extend (["--cert" , finder .custom_cert ])
207225 if finder .client_cert :
208226 args .extend (["--client-cert" , finder .client_cert ])
209- if finder .allow_all_prereleases :
210- args .append ("--pre" )
211227 if finder .prefer_binary :
212228 args .append ("--prefer-binary" )
213229
@@ -230,6 +246,8 @@ def install(
230246 # in the isolated build environment
231247 extra_environ = {"extra_environ" : {"_PIP_IN_BUILD_IGNORE_CONSTRAINTS" : "1" }}
232248
249+ if finder .uploaded_prior_to :
250+ args .extend (["--uploaded-prior-to" , finder .uploaded_prior_to .isoformat ()])
233251 args .append ("--" )
234252 args .extend (requirements )
235253
@@ -245,6 +263,177 @@ def install(
245263 )
246264
247265
266+ class InprocessBuildEnvironmentInstaller :
267+ """
268+ Build dependency installer that runs in the same pip process.
269+
270+ This contains a stripped down version of the install command with
271+ only the logic necessary for installing build dependencies. The
272+ finder, session, build tracker, and wheel cache are reused, but new
273+ instances of everything else are created as needed.
274+
275+ Options are inherited from the parent install command unless
276+ they don't make sense for build dependencies (in which case, they
277+ are hard-coded, see comments below).
278+ """
279+
280+ def __init__ (
281+ self ,
282+ * ,
283+ finder : PackageFinder ,
284+ build_tracker : BuildTracker ,
285+ wheel_cache : WheelCache ,
286+ build_constraints : Sequence [InstallRequirement ] = (),
287+ verbosity : int = 0 ,
288+ ) -> None :
289+ from pip ._internal .operations .prepare import RequirementPreparer
290+
291+ self ._finder = finder
292+ self ._build_constraints = build_constraints
293+ self ._wheel_cache = wheel_cache
294+ self ._level = 0
295+
296+ build_dir = TempDirectory (kind = "build-env-install" , globally_managed = True )
297+ self ._preparer = RequirementPreparer (
298+ build_isolation_installer = self ,
299+ # Inherited options or state.
300+ finder = finder ,
301+ session = finder ._link_collector .session ,
302+ build_dir = build_dir .path ,
303+ build_tracker = build_tracker ,
304+ verbosity = verbosity ,
305+ # This is irrelevant as it only applies to editable requirements.
306+ src_dir = "" ,
307+ # Hard-coded options (that should NOT be inherited).
308+ download_dir = None ,
309+ build_isolation = True ,
310+ check_build_deps = False ,
311+ progress_bar = "off" ,
312+ # TODO: hash-checking should be extended to build deps, but that is
313+ # deferred for later as it'd be a breaking change.
314+ require_hashes = False ,
315+ use_user_site = False ,
316+ lazy_wheel = False ,
317+ legacy_resolver = False ,
318+ )
319+
320+ def install (
321+ self ,
322+ requirements : Iterable [str ],
323+ prefix : _Prefix ,
324+ * ,
325+ kind : str ,
326+ for_req : InstallRequirement | None ,
327+ ) -> None :
328+ """Install entrypoint. Manages output capturing and error handling."""
329+ capture_logs = not logger .isEnabledFor (VERBOSE ) and self ._level == 0
330+ if capture_logs :
331+ # Hide the logs from the installation of build dependencies.
332+ # They will be shown only if an error occurs.
333+ capture_ctx : ContextManager [StringIO ] = capture_logging ()
334+ spinner : ContextManager [None ] = open_rich_spinner (f"Installing { kind } " )
335+ else :
336+ # Otherwise, pass-through all logs (with a header).
337+ capture_ctx , spinner = nullcontext (StringIO ()), nullcontext ()
338+ logger .info ("Installing %s ..." , kind )
339+
340+ try :
341+ self ._level += 1
342+ with spinner , capture_ctx as stream :
343+ self ._install_impl (requirements , prefix )
344+
345+ except DiagnosticPipError as exc :
346+ # Format similar to a nested subprocess error, where the
347+ # causing error is shown first, followed by the build error.
348+ logger .info (textwrap .dedent (stream .getvalue ()))
349+ logger .error ("%s" , exc , extra = {"rich" : True })
350+ logger .info ("" )
351+ raise BuildDependencyInstallError (
352+ for_req , requirements , cause = exc , log_lines = None
353+ )
354+
355+ except Exception as exc :
356+ logs : list [str ] | None = textwrap .dedent (stream .getvalue ()).splitlines ()
357+ if not capture_logs :
358+ # If logs aren't being captured, then display the error inline
359+ # with the rest of the logs.
360+ logs = None
361+ if isinstance (exc , PipError ):
362+ logger .error ("%s" , exc )
363+ else :
364+ logger .exception ("pip crashed unexpectedly" )
365+ raise BuildDependencyInstallError (
366+ for_req , requirements , cause = exc , log_lines = logs
367+ )
368+
369+ finally :
370+ self ._level -= 1
371+
372+ def _install_impl (self , requirements : Iterable [str ], prefix : _Prefix ) -> None :
373+ """Core build dependency install logic."""
374+ from pip ._internal .commands .install import installed_packages_summary
375+ from pip ._internal .req import install_given_reqs
376+ from pip ._internal .req .constructors import install_req_from_line
377+ from pip ._internal .wheel_builder import build
378+
379+ ireqs = [install_req_from_line (req , user_supplied = True ) for req in requirements ]
380+ ireqs .extend (self ._build_constraints )
381+
382+ resolver = self ._make_resolver ()
383+ resolved_set = resolver .resolve (ireqs , check_supported_wheels = True )
384+ self ._preparer .prepare_linked_requirements_more (
385+ resolved_set .requirements .values ()
386+ )
387+
388+ reqs_to_build = [
389+ r for r in resolved_set .requirements_to_install if not r .is_wheel
390+ ]
391+ _ , build_failures = build (reqs_to_build , self ._wheel_cache , verify = True )
392+ if build_failures :
393+ raise InstallWheelBuildError (build_failures )
394+
395+ installed = install_given_reqs (
396+ resolver .get_installation_order (resolved_set ),
397+ prefix = prefix .path ,
398+ # Hard-coded options (that should NOT be inherited).
399+ root = None ,
400+ home = None ,
401+ warn_script_location = False ,
402+ use_user_site = False ,
403+ # As the build environment is ephemeral, it's wasteful to
404+ # pre-compile everything since not all modules will be used.
405+ pycompile = False ,
406+ progress_bar = "off" ,
407+ )
408+
409+ env = get_environment (list (prefix .lib_dirs ))
410+ if summary := installed_packages_summary (installed , env ):
411+ logger .info (summary )
412+
413+ def _make_resolver (self ) -> BaseResolver :
414+ """Create a new resolver for one time use."""
415+ # Legacy installer never used the legacy resolver so create a
416+ # resolvelib resolver directly. Yuck.
417+ from pip ._internal .req .constructors import install_req_from_req_string
418+ from pip ._internal .resolution .resolvelib .resolver import Resolver
419+
420+ return Resolver (
421+ make_install_req = install_req_from_req_string ,
422+ # Inherited state.
423+ preparer = self ._preparer ,
424+ finder = self ._finder ,
425+ wheel_cache = self ._wheel_cache ,
426+ # Hard-coded options (that should NOT be inherited).
427+ ignore_requires_python = False ,
428+ use_user_site = False ,
429+ ignore_dependencies = False ,
430+ ignore_installed = True ,
431+ force_reinstall = False ,
432+ upgrade_strategy = "to-satisfy-only" ,
433+ py_version_info = None ,
434+ )
435+
436+
248437class BuildEnvironment :
249438 """Creates and manages an isolated environment to install build deps"""
250439
0 commit comments