@@ -75,53 +75,50 @@ def extract_required_dependency(self, stderr_output: str) -> Optional[str]:
7575
7676def sync_context_to_runtime ():
7777 """
78- Ensures omnipkg's active context matches the currently running Python interpreter.
79-
80- This is a critical pre-flight check to prevent state mismatches between the
81- runtime environment and the omnipkg configuration. It uses the CLI for robust
82- switching logic.
83-
84- Returns:
85- bool: True if the context is synchronized, False otherwise.
78+ Ensures omnipkg's active context matches the currently running Python interpreter
79+ by using the omnipkg API directly. This is the robust method for post-relaunch
80+ synchronization, avoiding the state conflicts of CLI subprocesses.
8681 """
87- current_python_version = f'{ sys .version_info .major } .{ sys .version_info .minor } '
88-
82+ print (_ ('🔄 Forcing omnipkg context to match script Python version: {}...' ).format (
83+ f'{ sys .version_info .major } .{ sys .version_info .minor } '
84+ ))
8985 try :
90- config_manager = ConfigManager ()
91- active_config_version = config_manager .config .get ('active_python_version' )
92-
93- # If the config already matches the runtime, we don't need to do anything.
94- if active_config_version == current_python_version :
95- # This is a minor optimization to avoid a subprocess call if not needed.
96- return True
97-
98- # If a change is needed, use the robust CLI which contains all the necessary logic.
99- print (_ ('🔄 Forcing omnipkg context to match script Python version: {}...' ).format (current_python_version ))
100- omnipkg_cmd_base = [sys .executable , '-m' , 'omnipkg.cli' ]
101-
102- # The 'swap' command is the source of truth for this operation.
103- result = subprocess .run (
104- omnipkg_cmd_base + ['swap' , 'python' , current_python_version ],
105- capture_output = True , text = True , check = True
106- )
86+ from omnipkg .core import ConfigManager
10787
108- print (_ ('✅ omnipkg context synchronized to Python {}' ).format (current_python_version ))
109- return True
110-
111- except subprocess .CalledProcessError as e :
112- # This block catches errors if the 'swap' command fails.
113- print (_ ('⚠️ Could not synchronize omnipkg context via CLI: {}' ).format (e ))
114- print (_ (' CLI output: {}' ).format (e .stdout ))
115- print (_ (' CLI error: {}' ).format (e .stderr ))
116- return False
88+ # Suppress messages here to avoid the "new environment" prompt if it occurs
89+ config_manager = ConfigManager (suppress_init_messages = True )
11790
91+ current_executable = str (Path (sys .executable ).resolve ())
92+
93+ # Optimization: If the config is already correct, do nothing.
94+ if config_manager .config .get ('python_executable' ) == current_executable :
95+ print (_ ('✅ Context is already synchronized.' ))
96+ return
97+
98+ # Use the ConfigManager's internal method to get the correct paths for the CURRENT interpreter.
99+ new_paths = config_manager ._get_paths_for_interpreter (current_executable )
100+ if not new_paths :
101+ raise RuntimeError (f"Could not determine paths for the current interpreter: { current_executable } " )
102+
103+ # Directly update and save the configuration. This is what 'swap' does internally.
104+ print (_ (' - Aligning configuration to the new runtime...' ))
105+ config_manager .set ('python_executable' , new_paths ['python_executable' ])
106+ config_manager .set ('site_packages_path' , new_paths ['site_packages_path' ])
107+ config_manager .set ('multiversion_base' , new_paths ['multiversion_base' ])
108+
109+ # Also update the default python symlinks to reflect the change.
110+ config_manager ._update_default_python_links (config_manager .venv_path , Path (current_executable ))
111+
112+ print (_ ('✅ omnipkg context synchronized successfully via API.' ))
113+ return
114+
118115 except Exception as e :
119- # This is a general catch-all for other unexpected errors.
120- print (_ ('⚠️ Unexpected error synchronizing omnipkg context: {}' ).format (e ))
116+ print (_ ('❌ A critical error occurred during context synchronization: {}' ).format (e ))
121117 import traceback
122118 traceback .print_exc ()
123- return False
124-
119+ # Exit because a failed sync is a fatal error for the script's logic.
120+ sys .exit (1 )
121+
125122def run_script_in_omnipkg_env (command_list , streaming_title ):
126123 """
127124 A centralized utility to run a command in a fully configured omnipkg environment.
@@ -194,64 +191,98 @@ def print_header(title):
194191def ensure_python_or_relaunch (required_version : str ):
195192 """
196193 A generic utility to ensure the script is running on a specific Python version.
197-
198- If the current interpreter does not match, it uses omnipkg to switch
199- to the required version and then re-launches the original script in a new
200- process. The original process is then terminated.
201-
202- Args:
203- required_version (str): The required version string (e.g., "3.11").
194+ This version uses a direct, non-mutating approach to find the target
195+ interpreter and relaunch, avoiding hangs from interactive subprocesses.
204196 """
205197 major , minor = map (int , required_version .split ('.' ))
206198 if sys .version_info [:2 ] == (major , minor ):
207199 return # Correct version, do nothing
208200
209- # If we get here, we need to switch and relaunch.
210201 print ('\n ' + '=' * 80 )
211202 print (_ (' 🚀 AUTOMATIC ENVIRONMENT CORRECTION' ))
212203 print ('=' * 80 )
213204 print (_ (' This script requires Python {}' ).format (required_version ))
214205 print (_ (' Currently running on: Python {}.{}' ).format (sys .version_info .major , sys .version_info .minor ))
215- print (_ (' Attempting to automatically switch using omnipkg ...' ))
216-
206+ print (_ (' Attempting to find and relaunch with the correct interpreter ...' ))
207+
217208 try :
209+ # Step 1: Capture the parent environment's identity. This is critical.
210+ from omnipkg .core import ConfigManager
211+ print (_ (' -> Capturing parent environment identity...' ))
212+ parent_cm = ConfigManager (suppress_init_messages = True )
213+ parent_env_id = parent_cm .env_id
214+ parent_venv_root = str (parent_cm .venv_path .resolve ())
215+ print (_ (' -> Parent env_id: {}' ).format (parent_env_id ))
216+
218217 omnipkg_cmd_base = [sys .executable , '-m' , 'omnipkg.cli' ]
219-
220- # Step 1: Adopt the required version
221- subprocess .run (omnipkg_cmd_base + ['python' , 'adopt' , required_version ], check = True , capture_output = True )
222-
223- # Step 2: Swap to the required version
224- subprocess .run (omnipkg_cmd_base + ['swap' , 'python' , required_version ], check = True , capture_output = True )
225-
226- # Step 3: Find the new executable path
227- info_result = subprocess .run (omnipkg_cmd_base + ['info' , 'python' ], check = True , capture_output = True , text = True )
228- new_python_exe = None
229- for line in info_result .stdout .splitlines ():
230- if '⭐ (currently active)' in line :
231- match = re .search (r':\s*(/\S+)' , line )
232- if match :
233- new_python_exe = match .group (1 ).strip ()
234- break
235-
218+
219+ def find_interpreter_path (version_str ):
220+ """
221+ Helper to find the executable path for a given version using a more
222+ robust parsing method.
223+ """
224+ try :
225+ # Use 'info python' as it's a reliable source of this data.
226+ info_result = subprocess .run (
227+ omnipkg_cmd_base + ['info' , 'python' ],
228+ check = True , capture_output = True , text = True , timeout = 30
229+ )
230+ # This regex looks for an absolute path starting with '/' or a drive letter 'C:\'
231+ # on a line that contains the required version string.
232+ path_regex = re .compile (r"(/[^\s]+|[\w]:\\[^\s]+)" )
233+
234+ for line in info_result .stdout .splitlines ():
235+ if f'Python { version_str } ' in line :
236+ match = path_regex .search (line )
237+ if match :
238+ # Return the first absolute path found on the correct line.
239+ return match .group (0 ).strip ()
240+ return None
241+ except Exception :
242+ return None
243+
244+ # Step 2: Find the path to the required Python executable.
245+ print (_ (' -> Querying omnipkg for Python {} executable path...' ).format (required_version ))
246+ new_python_exe = find_interpreter_path (required_version )
247+
248+ # Step 2b: If not found, adopt it first, then find it again.
236249 if not new_python_exe :
237- raise RuntimeError (_ ("Could not find the new Python {} executable after swapping." ).format (required_version ))
250+ print (_ (' -> Python {} is not yet managed. Adopting it now...' ).format (required_version ))
251+ subprocess .run (
252+ omnipkg_cmd_base + ['python' , 'adopt' , required_version ],
253+ check = True , capture_output = True , timeout = 300
254+ )
255+ # After adopting, we MUST try to find the path again.
256+ new_python_exe = find_interpreter_path (required_version )
257+
258+ if not new_python_exe or not Path (new_python_exe ).exists ():
259+ raise RuntimeError (_ ("Could not find a valid Python {} executable path after checking and adopting." ).format (required_version ))
238260
239261 print (_ (' ✅ Found Python {} at: {}' ).format (required_version , new_python_exe ))
240- print (_ ('\n STEP 4: Relaunching script with the correct interpreter...' ))
241-
242- # Relaunch the original script
262+ print (_ ('\n -> Relaunching script with the correct interpreter...' ))
263+
264+ # Step 3: Relaunch the original script, passing the parent's identity.
243265 original_script = os .path .abspath (sys .argv [0 ])
244266 script_args = sys .argv [1 :]
245-
246- # Use os.execv to replace the current process. This is cleaner than a wrapper.
267+
247268 new_env = os .environ .copy ()
248269 new_env ['OMNIPKG_RELAUNCHED' ] = '1'
270+ new_env ['OMNIPKG_ENV_ID_OVERRIDE' ] = parent_env_id
271+ new_env ['OMNIPKG_VENV_ROOT' ] = parent_venv_root
272+
249273 os .execve (new_python_exe , [new_python_exe , original_script ] + script_args , new_env )
250274
251275 except Exception as e :
252276 print ('\n ' + '-' * 80 )
253277 print (' ❌ An error occurred while trying to switch Python versions.' )
254- # ... (your existing error handling) ...
278+ if isinstance (e , subprocess .CalledProcessError ):
279+ print (' -> Subprocess Error Output:' )
280+ print (e .stderr .decode ('utf-8' ) if e .stderr else e .stdout .decode ('utf-8' ))
281+ else :
282+ print (f' -> Error: { e } ' )
283+ import traceback
284+ traceback .print_exc ()
285+ print ('-' * 80 )
255286 sys .exit (1 )
256287
257288def run_interactive_command (command_list , input_data , check = True ):
0 commit comments