11from __future__ import annotations
22
3+ import dataclasses
34import json
45import logging
56import operator
4041SeenKey = tuple [NormalizedName , tuple [str , ...], str , typing .Literal ["sdist" , "wheel" ]]
4142
4243
44+ @dataclasses .dataclass
45+ class SourceBuildResult :
46+ """Result of building a package from source.
47+
48+ Used to return multiple values from _build_from_source().
49+ """
50+
51+ wheel_filename : pathlib .Path | None
52+ sdist_filename : pathlib .Path | None
53+ unpack_dir : pathlib .Path
54+ sdist_root_dir : pathlib .Path
55+ build_env : build_environment .BuildEnvironment
56+ source_url_type : str
57+
58+
4359class Bootstrapper :
4460 def __init__ (
4561 self ,
@@ -194,105 +210,33 @@ def bootstrap(self, req: Requirement, req_type: RequirementType) -> Version:
194210 )
195211 # Remember that this is a prebuilt wheel, and where we got it.
196212 source_url_type = str (SourceType .PREBUILT )
213+ sdist_filename = None
214+ sdist_root_dir = None
215+ build_env = None
197216 else :
198- # Look a few places for an existing wheel that matches what we need,
199- # using caches for locations where we might have built the wheel
200- # before.
201-
202- # Check if we have previously built a wheel and still have it on the
203- # local filesystem.
204- if not wheel_filename and not cached_wheel_filename :
205- cached_wheel_filename , unpacked_cached_wheel = (
206- self ._look_for_existing_wheel (
207- req ,
208- resolved_version ,
209- self .ctx .wheels_build ,
210- )
211- )
212-
213- # Check if we have previously downloaded a wheel and still have it
214- # on the local filesystem.
215- if not wheel_filename and not cached_wheel_filename :
216- cached_wheel_filename , unpacked_cached_wheel = (
217- self ._look_for_existing_wheel (
218- req ,
219- resolved_version ,
220- self .ctx .wheels_downloads ,
221- )
222- )
223-
224- # Look for a wheel on the cache server and download it if there is
225- # one.
226- if not wheel_filename and not cached_wheel_filename :
227- cached_wheel_filename , unpacked_cached_wheel = (
228- self ._download_wheel_from_cache (req , resolved_version )
229- )
230-
231- if not unpacked_cached_wheel :
232- # We didn't find anything so we are going to have to build the
233- # wheel in order to process its installation dependencies.
234- logger .debug ("no cached wheel, downloading sources" )
235- source_filename = sources .download_source (
236- ctx = self .ctx ,
237- req = req ,
238- version = resolved_version ,
239- download_url = source_url ,
240- )
241- sdist_root_dir = sources .prepare_source (
242- ctx = self .ctx ,
243- req = req ,
244- source_filename = source_filename ,
245- version = resolved_version ,
246- )
247- else :
248- logger .debug (f"have cached wheel in { unpacked_cached_wheel } " )
249- sdist_root_dir = unpacked_cached_wheel / unpacked_cached_wheel .stem
250-
251- assert sdist_root_dir is not None
252-
253- if sdist_root_dir .parent .parent != self .ctx .work_dir :
254- raise ValueError (
255- f"'{ sdist_root_dir } /../..' should be { self .ctx .work_dir } "
256- )
257- unpack_dir = sdist_root_dir .parent
258-
259- build_env = build_environment .BuildEnvironment (
260- ctx = self .ctx ,
261- parent_dir = sdist_root_dir .parent ,
217+ # Look for an existing wheel in caches (3 levels: build, downloads,
218+ # cache server) before building from source.
219+ cached_wheel_filename , unpacked_cached_wheel = self ._find_cached_wheel (
220+ req , resolved_version
262221 )
263222
264- # need to call this function irrespective of whether we had the wheel cached
265- # so that the build dependencies can be bootstrapped
266- self ._prepare_build_dependencies (req , sdist_root_dir , build_env )
223+ # Build from source (download, prepare, build wheel/sdist)
224+ build_result = self ._build_from_source (
225+ req = req ,
226+ resolved_version = resolved_version ,
227+ source_url = source_url ,
228+ build_sdist_only = build_sdist_only ,
229+ cached_wheel_filename = cached_wheel_filename ,
230+ unpacked_cached_wheel = unpacked_cached_wheel ,
231+ )
267232
268- if cached_wheel_filename :
269- logger .debug (
270- f"getting install requirements from cached "
271- f"wheel { cached_wheel_filename .name } "
272- )
273- # prefer existing wheel even in sdist_only mode
274- # skip building even if it is a non-fromager built wheel
275- wheel_filename = cached_wheel_filename
276- build_sdist_only = False
277- elif build_sdist_only :
278- # get install dependencies from sdist and pyproject_hooks (only top-level and install)
279- logger .debug (
280- f"getting install requirements from sdist "
281- f"{ req .name } =={ resolved_version } ({ req_type } )"
282- )
283- wheel_filename = None
284- sdist_filename = self ._build_sdist (
285- req , resolved_version , sdist_root_dir , build_env
286- )
287- else :
288- # build wheel (build requirements, full build mode)
289- logger .debug (
290- f"building wheel { req .name } =={ resolved_version } "
291- f"to get install requirements ({ req_type } )"
292- )
293- wheel_filename , sdist_filename = self ._build_wheel (
294- req , resolved_version , sdist_root_dir , build_env
295- )
233+ # Unpack result
234+ wheel_filename = build_result .wheel_filename
235+ sdist_filename = build_result .sdist_filename
236+ unpack_dir = build_result .unpack_dir
237+ sdist_root_dir = build_result .sdist_root_dir
238+ build_env = build_result .build_env
239+ source_url_type = build_result .source_url_type
296240
297241 hooks .run_post_bootstrap_hooks (
298242 ctx = self .ctx ,
@@ -303,34 +247,15 @@ def bootstrap(self, req: Requirement, req_type: RequirementType) -> Version:
303247 wheel_filename = wheel_filename ,
304248 )
305249
306- if wheel_filename is not None :
307- assert unpack_dir is not None
308- logger .debug (
309- "get install dependencies of wheel %s" ,
310- wheel_filename .name ,
311- )
312- install_dependencies = dependencies .get_install_dependencies_of_wheel (
313- req = req ,
314- wheel_filename = wheel_filename ,
315- requirements_file_dir = unpack_dir ,
316- )
317- elif sdist_filename is not None :
318- assert sdist_root_dir is not None
319- assert build_env is not None
320- logger .debug (
321- "get install dependencies of sdist from directory %s" ,
322- sdist_root_dir ,
323- )
324- install_dependencies = dependencies .get_install_dependencies_of_sdist (
325- ctx = self .ctx ,
326- req = req ,
327- version = resolved_version ,
328- sdist_root_dir = sdist_root_dir ,
329- build_env = build_env ,
330- )
331- else :
332- # unreachable
333- raise RuntimeError ("wheel_filename and sdist_filename are None" )
250+ install_dependencies = self ._get_install_dependencies (
251+ req = req ,
252+ resolved_version = resolved_version ,
253+ wheel_filename = wheel_filename ,
254+ sdist_filename = sdist_filename ,
255+ sdist_root_dir = sdist_root_dir ,
256+ build_env = build_env ,
257+ unpack_dir = unpack_dir ,
258+ )
334259
335260 logger .debug (
336261 "install dependencies: %s" ,
@@ -505,6 +430,193 @@ def _download_prebuilt(
505430 server .update_wheel_mirror (self .ctx )
506431 return (wheel_filename , unpack_dir )
507432
433+ def _find_cached_wheel (
434+ self ,
435+ req : Requirement ,
436+ resolved_version : Version ,
437+ ) -> tuple [pathlib .Path | None , pathlib .Path | None ]:
438+ """Look for cached wheel in 3 locations.
439+
440+ Checks for cached wheels in order:
441+ 1. wheels_build directory (previously built)
442+ 2. wheels_downloads directory (previously downloaded)
443+ 3. Cache server (remote cache)
444+
445+ Returns:
446+ Tuple of (cached_wheel_filename, unpacked_cached_wheel).
447+ Both None if no cache hit.
448+ """
449+ # Check if we have previously built a wheel and still have it on the
450+ # local filesystem.
451+ cached_wheel , unpacked = self ._look_for_existing_wheel (
452+ req , resolved_version , self .ctx .wheels_build
453+ )
454+ if cached_wheel :
455+ return cached_wheel , unpacked
456+
457+ # Check if we have previously downloaded a wheel and still have it
458+ # on the local filesystem.
459+ cached_wheel , unpacked = self ._look_for_existing_wheel (
460+ req , resolved_version , self .ctx .wheels_downloads
461+ )
462+ if cached_wheel :
463+ return cached_wheel , unpacked
464+
465+ # Look for a wheel on the cache server and download it if there is one.
466+ cached_wheel , unpacked = self ._download_wheel_from_cache (req , resolved_version )
467+ if cached_wheel :
468+ return cached_wheel , unpacked
469+
470+ return None , None
471+
472+ def _get_install_dependencies (
473+ self ,
474+ req : Requirement ,
475+ resolved_version : Version ,
476+ wheel_filename : pathlib .Path | None ,
477+ sdist_filename : pathlib .Path | None ,
478+ sdist_root_dir : pathlib .Path | None ,
479+ build_env : build_environment .BuildEnvironment | None ,
480+ unpack_dir : pathlib .Path | None ,
481+ ) -> list [Requirement ]:
482+ """Extract install dependencies from wheel or sdist.
483+
484+ Returns:
485+ List of install requirements.
486+
487+ Raises:
488+ RuntimeError: If both wheel_filename and sdist_filename are None.
489+ """
490+ if wheel_filename is not None :
491+ assert unpack_dir is not None
492+ logger .debug (
493+ "get install dependencies of wheel %s" ,
494+ wheel_filename .name ,
495+ )
496+ return list (
497+ dependencies .get_install_dependencies_of_wheel (
498+ req = req ,
499+ wheel_filename = wheel_filename ,
500+ requirements_file_dir = unpack_dir ,
501+ )
502+ )
503+ elif sdist_filename is not None :
504+ assert sdist_root_dir is not None
505+ assert build_env is not None
506+ logger .debug (
507+ "get install dependencies of sdist from directory %s" ,
508+ sdist_root_dir ,
509+ )
510+ return list (
511+ dependencies .get_install_dependencies_of_sdist (
512+ ctx = self .ctx ,
513+ req = req ,
514+ version = resolved_version ,
515+ sdist_root_dir = sdist_root_dir ,
516+ build_env = build_env ,
517+ )
518+ )
519+ else :
520+ raise RuntimeError ("wheel_filename and sdist_filename are None" )
521+
522+ def _build_from_source (
523+ self ,
524+ req : Requirement ,
525+ resolved_version : Version ,
526+ source_url : str ,
527+ build_sdist_only : bool ,
528+ cached_wheel_filename : pathlib .Path | None ,
529+ unpacked_cached_wheel : pathlib .Path | None ,
530+ ) -> SourceBuildResult :
531+ """Build package from source.
532+
533+ Handles:
534+ 1. Download and prepare source (if not cached)
535+ 2. Create build environment
536+ 3. Prepare build dependencies
537+ 4. Build wheel or sdist (based on flags and cache state)
538+
539+ Returns:
540+ SourceBuildResult with all build artifacts.
541+
542+ Raises:
543+ Various exceptions from download, prepare, or build steps.
544+ This is where test-mode will catch exceptions.
545+ """
546+ # Download and prepare source (if no cached wheel)
547+ if not unpacked_cached_wheel :
548+ logger .debug ("no cached wheel, downloading sources" )
549+ source_filename = sources .download_source (
550+ ctx = self .ctx ,
551+ req = req ,
552+ version = resolved_version ,
553+ download_url = source_url ,
554+ )
555+ sdist_root_dir = sources .prepare_source (
556+ ctx = self .ctx ,
557+ req = req ,
558+ source_filename = source_filename ,
559+ version = resolved_version ,
560+ )
561+ else :
562+ logger .debug (f"have cached wheel in { unpacked_cached_wheel } " )
563+ sdist_root_dir = unpacked_cached_wheel / unpacked_cached_wheel .stem
564+
565+ assert sdist_root_dir is not None
566+
567+ if sdist_root_dir .parent .parent != self .ctx .work_dir :
568+ raise ValueError (f"'{ sdist_root_dir } /../..' should be { self .ctx .work_dir } " )
569+ unpack_dir = sdist_root_dir .parent
570+
571+ build_env = build_environment .BuildEnvironment (
572+ ctx = self .ctx ,
573+ parent_dir = sdist_root_dir .parent ,
574+ )
575+
576+ # Prepare build dependencies (always needed)
577+ self ._prepare_build_dependencies (req , sdist_root_dir , build_env )
578+
579+ # Decide what to build based on cache state and build mode
580+ wheel_filename : pathlib .Path | None
581+ sdist_filename : pathlib .Path | None
582+
583+ if cached_wheel_filename :
584+ logger .debug (
585+ f"getting install requirements from cached "
586+ f"wheel { cached_wheel_filename .name } "
587+ )
588+ # prefer existing wheel even in sdist_only mode
589+ wheel_filename = cached_wheel_filename
590+ sdist_filename = None
591+ elif build_sdist_only :
592+ logger .debug (
593+ f"getting install requirements from sdist "
594+ f"{ req .name } =={ resolved_version } "
595+ )
596+ wheel_filename = None
597+ sdist_filename = self ._build_sdist (
598+ req , resolved_version , sdist_root_dir , build_env
599+ )
600+ else :
601+ logger .debug (
602+ f"building wheel { req .name } =={ resolved_version } "
603+ f"to get install requirements"
604+ )
605+ wheel_filename , sdist_filename = self ._build_wheel (
606+ req , resolved_version , sdist_root_dir , build_env
607+ )
608+
609+ source_url_type = sources .get_source_type (self .ctx , req )
610+
611+ return SourceBuildResult (
612+ wheel_filename = wheel_filename ,
613+ sdist_filename = sdist_filename ,
614+ unpack_dir = unpack_dir ,
615+ sdist_root_dir = sdist_root_dir ,
616+ build_env = build_env ,
617+ source_url_type = source_url_type ,
618+ )
619+
508620 def _look_for_existing_wheel (
509621 self ,
510622 req : Requirement ,
0 commit comments