@@ -870,64 +870,96 @@ def _save_last_known_good_snapshot(self):
870870 print (" ✅ Snapshot saved." )
871871 except Exception as e :
872872 print (f" ⚠️ Could not save environment snapshot: { e } " )
873+
874+ # ADD THIS ENTIRE METHOD
875+ def _sort_packages_newest_first (self , packages : List [str ]) -> List [str ]:
876+ """
877+ Sorts packages by version, newest first, to ensure proper bubble creation.
878+ """
879+ from packaging .version import parse as parse_version , InvalidVersion
880+ import re
881+
882+ def get_version_key (pkg_spec ):
883+ """Extracts a sortable version key from a package spec."""
884+ match = re .search (r'(==|>=|<=|>|<)(.+)' , pkg_spec )
885+ if match :
886+ version_str = match .group (2 ).strip ()
887+ try :
888+ return parse_version (version_str )
889+ except InvalidVersion :
890+ return parse_version ('0.0.0' )
891+ return parse_version ('9999.0.0' )
873892
893+ return sorted (packages , key = get_version_key , reverse = True )
894+
895+ # REPLACE your current smart_install with this one
874896 def smart_install (self , packages : List [str ], dry_run : bool = False ) -> int :
897+ """
898+ Processes multiple package versions by sorting them newest-to-oldest
899+ and installing them iteratively to correctly trigger bubble creation.
900+ """
875901 if not self .connect_redis ():
876902 return 1
877903
878904 if dry_run :
879905 print ("🔬 Running in --dry-run mode. No changes will be made." )
880906 return 0
881907
882- print (f"🔍 Checking satisfaction for: { ', ' .join (packages )} " )
908+ # Sort packages newest to oldest
909+ sorted_packages = self ._sort_packages_newest_first (packages )
910+
911+ if sorted_packages != packages :
912+ print (f"🔄 Reordered packages for optimal installation: { ', ' .join (sorted_packages )} " )
913+
914+ # Process each package individually
915+ for package_spec in sorted_packages :
916+ print ("\n " + "─" * 60 )
917+ print (f"📦 Processing: { package_spec } " )
918+ print ("─" * 60 )
883919
884- satisfaction_check = self ._check_package_satisfaction (packages )
920+ satisfaction_check = self ._check_package_satisfaction ([ package_spec ] )
885921
886- if satisfaction_check ['all_satisfied' ]:
887- print (" \n ✅ All requirements already satisfied. No work needed. 🛡️ " )
888- return 0
922+ if satisfaction_check ['all_satisfied' ]:
923+ print (f"✅ Requirement already satisfied: { package_spec } " )
924+ continue
889925
890- packages_to_install = satisfaction_check ['needs_install' ]
891-
892- if satisfaction_check ['partial_satisfied' ]:
893- print ("\n ℹ️ Some packages already satisfied:" )
894- for pkg in satisfaction_check ['satisfied' ]:
895- print (f" ✅ { pkg } " )
896- print ("\n 📦 Proceeding to install remaining packages:" )
897- for pkg in packages_to_install :
898- print (f" 📥 { pkg } " )
899-
900- print ("\n 📸 Taking LIVE pre-installation snapshot..." )
901- packages_before = self .get_installed_packages (live = True )
902- print (f" - Found { len (packages_before )} packages" )
903-
904- print (f"\n ⚙️ Running pip install for: { ', ' .join (packages_to_install )} ..." )
905- return_code = self ._run_pip_install (packages_to_install )
906-
907- if return_code != 0 :
908- print ("❌ Pip installation failed. Aborting cleanup." )
909- return return_code
910-
911- print ("\n 🔬 Analyzing post-installation changes..." )
912- packages_after = self .get_installed_packages (live = True )
913- downgrades_to_fix = self ._detect_downgrades (packages_before , packages_after )
914-
915- if downgrades_to_fix :
916- print ("\n 🛡️ DOWNGRADE PROTECTION ACTIVATED!" )
917- for fix in downgrades_to_fix :
918- print (f" -> Fixing downgrade: { fix ['package' ]} from v{ fix ['good_version' ]} to v{ fix ['bad_version' ]} " )
919- self .bubble_manager .create_isolated_bubble (fix ['package' ], fix ['bad_version' ])
920- print (f" 🔄 Restoring '{ fix ['package' ]} ' to safe version v{ fix ['good_version' ]} in main environment..." )
921- subprocess .run ([self .config ["python_executable" ], "-m" , "pip" , "install" , f"{ fix ['package' ]} =={ fix ['good_version' ]} " ], capture_output = True , text = True )
922- print ("\n ✅ Environment protection complete!" )
923- else :
924- print ("✅ No downgrades detected. Installation completed safely." )
926+ packages_to_install = satisfaction_check ['needs_install' ]
927+
928+ print ("\n 📸 Taking LIVE pre-installation snapshot..." )
929+ packages_before = self .get_installed_packages (live = True )
930+ print (f" - Found { len (packages_before )} packages" )
925931
926- print ("\n 🧠 Updating knowledge base with final environment state..." )
927- self ._run_metadata_builder_for_delta (packages_before , packages_after )
928- self ._update_hash_index_for_delta (packages_before , packages_after )
929- self ._save_last_known_good_snapshot ()
932+ print (f"\n ⚙️ Running pip install for: { ', ' .join (packages_to_install )} ..." )
933+ return_code = self ._run_pip_install (packages_to_install )
934+
935+ if return_code != 0 :
936+ print (f"❌ Pip installation for { package_spec } failed. Continuing with next package." )
937+ continue
938+
939+ print ("\n 🔬 Analyzing post-installation changes..." )
940+ packages_after = self .get_installed_packages (live = True )
941+ downgrades_to_fix = self ._detect_downgrades (packages_before , packages_after )
942+
943+ if downgrades_to_fix :
944+ print ("\n 🛡️ DOWNGRADE PROTECTION ACTIVATED!" )
945+ for fix in downgrades_to_fix :
946+ print (f" -> Fixing downgrade: { fix ['package' ]} from v{ fix ['good_version' ]} to v{ fix ['bad_version' ]} " )
947+ self .bubble_manager .create_isolated_bubble (fix ['package' ], fix ['bad_version' ])
948+ print (f" 🔄 Restoring '{ fix ['package' ]} ' to safe version v{ fix ['good_version' ]} in main environment..." )
949+ subprocess .run ([self .config ["python_executable" ], "-m" , "pip" , "install" , "--quiet" , f"{ fix ['package' ]} =={ fix ['good_version' ]} " ], capture_output = True , text = True )
950+ print ("\n ✅ Environment protection complete!" )
951+ else :
952+ print ("✅ No downgrades detected. Installation completed safely." )
930953
954+ print ("\n 🧠 Updating knowledge base with final environment state..." )
955+ self ._run_metadata_builder_for_delta (packages_before , packages_after )
956+ self ._update_hash_index_for_delta (packages_before , packages_after )
957+
958+ print ("\n " + "=" * 60 )
959+ print ("🎉 All package operations complete." )
960+
961+ # We keep your snapshot feature, running it once at the very end
962+ self ._save_last_known_good_snapshot ()
931963 return 0
932964
933965 def _find_package_installations (self , package_name : str ) -> List [Dict ]:
@@ -1082,30 +1114,55 @@ def revert_to_last_known_good(self, force: bool = False):
10821114 print ("\n ✅ Environment successfully reverted to the last known good state." )
10831115 return 0
10841116
1117+ # REPLACE your current _check_package_satisfaction with this one
10851118 def _check_package_satisfaction (self , packages : List [str ]) -> dict :
1119+ """Check satisfaction with bubble pre-check optimization"""
1120+ satisfied = set ()
1121+ remaining_packages = []
1122+
1123+ # FAST PATH: Check for pre-existing bubbles BEFORE calling pip
1124+ for pkg_spec in packages :
1125+ try :
1126+ if '==' in pkg_spec :
1127+ pkg_name , version = pkg_spec .split ('==' , 1 )
1128+ bubble_path = self .multiversion_base / f"{ pkg_name } -{ version } "
1129+ if bubble_path .exists () and bubble_path .is_dir ():
1130+ satisfied .add (pkg_spec )
1131+ print (f" ⚡ Found existing bubble: { pkg_spec } " )
1132+ continue
1133+ remaining_packages .append (pkg_spec )
1134+ except ValueError :
1135+ remaining_packages .append (pkg_spec )
1136+
1137+ if not remaining_packages :
1138+ return {
1139+ 'all_satisfied' : True ,
1140+ 'satisfied' : sorted (list (satisfied )),
1141+ 'needs_install' : []
1142+ }
1143+
1144+ # SLOW PATH: Only call pip for packages without bubbles
10861145 req_file_path = None
10871146 try :
10881147 with tempfile .NamedTemporaryFile (mode = 'w' , suffix = '.txt' , delete = False ) as f :
1089- f .write ("\n " .join (packages ))
1148+ f .write ("\n " .join (remaining_packages ))
10901149 req_file_path = f .name
10911150
10921151 cmd = [self .config ["python_executable" ], "-m" , "pip" , "install" , "--dry-run" , "-r" , req_file_path ]
10931152 result = subprocess .run (cmd , capture_output = True , text = True , timeout = 30 )
1094-
1095- satisfied = set ()
1153+
10961154 output_lines = result .stdout .splitlines ()
1097-
10981155 for line in output_lines :
10991156 if line .startswith ("Requirement already satisfied:" ):
11001157 try :
11011158 satisfied_spec = line .split ("Requirement already satisfied: " )[1 ].strip ()
11021159 req_name = satisfied_spec .split ('==' )[0 ].lower ()
1103- for user_req in packages :
1160+ for user_req in remaining_packages :
11041161 if user_req .lower ().startswith (req_name ):
11051162 satisfied .add (user_req )
1106- except IndexError :
1163+ except ( IndexError , AttributeError ) :
11071164 continue
1108-
1165+
11091166 needs_install = [pkg for pkg in packages if pkg not in satisfied ]
11101167
11111168 return {
@@ -1116,8 +1173,13 @@ def _check_package_satisfaction(self, packages: List[str]) -> dict:
11161173 }
11171174
11181175 except Exception as e :
1119- print (f" ⚠️ Satisfaction check failed ({ e } ). Assuming all packages need installation." )
1120- return {'all_satisfied' : False , 'partial_satisfied' : False , 'satisfied' : [], 'needs_install' : packages }
1176+ print (f" ⚠️ Satisfaction check failed ({ e } ). Assuming remaining packages need installation." )
1177+ return {
1178+ 'all_satisfied' : False ,
1179+ 'partial_satisfied' : len (satisfied ) > 0 ,
1180+ 'satisfied' : sorted (list (satisfied )),
1181+ 'needs_install' : remaining_packages
1182+ }
11211183 finally :
11221184 if req_file_path and Path (req_file_path ).exists ():
11231185 Path (req_file_path ).unlink ()
0 commit comments