@@ -277,12 +277,55 @@ def bootstrap(self, req: Requirement, req_type: RequirementType) -> None:
277277 self ._bootstrap_impl (
278278 req , req_type , source_url , resolved_version , build_sdist_only
279279 )
280- except Exception as err :
280+ except Exception as build_error :
281281 if not self .test_mode :
282282 raise
283- self ._record_test_mode_failure (
284- req , str (resolved_version ), err , "bootstrap"
283+
284+ # Test mode: attempt pre-built fallback (may resolve different version)
285+ logger .warning (
286+ "test mode: build failed for %s==%s, attempting pre-built fallback: %s" ,
287+ req .name ,
288+ resolved_version ,
289+ build_error ,
285290 )
291+ try :
292+ wheel_url , fallback_version = self ._resolve_prebuilt_with_history (
293+ req = req ,
294+ req_type = req_type ,
295+ )
296+ if fallback_version != resolved_version :
297+ logger .warning (
298+ "test mode: version mismatch for %s - requested %s, fallback %s" ,
299+ req .name ,
300+ resolved_version ,
301+ fallback_version ,
302+ )
303+ # wheel_url passed as source_url; force_prebuilt ensures it's
304+ # treated as a wheel URL, not an sdist URL
305+ self ._bootstrap_impl (
306+ req ,
307+ req_type ,
308+ wheel_url ,
309+ fallback_version ,
310+ build_sdist_only ,
311+ force_prebuilt = True ,
312+ )
313+ logger .info (
314+ "test mode: successfully used pre-built wheel for %s==%s" ,
315+ req .name ,
316+ fallback_version ,
317+ )
318+ except Exception as fallback_error :
319+ logger .error (
320+ "test mode: pre-built fallback also failed for %s: %s" ,
321+ req .name ,
322+ fallback_error ,
323+ exc_info = True ,
324+ )
325+ # Record the original build error, not the fallback error
326+ self ._record_test_mode_failure (
327+ req , str (resolved_version ), build_error , "bootstrap"
328+ )
286329
287330 def _bootstrap_impl (
288331 self ,
@@ -291,6 +334,7 @@ def _bootstrap_impl(
291334 source_url : str ,
292335 resolved_version : Version ,
293336 build_sdist_only : bool ,
337+ force_prebuilt : bool = False ,
294338 ) -> None :
295339 """Internal implementation - performs the actual bootstrap work.
296340
@@ -299,9 +343,11 @@ def _bootstrap_impl(
299343 Args:
300344 req: The requirement to bootstrap.
301345 req_type: The type of requirement.
302- source_url: The resolved source URL.
346+ source_url: The resolved source URL (sdist or wheel URL) .
303347 resolved_version: The resolved version.
304348 build_sdist_only: Whether to build only sdist (no wheel).
349+ force_prebuilt: If True, treat source_url as a wheel URL and skip
350+ source build. Used for test-mode fallback after build failure.
305351
306352 Error Handling:
307353 Fatal errors (source build, prebuilt download) raise exceptions
@@ -322,7 +368,7 @@ def _bootstrap_impl(
322368 cached_wheel_filename : pathlib .Path | None = None
323369 unpacked_cached_wheel : pathlib .Path | None = None
324370
325- if pbi .pre_built :
371+ if pbi .pre_built or force_prebuilt :
326372 wheel_filename , unpack_dir = self ._download_prebuilt (
327373 req = req ,
328374 req_type = req_type ,
@@ -343,12 +389,11 @@ def _bootstrap_impl(
343389 req , resolved_version
344390 )
345391
346- # Build from source (handles test-mode fallback internally)
392+ # Build from source
347393 build_result = self ._build_from_source (
348394 req = req ,
349395 resolved_version = resolved_version ,
350396 source_url = source_url ,
351- req_type = req_type ,
352397 build_sdist_only = build_sdist_only ,
353398 cached_wheel_filename = cached_wheel_filename ,
354399 unpacked_cached_wheel = unpacked_cached_wheel ,
@@ -779,166 +824,72 @@ def _build_from_source(
779824 req : Requirement ,
780825 resolved_version : Version ,
781826 source_url : str ,
782- req_type : RequirementType ,
783827 build_sdist_only : bool ,
784828 cached_wheel_filename : pathlib .Path | None ,
785829 unpacked_cached_wheel : pathlib .Path | None ,
786830 ) -> SourceBuildResult :
787831 """Build package from source.
788832
789833 Orchestrates download, preparation, build environment setup, and build.
790- In test mode, attempts pre-built fallback on failure.
791834
792835 Raises:
793- Exception: In normal mode, if build fails.
794- In test mode, only if build fails AND fallback also fails .
836+ Exception: If any step fails. In test mode, bootstrap() handles
837+ fallback to pre-built wheels .
795838 """
796- try :
797- # Download and prepare source (if no cached wheel)
798- if not unpacked_cached_wheel :
799- logger .debug ("no cached wheel, downloading sources" )
800- source_filename = self ._download_source (
801- req = req ,
802- resolved_version = resolved_version ,
803- source_url = source_url ,
804- )
805- sdist_root_dir = self ._prepare_source (
806- req = req ,
807- resolved_version = resolved_version ,
808- source_filename = source_filename ,
809- )
810- else :
811- logger .debug (f"have cached wheel in { unpacked_cached_wheel } " )
812- sdist_root_dir = unpacked_cached_wheel / unpacked_cached_wheel .stem
813-
814- assert sdist_root_dir is not None
815-
816- if sdist_root_dir .parent .parent != self .ctx .work_dir :
817- raise ValueError (
818- f"'{ sdist_root_dir } /../..' should be { self .ctx .work_dir } "
819- )
820- unpack_dir = sdist_root_dir .parent
821-
822- build_env = self ._create_build_env (
839+ # Download and prepare source (if no cached wheel)
840+ if not unpacked_cached_wheel :
841+ logger .debug ("no cached wheel, downloading sources" )
842+ source_filename = self ._download_source (
823843 req = req ,
824844 resolved_version = resolved_version ,
825- parent_dir = sdist_root_dir .parent ,
826- )
827-
828- # Prepare build dependencies (always needed)
829- # Note: This may recursively call bootstrap() for build deps,
830- # which has its own error handling.
831- self ._prepare_build_dependencies (req , sdist_root_dir , build_env )
832-
833- # Build wheel or sdist
834- wheel_filename , sdist_filename = self ._do_build (
835- req = req ,
836- resolved_version = resolved_version ,
837- sdist_root_dir = sdist_root_dir ,
838- build_env = build_env ,
839- build_sdist_only = build_sdist_only ,
840- cached_wheel_filename = cached_wheel_filename ,
841- )
842-
843- source_type = sources .get_source_type (self .ctx , req )
844-
845- return SourceBuildResult (
846- wheel_filename = wheel_filename ,
847- sdist_filename = sdist_filename ,
848- unpack_dir = unpack_dir ,
849- sdist_root_dir = sdist_root_dir ,
850- build_env = build_env ,
851- source_type = source_type ,
845+ source_url = source_url ,
852846 )
853-
854- except Exception as build_error :
855- if not self .test_mode :
856- raise
857-
858- # Test mode: attempt pre-built fallback
859- fallback_result = self ._handle_test_mode_failure (
847+ sdist_root_dir = self ._prepare_source (
860848 req = req ,
861849 resolved_version = resolved_version ,
862- req_type = req_type ,
863- build_error = build_error ,
850+ source_filename = source_filename ,
864851 )
865- if fallback_result is None :
866- # Fallback failed, re-raise for bootstrap() to catch
867- raise
868-
869- return fallback_result
852+ else :
853+ logger .debug (f"have cached wheel in { unpacked_cached_wheel } " )
854+ sdist_root_dir = unpacked_cached_wheel / unpacked_cached_wheel .stem
870855
871- def _handle_test_mode_failure (
872- self ,
873- req : Requirement ,
874- resolved_version : Version ,
875- req_type : RequirementType ,
876- build_error : Exception ,
877- ) -> SourceBuildResult | None :
878- """Handle build failure in test mode by attempting pre-built fallback.
856+ assert sdist_root_dir is not None
879857
880- Args:
881- req: The requirement that failed to build.
882- resolved_version: The version that was attempted.
883- req_type: The type of requirement (for fallback resolution).
884- build_error: The original exception from the build attempt.
858+ if sdist_root_dir .parent .parent != self .ctx .work_dir :
859+ raise ValueError (f"'{ sdist_root_dir } /../..' should be { self .ctx .work_dir } " )
860+ unpack_dir = sdist_root_dir .parent
885861
886- Returns:
887- SourceBuildResult if fallback succeeded, None if fallback also failed.
888- """
889- logger .warning (
890- "test mode: build failed for %s==%s, attempting pre-built fallback: %s" ,
891- req .name ,
892- resolved_version ,
893- build_error ,
862+ build_env = self ._create_build_env (
863+ req = req ,
864+ resolved_version = resolved_version ,
865+ parent_dir = sdist_root_dir .parent ,
894866 )
895867
896- try :
897- wheel_url , fallback_version = self ._resolve_prebuilt_with_history (
898- req = req ,
899- req_type = req_type ,
900- )
868+ # Prepare build dependencies (always needed)
869+ # Note: This may recursively call bootstrap() for build deps,
870+ # which has its own error handling.
871+ self ._prepare_build_dependencies (req , sdist_root_dir , build_env )
901872
902- if fallback_version != resolved_version :
903- logger .warning (
904- "test mode: version mismatch for %s - requested %s, fallback %s" ,
905- req .name ,
906- resolved_version ,
907- fallback_version ,
908- )
909-
910- wheel_filename , unpack_dir = self ._download_prebuilt (
911- req = req ,
912- req_type = req_type ,
913- resolved_version = fallback_version ,
914- wheel_url = wheel_url ,
915- )
916-
917- logger .info (
918- "test mode: successfully used pre-built wheel for %s==%s" ,
919- req .name ,
920- fallback_version ,
921- )
922- # Package succeeded via fallback - no failure to record
873+ # Build wheel or sdist
874+ wheel_filename , sdist_filename = self ._do_build (
875+ req = req ,
876+ resolved_version = resolved_version ,
877+ sdist_root_dir = sdist_root_dir ,
878+ build_env = build_env ,
879+ build_sdist_only = build_sdist_only ,
880+ cached_wheel_filename = cached_wheel_filename ,
881+ )
923882
924- return SourceBuildResult (
925- wheel_filename = wheel_filename ,
926- sdist_filename = None ,
927- unpack_dir = unpack_dir ,
928- sdist_root_dir = None ,
929- build_env = None ,
930- source_type = SourceType .PREBUILT ,
931- )
883+ source_type = sources .get_source_type (self .ctx , req )
932884
933- except Exception as fallback_error :
934- logger .error (
935- "test mode: pre-built fallback also failed for %s: %s" ,
936- req .name ,
937- fallback_error ,
938- exc_info = True ,
939- )
940- # Return None to signal failure; bootstrap() will record via re-raised exception
941- return None
885+ return SourceBuildResult (
886+ wheel_filename = wheel_filename ,
887+ sdist_filename = sdist_filename ,
888+ unpack_dir = unpack_dir ,
889+ sdist_root_dir = sdist_root_dir ,
890+ build_env = build_env ,
891+ source_type = source_type ,
892+ )
942893
943894 def _look_for_existing_wheel (
944895 self ,
0 commit comments