Skip to content

Commit 5af3201

Browse files
author
Bryan Howard
committed
python env related
1 parent 9025d59 commit 5af3201

File tree

1 file changed

+50
-21
lines changed

1 file changed

+50
-21
lines changed

environment_manager.py

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,38 @@
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

55
import os
66
import sys
77
import subprocess
88
import 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

1314
def 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+
1836
class 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

119152
class 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

Comments
 (0)