11"""
22Multiverse healing test — uses direct versioned binaries (8pkg39, 8pkg311, etc.)
3- and reads interpreter paths from the omnipkg registry.
4- No swap needed, works in CI across shells.
3+ No pre-flight registry check needed: the shim auto-adopts the interpreter and
4+ installs packages on first use. We only read the registry AFTER the shim has
5+ run (adoption guaranteed complete) to get the exe path for payload subprocesses.
56"""
67import sys
78import os
1920from omnipkg .i18n import _
2021
2122
22- # --- REGISTRY HELPERS ---
23+ # --- HELPERS ---
2324
24- def get_registry (venv_root : Path ) -> dict :
25- """Read the omnipkg interpreter registry."""
26- registry_path = venv_root / ".omnipkg" / "interpreters" / "registry.json"
27- if not registry_path .exists ():
28- raise RuntimeError (f"Registry not found: { registry_path } " )
29- return json .loads (registry_path .read_text (encoding = "utf-8" ))
30-
31-
32- def get_interpreter (venv_root : Path , version : str ) -> Path :
33- """Get the python executable path for a given version from the registry."""
34- registry = get_registry (venv_root )
35- interpreters = registry .get ("interpreters" , {})
36- if version not in interpreters :
37- raise RuntimeError (
38- f"Python { version } not found in registry. "
39- f"Available: { list (interpreters .keys ())} "
40- )
41- exe = Path (interpreters [version ])
42- if not exe .exists ():
43- raise RuntimeError (f"Interpreter path does not exist: { exe } " )
44- return exe
25+ def detect_venv_root () -> Path :
26+ """Detect the omnipkg venv root from env or config."""
27+ override = os .environ .get ("OMNIPKG_VENV_ROOT" )
28+ if override :
29+ return Path (override )
30+ try :
31+ from omnipkg .core import ConfigManager
32+ cm = ConfigManager (suppress_init_messages = True )
33+ return cm .venv_path
34+ except Exception :
35+ pass
36+ raise RuntimeError ("Could not determine OMNIPKG venv root. Set OMNIPKG_VENV_ROOT." )
4537
4638
4739def get_versioned_bin (venv_root : Path , version : str , cmd : str ) -> list :
4840 """
49- Return a command prefix for a versioned omnipkg invocation.
50-
51- Strategy (in order):
52- 1. 8pkg.exe --python X.Y — preferred; exe is directly executable,
53- no shell needed, works in subprocess.
54- 2. cmd /c 8pkgXY.bat — fallback for Windows when only .bat exists;
55- .bat files require cmd.exe to interpret them
56- and CANNOT be launched via CreateProcess directly.
57- 3. 8pkgXY (no extension) — Unix shim / symlink.
58-
59- Returns a list so callers can do: run(get_versioned_bin(...) + ["install", ...])
41+ Return a command prefix for a versioned omnipkg shim.
42+ The shim handles auto-adopt + install — no registry pre-check needed.
43+
44+ Priority:
45+ 1. cmd.exe --python X.Y (cross-platform, no shell)
46+ 2. cmd /c cmdXY.bat (Windows .bat shim)
47+ 3. cmdXY (Unix symlink/shim)
48+ 4. cmd --python X.Y (plain Unix binary with flag)
6049 """
61- ver_tag = version .replace ("." , "" ) # "3.9" -> "39"
50+ ver_tag = version .replace ("." , "" )
6251 is_windows = sys .platform == "win32"
6352
6453 for bin_dir in [venv_root / "Scripts" , venv_root / "bin" ]:
65- # --- Prefer the plain exe with --python flag (cross-platform, no shell needed) ---
54+ # Plain . exe with --python flag — works everywhere without shell
6655 exe = bin_dir / f"{ cmd } .exe"
6756 if exe .exists ():
6857 return [str (exe ), "--python" , version ]
6958
70- plain = bin_dir / cmd
71- if plain .exists () and not is_windows :
72- return [str (plain ), "--python" , version ]
73-
74- # --- .bat fallback: must be wrapped in cmd /c on Windows ---
59+ # Windows .bat shim — must go through cmd /c
7560 for bat_suffix in [f"{ cmd } { ver_tag } .bat" , f"{ cmd } { ver_tag } .cmd" ]:
7661 bat = bin_dir / bat_suffix
7762 if bat .exists ():
78- if is_windows :
79- return ["cmd" , "/c" , str (bat )]
80- else :
81- # Should not happen, but handle gracefully
82- return [str (bat )]
63+ return (["cmd" , "/c" , str (bat )] if is_windows else [str (bat )])
8364
84- # --- Unix shim with no extension ---
65+ # Unix versioned shim (symlink like 8pkg39)
8566 shim = bin_dir / f"{ cmd } { ver_tag } "
8667 if shim .exists ():
8768 return [str (shim )]
8869
89- raise RuntimeError (
90- f"Could not find '{ cmd } ' binary for Python { version } in { venv_root } .\n "
91- f" Looked for: { cmd } .exe, { cmd } , { cmd } { ver_tag } .bat, { cmd } { ver_tag } .cmd, { cmd } { ver_tag } \n "
92- f" Under: { venv_root / 'Scripts' } and { venv_root / 'bin' } "
93- )
70+ # Plain Unix binary with --python flag
71+ plain = bin_dir / cmd
72+ if plain .exists () and not is_windows :
73+ return [str (plain ), "--python" , version ]
9474
75+ # Last resort: rely on PATH (CI often has 8pkg39 on PATH directly)
76+ shim_name = f"{ cmd } { ver_tag } "
77+ safe_print (f" [WARN] No shim found under { venv_root } — falling back to PATH: { shim_name } " )
78+ return [shim_name ]
9579
96- def detect_venv_root () -> Path :
97- """Detect the omnipkg venv root from env or config."""
98- override = os .environ .get ("OMNIPKG_VENV_ROOT" )
99- if override :
100- return Path (override )
101- # Fall back to reading config next to this python
102- try :
103- from omnipkg .core import ConfigManager
104- cm = ConfigManager (suppress_init_messages = True )
105- return cm .venv_path
106- except Exception :
107- pass
108- raise RuntimeError ("Could not determine OMNIPKG venv root. Set OMNIPKG_VENV_ROOT." )
80+
81+ def get_interpreter_after_adopt (venv_root : Path , version : str ) -> Path :
82+ """
83+ Read the interpreter exe from the registry.
84+ Only call this AFTER the versioned shim has already run (adoption complete).
85+ """
86+ registry_path = venv_root / ".omnipkg" / "interpreters" / "registry.json"
87+ if not registry_path .exists ():
88+ raise RuntimeError (f"Registry not found at { registry_path } " )
89+ registry = json .loads (registry_path .read_text (encoding = "utf-8" ))
90+ interpreters = registry .get ("interpreters" , {})
91+ if version not in interpreters :
92+ raise RuntimeError (
93+ f"Python { version } still not in registry after adoption. "
94+ f"Available: { list (interpreters .keys ())} "
95+ )
96+ exe = Path (interpreters [version ])
97+ if not exe .exists ():
98+ raise RuntimeError (f"Interpreter path does not exist: { exe } " )
99+ return exe
109100
110101
111102# --- SUBPROCESS HELPER ---
@@ -117,7 +108,6 @@ def run(cmd, description, check=True, env=None):
117108 _env = os .environ .copy ()
118109 if env :
119110 _env .update (env )
120- # Prevent recursive heal loops in subprocesses
121111 _env ["OMNIPKG_DISABLE_AUTO_ALIGN" ] = "1"
122112 _env ["OMNIPKG_SUBPROCESS_MODE" ] = "1"
123113
@@ -133,16 +123,12 @@ def run(cmd, description, check=True, env=None):
133123 env = _env ,
134124 )
135125 except (OSError , FileNotFoundError ) as exc :
136- # Happens when the executable itself can't be launched (e.g. .bat without cmd /c)
137126 safe_print (f" [LAUNCH ERROR] Could not start process: { exc } " )
138127 safe_print (f" Attempted executable: { cmd [0 ]!r} " )
139- safe_print (f" Full command: { cmd } " )
140128 if check :
141129 raise RuntimeError (
142130 f"Failed to launch '{ cmd [0 ]} ': { exc } \n "
143- f" Full command: { cmd } \n "
144- f" Hint: on Windows, .bat files cannot be launched directly via "
145- f"subprocess — they must be wrapped with ['cmd', '/c', ...]"
131+ f" Hint: on Windows, .bat files must be wrapped with ['cmd', '/c', ...]"
146132 ) from exc
147133 return ""
148134
@@ -155,20 +141,16 @@ def run(cmd, description, check=True, env=None):
155141 proc .stdout .close ()
156142 rc = proc .wait ()
157143 output = "" .join (lines )
158-
159144 safe_print (f" exit code: { rc } " )
160145
161146 if check and rc != 0 :
162- safe_print (f"\n [FAILURE DETAILS] Command exited with code { rc } " )
163- safe_print (f" Executable : { cmd [0 ]!r} " )
164- safe_print (f" Full cmd : { ' ' .join (str (c ) for c in cmd )} " )
147+ safe_print (f"\n [FAILURE DETAILS] rc={ rc } " )
148+ safe_print (f" Full cmd: { ' ' .join (str (c ) for c in cmd )} " )
165149 if output .strip ():
166- safe_print (" --- captured output ---" )
167150 for line in output .splitlines ():
168151 safe_print (f" { line } " )
169- safe_print (" --- end output ---" )
170152 else :
171- safe_print (" (no output captured — process may have failed to start )" )
153+ safe_print (" (no output captured)" )
172154 raise subprocess .CalledProcessError (rc , cmd , output = output )
173155
174156 return output
@@ -206,19 +188,19 @@ def multiverse_analysis():
206188 venv_root = detect_venv_root ()
207189 safe_print (f"[INFO] venv root: { venv_root } " )
208190
209- registry = get_registry (venv_root )
210- safe_print (f"[INFO] available interpreters: { list (registry ['interpreters' ].keys ())} " )
211-
212191 # === STEP 1: Python 3.9 — legacy scipy/numpy ===
213- safe_print ( " \n [STEP 1] Python 3.9 — installing legacy packages..." )
214-
192+ # The shim handles adopt + install automatically — no registry pre-check.
193+ safe_print ( " \n [STEP 1] Python 3.9 — installing legacy packages via shim (auto-adopts)..." )
215194 pkg39_cmd = get_versioned_bin (venv_root , "3.9" , "8pkg" )
216- py39 = get_interpreter (venv_root , "3.9" )
217195 safe_print (f"[INFO] pkg39 cmd prefix: { pkg39_cmd } " )
218196
219197 run (pkg39_cmd + ["install" , "numpy<2" , "scipy" ],
220198 "Installing numpy<2 + scipy into Python 3.9" )
221199
200+ # Now adoption is complete — safe to read registry for the exe path
201+ py39 = get_interpreter_after_adopt (venv_root , "3.9" )
202+ safe_print (f"[INFO] py39 exe: { py39 } " )
203+
222204 safe_print ("\n [STEP 1] Running legacy payload in Python 3.9..." )
223205 result = subprocess .run (
224206 [str (py39 ), __file__ , "--run-legacy" ],
@@ -237,15 +219,17 @@ def multiverse_analysis():
237219 safe_print (f"[OK] Legacy result: { legacy_data } " )
238220
239221 # === STEP 2: Python 3.11 — modern tensorflow ===
240- safe_print ("\n [STEP 2] Python 3.11 — installing tensorflow..." )
241-
222+ safe_print ("\n [STEP 2] Python 3.11 — installing tensorflow via shim (auto-adopts)..." )
242223 pkg311_cmd = get_versioned_bin (venv_root , "3.11" , "8pkg" )
243- py311 = get_interpreter (venv_root , "3.11" )
244224 safe_print (f"[INFO] pkg311 cmd prefix: { pkg311_cmd } " )
245225
246226 run (pkg311_cmd + ["install" , "tensorflow" ],
247227 "Installing tensorflow into Python 3.11" )
248228
229+ # Adoption complete — safe to read registry
230+ py311 = get_interpreter_after_adopt (venv_root , "3.11" )
231+ safe_print (f"[INFO] py311 exe: { py311 } " )
232+
249233 safe_print ("\n [STEP 2] Running modern payload in Python 3.11..." )
250234 result = subprocess .run (
251235 [str (py311 ), __file__ , "--run-modern" , json .dumps (legacy_data )],
@@ -290,4 +274,4 @@ def multiverse_analysis():
290274 safe_print ("[SUCCESS] Context switching, installs, and healing all working!" )
291275 else :
292276 safe_print ("[FAILED] Check output above." )
293- safe_print (f"[PERF] Total: { time .perf_counter () - t0 :.2f} s" )
277+ safe_print (f"[PERF] Total: { time .perf_counter () - t0 :.2f} s" )
0 commit comments