Skip to content

Commit 45609a8

Browse files
committed
fix: Re-introduce multiversion smart install loop to restore core functionality
1 parent ab31acd commit 45609a8

2 files changed

Lines changed: 116 additions & 54 deletions

File tree

omnipkg/core.py

Lines changed: 115 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -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()

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "omnipkg"
7-
version = "1.0.5"
7+
version = "1.0.6"
88
authors = [
99
{ name = "1minds3t", email = "omnipkg@proton.me" },
1010
]

0 commit comments

Comments
 (0)