diff --git a/src/toolManager/gui_fixed.py b/src/toolManager/gui_fixed.py index 1e96f0010..58d0865c4 100644 --- a/src/toolManager/gui_fixed.py +++ b/src/toolManager/gui_fixed.py @@ -3,7 +3,8 @@ import sys import ctypes import subprocess -import platform +from pathlib import Path +from platform_utils import IS_WINDOWS, IS_LINUX, IS_MAC from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView, QProgressBar, @@ -15,20 +16,20 @@ from PyQt6.QtGui import QFont import logging -PYTHON = sys.executable -SYSTEM = platform.system() -IS_WINDOWS = SYSTEM == "Windows" -IS_LINUX = SYSTEM == "Linux" -import os -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +PYTHON = sys.executable + +BASE_DIR = Path(__file__).resolve().parent if IS_WINDOWS: - BACKEND = os.path.join(BASE_DIR, "tool_manager_windows.py") + BACKEND = BASE_DIR / "tool_manager_windows.py" elif IS_LINUX: - BACKEND = os.path.join(BASE_DIR, "tool_manager_linux.py") + BACKEND = BASE_DIR / "tool_manager_linux.py" +elif IS_MAC: + BACKEND = BASE_DIR / "tool_manager_macos.py" else: - BACKEND = os.path.join(os.path.dirname(os.path.abspath(__file__)), "tool_manager_windows.py") + BACKEND = BASE_DIR / "tool_manager_linux.py" + TOOLS = { "esim": { @@ -101,7 +102,9 @@ def run(self): stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - text=True + text=True, + encoding="utf-8", + errors="ignore" ) process.stdin.write(self.password + "\n") process.stdin.flush() @@ -402,7 +405,7 @@ def get_sudo_password(self): if IS_LINUX and self.sudo_password is None: password, ok = QInputDialog.getText( self, "Authentication Required", - "Enter your sudo password:", QLineEdit.Password + "Enter your sudo password:", QLineEdit.EchoMode.Password ) if ok and password: self.sudo_password = password diff --git a/src/toolManager/main.py b/src/toolManager/main.py index e005821de..6b8d43c50 100644 --- a/src/toolManager/main.py +++ b/src/toolManager/main.py @@ -12,6 +12,7 @@ ) from PyQt6.QtCore import Qt, QThread, pyqtSignal from PyQt6.QtGui import QFont +from platform_utils import IS_WINDOWS, IS_LINUX, IS_MAC, distro_label try: from gui_fixed import ToolManagerGUI @@ -21,7 +22,24 @@ # ==================== CONFIG ==================== BASE_DIR = Path(__file__).resolve().parent INFO_JSON = BASE_DIR / "information.json" -FULL_GUI = BASE_DIR / "gui_fixed.py" + +if IS_WINDOWS: + BACKEND = BASE_DIR / "tool_manager_windows.py" + FULL_GUI = BASE_DIR / "gui_fixed.py" + OS_LABEL = "Windows Edition" +elif IS_LINUX: + BACKEND = BASE_DIR / "tool_manager_linux.py" + FULL_GUI = BASE_DIR / "updater_gui.py" + OS_LABEL = f"{distro_label()} Edition" +elif IS_MAC: + BACKEND = BASE_DIR / "tool_manager_macos.py" + FULL_GUI = BASE_DIR / "updater_gui.py" + OS_LABEL = f"{distro_label()} Edition" +else: + BACKEND = BASE_DIR / "tool_manager_linux.py" + FULL_GUI = BASE_DIR / "updater_gui.py" + OS_LABEL = "Linux Edition" + PYTHON = sys.executable ANALOG_TOOLS = ["esim", "kicad", "ngspice"] @@ -46,12 +64,16 @@ } def is_admin(): + if not IS_WINDOWS: + return True try: return ctypes.windll.shell32.IsUserAnAdmin() except Exception: return False def relaunch_as_admin(): + if not IS_WINDOWS: + return True script = str(Path(__file__).resolve()) # Swap to pythonw.exe to suppress the black console window @@ -97,7 +119,7 @@ def __init__(self, tools): def run(self): success = True - backend = str(BASE_DIR / "tool_manager_windows.py") + backend = str(BACKEND) for tool, version in self.tools: self.progress.emit( f"Installing {TOOL_LABELS.get(tool, tool)} {version}..." @@ -193,7 +215,7 @@ def _create_header(self): t1 = QLabel("eSim Tool Manager") t1.setFont(QFont("Arial", 24, QFont.Weight.Bold)) t1.setStyleSheet("color: white; background: transparent;") - t2 = QLabel("Package Management System • Windows Edition") + t2 = QLabel(OS_LABEL) t2.setFont(QFont("Arial", 11)) t2.setStyleSheet("color: rgba(255,255,255,0.9); background: transparent;") vbox.addWidget(t1) @@ -651,12 +673,12 @@ def _uninstall_all(self): def _run_uninstall(self, tools, label): try: - backend = str(BASE_DIR / "tool_manager_windows.py") + backend = str(BACKEND) for tool, _ in tools: subprocess.Popen( [PYTHON, backend, "uninstall", tool, "none"], creationflags=(subprocess.CREATE_NO_WINDOW - if sys.platform == "win32" else 0) + if IS_WINDOWS else 0) ) QMessageBox.information( self, "Uninstall Started", @@ -669,7 +691,7 @@ def _run_uninstall(self, tools, label): def main(): - if sys.platform == "win32" and not is_admin(): + if IS_WINDOWS and not is_admin(): # Note: We deliberately skip a manual PyQt consent dialog here because # Windows will natively prompt the user with a UAC shield anyway. relaunch_as_admin() diff --git a/src/toolManager/tool_manager_windows.py b/src/toolManager/tool_manager_windows.py index de268df7a..886443e15 100644 --- a/src/toolManager/tool_manager_windows.py +++ b/src/toolManager/tool_manager_windows.py @@ -13,6 +13,7 @@ import time import io from pathlib import Path +from platform_utils import IS_WINDOWS, MSYS2_PATH # Add local directory to path for backend utility imports _local_path = str(Path(__file__).resolve().parent) @@ -21,12 +22,12 @@ from utils import ( run_cmd_safe, run_cmd_stream, which, print_status, - DEFAULT_MSYS2_PATH, DEFAULT_ESIM_DIR, WIN_KICAD_PATHS, + DEFAULT_ESIM_DIR, WIN_KICAD_PATHS, WIN_NGSPICE_PATHS, WIN_LLVM_PATHS, get_msys2_bash, get_msys2_mingw_bin, get_msys2_mingw_root ) -if sys.platform == "win32": +if IS_WINDOWS: sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='ignore') sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='ignore') @@ -34,8 +35,6 @@ STATE_FILE = BASE_DIR / "information.json" BASE_DIR.mkdir(parents=True, exist_ok=True) -MSYS2_PATH = DEFAULT_MSYS2_PATH - DOWNLOAD_DIR = BASE_DIR / "Download" DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True) @@ -202,7 +201,7 @@ def _find_ngspice_exe(): return which("ngspice") or which("ngspice.exe") def find_llvm_fixed(version=None): - if platform.system() == "Windows": + if IS_WINDOWS: import ctypes HWND_BROADCAST = 0xFFFF WM_SETTINGCHANGE = 0x001A diff --git a/src/toolManager/updater_gui.py b/src/toolManager/updater_gui.py index 6bd40e60c..6e6371109 100644 --- a/src/toolManager/updater_gui.py +++ b/src/toolManager/updater_gui.py @@ -11,6 +11,7 @@ QAbstractItemView) from PyQt6.QtCore import Qt, QThread, pyqtSignal from PyQt6.QtGui import QFont +from platform_utils import IS_WINDOWS, distro_label class InstallerThread(QThread): progress = pyqtSignal(str, int) @@ -22,6 +23,10 @@ def __init__(self, packages_to_install): self.packages = packages_to_install def run(self): + if IS_WINDOWS: + self.finished.emit(False, "Package Updater is Linux only.\nOn Windows, use the main Tool Manager.") + return + total = len(self.packages) for pkg_idx, (package_name, version, script_name) in enumerate(self.packages): try: @@ -368,7 +373,7 @@ def create_header(self): title.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold)) title.setStyleSheet("color: white; background: transparent;") - subtitle = QLabel("Real-time progress • Ngspice 35-43 • Ubuntu 22.04") + subtitle = QLabel(f"Real-time progress • Ngspice 35-43 • {distro_label()}") subtitle.setFont(QFont("Segoe UI", 8)) subtitle.setStyleSheet("color: rgba(255,255,255,0.9); background: transparent;") diff --git a/src/toolManager/utils.py b/src/toolManager/utils.py index 093e5d094..ed31bc9d5 100644 --- a/src/toolManager/utils.py +++ b/src/toolManager/utils.py @@ -17,13 +17,18 @@ import threading import time from pathlib import Path +from platform_utils import IS_WINDOWS, get_mysys2_path # ==================== CONSTANTS & DEFAULTS ==================== # Default Windows installation paths -DEFAULT_MSYS2_PATH = Path(r"C:\msys64") DEFAULT_ESIM_DIR = Path(r"C:\FOSSEE\eSim") +try: + MSYS2_PATH = get_mysys2_path(); +except RuntimeError as e: + print(f"[ERROR]: {e}", flush=True) + WIN_KICAD_PATHS = [ (r"C:\Program Files\KiCad\9.0\bin\kicad.exe", "9"), (r"C:\Program Files\KiCad\8.0\bin\kicad.exe", "8"), @@ -49,10 +54,6 @@ # Locations for MSYS2/MinGW64. The FOSSEE fallback supports the bundled # MSYS2 environment provided by some eSim installers to ensure # zero-dependency operation. -FOSSEE_MSYS_CANDIDATES = [ - Path(r"C:\msys64\mingw64"), - Path(r"C:\FOSSEE\MSYS\mingw64"), -] # ==================== HELPERS ==================== @@ -62,7 +63,7 @@ def run_cmd_safe(cmd, timeout=30, cwd=None, env=None): Returns the subprocess.CompletedProcess result or None if failed. """ try: - if platform.system() == "Windows": + if IS_WINDOWS: startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.wShowWindow = subprocess.SW_HIDE @@ -95,7 +96,7 @@ def run_cmd_stream(cmd, timeout=900, cwd=None, env=None): Returns a tuple of (returncode, full_output_string). """ try: - if platform.system() == "Windows": + if IS_WINDOWS: si = subprocess.STARTUPINFO() si.dwFlags |= subprocess.STARTF_USESHOWWINDOW si.wShowWindow = subprocess.SW_HIDE @@ -163,24 +164,14 @@ def get_msys2_bash(): """ Returns the path to MSYS2 bash.exe if found, else None. """ - bash_path = DEFAULT_MSYS2_PATH / "usr" / "bin" / "bash.exe" + bash_path = MSYS2_PATH / "usr" / "bin" / "bash.exe" return bash_path if bash_path.exists() else None -def get_msys2_mingw_root(): - """ - Returns the path to MSYS2 mingw64 root if found, else None. - """ - for candidate in FOSSEE_MSYS_CANDIDATES: - if candidate.exists(): - return candidate - return None - def get_msys2_mingw_bin(): """ Returns the path to MSYS2 mingw64/bin if found, else None. """ - for candidate in FOSSEE_MSYS_CANDIDATES: - bin_dir = candidate / "bin" - if bin_dir.exists(): - return bin_dir - return None + + bin_dir = MSYS2_PATH / "bin" + if bin_dir.exists(): + return bin_dir