11# environment_manager.py
22# A comprehensive system for managing a dedicated virtual environment for graph execution.
3- # Now correctly handles venv creation from a Nuitka- compiled executable .
3+ # This version contains the definitive fix for venv creation in a compiled application .
44
55import os
66import sys
77import subprocess
88import venv
9- from PySide6 .QtCore import QObject , Signal , QThread
10- from PySide6 .QtWidgets import QDialog , QVBoxLayout , QHBoxLayout , QTextEdit , QPushButton , QLabel , QDialogButtonBox , QLineEdit , QFileDialog , QListWidget , QListWidgetItem
9+ from PySide6 .QtCore import QObject , Signal , QThread , Qt
10+ from PySide6 .QtWidgets import QDialog , QVBoxLayout , QHBoxLayout , QTextEdit , QPushButton , QLabel , QDialogButtonBox , QLineEdit , QFileDialog , QListWidget , QListWidgetItem , QMenu
11+ from PySide6 .QtGui import QAction , QGuiApplication
1112
1213
1314def is_frozen ():
1415 """Checks if the application is running as a frozen (e.g., Nuitka) executable."""
1516 return getattr (sys , "frozen" , False )
1617
1718
19+ class ClickableLabel (QLineEdit ):
20+ """A read-only QLineEdit styled as a label that supports right-click to copy."""
21+
22+ def __init__ (self , text , parent = None ):
23+ super ().__init__ (text , parent )
24+ self .setReadOnly (True )
25+ self .setContextMenuPolicy (Qt .CustomContextMenu )
26+ self .customContextMenuRequested .connect (self .show_context_menu )
27+
28+ def show_context_menu (self , pos ):
29+ menu = QMenu (self )
30+ copy_action = QAction ("Copy" , self )
31+ copy_action .triggered .connect (lambda : QGuiApplication .clipboard ().setText (self .text ()))
32+ menu .addAction (copy_action )
33+ menu .exec (self .mapToGlobal (pos ))
34+
35+
1836class EnvironmentWorker (QObject ):
1937 """
2038 Worker to run environment tasks (creation, installation, verification) in a thread.
@@ -51,22 +69,39 @@ def run_setup(self):
5169 self .progress .emit (f"Creating virtual environment at: { self .venv_path } " )
5270
5371 if is_frozen ():
54- # For compiled apps, find the bundled Python runtime and use it to create the venv.
72+ # --- Definitive Fix for Frozen Apps ---
73+ # The standard 'venv' module fails when run from a compiled exe.
74+ # We must create the venv structure manually.
5575 base_path = os .path .dirname (sys .executable )
56- runtime_python_exe = os .path .join (base_path , "python_runtime" , "python.exe " )
76+ runtime_python_home = os .path .join (base_path , "python_runtime" )
5777
58- if not os .path .exists (runtime_python_exe ):
59- error_msg = f"Bundled Python runtime not found at '{ runtime_python_exe } '. " "The application build is incomplete."
60- self .finished .emit (False , error_msg )
78+ if not os .path .exists (os .path .join (runtime_python_home , "python.exe" )):
79+ self .finished .emit (False , f"Bundled Python runtime not found at '{ runtime_python_home } '." )
6180 return
6281
63- cmd = [runtime_python_exe , "-m" , "venv" , self .venv_path ]
64- result = subprocess .run (cmd , capture_output = True , text = True , encoding = "utf-8" )
82+ # 1. Create the builder without pip to avoid the failing 'ensurepip' call.
83+ builder = venv .EnvBuilder (with_pip = False )
84+ builder .create (self .venv_path )
85+
86+ # 2. Overwrite the incorrect pyvenv.cfg file.
87+ # This is the most critical step. It tells the new venv where to find
88+ # the full Python installation (our bundled runtime).
89+ cfg_path = os .path .join (self .venv_path , "pyvenv.cfg" )
90+ with open (cfg_path , "w" ) as f :
91+ f .write (f"home = { runtime_python_home } \n " )
92+ f .write ("include-system-site-packages = false\n " )
93+ f .write (f"version = { sys .version_info .major } .{ sys .version_info .minor } .{ sys .version_info .micro } \n " )
94+
95+ # 3. Manually install pip into the newly created environment.
96+ venv_python_exe = self .get_venv_python_executable ()
97+ self .progress .emit ("Bootstrapping pip..." )
98+ pip_bootstrap_cmd = [venv_python_exe , "-m" , "ensurepip" ]
99+ result = subprocess .run (pip_bootstrap_cmd , capture_output = True , text = True , encoding = "utf-8" )
65100 if result .returncode != 0 :
66- self .finished .emit (False , f"Failed to create venv : { result .stderr } " )
101+ self .finished .emit (False , f"Failed to bootstrap pip : { result .stderr } " )
67102 return
68103 else :
69- # For development, use the standard venv creation.
104+ # For development, the standard venv creation works fine .
70105 venv .create (self .venv_path , with_pip = True )
71106
72107 venv_python_exe = self .get_venv_python_executable ()
@@ -75,7 +110,6 @@ def run_setup(self):
75110 return
76111
77112 self .progress .emit (f"Installing { len (self .requirements )} dependencies..." )
78- # Use python -m pip to be robust
79113 cmd = [venv_python_exe , "-m" , "pip" , "install" ] + self .requirements
80114 process = subprocess .Popen (cmd , stdout = subprocess .PIPE , stderr = subprocess .STDOUT , text = True , encoding = "utf-8" )
81115 for line in iter (process .stdout .readline , "" ):
@@ -88,7 +122,6 @@ def run_setup(self):
88122 self .finished .emit (False , "Failed to install one or more packages." )
89123
90124 def run_verify (self ):
91- """Verifies the venv and checks for installed packages."""
92125 self .progress .emit ("Starting verification..." )
93126 venv_python_exe = self .get_venv_python_executable ()
94127 if not os .path .exists (venv_python_exe ):
@@ -117,10 +150,6 @@ def run_verify(self):
117150
118151
119152class EnvironmentManagerDialog (QDialog ):
120- """
121- A polished dialog for managing the Python execution environment.
122- """
123-
124153 def __init__ (self , venv_path , requirements , parent = None ):
125154 super ().__init__ (parent )
126155 self .setWindowTitle ("Execution Environment Manager" )
@@ -166,8 +195,8 @@ def __init__(self, venv_path, requirements, parent=None):
166195 action_layout .addWidget (self .verify_button )
167196 layout .addLayout (action_layout )
168197
169- self . status_display = QLineEdit ( "Status: Ready" )
170- self .status_display . setReadOnly ( True )
198+ # UI FIX: Use the custom ClickableLabel for the status display.
199+ self .status_display = ClickableLabel ( "Status: Ready" )
171200 layout .addWidget (self .status_display )
172201
173202 self .output_log = QTextEdit ()
@@ -183,7 +212,7 @@ def __init__(self, venv_path, requirements, parent=None):
183212
184213 def update_status_color (self , status ):
185214 """Updates the status display's background color."""
186- style = "color: white; padding: 4px; border-radius: 4px;"
215+ style = "color: white; padding: 4px; border-radius: 4px; border: 1px solid #2E2E2E; "
187216 if status is None :
188217 self .status_display .setStyleSheet (f"background-color: #5A5A5A; { style } " )
189218 elif status == "running" :
0 commit comments