@@ -96,6 +96,13 @@ def _get_requirements_from_args(
9696 default = False ,
9797 help = "Skip generating constraints.txt file to allow building collections with conflicting versions" ,
9898)
99+ @click .option (
100+ "--test-mode" ,
101+ "test_mode" ,
102+ is_flag = True ,
103+ default = False ,
104+ help = "Test mode: mark failed packages as pre-built and continue, report failures at end" ,
105+ )
99106@click .argument ("toplevel" , nargs = - 1 )
100107@click .pass_obj
101108def bootstrap (
@@ -105,6 +112,7 @@ def bootstrap(
105112 cache_wheel_server_url : str | None ,
106113 sdist_only : bool ,
107114 skip_constraints : bool ,
115+ test_mode : bool ,
108116 toplevel : list [str ],
109117) -> None :
110118 """Compute and build the dependencies of a set of requirements recursively
@@ -115,6 +123,20 @@ def bootstrap(
115123 """
116124 logger .info (f"cache wheel server url: { cache_wheel_server_url } " )
117125
126+ if test_mode :
127+ logger .info (
128+ "test mode enabled: will mark failed packages as pre-built and continue"
129+ )
130+ return _bootstrap_test_mode (
131+ wkctx = wkctx ,
132+ requirements_files = requirements_files ,
133+ previous_bootstrap_file = previous_bootstrap_file ,
134+ cache_wheel_server_url = cache_wheel_server_url ,
135+ sdist_only = sdist_only ,
136+ skip_constraints = skip_constraints ,
137+ toplevel = toplevel ,
138+ )
139+
118140 to_build = _get_requirements_from_args (toplevel , requirements_files )
119141 if not to_build :
120142 raise RuntimeError (
@@ -520,3 +542,216 @@ def bootstrap_parallel(
520542 timedelta (seconds = round (time .perf_counter () - start_build , 0 )),
521543 timedelta (seconds = round (time .perf_counter () - start , 0 )),
522544 )
545+
546+
547+ def _bootstrap_test_mode (
548+ wkctx : context .WorkContext ,
549+ requirements_files : list [str ],
550+ previous_bootstrap_file : str | None ,
551+ cache_wheel_server_url : str | None ,
552+ sdist_only : bool ,
553+ skip_constraints : bool ,
554+ toplevel : list [str ],
555+ ) -> None :
556+ """Bootstrap in test mode: mark failed packages as pre-built and continue."""
557+ import sys
558+
559+ from packaging .utils import canonicalize_name
560+
561+ to_build = _get_requirements_from_args (toplevel , requirements_files )
562+ if not to_build :
563+ raise RuntimeError (
564+ "Pass a requirement specificiation or use -r to pass a requirements file"
565+ )
566+
567+ logger .info ("bootstrapping %r variant of %s in test mode" , wkctx .variant , to_build )
568+
569+ failed_packages : list [str ] = []
570+ attempt_count = 0
571+ max_attempts = len (to_build ) + 1 # n failures + 1 success attempt
572+
573+ while attempt_count < max_attempts :
574+ attempt_count += 1
575+ logger .info (f"test mode: bootstrap attempt { attempt_count } " )
576+
577+ try :
578+ wkctx .dependency_graph .clear ()
579+ if previous_bootstrap_file :
580+ logger .info (
581+ "reading previous bootstrap data from %s" , previous_bootstrap_file
582+ )
583+ prev_graph = dependency_graph .DependencyGraph .from_file (
584+ previous_bootstrap_file
585+ )
586+ else :
587+ logger .info ("no previous bootstrap data" )
588+ prev_graph = None
589+
590+ if sdist_only :
591+ logger .info ("sdist-only (fast mode), getting metadata from sdists" )
592+ else :
593+ logger .info ("build all missing wheels" )
594+
595+ pre_built = wkctx .settings .list_pre_built ()
596+ if pre_built :
597+ logger .info ("treating %s as pre-built wheels" , sorted (pre_built ))
598+
599+ server .start_wheel_server (wkctx )
600+
601+ with progress .progress_context (total = len (to_build * 2 )) as progressbar :
602+ bt = bootstrapper .Bootstrapper (
603+ wkctx ,
604+ progressbar ,
605+ prev_graph ,
606+ cache_wheel_server_url ,
607+ sdist_only = sdist_only ,
608+ )
609+
610+ logger .info ("resolving top-level dependencies before building" )
611+ for req in to_build :
612+ token = requirement_ctxvar .set (req )
613+ pbi = wkctx .package_build_info (req )
614+ source_url , version = bt .resolve_version (
615+ req = req ,
616+ req_type = RequirementType .TOP_LEVEL ,
617+ )
618+ logger .info ("%s resolves to %s" , req , version )
619+ wkctx .dependency_graph .add_dependency (
620+ parent_name = None ,
621+ parent_version = None ,
622+ req_type = requirements_file .RequirementType .TOP_LEVEL ,
623+ req = req ,
624+ req_version = version ,
625+ download_url = source_url ,
626+ pre_built = pbi .pre_built ,
627+ )
628+ requirement_ctxvar .reset (token )
629+
630+ for req in to_build :
631+ token = requirement_ctxvar .set (req )
632+ bt .bootstrap (req , requirements_file .RequirementType .TOP_LEVEL )
633+ progressbar .update ()
634+ requirement_ctxvar .reset (token )
635+
636+ logger .info ("test mode: bootstrap completed successfully" )
637+ break
638+
639+ except Exception as err :
640+ logger .warning (
641+ f"test mode: bootstrap failed on attempt { attempt_count } : { err } "
642+ )
643+
644+ failed_package = _extract_failed_package_from_error (err , wkctx )
645+
646+ if failed_package :
647+ canonical_name = str (canonicalize_name (failed_package ))
648+ if canonical_name not in failed_packages :
649+ failed_packages .append (canonical_name )
650+ logger .info (
651+ f"test mode: marking { canonical_name } as pre-built due to failure"
652+ )
653+
654+ _mark_package_as_pre_built (wkctx , canonical_name )
655+ continue
656+ else :
657+ logger .warning (
658+ f"test mode: { canonical_name } already marked as pre-built, still failing"
659+ )
660+ logger .error (
661+ "test mode: pre-built package still failing, stopping attempts"
662+ )
663+ break
664+ else :
665+ logger .error (
666+ f"test mode: unable to identify failed package from error: { err } "
667+ )
668+ break
669+ if attempt_count >= max_attempts :
670+ logger .warning (
671+ f"test mode: reached maximum attempt limit ({ max_attempts } ) - "
672+ f"stopping to prevent infinite loops"
673+ )
674+
675+ constraints_filename = wkctx .work_dir / "constraints.txt"
676+ if skip_constraints :
677+ logger .info ("skipping constraints.txt generation as requested" )
678+ else :
679+ logger .info (f"writing installation dependencies to { constraints_filename } " )
680+ try :
681+ with open (constraints_filename , "w" ) as f :
682+ if not write_constraints_file (graph = wkctx .dependency_graph , output = f ):
683+ logger .warning (
684+ f"Could not produce a pip compatible constraints file. Please review { constraints_filename } for more details"
685+ )
686+ except Exception as err :
687+ logger .warning (f"Failed to write constraints file: { err } " )
688+
689+ metrics .summarize (wkctx , "Test Mode Bootstrapping" )
690+
691+ if failed_packages :
692+ logger .error ("test mode: the following packages failed to build:" )
693+ for package in sorted (failed_packages ):
694+ logger .error (f" - { package } " )
695+ logger .error (f"test mode: { len (failed_packages )} package(s) failed to build" )
696+ sys .exit (1 )
697+ else :
698+ logger .info ("test mode: all packages built successfully" )
699+
700+
701+ def _extract_failed_package_from_error (
702+ error : Exception , wkctx : context .WorkContext
703+ ) -> str | None :
704+ """Extract the package name that caused the build failure from the error."""
705+ current_req = requirement_ctxvar .get (None )
706+ if current_req :
707+ return current_req .name
708+
709+ error_str = str (error )
710+ import re
711+
712+ # Pattern: "package_name-version" in error messages
713+ version_pattern = r"([a-zA-Z0-9_-]+)-\d+(?:\.\d+)*(?:[a-zA-Z0-9._-]*)"
714+ match = re .search (version_pattern , error_str )
715+ if match :
716+ return match .group (1 )
717+
718+ # Pattern: package names in quotes
719+ quoted_pattern = r"'([a-zA-Z0-9_-]+)'"
720+ match = re .search (quoted_pattern , error_str )
721+ if match :
722+ return match .group (1 )
723+
724+ return None
725+
726+
727+ def _mark_package_as_pre_built (wkctx : context .WorkContext , package_name : str ) -> None :
728+ """Mark a package as pre-built in the settings."""
729+ from packaging .utils import canonicalize_name
730+
731+ from fromager .packagesettings import Package , PackageSettings , VariantInfo
732+
733+ canonical_name = Package (canonicalize_name (package_name , validate = True ))
734+ package_settings = wkctx .settings .package_setting (canonical_name )
735+ variant_info = VariantInfo (pre_built = True )
736+
737+ new_variants = dict (package_settings .variants )
738+ new_variants [wkctx .variant ] = variant_info
739+
740+ # Pydantic models are immutable, so create new instance
741+ new_package_settings = PackageSettings (
742+ name = package_settings .name ,
743+ has_config = package_settings .has_config ,
744+ build_dir = package_settings .build_dir ,
745+ changelog = package_settings .changelog ,
746+ config_settings = package_settings .config_settings ,
747+ env = package_settings .env ,
748+ download_source = package_settings .download_source ,
749+ resolver_dist = package_settings .resolver_dist ,
750+ build_options = package_settings .build_options ,
751+ git_options = package_settings .git_options ,
752+ project_override = package_settings .project_override ,
753+ variants = new_variants ,
754+ )
755+
756+ wkctx .settings ._package_settings [canonical_name ] = new_package_settings
757+ wkctx .settings ._pbi_cache .clear ()
0 commit comments