@@ -2041,9 +2041,17 @@ def safe_input(prompt, default="", auto_value=None):
20412041 print (f" Skipping Python { ver } ." )
20422042 continue
20432043
2044- # Skip if already matches lock
2045- if _interp_matches_lock (py_path , active ):
2044+ # === PER-PYTHON SHA SKIP (fast path — no subprocess) ===
2045+ _plat = meta .get ("platform" , platform .system ())
2046+ _py_sha = _per_python_sha (ver , active , bubbles , _plat )
2047+ if _per_python_sha_check (lock_path , ver , _py_sha ):
2048+ print (f" ✅ Python { ver } already synced (SHA match) — skipping." )
2049+ continue
2050+
2051+ # Skip if already matches lock (slower live pip freeze check)
2052+ if _interp_matches_lock (py_path , active , bubbles ):
20462053 print (f" ✅ Python { ver } already matches lock — skipping." )
2054+ _per_python_sha_write (lock_path , ver , _py_sha ) # cache for next run
20472055 continue
20482056
20492057 print (f"\n 🧹 Clearing site-packages: { sp_path } " )
@@ -2101,6 +2109,7 @@ def safe_input(prompt, default="", auto_value=None):
21012109 _run_cmd ([f"8pkg{ short } " , "install" , f"{ pkg } =={ bver } " ], check = False )
21022110
21032111 print (f"\n ✅ Python { ver } synced." )
2112+ _per_python_sha_write (lock_path , ver , _py_sha )
21042113
21052114 # Stop daemon only after all shim calls are done — keeping it alive during
21062115 # the loop prevents execv fallback on adopt/bubble/doctor calls.
@@ -2122,26 +2131,69 @@ def safe_input(prompt, default="", auto_value=None):
21222131
21232132# ── sync helpers ──────────────────────────────────────────────────────────────
21242133
2125- def _interp_matches_lock (py_path : Path , active : dict ) -> bool :
2126- """Returns True if the interpreter's installed packages match the lock active dict."""
2134+ def _per_python_sha (ver : str , active : dict , bubbles : dict , plat : str ) -> str :
2135+ """Compute a deterministic SHA for a single Python version's sync state."""
2136+ active_str = "," .join (f"{ k } =={ v } " for k , v in sorted (active .items ()))
2137+ bubble_str = ";" .join (
2138+ f"{ k } :{ ',' .join (sorted (vs ))} " for k , vs in sorted (bubbles .items ())
2139+ )
2140+ raw = f"{ ver } |{ plat } |{ active_str } |{ bubble_str } "
2141+ return hashlib .sha256 (raw .encode ()).hexdigest ()[:16 ]
2142+
2143+ def _per_python_sha_path (lock_path : Path , ver : str ) -> Path :
2144+ """Sidecar file path: <lock_dir>/per_python/<lock_stem>/<ver>.sha"""
2145+ return lock_path .parent / "per_python" / lock_path .stem / f"{ ver } .sha"
2146+
2147+ def _per_python_sha_check (lock_path : Path , ver : str , sha : str ) -> bool :
2148+ """Returns True if the stored SHA matches — safe to skip this Python."""
21272149 try :
2128- r = subprocess .run (
2129- [str (py_path ), "-m" , "pip" , "freeze" ],
2130- capture_output = True , text = True , timeout = 30 ,
2131- )
2132- if r .returncode != 0 :
2150+ p = _per_python_sha_path (lock_path , ver )
2151+ return p .exists () and p .read_text ().strip () == sha
2152+ except Exception :
2153+ return False
2154+
2155+ def _per_python_sha_write (lock_path : Path , ver : str , sha : str ) -> None :
2156+ """Write SHA sidecar after a successful sync."""
2157+ try :
2158+ p = _per_python_sha_path (lock_path , ver )
2159+ p .parent .mkdir (parents = True , exist_ok = True )
2160+ p .write_text (sha )
2161+ except Exception :
2162+ pass # non-fatal — just means next run won't skip
2163+
2164+ def _interp_matches_lock (py_path : Path , active : dict , bubbles : dict | None = None ) -> bool :
2165+ """
2166+ FS-based check — no pip subprocess.
2167+ Scans dist-info for active packages and .omnipkg_versions/ for bubbles.
2168+ pip freeze is intentionally NOT used: it is blind to omnipkg bubble structure.
2169+ """
2170+ try :
2171+ sp = _site_packages_for (py_path )
2172+ if not sp or not sp .exists ():
21332173 return False
2134- installed = {}
2135- for line in r .stdout .splitlines ():
2136- if "==" in line :
2137- k , v = line .split ("==" , 1 )
2138- installed [k .lower ().replace ("-" , "_" )] = v .strip ()
2139- _SKIP = {"omnipkg" , "pip" , "setuptools" , "wheel" , "pkg_resources" }
2174+
2175+ _SKIP = {"omnipkg" , "pip" , "setuptools" , "wheel" , "pkg-resources" , "pkg_resources" }
2176+
2177+ # ── active packages: scan dist-info on fs ────────────────────────────
2178+ installed = _scan_active_from_fs (sp )
21402179 for pkg , ver in active .items ():
2141- if pkg .lower ().replace ("-" , "_" ) in _SKIP :
2180+ norm = re .sub (r"[-_.]+" , "-" , pkg ).lower ()
2181+ if norm in _SKIP :
21422182 continue
2143- if installed .get (pkg . lower (). replace ( "-" , "_" ) ) != ver :
2183+ if installed .get (norm ) != ver :
21442184 return False
2185+
2186+ # ── bubbles: scan .omnipkg_versions/ on fs ───────────────────────────
2187+ if bubbles :
2188+ mv_base = sp / ".omnipkg_versions"
2189+ installed_bubbles = _scan_bubbles_from_fs (mv_base )
2190+ for pkg , versions in bubbles .items ():
2191+ norm = re .sub (r"[-_.]+" , "-" , pkg ).lower ()
2192+ installed_vers = set (installed_bubbles .get (norm , []))
2193+ for bver in versions :
2194+ if bver not in installed_vers :
2195+ return False
2196+
21452197 return True
21462198 except Exception :
21472199 return False
@@ -2216,6 +2268,13 @@ def _extract_bad_packages(error_text: str) -> set[str]:
22162268 skipped : list [str ] = []
22172269
22182270 while remaining :
2271+ import glob as _glob , os as _os
2272+ _sp = Path (py_path ).parent .parent / "lib"
2273+ _egg_paths = _glob .glob (str (_sp / "python*/site-packages/pip-*.egg" ))
2274+ _env = _os .environ .copy ()
2275+ if _egg_paths :
2276+ _existing = _env .get ("PYTHONPATH" , "" )
2277+ _env ["PYTHONPATH" ] = _os .pathsep .join (_egg_paths + ([_existing ] if _existing else []))
22192278 cmd = [str (py_path ), "-m" , "pip" , "install" , "--no-deps" , "--no-cache-dir" ] + remaining
22202279 print (f" $ { ' ' .join (cmd [:4 ])} ... ({ len (remaining )} packages)" )
22212280
@@ -2225,7 +2284,7 @@ def _extract_bad_packages(error_text: str) -> set[str]:
22252284 cmd ,
22262285 stdout = subprocess .PIPE ,
22272286 stderr = subprocess .PIPE ,
2228- text = True , encoding = "utf-8" , errors = "replace" ,
2287+ text = True , encoding = "utf-8" , errors = "replace" , env = _env ,
22292288 )
22302289 assert proc .stdout is not None
22312290 assert proc .stderr is not None
0 commit comments